第七章 项目进阶,构建安全高效的企业服务
第七章 项目进阶,构建安全高效的企业服务
1、Spring Security
1.1 基本介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
一般流程
登录 ->认证 -> 授权
- 当用户登录时,前端将用户输入的用户名、密码信息传输到后台,后台用一个类对象将其封装起来,通常使用的是
UsernamePasswordAuthenticationToken
这个类。 - 程序负责验证这个类对象。验证方法是调用
Service
根据username
从数据库中取用户信息到实体类的实例中,比较两者的密码,如果密码正确就成功登陆,同时把包含着用户的用户名、密码、所具有的权限等信息的类对象放到SecurityContextHolder
(安全上下文容器,类似Session)中去。 - 用户访问一个资源的时候,首先判断是否是受限资源。然后判断是否未登录,没有则跳到登录页面。
- 如果用户已经登录,访问一个受限资源的时候,程序要根据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
- 依赖
<!--thymeleaf -security--><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency>
- 在
discuss-detail.html
中引入
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
- 按钮显示
- 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. 清除数据
清除数据的方式有两种:
- 手动删除数据库中的数据
- 利用程序一次性删除
@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
新增、评论、点赞、加精计算分数,置顶不计算分数
为了防止存入的数据重复,造成重复的加分,所以存入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、将文件上传到服务器
七牛云官网
七牛云
对象存储
使用
- 依赖
<dependency><groupId>com.qiniu</groupId><artifactId>qiniu-java-sdk</artifactId><version>7.11.0</version>
</dependency>
- 配置
# 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);}
第七章 项目进阶,构建安全高效的企业服务相关推荐
- 【仿牛客网笔记】项目进阶,构建安全高效的企业服务——热帖排行
p:投票数 T:发布时间间隔 G:系数,通常为1.5,1.8 计算帖子的分数 注入RedisTemplate 帖子刷新 实现定时任务 刷新帖子 实现更新帖子分数 刷新帖子分数任务 配置Trigger ...
- 项目1在线交流平台-7.构建安全高效的企业服务-2.使用Security自定义社区网页认证与授权
文章目录 功能需求 一. 废弃登录检查的拦截器 二.授权配置 1. 导包 2. Security配置 2.1 `WebSecurity` 2.2`HttpSecurity` ` http.author ...
- 项目1在线交流平台-7.构建安全高效的企业服务-3. Security整合Kafka,ES,Thymeleaf实例-对帖子置顶、加精、删除
文章目录 功能需求 一.置顶.加精.删除帖子功能的实现 1. dao层处理数据 接口定义 sal语句定义 2. service层业务处理 3. Controller层处理按钮事件异步请求 异步请求及k ...
- PMBOK(第六版) 学习笔记 ——《第七章 项目成本管理》
系列文章目录 PMBOK(第六版) 学习笔记 --<第一章 引论> PMBOK(第六版) 学习笔记 --<第二章 项目运行环境> PMBOK(第六版) 学习笔记 --<第 ...
- IT项目管理总结:第七章 项目成本管理
第七章 项目成本管理 成本和项目成本管理 –成本(Cost):实现一个特定目标而牺牲或放弃的资源 –项目成本管理(Project cost management):包括用来确保在批准的预算范围内完成项 ...
- 【信息系统项目管理师】第七章 项目成本管理(考点汇总篇)
[信息系统项目管理师]第七章 项目成本管理(考点汇总篇) 考点分析与预测 成本管理一般上午考察三到四分,非常重要,成本控制的好不好,直接关乎项目的质量.案例分析可能会出计算题,主要出现在挣值和预测技术 ...
- 高项_第七章项目成本管理
第七章项目成本管理 上午.案例分析.论文写作都会进行考察.项目成本管理一本上午考察3分,非常重要,要是成本控制的不好,直接关乎项目的质量,因此成本管理次张杰非常重要,案例分析可能会出案例分析计算,主要 ...
- 信息系统项目管理师---第七章项目成本管理历年考题
信息系统项目管理师-第七章项目成本管理历年考题 1.2005 年 5 月第 40 题:每次项目经理会见其所负责项目的赞助商时,赞助商都强调对该项目进行成本控制的重要性.她总是询问有关成本绩效的情况,如 ...
- 《信息系统项目管理师总结》第七章 项目沟通管理
<信息系统项目管理师总结> 第七章 项目沟通管理 目录 <信息系统项目管理师总结> 第七章 项目沟通管理 一.规划沟通管理 >>> 输入: 1.项目管理计划 ...
最新文章
- c++重载(以运算符重载为主)
- 比好莱坞市场大6倍—— 体育因数据分析而不同
- typedef struct 用法
- (六)python3 只需3小时带你轻松入门——循环
- using namespace std
- oracle同机单实例加入集群,将oracle同机单实例加入rac集群的操作步骤
- 计算机网络中的交换技术
- Python 3.65 安装geopandas
- java jar在电脑哪里_例举jar文件怎么打开
- UI设计师必备|Web设计尺寸规范
- 解决远程桌面不能用大法
- 计算机网络安全 的论文,计算机网络安全论文6000字
- UIWebView的使用---safri
- 用flash做连线题(线的一端跟随鼠标)
- 不看后悔!新手小白必看的保姆级教程!一篇文章学会数据仓库!
- 如何打造属于自己的专属武器库
- mysql查看表存不存在
- PB8.0应用程序编译发布技术研究
- 【SQLite预习课1】SQLite简介——MySQL的简洁版
- 近百个Android优秀开源项目