第七章 项目进阶,构建安全高效的企业服务

1、Spring Security


1.1 基本介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
一般流程

登录 ->认证 -> 授权

  1. 当用户登录时,前端将用户输入的用户名、密码信息传输到后台,后台用一个类对象将其封装起来,通常使用的是UsernamePasswordAuthenticationToken这个类。
  2. 程序负责验证这个类对象。验证方法是调用Service根据username从数据库中取用户信息到实体类的实例中,比较两者的密码,如果密码正确就成功登陆,同时把包含着用户的用户名、密码、所具有的权限等信息的类对象放到SecurityContextHolder(安全上下文容器,类似Session)中去。
  3. 用户访问一个资源的时候,首先判断是否是受限资源。然后判断是否未登录,没有则跳到登录页面。
  4. 如果用户已经登录,访问一个受限资源的时候,程序要根据url去数据库中取出该资源所对应的所有可以访问的角色,然后拿着当前用户的所有角色一一对比,判断用户是否可以访问(这里就是和权限相关)。

优点

  • Spring Security对Spring整合较好,使用起来更加方便;
  • 有更强大的Spring社区进行支持;
  • 支持第三方的 oauth 授权。

1.2 SpringSercurityDemo

1. 导包

<!--spring Security-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. 实体类编写权限

2.1 实现UserDetails接口
public class User implements UserDetails {}
2.2 实现方法并赋予权限
// 账号是否过期
@Override
public boolean isAccountNonExpired() {return true;
}// 账号是否锁定
@Override
public boolean isAccountNonLocked() {return true;
}// 凭证未过期
@Override
public boolean isCredentialsNonExpired() {return true;
}// 账号是否可用
@Override
public boolean isEnabled() {return true;
}// 获得权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> list=new ArrayList<>();list.add(new GrantedAuthority() {// 每一个封装一个权限@Overridepublic String getAuthority() {switch (type){case 1:return "ADMIN";default:return "USER";}}});return list;
}

3. 业务层

实现UserDetailsService接口

public class UserService implements UserDetailsService {}

重写方法:通过username加载用户

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return this.findUserByName(username);
}

4. SpringSercurity配置类

4.1 忽略静态资源
@Override
public void configure(WebSecurity web) throws Exception {// 忽略静态资源//super.configure(web);web.ignoring().antMatchers("/resource/**");
}
4.2 认证
// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {//super.configure(auth);//内置的认证规则   加密auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));//自定义认证规则auth.authenticationProvider(new AuthenticationProvider() {@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {String username = authentication.getName();String password = (String) authentication.getCredentials(); // 凭证User user = userService.findUserByName(username);if (user==null){throw  new UsernameNotFoundException("账号不存在");}// md5加密生成密码password = CommunityUtil.md5(password + user.getSalt());// 判断密码是否与数据库一直if (!user.getPassword().equals(password)){throw new BadCredentialsException("密码不正确");}//principal: 主要信息, credentials:整数(密码), authorities:权限return new UsernamePasswordAuthenticationToken(user,user.getUsername(),user.getAuthorities());}// 当前的AuthenticationProvider支持那种类型的认证@Overridepublic boolean supports(Class<?> aClass) {// UsernamePasswordAuthenticationToken:Authentication接口的常用实现类return UsernamePasswordAuthenticationToken.class.equals(aClass); //用户密码验证 还以扫脸等验证}});
}
4.3 授权
// 授权
@Override
protected void configure(HttpSecurity http) throws Exception {//super.configure(http);// 登录相关配置http.formLogin().loginPage("/loginpage").loginProcessingUrl("/login")// 登录成功时.successHandler(new AuthenticationSuccessHandler() {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.sendRedirect(request.getContextPath()+"/index");}})// 登录失败时.failureHandler(new AuthenticationFailureHandler() {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {request.setAttribute("error",e.getMessage());request.getRequestDispatcher("/loginpage").forward(request,response);}});// 退出相关配置http.logout().logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler() {@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.sendRedirect(request.getContextPath()+"/index");}});//授权配置http.authorizeRequests().antMatchers("/letter").hasAnyAuthority("USER","ADMIN").antMatchers("/admin").hasAnyAuthority("ADMIN").and().exceptionHandling().accessDeniedPage("/denied");
}

授权中处理验证码

// 增加filter,处理验证码
http.addFilterBefore(new Filter() {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;if (request.getServletPath().equals("/login")){String verifyCode=request.getParameter("verifyCode");if (verifyCode==null||!verifyCode.equalsIgnoreCase("1234")){request.setAttribute("error","验证码错误!");request.getRequestDispatcher("/loginpage").forward(request,response);return;}}// 放行filterChain.doFilter(request,response);}
}, UsernamePasswordAuthenticationFilter.class);

授权中处理RememberMe

// 记住我
http.rememberMe().tokenRepository(new InMemoryTokenRepositoryImpl()).tokenValiditySeconds(3600*24).userDetailsService(userService);

5. 前端页面

配置拒绝访问页面

HomeController

// 拒绝访问的提示
@RequestMapping(value = "/denied",method = RequestMethod.GET)
public String getDenyPage(){return "/error/404";
}

将用户信息传入前端页面

@RequestMapping(path = "/index", method = RequestMethod.GET)
public String getIndexPage(Model model) {// 认证成功后,结果通过securityContextHolder存入SecurityContext中Object obj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();if (obj instanceof User){model.addAttribute("loginUser",obj);}return "/index";
}

index.html


login.html

2、权限控制

2.1 登录检查

废弃拦截器

2.2 授权配置

SecurityConfig

  • 忽略拦截静态资源
  • 授权
  • 修改退出路径
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {@Overridepublic void configure(WebSecurity web) throws Exception {// 忽略拦截静态资源web.ignoring().antMatchers("/resources/**");}// 认证用自己的//授权@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/user/setting","/user/upload","/discuss/add","/comment/add/**","/letter/**","/notice/**","/like","/follow","/unfollow").hasAnyAuthority(AUTHORITY_USER, AUTHORITY_ADMIN, AUTHORITY_MODERATOR).anyRequest().permitAll() // 除了上述请求,其他都允许访问.and().csrf().disable(); // 关闭csrf检查// 权限不够时的处理http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {// 没有登录时的处理@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {// 获取当前请求的请求头,判断同步还是异步String xRequestedWith = request.getHeader("x-requested-with");if ("XMLHttpRequest".equals(xRequestedWith)) {// 异步response.setContentType("application/plain;charset=utf-8");PrintWriter writer = response.getWriter();writer.write(CommunityUtil.getJsonString(403, "你还没有登录!"));} else {// 同步response.sendRedirect(request.getContextPath() + "/login");}}}).accessDeniedHandler(new AccessDeniedHandler() {// 权限不足时的处理@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {// 获取当前请求的请求头,判断同步还是异步String xRequestedWith = request.getHeader("x-requested-with");if ("XMLHttpRequest".equals(xRequestedWith)) {// 异步response.setContentType("application/plain;charset=utf-8");PrintWriter writer = response.getWriter();writer.write(CommunityUtil.getJsonString(403, "你没有访问此功能的权限!"));} else {// 同步response.sendRedirect(request.getContextPath() + "/denied");}}});// Security默认拦截/logout的请求// 覆盖它默认的逻辑,才能执行我们自己的推出代码http.logout().logoutUrl("/securityLogout");}
}

denied页面 HomeController

    /*** 获取错误页面* @return*/@RequestMapping(value = "/denied", method = RequestMethod.GET)public String getDeniedPage() {return "/error/404";}

UserService

  • 获取用户的权限
// 获取用户权限public Collection<? extends GrantedAuthority> getAuthorities(int userId) {User user = this.findUserById(userId);List<GrantedAuthority> list =new ArrayList<>();list.add(new GrantedAuthority() {@Overridepublic String getAuthority() {switch (user.getType()){case 1:return AUTHORITY_ADMIN;case 2:return AUTHORITY_MODERATOR;default:return AUTHORITY_USER;}}});return list;}

2.3 认证方案

将认证的结果存入Security

loginTicketInterceptor

退出时清理数据

2.4 CSRF配置


攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

SpringSecurity中,服务器给浏览器发送一个Tocken,

以发布帖子为例

index.html

<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">

index.js

// 发送ajax请求之前,将CSRF令牌设置到请求的消息头中
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function (e, xhr, options) {xhr.setRequestHeader(header, token);
});

关闭防止CSRF攻击

Security配置类Configure(HttpSecurity http)

.and().csrf().disable(); // 关闭csrf检查

3、置顶、加精、删除

3.1 功能实现

在dao接口中添加两个方法

DiscussPostMapper

// 修改类型
int updateType(int id,int type);// 修改状态
int updateStatus(int id,int status);

DiscussPostMapper.xml

    <update id="updateType">update discuss_postset type = #{type}where id = #{id}</update><update id="updateStatus">update discuss_postset status=#{status}where id = #{id}</update>

DiscusspostService

/*** 更新帖子类型* @param id* @param type* @return*/public int updateType(int id,int type){return discussPostMapper.updateType(id, type);}/*** 更新帖子状态* @param id* @param status* @return*/public int updateStatus(int id,int status){return discussPostMapper.updateStatus(id,status);}

DiscussPostController

置顶

/*** 置顶* @param id* @return*/
@RequestMapping(value = "/top",method = RequestMethod.POST)
@ResponseBody
public String setTop(int id){discussPostService.updateType(id,1); // 0:普通,1:置顶// 触发发帖事件Event  event = new Event().setTopic(TOPIC_PUBLISH).setUserId(hostHolder.getUser().getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(id);eventProducer.fireEvent(event);return CommunityUtil.getJSONString(0);
}

加精

/*** 加精* @param id* @return*/
@RequestMapping(value = "/wonderful",method = RequestMethod.POST)
@ResponseBody
public String setWonderful(int id){discussPostService.updateStatus(id,1);// 0:正常 1:精华 2:拉黑// 触发帖子事件Event event =new Event().setTopic(TOPIC_PUBLISH).setUserId(hostHolder.getUser().getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(id);eventProducer.fireEvent(event);return CommunityUtil.getJSONString(0);
}

删除

/*** 删除* @param id* @return*/
@RequestMapping(value = "/delete",method = RequestMethod.POST)
@ResponseBody
public String setDelete(int id){discussPostService.updateStatus(id,2);// 0: 正常 1:精华 2:拉黑// 触发帖子事件Event event = new Event().setTopic(TOPIC_DELETE).setUserId(hostHolder.getUser().getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(id);eventProducer.fireEvent(event);return CommunityUtil.getJSONString(0);
}

前端处理

discuss-detail.html

discuss.js

$(function (){$("#topBtn").click(setTop);$("#wonderfulBtn").click(setWonderful);$("#deleteBtn").click(setDelete);
});// 置顶
function setTop(){$.post(CONTEXT_PATH+"/discuss/top",{"id":$("#postId").val()},function (data){data=$.parseJSON(data);if(data.code==0){// 点击后,按钮不可用$("#topBtn").attr("disabled","disabled")}else{alert(data.msg);}})
}// 加精
function setWonderful(){$.post(CONTEXT_PATH+"/discuss/wonderful",{"id":$("#postId").val()},function (data){data=$.parseJSON(data);if(data.code==0){// 点击后,按钮不可用$("#wonderfulBtn").attr("disabled","disabled")}else{alert(data.msg);}})
}// 删除
function setDelete(){$.post(CONTEXT_PATH+"/discuss/delete",{"id":$("#postId").val()},function (data){data=$.parseJSON(data);if(data.code==0){// 跳转首页location.href = CONTEXT_PATH+"/index"}else{alert(data.msg);}})
}

此时基本功能已经实现

3.2 权限管理

SecurityConfig中进行权限管理

http.authorizeRequests().antMatchers("/discuss/top","/discuss/wonderful").hasAnyAuthority(AUTHORITY_MODERATOR).antMatchers("/discuss/delete").hasAnyAuthority(AUTHORITY_ADMIN).anyRequest().permitAll() // 除了上述请求,其他都允许访问.and().csrf().disable(); // 关闭csrf检查

只有对应的权限才可以进行响应的操作

  • moderator 可以加精和置顶
  • admin 可以进行帖子的删除

3.3 按钮显示

thymeleaf 整合 SpringSecurity

  1. 依赖
<!--thymeleaf -security--><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency>
  1. discuss-detail.html中引入
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
  1. 按钮显示
  • moderator 显示置顶、加精按钮
  • admin 显示删除按钮

4、Redis高级数据类型


测试

// 统计20万个重复数据的独立总数
@Test
public void testHyperLoglog() {String redisKey = "test:hll:01";for (int i = 1; i <= 10000; i++) {redisTemplate.opsForHyperLogLog().add(redisKey, i);}for (int i = 1; i <= 10000; i++) {int r = (int) (Math.random() * 10000 + 1);redisTemplate.opsForHyperLogLog().add(redisKey, r);}long size = redisTemplate.opsForHyperLogLog().size(redisKey);System.out.println(size);//99562
}// 将三组数据合并,再统计合并后的重复数据的独立总数
@Test
public void testHyperLoglogUnion() {String redisKey2 = "test:hll:02";for (int i = 1; i <= 10000; i++) {redisTemplate.opsForHyperLogLog().add(redisKey2, i);}String redisKey3 = "test:hll:03";for (int i = 5001; i <= 15000; i++) {redisTemplate.opsForHyperLogLog().add(redisKey3, i);}String redisKey4 = "test:hll:04";for (int i = 10001; i <= 20000; i++) {redisTemplate.opsForHyperLogLog().add(redisKey4, i);}String unionKey = "test:hll:union";redisTemplate.opsForHyperLogLog().union(unionKey, redisKey2, redisKey3, redisKey4);long size = redisTemplate.opsForHyperLogLog().size(unionKey);System.out.println(size);//19891
}// 统计一组数据的布尔值
@Test
public void testBitmap() {String redisKey = "test:bm:01";// 记录redisTemplate.opsForValue().setBit(redisKey, 1, true);redisTemplate.opsForValue().setBit(redisKey, 4, true);redisTemplate.opsForValue().setBit(redisKey, 7, true);// 查询某一位的值Boolean bit = redisTemplate.opsForValue().getBit(redisKey, 7);System.out.println(bit);//trueObject obj = redisTemplate.execute(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {return connection.bitCount(redisKey.getBytes());}});System.out.println(obj);//3
}// 统计3组数据的布尔值,并对这3组数据做OR运算
@Test
public void testBitMapOperation() {String redisKey2 = "test:bm:02";redisTemplate.opsForValue().setBit(redisKey2, 0, true);redisTemplate.opsForValue().setBit(redisKey2, 1, true);redisTemplate.opsForValue().setBit(redisKey2, 2, true);String redisKey3 = "test:bm:03";redisTemplate.opsForValue().setBit(redisKey3, 2, true);redisTemplate.opsForValue().setBit(redisKey3, 3, true);redisTemplate.opsForValue().setBit(redisKey3, 4, true);String redisKey4 = "test:bm:04";redisTemplate.opsForValue().setBit(redisKey4, 4, true);redisTemplate.opsForValue().setBit(redisKey4, 5, true);redisTemplate.opsForValue().setBit(redisKey4, 6, true);String redisKey = "test:bm:or";Object obj = redisTemplate.execute(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {connection.bitOp(RedisStringCommands.BitOperation.OR,redisKey.getBytes(), redisKey2.getBytes(), redisKey3.getBytes(), redisKey4.getBytes());return connection.bitCount(redisKey.getBytes());}});System.out.println(obj);//7}

5、网站数据统计

5.1 定义RedisKey

private static final String PREFIX_UV = "uv"; // 独立访客
private static final String PREFIX_DAU = "dau"; // 日活跃用户// 单日UV
public static String getUVKey(String date){return PREFIX_UV+SPLIT+date;
}// 区间UV
public static String getUVKey(String start,String end){return PREFIX_UV+SPLIT+start+SPLIT+end;
}// 单日活跃用户
public static String getDAUKey(String date){return  PREFIX_DAU+SPLIT+date;
}// 区间活跃用户
public static String getDAUKey(String start,String end){return PREFIX_DAU+SPLIT+start+SPLIT+end;
}

5.2 DataService

@Service
public class DataService {@Autowiredprivate RedisTemplate redisTemplate;// 日期格式化private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");/*** 将指定的ip记录UV** @param ip*/public void recordUV(String ip) {// 获取keyString redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));// 存数据redisTemplate.opsForHyperLogLog().add(redisKey, ip);}/*** 统计指定日期范围的UV** @param start* @param end* @return*/public long calculateUV(Date start, Date end) {// 判空if (start == null || end == null) {throw new IllegalArgumentException("参数不能为空");}// 整理该日期范围内的keyList<String> keyList = new ArrayList<>();Calendar calendar = Calendar.getInstance();// 设置开始日期calendar.setTime(start);// 时间在start 与 end 之内while (!calendar.getTime().after(end)) {// 获取每一天的keyString key = RedisKeyUtil.getUVKey(df.format(calendar.getTime()));keyList.add(key);// 加一天calendar.add(Calendar.DATE, 1);}// 合并这些数据String redisKey = RedisKeyUtil.getUVKey(df.format(start), df.format(end));redisTemplate.opsForHyperLogLog().union(redisKey, keyList.toArray());// 返回统计结果return redisTemplate.opsForHyperLogLog().size(redisKey);}/*** 将指定用户计入DAU** @param userId*/public void recordDAU(int userId) {String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));redisTemplate.opsForValue().setBit(redisKey, userId, true);}public long calculateDAU(Date start, Date end) {if (start == null || end == null) {throw new IllegalArgumentException("参数不能为空");}// 整理该日期的keyList<byte[]> keyList = new ArrayList<>();Calendar calendar = Calendar.getInstance();calendar.setTime(start);// 时间在start 与 end 之内while (!calendar.getTime().after(end)) {// 获取每一天的keyString key = RedisKeyUtil.getDAUKey(df.format(calendar.getTime()));keyList.add(key.getBytes());calendar.add(Calendar.DATE, 1);}// 进行OR运算return (long) redisTemplate.execute(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {String redisKey = RedisKeyUtil.getDAUKey(df.format(start), df.format(end));connection.bitOp(RedisStringCommands.BitOperation.OR,redisKey.getBytes(), keyList.toArray(new byte[0][0]));return connection.bitCount(redisKey.getBytes());}});}}

5.3 DataInterceptor

@Component
public class DataInterceptor implements HandlerInterceptor {@Autowiredprivate DataService dataService;@Autowiredprivate HostHolder hostHolder;// 处理请求之前@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 统计uvString ip = request.getRemoteUser();dataService.recordUV(ip);// 统计dauUser user = hostHolder.getUser();if(user!=null){dataService.recordDAU(user.getId());}return true;}
}

DataInterceptor注册到webConfig

5.4 DataController

@Controller
public class DataController {@Autowiredprivate DataService dataService;/*** 获取统计页面** @return*/@RequestMapping(value = "/data", method = {RequestMethod.GET, RequestMethod.POST})public String getDataPage() {return "/site/admin/data";}/*** 统计网站uv* @param start* @param end* @param model* @return*/@RequestMapping(value = "/data/uv", method = RequestMethod.POST)public String getUV(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,@DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model) {long uv = dataService.calculateUV(start, end);model.addAttribute("uvResult", uv);model.addAttribute("uvStartDate", start);model.addAttribute("uvEndDate", end);return "forward:/data";}/*** 统计网站dau* @param start* @param end* @param model* @return*/@RequestMapping(value = "/data/dau",method = RequestMethod.POST)public String getDAU(@DateTimeFormat(pattern = "yyyy-MM-dd") Date start,@DateTimeFormat(pattern = "yyyy-MM-dd") Date end, Model model){long dau = dataService.calculateDAU(start, end);model.addAttribute("dauResult",dau);model.addAttribute("dauStartDate",start);model.addAttribute("dauEndDate",end);return "forward:/data";}
}

5.5 data.html


6、任务执行与调度




演示

6.1 JDK线程池

// 普通线程池    ExecutorService
private ExecutorService executorService= Executors.newFixedThreadPool(5);// 可能执行定时任务的线程池 ScheduledExecutorService
private ScheduledExecutorService scheduledExecutorService=Executors.newScheduledThreadPool(5);private void sleep(long m){try {Thread.sleep(m);} catch (InterruptedException e) {e.printStackTrace();}
}// 1.jdk普通线程池
@Test
public void testExecutorService(){Runnable task=new Runnable() {@Overridepublic void run() {LOGGER.debug("hello ExecutorService");}};for (int i = 0; i < 10; i++) {executorService.submit(task);}sleep(10000);//阻塞10秒钟
}//2.jdk定时任务线程池
@Test
public void testScheduledExecutorService(){Runnable task=new Runnable() {@Overridepublic void run() {LOGGER.debug("hello ScheduledExecutorService");}};// 延迟执行10秒 一秒一次scheduledExecutorService.scheduleAtFixedRate(task,10000,1000, TimeUnit.MILLISECONDS);sleep(30000);
}

6.2 Spring线程池

@Async
public void execute1(){LOGGER.debug("hello execute1");
}@Scheduled(initialDelay = 10000,fixedRate = 1000)
public void execute2(){LOGGER.debug("hello execute2");
}
// 3.Spring 普通线程池
@Test
public void testThreadPoolTaskExecutor(){Runnable task=new Runnable() {@Overridepublic void run() {LOGGER.debug("hello ThreadPoolTaskExecutor");}};for (int i = 0; i < 10; i++) {taskExecutor.submit(task);}sleep(10000);
}// 4.Spring定时任务线程池
@Test
public void testThreadPoolTaskScheduler(){Runnable task=new Runnable() {@Overridepublic void run() {LOGGER.debug("hello ThreadPoolTaskScheduler");}};Date startTime=new Date(System.currentTimeMillis()+10000);taskScheduler.scheduleAtFixedRate(task,startTime,1000);sleep(30000);
}// 5.Spring普通线程池简化使用方式
@Test
public void testThreadPoolTaskExecutorSimple(){for (int i = 0; i < 10; i++) {alphaService.execute1();}sleep(10000);
}// 6.Spring定时任务简化使用方式
@Test
public void testThreadPoolTaskSchedulerSimple(){sleep(30000);
}

执行定时任务需要编写配置文件

6.3 Spring Quartz

核心组件

Scheduler:代表一个Quartz的独立运行容器

Job:通过job接口定义任务

JobDetail:配置Job

Trigger:触发器 配置Job

1. 起步依赖

<!--Spring quartz-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2. 配置

任务持久化到数据库(默认存在内存中)

spring.quartz.job-store-type=jdbc
spring.quartz.scheduler-name=communityScheduler
spring.quartz.properties.org.quartz.scheduler.instanced=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=5
spring:quartz:job-store-type: jdbcscheduler-name: communitySchedulerproperties:org:quartz:scheduler:instanced: AUTOjobStore:class: org.quartz.impl.jdbcjobstore.JobstoreTXdriverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegateisClustered: truethreadPool:class: org.quartz.simpl.SimpleThreadPoolthreadCount: 5

3. 编写Job

public class AlphaJob implements Job {@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {System.out.println(Thread.currentThread().getName()+":execute a quartz job.");}
}

4. 配置Job和Trigger

@Configuration
public class QuartzConfig {/*FactoryBean 简化Bean实例化过程:1、通过FactoryBean封装Bean的实例化过程2、将FactoryBean装配到 Spring容器里3、将FactoryBean注入给其他的bean4、该bean得到的是FactoryBean所管理的对象实例*//*** 配置JobDetail* @return*/@Beanpublic JobDetailFactoryBean alphaJobDetail() {// 实例化JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();factoryBean.setJobClass(AlphaJob.class);factoryBean.setName("alphaJob");factoryBean.setGroup("alphaJobGroup");factoryBean.setDurability(true); // 任务持久保存factoryBean.setRequestsRecovery(true); // 是否可恢复return factoryBean;}/*** 配置Trigger(SimpleTriggerFactoryBean,CronTriggerFactoryBean)* @param alphaJobDetail* @return*/@BeanSimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail) {// 实例化SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();factoryBean.setJobDetail(alphaJobDetail);factoryBean.setName("alphaTrigger");factoryBean.setGroup("alphaTriggerGroup");factoryBean.setRepeatInterval(3000); // 频率,多长时间执行一次factoryBean.setJobDataMap(new JobDataMap());return factoryBean;}
}

5. 启动测试

项目启动,数据存入数据库中

6. 清除数据

清除数据的方式有两种:

  1. 手动删除数据库中的数据
  2. 利用程序一次性删除
@Autowiredprivate Scheduler scheduler;@Testpublic void testDeleteJob(){try {// 删除数据库中,job名字为:alphaJob group名字为:alphaJobGroup 的所有数据boolean result = scheduler.deleteJob(new JobKey("alphaJob", "alphaJobGroup"));System.out.println(result); // true} catch (SchedulerException e) {e.printStackTrace();}}

测试完毕后将bean进行注释,防止测试数据下次直接运行并存入数据库中

7、热帖排行

利用定时任务和Redis缓存进行分数的计算

7.1 定义RedisKey

将变化的帖子存入缓存redis中,如果缓存中有帖子,那么就刷新分数,如果没有则不刷新。

private static final String PREFIX_POST = "post"; // 帖子// 帖子分数
public static  String getPostScoreKey(){return PREFIX_POST+SPLIT+"score";
}

7.2 将需要计算的分数帖子存入Redis

  1. 新增、评论、点赞、加精计算分数,置顶不计算分数

  2. 为了防止存入的数据重复,造成重复的加分,所以存入Set中

1. 新增帖子

// 计算分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,post.getId());

2. 加精

// 计算分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,id);

3. 点赞

// 计算分数
if(entityType == ENTITY_TYPE_POST){String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey,postId);
}

4. 评论

// 计算分数
String redisKey = RedisKeyUtil.getPostScoreKey();
redisTemplate.opsForSet().add(redisKey,discussPostId);

7.3 编写定时任务

public class PostScoreRefresh implements Job, CommunityConstant {// 开启日志private static final Logger logger= LoggerFactory.getLogger(PostScoreRefresh.class);// 牛客纪元private static final Date epoch;static {try {epoch=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse("2014-08-01 00:00:00");} catch (ParseException e) {throw  new RuntimeException("初始化牛客纪元失败",e);}}@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate LikeService likeService;@Autowiredprivate ElasticsearchService elasticsearchService;@Overridepublic void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {// 获取keyString redisKey = RedisKeyUtil.getPostScoreKey();// 就是一个绑定key的对象,我们可以通过这个对象来进行与key相关的操作【集合】BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);// 判断是否有数据if(operations.size()==0){logger.info("任务取消,没有需要刷新的帖子!");return;}logger.info("任务开始,正在刷新帖子分数:"+operations.size());// 依次处理分数while (operations.size()>0){this.refresh((Integer)operations.pop());}logger.info("任务结束,帖子分数刷新完毕!");}private void refresh(int postId){// 根据id获取帖子DiscussPost post = discussPostService.findDiscussPostById(postId);if(post==null){logger.error("帖子不存在:id ="+postId);return;}// 是否精华boolean wonderful = post.getStatus()==1;// 评论数量int commentCount =post.getCommentCount();// 点赞数量long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST,postId);// 计算分数:log( 精华分 + 评论数*10 + 点赞数*2 + 收藏数*2 )+(发布时间 - 牛客纪元)double w =(wonderful?75:0)+commentCount*10+likeCount*2;// 分数 = 帖子权重 + 距离天数double score = Math.log10(Math.max(w,1)) // 如果w小于1,就返回 1+(post.getCreateTime().getTime()-epoch.getTime())/(1000*3600*24);// 毫秒换算天// 更新帖子分数discussPostService.updateScore(postId,score);// 同步搜索数据post.setScore(score);elasticsearchService.saveDiscussPost(post);}
}

7.4 配置定时任务

/*** 任务:刷新帖子分数* @return*/@Beanpublic JobDetailFactoryBean postScoreRefreshDetail(){JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();factoryBean.setJobClass(PostScoreRefresh.class);factoryBean.setName("postScoreRefreshJob");factoryBean.setGroup("communityGroup");factoryBean.setDurability(true); // 是否长期保存factoryBean.setRequestsRecovery(true); // 是否可恢复return factoryBean;}
/*** 触发器:刷新帖子分数* @param postScoreRefreshDetail* @return*/@Beanpublic SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshDetail){SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();factoryBean.setJobDetail(postScoreRefreshDetail);factoryBean.setName("postScoreRefreshTrigger");factoryBean.setGroup("communityTriggerGroup");factoryBean.setRepeatInterval(1000*60*5); // 五分钟执行一遍factoryBean.setJobDataMap(new JobDataMap());return factoryBean;}

7.5 重置查询帖子方法

增加一个orderMode参数,用来表示排序方式

  • orderMode == 0 时,按照时间类型排序
  • orderMode == 1 时,按照时间类型分数进行排序

1. DiscussPostMapper

// 根据用户id查询帖子列表,动态sql(也可以不使用userId)
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit,int orderMode);

2. DiscussPostMapper.xml

<select id="selectDiscussPosts" resultType="DiscussPost">select<include refid="selectFields"></include>from discuss_postwhere status!=2<if test="userId!=0">and user_id=#{userId}</if><if test="orderMode==0">order by type desc,create_time desc /*按照创建时间排序*/</if><if test="orderMode==1">order by type desc,score desc,create_time desc /*按照分数排序*/</if>limit #{offset},#{limit}</select>

3. DiscussPostService

/*** 查询帖子列表* @param userId* @param offset* @param limit* @return*/
public List<DiscussPost> findDiscussPosts(int userId,int offset,int limit,int orderMode){return discussPostMapper.selectDiscussPosts(userId, offset, limit,orderMode);
}

4. HomeController

/*** 获取index页面* @param model* @param page* @return*/
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String GetIndexPage(Model model, Page page, @RequestParam(value = "orderMode",defaultValue = "0") int orderMode) {// 方法调用前,SpringMVC会自动实力胡model和page,并将Page注入Model// 所以,在thymealeaf中可以直接访问Page对象中的数据page.setRows(discussPostService.findDiscussPostRows(0));page.setPath("/index?orderMode"+orderMode);// 获取索引帖子信息List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit(),orderMode);System.out.println(list);// 向页面返回的集合List<Map<String, Object>> discussPosts = new ArrayList<>();if (list != null) {for (DiscussPost post : list) {Map<String, Object> map = new HashMap<>();map.put("post", post);User user = userService.findUserById(post.getUserId());map.put("user", user);// 点赞数量long likeCount=likeService.findEntityLikeCount(ENTITY_TYPE_POST,post.getId());map.put("likeCount",likeCount);discussPosts.add(map);}}model.addAttribute("discussPosts", discussPosts);return "/index";
}

7.6 页面处理

index.html

8、生成长图

8.1 基本使用

wkhtmltopdf下载

官网下载

为了使用方便,可以配置一下环境变量

生成pdf

wkhtmltopdf https://www.nowcoder.com D:\Environment\work\data\wk-pdfs/1.pdf

生成image 75Mb

wkhtmltoimage https://www.nowcoder.com D:\Environment\work\data\wk-images\1.png

压缩图片 1.48Mb

wkhtmltoimage --quality 75 https://www.nowcoder.com D:\Environment\work\data\wk-images\2.png

用Java代码测试

public class WKTests {public static void main(String[] args) {String cmd = "d:/attachment/wkhtmltopdf/bin/wkhtmltoimage --quality 75 https://www.nowcoder.com " +"d:/Environment/work/data/wk-images/3.png";try {// 向操作系统提交命令Runtime.getRuntime().exec(cmd);System.out.println("ok");} catch (IOException e) {e.printStackTrace();}}
}

配置文件

# wk
wk:image:command: d:/attachment/wkhtmltopdf/bin/wkhtmltoimagestorage: d:/Environment/work/data/wk-images

自动创建文件夹目录

@Slf4j
@Configuration
public class WkConfig {@Value("${wk.image.storage}")private String wkImageStorage;@PostConstructpublic void  init(){// 创建 WK图片目录File file = new File(wkImageStorage);if(!file.exists()){file.mkdirs();log.info("创建WK图片目录:"+wkImageStorage);}}
}

目录创建成功

8.2 模拟开发分享功能

1. ShareController

生成长图

/*** 生成长图* @param htmlUrl* @return*/@RequestMapping(value = "/share",method = RequestMethod.GET)@ResponseBodypublic String share(String htmlUrl){// 生成文件名String fileName = CommunityUtil.generateUUID();// 异步生成长图Event event = new Event().setTopic(TOPIC_SHARE).setData("htmlUrl",htmlUrl).setData("fileName",fileName).setData("suffix","png");eventProducer.fireEvent(event);// 返回访问路径Map<String,Object> map = new HashMap<>();map.put("shareUrl",domain+contextPath+"/share/image/"+fileName);return CommunityUtil.getJSONString(0,null,map);}

获取长图

/*** 获取分享图片* @param fileName* @param response*/@RequestMapping(value = "/share/image/{fileName}",method = RequestMethod.GET)public void getShareImage(@PathVariable("fileName") String fileName, HttpServletResponse response){if(StringUtils.isBlank(fileName)){throw  new IllegalArgumentException("文件名不能为空!");}response.setContentType("image/png");File file = new File((wkImageStorage + "/" + fileName + ".png"));try {// 读取文件OutputStream os = response.getOutputStream();FileInputStream fis = new FileInputStream(file);byte[]buffer = new byte[1024];int len=0;while ((len=fis.read())!=-1){os.write(buffer,0,len);}} catch (IOException e) {log.error("获取长图失败:"+e.getMessage());}}

整体代码

@Slf4j
@Controller
public class ShareController implements CommunityConstant {@Autowiredprivate EventProducer eventProducer;@Value("${community.path.domain}")private String domain;@Value("${server.servlet.context-path}")private String contextPath;@Value("${wk.image.storage}")private String wkImageStorage;/*** 生成长图* @param htmlUrl* @return*/@RequestMapping(value = "/share",method = RequestMethod.GET)@ResponseBodypublic String shate(String htmlUrl){// 生成文件名String fileName = CommunityUtil.generateUUID();// 异步生成长图Event event = new Event().setTopic(TOPIC_SHARE).setData("htmlUrl",htmlUrl).setData("fileName",fileName).setData("suffix","png");eventProducer.fireEvent(event);// 返回访问路径Map<String,Object> map = new HashMap<>();map.put("shareUrl",domain+contextPath+"/share/image/"+fileName);return CommunityUtil.getJSONString(0,null,map);}/*** 获取分享图片* @param fileName* @param response*/@RequestMapping(value = "/share/image/{fileName}",method = RequestMethod.GET)public void getShareImage(@PathVariable("fileName") String fileName, HttpServletResponse response){if(StringUtils.isBlank(fileName)){throw  new IllegalArgumentException("文件名不能为空!");}response.setContentType("image/png");File file = new File((wkImageStorage + "/" + fileName + ".png"));try {// 读取文件OutputStream os = response.getOutputStream();FileInputStream fis = new FileInputStream(file);byte[]buffer = new byte[1024];int len=0;while ((len=fis.read())!=-1){os.write(buffer,0,len);}} catch (IOException e) {log.error("获取长图失败:"+e.getMessage());}}
}

2. EventConsumer

// 消费分享事件@KafkaListener(topics = TOPIC_SHARE)public void handleShareMessage(ConsumerRecord record) {if (record == null || record.value() == null) {logger.error("消息内容为空!");return;}Event event = JSONObject.parseObject(record.value().toString(), Event.class);if (event == null) {logger.error("消息格式错误");return;}String htmlUrl = (String) event.getData().get("htmlUrl");String fileName = (String) event.getData().get("fileName");String suffix = (String) event.getData().get("suffix");String cmd = wkImageCommand + " --quality 75 " + htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;try {Runtime.getRuntime().exec(cmd);logger.info("生成长图成功"+cmd);} catch (IOException e) {logger.error("生成长图失败:"+e.getMessage());}}

3. 测试

页面输入

localhost:8080/community/share?htmlUrl=https://www.nowcoder.com/

页面返回

{"code":0,"shareUrl":"http://localhost:8080/community/share/image/22fd0374e7dc4d2f9625046d582be656"}

shareUrl可直接访问图片

9、将文件上传到服务器


七牛云官网

七牛云
对象存储

使用

  1. 依赖
<dependency><groupId>com.qiniu</groupId><artifactId>qiniu-java-sdk</artifactId><version>7.11.0</version>
</dependency>
  1. 配置
# qi niu
qiniu.key.access=gMqNj2q-hhrAKZKsj9TBBsp-AR8iWzO2RNikdf98
qiniu.key.secret=8QOWD979tqtht6oYq5tCP_9belaSjRP5GFIQ1Wpr
qiniu.bucket.header.name=community-header-198
qiniu.bucket.header.url=http://rf1wyikyd.hb-bkt.clouddn.com
qiniu.bucket.share.name=community-share-198
qiniu.bucket.share.url=http://rf1wmvp4q.hb-bkt.clouddn.com
# qiniu
qiniu:key:access: 90MzJe4vKjHERU7OaLESb4qe299NOIV-lJSgfEY8secret: 9AfAPfw7mJa35JqcwJu-yBlaWlTf2qYBYOkv6W-Tbucket:header:name: community-header-20221029url: http://rkhsjllq1.hb-bkt.clouddn.comshare:name: community-share-20221029url: http://rkhsds3k9.hb-bkt.clouddn.com

9.1 客户端上传

1. 重构原来的setting请求

UserController

@Value("${qiniu.key.access}")
private String accessKey;@Value("${qiniu.key.secret}")
private String secretKey;@Value("${qiniu.bucket.header.name}")
private String headerBucketName;@Value("${qiniu.bucket.header.url}")
private String headerBucketUrl;/*** 获取账号设置页面* @return*/
@LoginRequired
@RequestMapping(value = "/setting", method = RequestMethod.GET)
public String getSettingPage(Model model) {// 1.上传文件名称String fileName = CommunityUtil.generateUUID();// 2.设置响应信息StringMap policy = new StringMap();// 成功返回code:0policy.put("returnBody", CommunityUtil.getJSONString(0));// 3.生成上传凭证Auth auth = Auth.create(accessKey, secretKey);String uploadToken = auth.uploadToken(headerBucketName, fileName, 3600, policy);model.addAttribute("uploadToken", uploadToken);model.addAttribute("fileName", fileName);return "/site/setting";
}

2. 更新头像路径

原来的获取头像和上传头像废弃掉

/*** 更新头像路径* @param fileName* @return*/
@RequestMapping(value = "/header/url", method = RequestMethod.POST)
@ResponseBody
public String updateHeaderUrl(String fileName) {if (StringUtils.isBlank(fileName)) {return CommunityUtil.getJSONString(1, "文件名不能为空");}// 获取头像路径String url = headerBucketUrl + "/" + fileName;userService.updateHeader(hostHolder.getUser().getId(), url);return CommunityUtil.getJSONString(0);
}

3. setting页面的重构

<!--上传到七牛云--><form class="mt-5" id="uploadForm"><div class="form-group row mt-4"><label for="head-image" class="col-sm-2 col-form-label text-right">选择头像:</label><div class="col-sm-10"><div class="custom-file"><input type="hidden" name="token" th:value="${uploadToken}"><input type="hidden" name="key" th:value="${fileName}"><input type="file" class="custom-file-input" id="head-image" name="file" lang="es" required=""><label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label><div class="invalid-feedback">该账号不存在!</div></div></div></div><div class="form-group row mt-4"><div class="col-sm-2"></div><div class="col-sm-10 text-center"><button type="submit" class="btn btn-info text-white form-control">立即上传</button></div></div></form>

4.编写setting.js

不要忘记在setting页面进行引入

$(function(){$("#uploadForm").submit(upload);
});function upload() {$.ajax({url: "http://upload-z1.qiniup.com",method: "post",processData: false,contentType: false,data: new FormData($("#uploadForm")[0]),success: function(data) {if(data && data.code == 0) {// 更新头像访问路径$.post(CONTEXT_PATH + "/user/header/url",{"fileName":$("input[name='key']").val()},function(data) {data = $.parseJSON(data);if(data.code == 0) {window.location.reload();} else {alert(data.msg);}});} else {alert("上传失败!");}}});return false;
}

9.2 服务器直传

1. ShareController

2. EventConsumer

在原有的图片上传加入定时器,知道图片生成成功之后,才进行图片上传

3. 定时器内部类

class UploadTask implements Runnable {// 文件名称private String fileName;// 文件后缀private String suffix;// 启动任务的返回值private Future future;// 开始时间private long startTime;// 上传次数private int uploadTimes;public UploadTask(String fileName, String suffix) {this.fileName = fileName;this.suffix = suffix;this.startTime = System.currentTimeMillis();}public void setFuture(Future future) {this.future = future;}@Overridepublic void run() {// 生成失败if (System.currentTimeMillis() - startTime > 30000) {logger.error("执行时间过长,终止任务:" + fileName);future.cancel(true);return;}// 上传失败if (uploadTimes >= 3) {logger.error("上传次数过多,终止任务:" + fileName);future.cancel(true);return;}String path = wkImageStorage + "/" + fileName + suffix;File file = new File(path);if (file.exists()) {logger.info(String.format("开始第%d次上传[%s].", ++uploadTimes, fileName));// 设置响应信息StringMap policy = new StringMap();policy.put("returnBody", CommunityUtil.getJSONString(0));// 生成上传凭证Auth auth = Auth.create(accessKey, secretKey);String uploadToken = auth.uploadToken(shareBucketName, fileName, 3600, policy);// 指定上传机房UploadManager manager = new UploadManager(new Configuration(Zone.zone1()));try {// 开始上传图片Response response = manager.put(path, fileName, uploadToken, null, "image/" + suffix, false);// 处理响应结果JSONObject json = JSONObject.parseObject(response.bodyString());if (json == null || json.get("code") == null || !json.get("code").toString().equals("0")) {logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));} else {logger.info(String.format("第%d次上传成功[%s].", uploadTimes, fileName));future.cancel(true);}} catch (QiniuException e) {logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));}} else {logger.info("等待图片生成[" + fileName + "].");}}
}

4. 测试

http://localhost:8080/community/share?htmlUrl=https://www.nowcoder.com/

10、优化网站性能



10.1 Caffeine的使用

1. 导包

<!--本地缓存 咖啡因-->
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.0.2</version>
</dependency>

2. 配置

# caffeine
caffeine:posts:max-size: 15 # 缓存多少数据expire-second: 180 # 缓存多少时间 3分钟
# caffeine
caffeine.posts.max-size=15 # 缓存多少数据
caffeine.posts.expire-second=180 # 过期时间 3分钟

3. 优化Service


@Value("${caffeine.posts.max-size}")
private long maxSize;@Value("${caffeine.posts.expire-second}")
private long expireSecond;// 帖子列表集合
private LoadingCache<String, List<DiscussPost>> postListCache;// 帖子总数缓存
private LoadingCache<Integer, Integer> postRowsCache;@PostConstruct
public void init() {// 初始化帖子列表缓存postListCache = Caffeine.newBuilder().maximumSize(maxSize).expireAfterWrite(expireSecond, TimeUnit.SECONDS).build(new CacheLoader<String, List<DiscussPost>>() {@Overridepublic @Nullable List<DiscussPost> load(String key) throws Exception {if (key==null||key.length()==0){throw new IllegalArgumentException("参数错误!");}String[] params = key.split(":");if (params==null||params.length!=2){throw new IllegalArgumentException("参数错误!");}int offset=Integer.valueOf(params[0]);int limit=Integer.valueOf(params[1]);// 二级缓存 redis -> mysqllogger.debug("load post list from DB");return mapper.selectDiscussPosts(0,offset,limit,1);}});// 初始化帖子总数缓存postRowsCache=Caffeine.newBuilder().maximumSize(maxSize).expireAfterWrite(expireSecond,TimeUnit.SECONDS).build(new CacheLoader<Integer, Integer>() {@Overridepublic @Nullable Integer load(Integer key) throws Exception {logger.debug("load post rows from DB");return mapper.selectDiscussPostRows(key);}});
}
public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {if (userId == 0 && orderMode == 1) {return postListCache.get(offset + ":" + limit);}logger.debug("load post list from DB");return mapper.selectDiscussPosts(userId, offset, limit, orderMode);}public int findDiscussPostRows(int userId) {if (userId==0){return postRowsCache.get(userId);}logger.debug("load post rows from DB");return mapper.selectDiscussPostRows(userId);}

第七章 项目进阶,构建安全高效的企业服务相关推荐

  1. 【仿牛客网笔记】项目进阶,构建安全高效的企业服务——热帖排行

    p:投票数 T:发布时间间隔 G:系数,通常为1.5,1.8 计算帖子的分数 注入RedisTemplate 帖子刷新 实现定时任务 刷新帖子 实现更新帖子分数 刷新帖子分数任务 配置Trigger ...

  2. 项目1在线交流平台-7.构建安全高效的企业服务-2.使用Security自定义社区网页认证与授权

    文章目录 功能需求 一. 废弃登录检查的拦截器 二.授权配置 1. 导包 2. Security配置 2.1 `WebSecurity` 2.2`HttpSecurity` ` http.author ...

  3. 项目1在线交流平台-7.构建安全高效的企业服务-3. Security整合Kafka,ES,Thymeleaf实例-对帖子置顶、加精、删除

    文章目录 功能需求 一.置顶.加精.删除帖子功能的实现 1. dao层处理数据 接口定义 sal语句定义 2. service层业务处理 3. Controller层处理按钮事件异步请求 异步请求及k ...

  4. PMBOK(第六版) 学习笔记 ——《第七章 项目成本管理》

    系列文章目录 PMBOK(第六版) 学习笔记 --<第一章 引论> PMBOK(第六版) 学习笔记 --<第二章 项目运行环境> PMBOK(第六版) 学习笔记 --<第 ...

  5. IT项目管理总结:第七章 项目成本管理

    第七章 项目成本管理 成本和项目成本管理 –成本(Cost):实现一个特定目标而牺牲或放弃的资源 –项目成本管理(Project cost management):包括用来确保在批准的预算范围内完成项 ...

  6. 【信息系统项目管理师】第七章 项目成本管理(考点汇总篇)

    [信息系统项目管理师]第七章 项目成本管理(考点汇总篇) 考点分析与预测 成本管理一般上午考察三到四分,非常重要,成本控制的好不好,直接关乎项目的质量.案例分析可能会出计算题,主要出现在挣值和预测技术 ...

  7. 高项_第七章项目成本管理

    第七章项目成本管理 上午.案例分析.论文写作都会进行考察.项目成本管理一本上午考察3分,非常重要,要是成本控制的不好,直接关乎项目的质量,因此成本管理次张杰非常重要,案例分析可能会出案例分析计算,主要 ...

  8. 信息系统项目管理师---第七章项目成本管理历年考题

    信息系统项目管理师-第七章项目成本管理历年考题 1.2005 年 5 月第 40 题:每次项目经理会见其所负责项目的赞助商时,赞助商都强调对该项目进行成本控制的重要性.她总是询问有关成本绩效的情况,如 ...

  9. 《信息系统项目管理师总结》第七章 项目沟通管理

    <信息系统项目管理师总结> 第七章 项目沟通管理 目录 <信息系统项目管理师总结> 第七章 项目沟通管理 一.规划沟通管理 >>> 输入: 1.项目管理计划 ...

最新文章

  1. c++重载(以运算符重载为主)
  2. 比好莱坞市场大6倍—— 体育因数据分析而不同
  3. typedef struct 用法
  4. (六)python3 只需3小时带你轻松入门——循环
  5. using namespace std
  6. oracle同机单实例加入集群,将oracle同机单实例加入rac集群的操作步骤
  7. 计算机网络中的交换技术
  8. Python 3.65 安装geopandas
  9. java jar在电脑哪里_例举jar文件怎么打开
  10. UI设计师必备|Web设计尺寸规范
  11. 解决远程桌面不能用大法
  12. 计算机网络安全 的论文,计算机网络安全论文6000字
  13. UIWebView的使用---safri
  14. 用flash做连线题(线的一端跟随鼠标)
  15. 不看后悔!新手小白必看的保姆级教程!一篇文章学会数据仓库!
  16. 如何打造属于自己的专属武器库
  17. mysql查看表存不存在
  18. PB8.0应用程序编译发布技术研究
  19. 【SQLite预习课1】SQLite简介——MySQL的简洁版
  20. 近百个Android优秀开源项目

热门文章

  1. mysql数据库(2)
  2. c语言数星星结构体,数星星(结构体专题)
  3. 在超声波热量表、水表中MS1030、MS726、MCU MS616F512应用方案分享
  4. 轻舟已过万重山——计算机达人成长之路(31)
  5. 三校生计算机应用基础模块,三校生高考《计算机应用基础》电子教案.doc
  6. 基于Redis实现微信抢红包功能
  7. Android APP全局置灰
  8. 微软的IntelliPoint和招商银行专业版是有冲突的!!!
  9. 统计思维就是透过现象看本质
  10. 渐变马赛克——水晶格图标