主要技术架构:

SpringBoot Spring SpringMVC MyBatis Redis Kakfa Elasticsearch Spring Security Spring Actator

1.Spring Security 构建安全的企业服务

Spring Security底层机制

2.Spring Security Demo

在Maven Repository搜索spring boot security配置文件,在resources文件包内的pom.xml文件中导入相关的配置文件依赖。

User类需要实现UserDetails接口

public class User implements UserDetails {private int id;private String username;private String password;private String salt;private String email;private int type;private int status;private String activationCode;private String headerUrl;private Date createTime;public int getId() {return id;}public void setId(int id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getSalt() {return salt;}public void setSalt(String salt) {this.salt = salt;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public int getType() {return type;}public void setType(int type) {this.type = type;}public int getStatus() {return status;}public void setStatus(int status) {this.status = status;}public String getActivationCode() {return activationCode;}public void setActivationCode(String activationCode) {this.activationCode = activationCode;}public String getHeaderUrl() {return headerUrl;}public void setHeaderUrl(String headerUrl) {this.headerUrl = headerUrl;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", password='" + password + '\'' +", salt='" + salt + '\'' +", email='" + email + '\'' +", type=" + type +", status=" + status +", activationCode='" + activationCode + '\'' +", headerUrl='" + headerUrl + '\'' +", createTime=" + createTime +'}';}// true: 账号未过期.@Overridepublic boolean isAccountNonExpired() {return true;}// true: 账号未锁定.@Overridepublic boolean isAccountNonLocked() {return true;}// true: 凭证未过期.@Overridepublic boolean isCredentialsNonExpired() {return true;}// true: 账号可用.@Overridepublic boolean isEnabled() {return true;}@Overridepublic 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;}}

Userservice需要UserDetailsService,自动判断账号密码是否正确

@Service
public class UserService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;public User findUserByName(String username) {return userMapper.selectByName(username);}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return this.findUserByName(username);}
}

spring security 配置类

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;@Overridepublic void configure(WebSecurity web) throws Exception {// 忽略静态资源的访问web.ignoring().antMatchers("/resources/**");}// AuthenticationManager: 认证的核心接口.// AuthenticationManagerBuilder: 用于构建AuthenticationManager对象的工具.// ProviderManager: AuthenticationManager接口的默认实现类.@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 内置的认证规则// auth.userDetailsService(userService).passwordEncoder(new Pbkdf2PasswordEncoder("12345"));// 自定义认证规则// AuthenticationProvider: ProviderManager持有一组AuthenticationProvider,每个AuthenticationProvider负责一种认证.// 委托模式: ProviderManager将认证委托给AuthenticationProvider.auth.authenticationProvider(new AuthenticationProvider() {// Authentication: 用于封装认证信息的接口,不同的实现类代表不同类型的认证信息.@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("账号不存在!");}password = CommunityUtil.md5(password + user.getSalt());if (!user.getPassword().equals(password)) {throw new BadCredentialsException("密码不正确!");}// principal: 主要信息; credentials: 证书; authorities: 权限;return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());}// 当前的AuthenticationProvider支持哪种类型的认证.@Overridepublic boolean supports(Class<?> aClass) {// UsernamePasswordAuthenticationToken: Authentication接口的常用的实现类.return UsernamePasswordAuthenticationToken.class.equals(aClass);}});}@Overrideprotected void configure(HttpSecurity http) throws Exception {// 登录相关配置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);// 记住我http.rememberMe().tokenRepository(new InMemoryTokenRepositoryImpl()).tokenValiditySeconds(3600 * 24).userDetailsService(userService);}
}

Homecontroller

@Controller
public class HomeController {@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";}@RequestMapping(path = "/discuss", method = RequestMethod.GET)public String getDiscussPage() {return "/site/discuss";}@RequestMapping(path = "/letter", method = RequestMethod.GET)public String getLetterPage() {return "/site/letter";}@RequestMapping(path = "/admin", method = RequestMethod.GET)public String getAdminPage() {return "/site/admin";}@RequestMapping(path = "/loginpage", method = {RequestMethod.GET, RequestMethod.POST})public String getLoginPage() {return "/site/login";}// 拒绝访问时的提示页面@RequestMapping(path = "/denied", method = RequestMethod.GET)public String getDeniedPage() {return "/error/404";}}

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录</title>
</head>
<body><h1>登录社区</h1><form method="post" th:action="@{/login}"><p style="color:red;" th:text="${error}"><!--提示信息--></p><p>账号:<input type="text" name="username" th:value="${param.username}"></p><p>密码:<input type="password" name="password" th:value="${param.password}"></p><p>验证码:<input type="text" name="verifyCode"> <i>1234</i></p><p><input type="checkbox" name="remember-me"> 记住我</p><p><input type="submit" value="登录"></p></form></body>
</html>

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>首页</title>
</head>
<body><h1>社区首页</h1><!--欢迎信息--><p th:if="${loginUser!=null}">欢迎你, <span th:text="${loginUser.username}"></span>!</p><ul><li><a th:href="@{/discuss}">帖子详情</a></li><li><a th:href="@{/letter}">私信列表</a></li><li><a th:href="@{/loginpage}">登录</a></li><!--<li><a th:href="@{/loginpage}">退出</a></li>--><li><form method="post" th:action="@{/logout}"><a href="javascript:document.forms[0].submit();">退出</a></form></li></ul></body>
</html>

3. 置顶、加精、删除

在Maven Repository搜索thymeleaf springsecurity5 配置文件,在resources文件包内的pom.xml文件中导入相关的配置文件依赖。

实现置顶、加精、删除功能

discusspostMapper

@Mapper
public interface DiscussPostMapper {List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit, int orderMode);// @Param注解用于给参数取别名,// 如果只有一个参数,并且在<if>里使用,则必须加别名.int selectDiscussPostRows(@Param("userId") int userId);int insertDiscussPost(DiscussPost discussPost);DiscussPost selectDiscussPostById(int id);int updateCommentCount(int id, int commentCount);int updateType(int id, int type);int updateStatus(int id, int status);int updateScore(int id, double score);}

discusspost-mapper

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nowcoder.community.dao.DiscussPostMapper"><sql id="selectFields">id, user_id, title, content, type, status, create_time, comment_count, score</sql><sql id="insertFields">user_id, title, content, type, status, create_time, comment_count, score</sql><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><select id="selectDiscussPostRows" resultType="int">select count(id)from discuss_postwhere status != 2<if test="userId!=0">and user_id = #{userId}</if></select><insert id="insertDiscussPost" parameterType="DiscussPost" keyProperty="id">insert into discuss_post(<include refid="insertFields"></include>)values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})</insert><select id="selectDiscussPostById" resultType="DiscussPost">select <include refid="selectFields"></include>from discuss_postwhere id = #{id}</select><update id="updateCommentCount">update discuss_post set comment_count = #{commentCount} where id = #{id}</update><update id="updateType">update discuss_post set type = #{type} where id = #{id}</update><update id="updateStatus">update discuss_post set status = #{status} where id = #{id}</update><update id="updateScore">update discuss_post set score = #{score} where id = #{id}</update></mapper>

discusspostService

@Service
public class DiscussPostService {private static final Logger logger = LoggerFactory.getLogger(DiscussPostService.class);@Autowiredprivate DiscussPostMapper discussPostMapper;@Autowiredprivate SensitiveFilter sensitiveFilter;@Value("${caffeine.posts.max-size}")private int maxSize;@Value("${caffeine.posts.expire-seconds}")private int expireSeconds;// Caffeine核心接口: Cache, LoadingCache, AsyncLoadingCache// 帖子列表缓存private LoadingCache<String, List<DiscussPost>> postListCache;// 帖子总数缓存private LoadingCache<Integer, Integer> postRowsCache;@PostConstructpublic void init() {// 初始化帖子列表缓存postListCache = Caffeine.newBuilder().maximumSize(maxSize).expireAfterWrite(expireSeconds, TimeUnit.SECONDS).build(new CacheLoader<String, List<DiscussPost>>() {@Nullable@Overridepublic List<DiscussPost> load(@NonNull 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 discussPostMapper.selectDiscussPosts(0, offset, limit, 1);}});// 初始化帖子总数缓存postRowsCache = Caffeine.newBuilder().maximumSize(maxSize).expireAfterWrite(expireSeconds, TimeUnit.SECONDS).build(new CacheLoader<Integer, Integer>() {@Nullable@Overridepublic Integer load(@NonNull Integer key) throws Exception {logger.debug("load post rows from DB.");return discussPostMapper.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 discussPostMapper.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 discussPostMapper.selectDiscussPostRows(userId);}public int addDiscussPost(DiscussPost post) {if (post == null) {throw new IllegalArgumentException("参数不能为空!");}// 转义HTML标记post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));post.setContent(HtmlUtils.htmlEscape(post.getContent()));// 过滤敏感词post.setTitle(sensitiveFilter.filter(post.getTitle()));post.setContent(sensitiveFilter.filter(post.getContent()));return discussPostMapper.insertDiscussPost(post);}public DiscussPost findDiscussPostById(int id) {return discussPostMapper.selectDiscussPostById(id);}public int updateCommentCount(int id, int commentCount) {return discussPostMapper.updateCommentCount(id, commentCount);}public int updateType(int id, int type) {return discussPostMapper.updateType(id, type);}public int updateStatus(int id, int status) {return discussPostMapper.updateStatus(id, status);}public int updateScore(int id, double score) {return discussPostMapper.updateScore(id, score);}}

discusspostContrller

@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;@Autowiredprivate CommentService commentService;@Autowiredprivate LikeService likeService;@Autowiredprivate EventProducer eventProducer;@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping(path = "/add", method = RequestMethod.POST)@ResponseBodypublic String addDiscussPost(String title, String content) {User user = hostHolder.getUser();if (user == null) {return CommunityUtil.getJSONString(403, "你还没有登录哦!");}DiscussPost post = new DiscussPost();post.setUserId(user.getId());post.setTitle(title);post.setContent(content);post.setCreateTime(new Date());discussPostService.addDiscussPost(post);// 触发发帖事件Event event = new Event().setTopic(TOPIC_PUBLISH).setUserId(user.getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(post.getId());eventProducer.fireEvent(event);// 计算帖子分数String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey, post.getId());// 报错的情况,将来统一处理.return CommunityUtil.getJSONString(0, "发布成功!");}@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {// 帖子DiscussPost post = discussPostService.findDiscussPostById(discussPostId);model.addAttribute("post", post);// 作者User user = userService.findUserById(post.getUserId());model.addAttribute("user", user);// 点赞数量long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, discussPostId);model.addAttribute("likeCount", likeCount);// 点赞状态int likeStatus = hostHolder.getUser() == null ? 0 :likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, discussPostId);model.addAttribute("likeStatus", likeStatus);// 评论分页信息page.setLimit(5);page.setPath("/discuss/detail/" + discussPostId);page.setRows(post.getCommentCount());// 评论: 给帖子的评论// 回复: 给评论的评论// 评论列表List<Comment> commentList = commentService.findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());// 评论VO列表List<Map<String, Object>> commentVoList = new ArrayList<>();if (commentList != null) {for (Comment comment : commentList) {// 评论VOMap<String, Object> commentVo = new HashMap<>();// 评论commentVo.put("comment", comment);// 作者commentVo.put("user", userService.findUserById(comment.getUserId()));// 点赞数量likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, comment.getId());commentVo.put("likeCount", likeCount);// 点赞状态likeStatus = hostHolder.getUser() == null ? 0 :likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, comment.getId());commentVo.put("likeStatus", likeStatus);// 回复列表List<Comment> replyList = commentService.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);// 回复VO列表List<Map<String, Object>> replyVoList = new ArrayList<>();if (replyList != null) {for (Comment reply : replyList) {Map<String, Object> replyVo = new HashMap<>();// 回复replyVo.put("reply", reply);// 作者replyVo.put("user", userService.findUserById(reply.getUserId()));// 回复目标User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());replyVo.put("target", target);// 点赞数量likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_COMMENT, reply.getId());replyVo.put("likeCount", likeCount);// 点赞状态likeStatus = hostHolder.getUser() == null ? 0 :likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_COMMENT, reply.getId());replyVo.put("likeStatus", likeStatus);replyVoList.add(replyVo);}}commentVo.put("replys", replyVoList);// 回复数量int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());commentVo.put("replyCount", replyCount);commentVoList.add(commentVo);}}model.addAttribute("comments", commentVoList);return "/site/discuss-detail";}// 置顶@RequestMapping(path = "/top", method = RequestMethod.POST)@ResponseBodypublic String setTop(int id) {discussPostService.updateType(id, 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);}// 加精@RequestMapping(path = "/wonderful", method = RequestMethod.POST)@ResponseBodypublic String setWonderful(int id) {discussPostService.updateStatus(id, 1);// 触发发帖事件Event event = new Event().setTopic(TOPIC_PUBLISH).setUserId(hostHolder.getUser().getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(id);eventProducer.fireEvent(event);// 计算帖子分数String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey, id);return CommunityUtil.getJSONString(0);}// 删除@RequestMapping(path = "/delete", method = RequestMethod.POST)@ResponseBodypublic String setDelete(int id) {discussPostService.updateStatus(id, 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);}}

在消费者中加入删除方法,修改html文件。

 // 消费删帖事件@KafkaListener(topics = {TOPIC_DELETE})public void handleDeleteMessage(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;}elasticsearchService.deleteDiscussPost(event.getEntityId());}

处理权限问题,修改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).antMatchers("/discuss/top","/discuss/wonderful").hasAnyAuthority(AUTHORITY_MODERATOR).antMatchers("/discuss/delete","/data/**").hasAnyAuthority(AUTHORITY_ADMIN).anyRequest().permitAll().and().csrf().disable();// 权限不够时的处理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");}}

按钮显示

<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"><button type="button" class="btn btn-danger btn-sm" id="topBtn"th:disabled="${post.type==1}" sec:authorize="hasAnyAuthority('moderator')">置顶</button><button type="button" class="btn btn-danger btn-sm" id="wonderfulBtn"th:disabled="${post.status==1}" sec:authorize="hasAnyAuthority('moderator')">加精</button><button type="button" class="btn btn-danger btn-sm" id="deleteBtn"th:disabled="${post.status==2}" sec:authorize="hasAnyAuthority('admin')">删除</button>

4.Redis高级数据类型

用于统计网站数据

测试功能

// 统计20万个重复数据的独立总数.@Testpublic void testHyperLogLog() {String redisKey = "test:hll:01";for (int i = 1; i <= 100000; i++) {redisTemplate.opsForHyperLogLog().add(redisKey, i);}for (int i = 1; i <= 100000; i++) {int r = (int) (Math.random() * 100000 + 1);redisTemplate.opsForHyperLogLog().add(redisKey, r);}long size = redisTemplate.opsForHyperLogLog().size(redisKey);System.out.println(size);}// 将3组数据合并, 再统计合并后的重复数据的独立总数.@Testpublic 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);}// 统计一组数据的布尔值@Testpublic 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);// 查询System.out.println(redisTemplate.opsForValue().getBit(redisKey, 0));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 1));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 2));// 统计Object obj = redisTemplate.execute(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection connection) throws DataAccessException {return connection.bitCount(redisKey.getBytes());}});System.out.println(obj);}// 统计3组数据的布尔值, 并对这3组数据做OR运算.@Testpublic 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() {@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);System.out.println(redisTemplate.opsForValue().getBit(redisKey, 0));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 1));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 2));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 3));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 4));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 5));System.out.println(redisTemplate.opsForValue().getBit(redisKey, 6));}}

功能实现

定义RedisUtil

public class RedisKeyUtil {private static final String SPLIT = ":";private static final String PREFIX_ENTITY_LIKE = "like:entity";private static final String PREFIX_USER_LIKE = "like:user";private static final String PREFIX_FOLLOWEE = "followee";private static final String PREFIX_FOLLOWER = "follower";private static final String PREFIX_KAPTCHA = "kaptcha";private static final String PREFIX_TICKET = "ticket";private static final String PREFIX_USER = "user";private static final String PREFIX_UV = "uv";private static final String PREFIX_DAU = "dau";private static final String PREFIX_POST = "post";// 某个实体的赞// like:entity:entityType:entityId -> set(userId)public static String getEntityLikeKey(int entityType, int entityId) {return PREFIX_ENTITY_LIKE + SPLIT + entityType + SPLIT + entityId;}// 某个用户的赞// like:user:userId -> intpublic static String getUserLikeKey(int userId) {return PREFIX_USER_LIKE + SPLIT + userId;}// 某个用户关注的实体// followee:userId:entityType -> zset(entityId,now)public static String getFolloweeKey(int userId, int entityType) {return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;}// 某个实体拥有的粉丝// follower:entityType:entityId -> zset(userId,now)public static String getFollowerKey(int entityType, int entityId) {return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;}// 登录验证码public static String getKaptchaKey(String owner) {return PREFIX_KAPTCHA + SPLIT + owner;}// 登录的凭证public static String getTicketKey(String ticket) {return PREFIX_TICKET + SPLIT + ticket;}// 用户public static String getUserKey(int userId) {return PREFIX_USER + SPLIT + userId;}// 单日UVpublic static String getUVKey(String date) {return PREFIX_UV + SPLIT + date;}// 区间UVpublic static String getUVKey(String startDate, String endDate) {return PREFIX_UV + SPLIT + startDate + SPLIT + endDate;}// 单日活跃用户public static String getDAUKey(String date) {return PREFIX_DAU + SPLIT + date;}// 区间活跃用户public static String getDAUKey(String startDate, String endDate) {return PREFIX_DAU + SPLIT + startDate + SPLIT + endDate;}// 帖子分数public static String getPostScoreKey() {return PREFIX_POST + SPLIT + "score";}}

新建一个DataService

@Service
public class DataService {@Autowiredprivate RedisTemplate redisTemplate;private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");// 将指定的IP计入UVpublic void recordUV(String ip) {String redisKey = RedisKeyUtil.getUVKey(df.format(new Date()));redisTemplate.opsForHyperLogLog().add(redisKey, ip);}// 统计指定日期范围内的UVpublic 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);while (!calendar.getTime().after(end)) {String 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);}// 将指定用户计入DAUpublic void recordDAU(int userId) {String redisKey = RedisKeyUtil.getDAUKey(df.format(new Date()));redisTemplate.opsForValue().setBit(redisKey, userId, true);}// 统计指定日期范围内的DAUpublic 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);while (!calendar.getTime().after(end)) {String 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());}});}}

新建一个拦截器用于统计数据

@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.getRemoteHost();dataService.recordUV(ip);// 统计DAUUser user = hostHolder.getUser();if (user != null) {dataService.recordDAU(user.getId());}return true;}
}

webMvcConfig

  registry.addInterceptor(dataInterceptor).excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");

DataController

@Controller
public class DataController {@Autowiredprivate DataService dataService;// 统计页面@RequestMapping(path = "/data", method = {RequestMethod.GET, RequestMethod.POST})public String getDataPage() {return "/site/admin/data";}// 统计网站UV@RequestMapping(path = "/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";}// 统计活跃用户@RequestMapping(path = "/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.任务执行和调度

常规线程池的使用

Spring线程池需要在properties中配置

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);// JDK普通线程池private ExecutorService executorService = Executors.newFixedThreadPool(5);// JDK可执行定时任务的线程池private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);// Spring普通线程池@Autowiredprivate ThreadPoolTaskExecutor taskExecutor;// Spring可执行定时任务的线程池@Autowiredprivate ThreadPoolTaskScheduler taskScheduler;@Autowiredprivate AlphaService alphaService;private void sleep(long m) {try {Thread.sleep(m);} catch (InterruptedException e) {e.printStackTrace();}}// 1.JDK普通线程池@Testpublic 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);}// 2.JDK定时任务线程池@Testpublic void testScheduledExecutorService() {Runnable task = new Runnable() {@Overridepublic void run() {logger.debug("Hello ScheduledExecutorService");}};scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS);sleep(30000);}// 3.Spring普通线程池@Testpublic 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定时任务线程池@Testpublic 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普通线程池(简化)@Testpublic void testThreadPoolTaskExecutorSimple() {for (int i = 0; i < 10; i++) {alphaService.execute1();}sleep(10000);}// 6.Spring定时任务线程池(简化)@Testpublic void testThreadPoolTaskSchedulerSimple() {sleep(30000);}}

6.quartz的使用

在Maven Repository搜索spring boot quatz配置文件,在resources文件包内的pom.xml文件中导入相关的配置文件依赖。

Spring Quartz(将数据存储到数据库,分布式时可以共享数据)核心调度接口Scheduler定义任务的接口Job的execute方法Jobdetail接口来配置Job的名字、组等Trigger接口配置Job的什么时候运行、运行频率QuartzConfig:配置 -> 数据库 -> 调用

新建一个quartz包,创建AlphaJob测试

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

创建QuartzConfig

// 配置 -> 数据库 -> 调用
@Configuration
public class QuartzConfig {// FactoryBean可简化Bean的实例化过程:// 1.通过FactoryBean封装Bean的实例化过程.// 2.将FactoryBean装配到Spring容器里.// 3.将FactoryBean注入给其他的Bean.// 4.该Bean得到的是FactoryBean所管理的对象实例.// 配置JobDetail// @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)// @Beanpublic SimpleTriggerFactoryBean 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;}
}

properties

# QuartzProperties
spring.quartz.job-store-type=jdbc
spring.quartz.scheduler-name=communityScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=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

test测试

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class QuartzTests {@Autowiredprivate Scheduler scheduler;@Testpublic void testDeleteJob() {try {boolean result = scheduler.deleteJob(new JobKey("alphaJob", "alphaJobGroup"));System.out.println(result);} catch (SchedulerException e) {e.printStackTrace();}}}

7.热度排行榜功能实现

将数据存入redis

DiscussPostController

 @RequestMapping(path = "/add", method = RequestMethod.POST)@ResponseBodypublic String addDiscussPost(String title, String content) {User user = hostHolder.getUser();if (user == null) {return CommunityUtil.getJSONString(403, "你还没有登录哦!");}DiscussPost post = new DiscussPost();post.setUserId(user.getId());post.setTitle(title);post.setContent(content);post.setCreateTime(new Date());discussPostService.addDiscussPost(post);// 触发发帖事件Event event = new Event().setTopic(TOPIC_PUBLISH).setUserId(user.getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(post.getId());eventProducer.fireEvent(event);// 计算帖子分数String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey, post.getId());// 报错的情况,将来统一处理.return CommunityUtil.getJSONString(0, "发布成功!");}// 加精@RequestMapping(path = "/wonderful", method = RequestMethod.POST)@ResponseBodypublic String setWonderful(int id) {discussPostService.updateStatus(id, 1);// 触发发帖事件Event event = new Event().setTopic(TOPIC_PUBLISH).setUserId(hostHolder.getUser().getId()).setEntityType(ENTITY_TYPE_POST).setEntityId(id);eventProducer.fireEvent(event);// 计算帖子分数String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey, id);return CommunityUtil.getJSONString(0);}

commentController

 @RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment) {comment.setUserId(hostHolder.getUser().getId());comment.setStatus(0);comment.setCreateTime(new Date());commentService.addComment(comment);// 触发评论事件Event event = new Event().setTopic(TOPIC_COMMENT).setUserId(hostHolder.getUser().getId()).setEntityType(comment.getEntityType()).setEntityId(comment.getEntityId()).setData("postId", discussPostId);if (comment.getEntityType() == ENTITY_TYPE_POST) {DiscussPost target = discussPostService.findDiscussPostById(comment.getEntityId());event.setEntityUserId(target.getUserId());} else if (comment.getEntityType() == ENTITY_TYPE_COMMENT) {Comment target = commentService.findCommentById(comment.getEntityId());event.setEntityUserId(target.getUserId());}eventProducer.fireEvent(event);if (comment.getEntityType() == ENTITY_TYPE_POST) {// 触发发帖事件event = new Event().setTopic(TOPIC_PUBLISH).setUserId(comment.getUserId()).setEntityType(ENTITY_TYPE_POST).setEntityId(discussPostId);eventProducer.fireEvent(event);// 计算帖子分数String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey, discussPostId);}return "redirect:/discuss/detail/" + discussPostId;}

Likecontroller

 @RequestMapping(path = "/like", method = RequestMethod.POST)@ResponseBodypublic String like(int entityType, int entityId, int entityUserId, int postId) {User user = hostHolder.getUser();// 点赞likeService.like(user.getId(), entityType, entityId, entityUserId);// 数量long likeCount = likeService.findEntityLikeCount(entityType, entityId);// 状态int likeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);// 返回的结果Map<String, Object> map = new HashMap<>();map.put("likeCount", likeCount);map.put("likeStatus", likeStatus);// 触发点赞事件if (likeStatus == 1) {Event event = new Event().setTopic(TOPIC_LIKE).setUserId(hostHolder.getUser().getId()).setEntityType(entityType).setEntityId(entityId).setEntityUserId(entityUserId).setData("postId", postId);eventProducer.fireEvent(event);}if(entityType == ENTITY_TYPE_POST) {// 计算帖子分数String redisKey = RedisKeyUtil.getPostScoreKey();redisTemplate.opsForSet().add(redisKey, postId);}return CommunityUtil.getJSONString(0, null, map);}

使用Quartz定时计算热度

在Quartz包下创建PostScorerefreshJob

public class PostScoreRefreshJob implements Job, CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate LikeService likeService;@Autowiredprivate ElasticsearchService elasticsearchService;// 牛客纪元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);}}@Overridepublic void execute(JobExecutionContext context) throws JobExecutionException {String redisKey = RedisKeyUtil.getPostScoreKey();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) {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);// 计算权重double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;// 分数 = 帖子权重 + 距离天数double score = Math.log10(Math.max(w, 1))+ (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);// 更新帖子分数discussPostService.updateScore(postId, score);// 同步搜索数据post.setScore(score);elasticsearchService.saveDiscussPost(post);}}

配置QuartzConfig

 // 刷新帖子分数任务@Beanpublic JobDetailFactoryBean postScoreRefreshJobDetail() {JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();factoryBean.setJobClass(PostScoreRefreshJob.class);factoryBean.setName("postScoreRefreshJob");factoryBean.setGroup("communityJobGroup");factoryBean.setDurability(true);factoryBean.setRequestsRecovery(true);return factoryBean;}@Beanpublic SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) {SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();factoryBean.setJobDetail(postScoreRefreshJobDetail);factoryBean.setName("postScoreRefreshTrigger");factoryBean.setGroup("communityTriggerGroup");factoryBean.setRepeatInterval(1000 * 60 * 5);factoryBean.setJobDataMap(new JobDataMap());return factoryBean;}

重构代码discusspost-mapper.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>

8.生成长图

服务端生成长图

安装wkhtmlltopdf

http://wkhtmlopdf.org

WkTest

public class WkTests {public static void main(String[] args) {String cmd = "d:/work/wkhtmltopdf/bin/wkhtmltoimage --quality 75  https://www.nowcoder.com d:/work/data/wk-images/3.png";try {Runtime.getRuntime().exec(cmd);System.out.println("ok.");} catch (IOException e) {e.printStackTrace();}}}

applictaion.properties

# wk
wk.image.command=d:/work/wkhtmltopdf/bin/wkhtmltoimage
wk.image.storage=d:/work/data/wk-images

新建WKConfig

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

ShareController

@Controller
public class ShareController implements CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(ShareController.class);@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;@Value("${qiniu.bucket.share.url}")private String shareBucketUrl;@RequestMapping(path = "/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);map.put("shareUrl", shareBucketUrl + "/" + fileName);return CommunityUtil.getJSONString(0, null, map);}// 废弃// 获取长图@RequestMapping(path = "/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 b = 0;while ((b = fis.read(buffer)) != -1) {os.write(buffer, 0, b);}} catch (IOException e) {logger.error("获取长图失败: " + e.getMessage());}}}

Event消费者

// 消费分享事件@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());}}

将文件上传至云服务器

在Maven Repository搜索qiniu配置文件,在resources文件包内的pom.xml文件中导入相关的配置文件依赖,修改properties。

# qiniu
qiniu.key.access=6RA-Uus95ZT_1znMrCMD8BpqfjT-K7OKmQTfKB48
qiniu.key.secret=kPNnLFz2_tzztKUVpSLm0lYngtuHWyIq5LzTmLIL
qiniu.bucket.header.name=community_header
quniu.bucket.header.url=http://pvghrij81.bkt.clouddn.com
qiniu.bucket.share.name=community_share
qiniu.bucket.share.url=http://pvghvvuzm.bkt.clouddn.com
@Controller
@RequestMapping("/user")
public class UserController implements CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(UserController.class);@Value("${community.path.upload}")private String uploadPath;@Value("${community.path.domain}")private String domain;@Value("${server.servlet.context-path}")private String contextPath;@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate LikeService likeService;@Autowiredprivate FollowService followService;@Value("${qiniu.key.access}")private String accessKey;@Value("${qiniu.key.secret}")private String secretKey;@Value("${qiniu.bucket.header.name}")private String headerBucketName;@Value("${quniu.bucket.header.url}")private String headerBucketUrl;@LoginRequired@RequestMapping(path = "/setting", method = RequestMethod.GET)public String getSettingPage(Model model) {// 上传文件名称String fileName = CommunityUtil.generateUUID();// 设置响应信息StringMap policy = new StringMap();policy.put("returnBody", CommunityUtil.getJSONString(0));// 生成上传凭证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";}// 更新头像路径@RequestMapping(path = "/header/url", method = RequestMethod.POST)@ResponseBodypublic 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);}// 废弃@LoginRequired@RequestMapping(path = "/upload", method = RequestMethod.POST)public String uploadHeader(MultipartFile headerImage, Model model) {if (headerImage == null) {model.addAttribute("error", "您还没有选择图片!");return "/site/setting";}String fileName = headerImage.getOriginalFilename();String suffix = fileName.substring(fileName.lastIndexOf("."));if (StringUtils.isBlank(suffix)) {model.addAttribute("error", "文件的格式不正确!");return "/site/setting";}// 生成随机文件名fileName = CommunityUtil.generateUUID() + suffix;// 确定文件存放的路径File dest = new File(uploadPath + "/" + fileName);try {// 存储文件headerImage.transferTo(dest);} catch (IOException e) {logger.error("上传文件失败: " + e.getMessage());throw new RuntimeException("上传文件失败,服务器发生异常!", e);}// 更新当前用户的头像的路径(web访问路径)// http://localhost:8080/community/user/header/xxx.pngUser user = hostHolder.getUser();String headerUrl = domain + contextPath + "/user/header/" + fileName;userService.updateHeader(user.getId(), headerUrl);return "redirect:/index";}// 废弃@RequestMapping(path = "/header/{fileName}", method = RequestMethod.GET)public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {// 服务器存放路径fileName = uploadPath + "/" + fileName;// 文件后缀String suffix = fileName.substring(fileName.lastIndexOf("."));// 响应图片response.setContentType("image/" + suffix);try (FileInputStream fis = new FileInputStream(fileName);OutputStream os = response.getOutputStream();) {byte[] buffer = new byte[1024];int b = 0;while ((b = fis.read(buffer)) != -1) {os.write(buffer, 0, b);}} catch (IOException e) {logger.error("读取头像失败: " + e.getMessage());}}// 个人主页@RequestMapping(path = "/profile/{userId}", method = RequestMethod.GET)public String getProfilePage(@PathVariable("userId") int userId, Model model) {User user = userService.findUserById(userId);if (user == null) {throw new RuntimeException("该用户不存在!");}// 用户model.addAttribute("user", user);// 点赞数量int likeCount = likeService.findUserLikeCount(userId);model.addAttribute("likeCount", likeCount);// 关注数量long followeeCount = followService.findFolloweeCount(userId, ENTITY_TYPE_USER);model.addAttribute("followeeCount", followeeCount);// 粉丝数量long followerCount = followService.findFollowerCount(ENTITY_TYPE_USER, userId);model.addAttribute("followerCount", followerCount);// 是否已关注boolean hasFollowed = false;if (hostHolder.getUser() != null) {hasFollowed = followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);}model.addAttribute("hasFollowed", hasFollowed);return "/site/profile";}}

shareController

@Controller
public class ShareController implements CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(ShareController.class);@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;@Value("${qiniu.bucket.share.url}")private String shareBucketUrl;@RequestMapping(path = "/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);map.put("shareUrl", shareBucketUrl + "/" + fileName);return CommunityUtil.getJSONString(0, null, map);}// 废弃// 获取长图@RequestMapping(path = "/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 b = 0;while ((b = fis.read(buffer)) != -1) {os.write(buffer, 0, b);}} catch (IOException e) {logger.error("获取长图失败: " + e.getMessage());}}}

event消费者

@Component
public class EventConsumer implements CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);@Autowiredprivate MessageService messageService;@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate ElasticsearchService elasticsearchService;@Value("${wk.image.command}")private String wkImageCommand;@Value("${wk.image.storage}")private String wkImageStorage;@Value("${qiniu.key.access}")private String accessKey;@Value("${qiniu.key.secret}")private String secretKey;@Value("${qiniu.bucket.share.name}")private String shareBucketName;@Autowiredprivate ThreadPoolTaskScheduler taskScheduler;@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})public void handleCommentMessage(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;}// 发送站内通知Message message = new Message();message.setFromId(SYSTEM_USER_ID);message.setToId(event.getEntityUserId());message.setConversationId(event.getTopic());message.setCreateTime(new Date());Map<String, Object> content = new HashMap<>();content.put("userId", event.getUserId());content.put("entityType", event.getEntityType());content.put("entityId", event.getEntityId());if (!event.getData().isEmpty()) {for (Map.Entry<String, Object> entry : event.getData().entrySet()) {content.put(entry.getKey(), entry.getValue());}}message.setContent(JSONObject.toJSONString(content));messageService.addMessage(message);}// 消费发帖事件@KafkaListener(topics = {TOPIC_PUBLISH})public void handlePublishMessage(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;}DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());elasticsearchService.saveDiscussPost(post);}// 消费删帖事件@KafkaListener(topics = {TOPIC_DELETE})public void handleDeleteMessage(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;}elasticsearchService.deleteDiscussPost(event.getEntityId());}// 消费分享事件@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());}// 启用定时器,监视该图片,一旦生成了,则上传至七牛云.UploadTask task = new UploadTask(fileName, suffix);Future future = taskScheduler.scheduleAtFixedRate(task, 500);task.setFuture(future);}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 + "].");}}}

项目代码及相关资源:Ming-XMU (Yiming Zhang) · GitHub

麻烦点点小星星!!!!!!

CSDN下载需要积分基于SpringBoot仿牛客网讨论社区项目-Java文档类资源-CSDN下载

从零开始—仿牛客网讨论社区项目(一)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(二)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(三)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(四)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(五)_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(六)_芙蓉铁蛋的博客-CSDN博客

仿牛客网讨论社区项目—优化网站性能_芙蓉铁蛋的博客-CSDN博客

仿牛客网讨论社区项目—项目总结及项目常见面试题_芙蓉铁蛋的博客-CSDN博客

从零开始—仿牛客网讨论社区项目(六)相关推荐

  1. 从零开始—仿牛客网讨论社区项目(一)

    主要技术架构: SpringBoot Spring SpringMVC MyBatis Redis Kakfa Elasticsearch Spring Security Spring Actator ...

  2. 仿牛客网讨论社区项目—优化网站性能

    性能优化: 1.考虑加入缓存优化 优化热门帖子列表 GitHub中搜索caffeine 在Maven Repository搜索caffeine配置文件,在resources文件包内的pom.xml文件 ...

  3. Java牛客项目课_仿牛客网讨论区_第八章

    文章目录 第八章.项目发布与总结 8.1. 单元测试 8.2.项目监控 8.3.项目部署 宝塔面板.yum.rpm.压缩包 安装 unzip.Java1.8.Maven.MySQL.Redis.Kaf ...

  4. 框架复习笔记-Java-案例:牛客网讨论社区

    文章目录 搭建开发环境 Spring入门 Spring IoC实例分析 Spring MVC入门 请求 响应 Mybatis入门 案例:开发者社区首页 调试技巧 日志 版本控制-Git 发送邮件 开发 ...

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

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

  6. 仿牛客网社区项目 全栈总结

    学习仿牛客网社区项目 代码&资源 各章节总结 第一章 第二章 第三章 第四章 第五章 第六章 第七章 第八章 争取让每个知识点都有链接可点 项目总结 网站架构图 常见面试题 MySQL Red ...

  7. 云服务器上部署仿牛客网项目

    云服务器上部署仿牛客网项目 安装JRE 安装Maven 安装MySQL 给mysql导入数据 安装Redis 安装kafka 安装ElasticSearch Wkhtmltopdf 安装tomcat ...

  8. 2021-04-10 仿牛客网第六章

    一.Elasticsearch入门 仿牛客网 内容部分引用至 https://blog.csdn.net/weixin_44406146 目录 一.Elasticsearch入门 Elasticsea ...

  9. 【牛客网C++服务器项目学习】-Day09-网络模型个人总结

    项目学习地址:[牛客网C++服务器项目学习] day09 项目学习进入了第四章--网络编程.这一章的学习,前半段是对计算机网络体系进行一个大致的讲解.我自己在今年4月份系统性的看了<计算机网络自 ...

最新文章

  1. 解决Android studio 非法字符的问题
  2. python画折线图详解-利用python画出折线图
  3. scoped原理、不足、弥补或替代
  4. oracle standby同步,ORACLE 利用rman增量备份同步standby库
  5. C++多继承中重写不同基类中相同原型的虚函数
  6. 进程的并发与并行,三种状态
  7. [蓝桥杯2020国赛]游园安排
  8. Java 遍历指定目录下的所有目录
  9. 10-fold Cross Validation
  10. 再品Resnet残差网络
  11. java打印unicode_java程序实现Unicode码和中文互相转换
  12. python相机拍照显示_Python无法从Raspberry Pi相机以最高分辨率拍照
  13. MySQL安装包下载地址(含所有版本)
  14. JAVA数据库的操作(增、删、改、查)
  15. 显示当前系统时间和日期
  16. 前端面试题(css)
  17. 像素时代的黄昏和“淘宝叛军”
  18. Photoshop如何使用图像调色之实例演示?
  19. 荣耀magic v参数配置
  20. spring boot整合MySQL数据库

热门文章

  1. 移动开发者必须了解的55个APP运营推广工具!
  2. 快速生成树(RSTP)
  3. visio 封闭图形的填充过程
  4. MOOC编程判定身高是否与预计的相符合。(10分)
  5. 弘辽科技:如何制作一个高点击率主图,这些细节一定要记住
  6. 2007年北京房价必然回落
  7. P2573 [SCOI2012]滑雪
  8. WQ7033开发指南(基础篇)之1.3 如何进行单线烧录
  9. 基于LabVIEW的电阻合格率检测装置仿真设计
  10. 计算机毕业设计之java+ssm框架个人博客系统多用户