文章目录
- 社区论坛P01- SpringBoot项目无法识别@RunWith注解
- 社区论坛P02 - Spring容器管理Bean
- 社区论坛P03 - SpringMvc处理请求与响应[Get\Post\Model\响应json]
- 社区论坛P04 - MyBatis的基本使用流程
- 社区论坛P05 - 开发社区首页
- 社区论坛P06 - 项目调试与日志
- 社区论坛P07 - 利用Spring mail发送邮件
- 社区论坛P08 - 开发注册页面
- 社区论坛P09 - 开发登录页面
- 社区论坛P10 - 显示登录信息
- 社区论坛P11 - 检查登录状态
- 社区论坛P12 - 发布帖子
- 社区论坛P13 - 帖子详情
- 社区论坛P14 - 显示评论
- 社区论坛P15 - 添加评论
- 社区论坛P16 - 发送私信
- 社区论坛P17 - 私信详情
- 社区论坛P18 - 发送私信
- 社区论坛P19 - 统一处理异常
- 社区论坛P20 - 统一记录日志
- 社区论坛P21 - Spring整合redis
- 社区论坛P22 - 点赞
- 社区论坛P23 - 我收到的赞
- 社区论坛P24 - 关注和取消关注
- 社区论坛P25 - 统计用户的粉丝数和关注数
- 社区论坛P26 - 关注列表和粉丝列表
- 社区论坛P27 - 优化登录模块
- 社区论坛P28 - Spring整合kafka
- 社区论坛P29 - 显示系统通知
- 社区论坛P30 - Spring整合elasticsearch
社区论坛P01- SpringBoot项目无法识别@RunWith注解
原有的pom文件中只有:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency>
需要添加下面的jar包:
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency>
社区论坛P02 - Spring容器管理Bean
获取容器对象并管理bean
① 情况1:自己编写的与主配置类同一个包及子包下面的类
写一个接口AlphaDao,及两个实现类AlphaHebernateImpl、AlphaMybatisImpl
public interface AlphaDao {public String select();
}
@Repository("alphaHebernate")
public class AlphaHebernateImpl implements AlphaDao{public String select(){return "hello Herinate";};
}
//指定优先级,当两个类的接口相同时,通过类型获取bean,优先获取到这个bean
@Primary
//可以指定bean的名称,通过名称获取bean
@Repository("alphaMybatis")
public class AlphaMybatisImpl implements AlphaDao {@Overridepublic String select() {return "hello Mybatis";}
}
② 情况2:第三方类需要通过配置类让Spring容器管理
//与主配置类同一个包及子包下面的类都可以被扫描到IO容器中(@Repository,@Service,@Controller,@Conponent)
//对于其他类想要装入IO容器中,可以使用配置类
@Configuration
public class AlphaConfig {//bean的名称即为方法名@Beanpublic SimpleDateFormat simpleDateFormat(){return new SimpleDateFormat("yy-mm-dd");}
}
③ 情况3:bean的生命周期
@Service
public class AlphaService {public AlphaService(){System.out.println("实例化Alpha Service....");}//在构造方法调用之后调用@PostConstructpublic void init(){System.out.println("初始化AlphaService.....");}//在对象销毁之前调用@PreDestroypublic void destroy(){System.out.println("即将销毁AlphaService.....");}
}
④ 测试
@RunWith(SpringRunner.class)
@SpringBootTest
//加载配置类
@ContextConfiguration(classes =CommunityApplication.class)
//通过实现ApplicationContextAware接口,即可获取该类的容器对象,从而管理容器中的类
public class CommunityApplicationTests implements ApplicationContextAware {private ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.applicationContext = applicationContext;}@Testpublic void testApplicationContext(){System.out.println(applicationContext);//通过容器管理对象获取容器中的bean,可以通过bean的名字或者bean的类型来获取AlphaDao bean = applicationContext.getBean(AlphaDao.class);System.out.println(bean.select());AlphaDao alphaMybatis = applicationContext.getBean("alphaHebernate", AlphaDao.class);System.out.println(alphaMybatis.select());}@Testpublic void test(){AlphaService bean = applicationContext.getBean(AlphaService.class);System.out.println(bean);}/*** 以上为原始方法,实际可以直接使用注解*/@Autowiredprivate AlphaService alphaService;//接口下有多个实现类时,使用@Qualifier("alphaMybatis")指明@Autowired@Qualifier("alphaMybatis")private AlphaDao alphaDao;@Autowiredprivate SimpleDateFormat simpleDateFormat;
}
社区论坛P03 - SpringMvc处理请求与响应[Get\Post\Model\响应json]
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@Controller
@RequestMapping("/alpha")
public class AlphaController {@RequestMapping("/hello")@ResponseBodypublic String test() {return "hello java";}/*** @RequestParam和@PathVariable都用于从request中接收请求的* * 它们都是用户的输入,只不过输入的部分不同,一个在URL路径部分,另一个在参数部分,* * 简单的说就是url写法不同,如下:* * 使用@RequestParam时,URL是这样的:http://host:port/path?参数名=参数值* * 使用@PathVariable时,URL是这样的:http://host:port/path/参数值*/// get请求方式,并且路径中带有参数,如何接收请求?// http://localhost:8080/community/alpha/student?current=1&limit=10@GetMapping(value = "/student")@ResponseBodypublic String testGet(@RequestParam(name = "current", defaultValue = "1", required = false) int current,@RequestParam(name = "limit", defaultValue = "10", required = false) int limit) {System.out.println(current);System.out.println(limit);return "get method";}// http://localhost:8080/community/alpha/student/100@GetMapping("/student/{id}")@ResponseBodypublic String testGet2(@PathVariable(value = "id")String id){System.out.println(id);return "get method2";}// 处理post请求// http://localhost:8080/community/html/teacher.html 请求页面的位置,通过页面中的表单提交请求数据@ResponseBody@PostMapping("/teacher")public String testPost(String name,int age){System.out.println(name+":"+age);return "post method";}// 响应数据方式1// http://localhost:8080/community/alpha/peoper@RequestMapping("/peoper")public ModelAndView testResponse(){ModelAndView modelAndView = new ModelAndView();modelAndView.addObject("name","张三");modelAndView.addObject("age",18);modelAndView.setViewName("/demo/peoper");return modelAndView;}//响应数据方式2// http://localhost:8080/community/alpha/school@RequestMapping("/school")public String testResponse2(Model model){model.addAttribute("name","希望小学");model.addAttribute("address","深圳");return "/demo/school";}//响应json数据// http://localhost:8080/community/alpha/emp@RequestMapping("/emp")@ResponseBody //需要加这个注解public Map<String, Object> testJson(){Map<String,Object> emp = new HashMap<String, Object>();emp.put("zhangsan",18);emp.put("lisi",23);emp.put("wangwu",29);//响应对象时,会将这个map自动转为json字符串return emp;// {"lisi":23,"zhangsan":18,"wangwu":29} json对象(字符串)}//响应json数据// http://localhost:8080/community/alpha/emps@RequestMapping("/emps")@ResponseBody //需要加这个注解public List<Map<String,Object>> testJson2(){ArrayList<Map<String,Object>> list = new ArrayList<>();Map<String,Object> emp1 = new HashMap<String, Object>();emp1.put("zhangsan",18);emp1.put("lisi",23);emp1.put("wangwu",29);Map<String,Object> emp2 = new HashMap<String, Object>();emp2.put("zhangsan",18);emp2.put("lisi",23);emp2.put("wangwu",29);list.add(emp1);list.add(emp2);//响应对象时,会将这个map自动转为json字符串return list;// [{"lisi":23,"zhangsan":18,"wangwu":29},{"lisi":23,"zhangsan":18,"wangwu":29}]// json数组}
}
所用到的页面:
teacher.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>增加学生</title></head><body><form method="post" action="/community/alpha/teacher"><p>姓名:<input type="text" name="name"></p><p>年龄:<input type="text" name="age"></p><p><input type="submit" value="保存"></p></form></body>
</html>
peoper.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"><title>title</title>
</head>
<body><p th:text="${name}"></p><p th:text="${age}"></p>
</body>
</html>
school.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"><title>title</title>
</head>
<body><p th:text="${name}"></p><p th:text="${address}"></p>
</body>
</html>
社区论坛P04 - MyBatis的基本使用流程
所有MyBatis的相关用法,可查询官网:https://mybatis.org/mybatis-3/zh/index.html
① 配置数据库与MyBatis
<!-- 配置数据库-->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.16</version>
</dependency><!-- 配置MyBatis-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.1</version>
</dependency>
application.properties:
# DataSourceProperties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type=com.mysql.cj.jdbc.MysqlConnectionPoolDataSource
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000# mapper文件所在的位置
mybatis.mapper-locations=classpath:mapper/*.xml
# 类型别名可为Java类型设置一个缩写名字。 它仅用于XML配置,意在降低冗余的全限定类名书写。
mybatis.type-aliases-package=com.nowcoder.community.entity
# mybatis默认是属性名和数据库字段名一一对应的,即数据库表列:user_name 实体类属性:user_name
# 但是java中一般使用驼峰命名,数据库表列:user_name 实体类属性:userName
# 将user_name映射为userName
mybatis.configuration.map-underscore-to-camel-case=true
# 开启主键自增,insert数据时会自增主键
mybatis.configuration.use-generated-keys=true
② 编写实体类
@Data
public class User {//属性与数据库字段对应,并且是驼峰命名,即如果数据库为activation_code,则属性为activationCodeprivate 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;
}
③ 编写dao接口
@Mapper
public interface UserMapper {//查询用户User selectById(int id);User selectByName(String username);User selectByEmail(String email);//添加用户int insertUser(User user);//更新用户信息int updateStatus(int id,int status);int updateHeaderUrl(int id,String headerUrl);int updatePassword(int id,String password);
}
④ 编写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.UserMapper"><!--UserMappe接口中的selectById()方法--><select id="selectById" parameterType="int" resultType="User">select id,username,password,salt,email,type,status,activation_code,header_url,create_timefrom user where id = #{id}</select><select id="selectByName" parameterType="String" resultType="User">select id,username,password,salt,email,type,status,activation_code,header_url,create_timefrom user where username=#{username}</select><select id="selectByEmail" parameterType="String" resultType="User">select id,username,password,salt,email,type,status,activation_code,header_url,create_timefrom user where email=#{email}</select><insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">insert into user (username,password,salt,email,type,status,activation_code,header_url,create_time)values (#{username},#{password},#{salt},#{email},#{type},#{status},#{activationCode},#{headerUrl},#{createTime})</insert><update id="updateStatus">update user set status=#{status} where id=#{id}</update><update id="updateHeaderUrl">update user set header_url=#{headerUrl} where id=#{id}</update><update id="updatePassword">update user set password=#{password} where id=#{id}</update>
</mapper>
⑤ 单元测试:
对于dao层的方法,每写一个方法都要进行单元测试
import com.nowcoder.community.dao.UserMapper;
import com.nowcoder.community.entity.User;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MapperTests {@Autowiredprivate UserMapper userMapper;@Testpublic void testSelect(){User user1 = userMapper.selectById(101);assertEquals("nowcoder101@sina.com",user1.getEmail());User user2 = userMapper.selectByName("liubei");assertEquals(101,user2.getId());User user3 = userMapper.selectByEmail("nowcoder101@sina.com");assertEquals(101,user3.getId());}@Testpublic void testInset(){User user = new User();user.setUsername("hengheng");user.setPassword("123");user.setSalt("12345");user.setType(0);user.setStatus(2);int result = userMapper.insertUser(user);assertEquals(1,result);}@Testpublic void testUpdate(){int num = userMapper.updateStatus(150, 2);assertEquals(1,num);int num1 = userMapper.updateHeaderUrl(150, "http://hengheng.blog.com");assertEquals(1,num1);int num3 = userMapper.updatePassword(150, "345");assertEquals(1,num3);}
}
社区论坛P05 - 开发社区首页
访问社区首页,分页显示帖子列表
dao层—>service层---->controller层:controller层调用service层,service层调用dao层
① 数据访问层Dao :
/*** discuss_post表对应的实体类*/
@Data
public class DiscussPost {private int id;private int userId;private String title;private String content;private int type;private int status;private Date createTime;private int commentCount;private double score;
}
/*** 操作discuss_post表的接口*/
@Mapper
public interface DiscussPostMapper {/*** 分页查询帖子列表* @param userId 如果userId=0代表查询首页,否则为查询个人主页* @param offset 分页查询时当前页的起始行* @param limit 分页查询时每页显示的帖子数* @return 帖子列表*/List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);/*** 查询帖子的总行数,用于计算帖子的总页数=总行数/limit,如果userId=0代表查询首页,否则为查询个人主页* 如果方法仅有一个参数,且这个参数需要用于动态SQL中,需要@Param注解起别名* @return 帖子的总行数*/int selectDiscussPostRow(@Param("userId") int userId);
}
<?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,create_time,status,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>order by type desc,create_time desclimit #{offset},#{limit}</select><select id="selectDiscussPostRow" resultType="int">select count(id)from discuss_postwhere status!=2<if test="userId!=0">and user_id=#{userId}</if></select>
</mapper>
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MapperTests {@Autowiredprivate DiscussPostMapper discussPostMapper;@Testpublic void testSelectPost(){List<DiscussPost> discussPosts = discussPostMapper.selectDiscussPosts(0, 0, 10);assertEquals(10,discussPosts.size());int result = discussPostMapper.selectDiscussPostRow(149);assertEquals(3,result);}
}
② 业务逻辑层Service :
@Service
public class DiscussPostService {@Autowiredprivate DiscussPostMapper discussPostMapper;public List<DiscussPost> findDiscussPosts(int userId,int offset,int limit){return discussPostMapper.selectDiscussPosts(userId,offset,limit);}public int findDiscussPostRow(int userId){return discussPostMapper.selectDiscussPostRow(userId);}
}
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public User findUserById(int id){return userMapper.selectById(id);}
}
③ 表现层Controller :
/*** 封装分页相关的信息.*/
@Getter
public class Page {// 当前页码private int current = 1;// 每页显示行数private int limit = 10;// 数据总数(用于计算总页数)private int rows;// 查询路径(用于复用分页链接)private String path;public void setCurrent(int current) {if (current >= 1) {this.current = current;}}public void setLimit(int limit) {if (limit >= 1 && limit <= 100) {this.limit = limit;}}public void setRows(int rows) {if (rows >= 0) {this.rows = rows;}}public void setPath(String path) {this.path = path;}/*** 获取当前页的起始行** @return*/public int getOffset() {// current * limit - limitreturn (current - 1) * limit;}/*** 获取总页数** @return*/public int getTotal() {// rows / limit [+1]if (rows % limit == 0) {return rows / limit;} else {return rows / limit + 1;}}/*** 获取起始页码** @return*/public int getFrom() {int from = current - 2;return from < 1 ? 1 : from;}/*** 获取结束页码** @return*/public int getTo() {int to = current + 2;int total = getTotal();return to > total ? total : to;}}
@Controller
public class HomeController {@AutowiredDiscussPostService discussPostService;@AutowiredUserService userService;@GetMapping("/index")public String getIndexPage(Model model, Page page){page.setPath("/index");page.setRows(discussPostService.findDiscussPostRow(0));List<DiscussPost> list= discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());List<Map<String,Object>> discussPosts = new ArrayList<>();if(list!=null){for(DiscussPost discussPost:list){HashMap<String,Object> map = new HashMap<>();map.put("post",discussPost);map.put("user",userService.findUserById(discussPost.getUserId()));discussPosts.add(map);}}model.addAttribute("discussPosts",discussPosts);return "/index";}
}
<!-- 帖子列表 -->
<ul class="list-unstyled"><li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}"><a href="site/profile.html"><img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;"></a><div class="media-body"><h6 class="mt-0 mb-3"><a href="#" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a><span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span><span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}">精华</span></h6><div class="text-muted font-size-12"><u class="mr-3" th:utext="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b><ul class="d-inline float-right"><li class="d-inline ml-2">赞 11</li><li class="d-inline ml-2">|</li><li class="d-inline ml-2">回帖 7</li></ul></div></div> </li>
</ul>
<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows>0}"><ul class="pagination justify-content-center"><li class="page-item"><a class="page-link" th:href="@{${page.path}(current=1)}">首页</a></li><li th:class="|page-item ${page.current==1?'disabled':''}|"><a class="page-link" th:href="@{${page.path}(current=${page.current-1})}">上一页</a></li><li th:class="|page-item ${i==page.current?'active':''}|" th:each="i:${#numbers.sequence(page.from,page.to)}"><a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}">1</a></li><li th:class="|page-item ${page.current==page.total?'disabled':''}|"><a class="page-link" th:href="@{${page.path}(current=${page.current+1})}">下一页</a></li><li class="page-item"><a class="page-link" th:href="@{${page.path}(current=${page.total})}">末页</a></li></ul>
</nav>
社区论坛P06 - 项目调试与日志
① 项目debug快捷键:
F8代表步进,即一行一行的去调试
F7代表进入当前方法,然后F8退出当前进入的方法
F9代表到下一个断点处
② 日志:SpringBoot内置了logback日志
在application.properties中配置日志:
# debug以上级别的日志会被打印在控制台
logging.level.com.nowcoder.community=debug
# 将日志打印到文件,所有级别的日志都会打印到一个文件中,如果想要打印到不同文件,需要配置logback.xml文件
logging.file=d://log
测试日志的使用:
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class LoggerTests {//SpringBoot内置了logback日志private static final Logger logger = LoggerFactory.getLogger(LoggerTests.class);@Testpublic void testLogger(){logger.debug("debug log");logger.info("info log");logger.warn("warn log");logger.error("error log");}
}
将不同级别的日志打印到不同的文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration><contextName>community</contextName><property name="LOG_PATH" value="D:/work/data"/><property name="APPDIR" value="community"/><!-- error file --><appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/${APPDIR}/log_error.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>5MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><maxHistory>30</maxHistory></rollingPolicy><append>true</append><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern><charset>utf-8</charset></encoder><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>error</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- warn file --><appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/${APPDIR}/log_warn.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>5MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><maxHistory>30</maxHistory></rollingPolicy><append>true</append><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern><charset>utf-8</charset></encoder><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>warn</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- info file --><appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_PATH}/${APPDIR}/log_info.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>5MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><maxHistory>30</maxHistory></rollingPolicy><append>true</append><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern><charset>utf-8</charset></encoder><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>info</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- console --><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern><charset>utf-8</charset></encoder><filter class="ch.qos.logback.classic.filter.ThresholdFilter"><level>debug</level></filter></appender><logger name="com.nowcoder.community" level="debug"/><root level="info"><appender-ref ref="FILE_ERROR"/><appender-ref ref="FILE_WARN"/><appender-ref ref="FILE_INFO"/><appender-ref ref="STDOUT"/></root>
</configuration>
社区论坛P07 - 利用Spring mail发送邮件
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId><version>2.1.5.RELEASE</version>
</dependency>
# MailProperties
spring.mail.host=smtp.163.com
spring.mail.port=465
spring.mail.username=18751887307@163.com
spring.mail.password=ITDXDXHGMJGGRCOR
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.ssl.enable=true
package com.nowcoder.community.util;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;@Component
public class MailClient {private static final Logger logger = LoggerFactory.getLogger(MailClient.class);@Autowiredprivate JavaMailSender mailSender;@Value("${spring.mail.username}")private String from;public void sendMail(String to, String subject, String content) {try {MimeMessage message = mailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message);helper.setFrom(from);helper.setTo(to);helper.setSubject(subject);helper.setText(content, true);mailSender.send(helper.getMimeMessage());} catch (MessagingException e) {logger.error("发送邮件失败:" + e.getMessage());}}
}
测试类:
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {@Autowiredprivate MailClient mailClient;@Autowiredprivate TemplateEngine templateEngine;//发送普通邮件@Testpublic void testSendMail(){mailClient.sendMail("18856131978@163.com","你好","haf");}//发送html邮件@Testpublic void testHtmlMail() {Context context = new Context();context.setVariable("username", "sunday");String content = templateEngine.process("/mail/demo", context);mailClient.sendMail("18856131978@163.com", "HTML", content);}
}
发送邮件的格式:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>邮件示例</title>
</head>
<body><p>欢迎你, <span style="color:red;" th:text="${username}"></span>!</p>
</body>
</html>
社区论坛P08 - 开发注册页面
① 访问注册页面:
@Controller
public class LoginController {/*** 处理打开注册页面的请求* @return 注册页面*/@GetMapping("/register")public String getRegisterPage(){return "/site/register";}
}
在index.html页面:
<li class="nav-item ml-3 btn-group-vertical">//对于相对路径,使用@{} ,只需要修改相对路径即可<a class="nav-link" th:href="@{/register}">注册</a>
</li>
② 提交注册数据:
@Service
public class UserService implements CommunityConstant {@Autowiredprivate UserMapper userMapper;@Value("${community.path.domain}")private String domain;@Value("${server.servlet.context-path}")private String contextPath;@Autowiredprivate TemplateEngine templateEngine;@Autowiredprivate MailClient mailClient;public User findUserById(int id){return userMapper.selectById(id);}public Map<String,Object> register(User user) {Map<String,Object> map = new HashMap<>();if(user==null){throw new IllegalArgumentException("参数不能为空");}//空值判断if(StringUtils.isBlank(user.getUsername())){map.put("usernameMsg","用户名不能为空");return map;}if(StringUtils.isBlank(user.getEmail())){map.put("emainMsg","邮箱不能为空");return map;}if(StringUtils.isBlank(user.getPassword())){map.put("passwordMsg","密码不能为空");return map;}//通过id查询用户,如果查出来用户不为空,说明用户已存在User u = userMapper.selectByName(user.getUsername());if(u!=null){map.put("usernameMsg","用户已存在");return map;}//通过邮箱查询用户,如果查出来用户不为空,说明用户已存在User u2 = userMapper.selectByEmail(user.getEmail());if(u2!=null){map.put("emailMsg","用户邮箱已存在");return map;}//补充用户注册信息user.setSalt(CommunityUtil.generateUUID().substring(0,5));user.setPassword(CommunityUtil.md5(user.getPassword()+user.getSalt()));user.setType(0);user.setStatus(0);user.setActivationCode(CommunityUtil.generateUUID());user.setCreateTime(new Date());user.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png",new Random().nextInt(1000)));//注册用户,即将用户信息插入数据库userMapper.insertUser(user);//注册完成后发送激活邮件Context context = new Context();context.setVariable("email",user.getEmail());String url = domain + contextPath + "/activation/" + user.getId() + "/" + user.getActivationCode();context.setVariable("url",url);String content = templateEngine.process("/mail/activation", context);mailClient.sendMail(user.getEmail(),"激活码激活",content);return map;}
}
activation.html:
<div><p><b th:text="${email}">xxx@xxx.com</b>, 您好!</p><p>您正在注册牛客网, 这是一封激活邮件, 请点击 <a th:href="${url}">此链接</a>,激活您的牛客账号!</p>
</div>
@Controller
public class LoginController {@Autowiredprivate UserService userService;/*** 处理打开注册页面的请求* @return 注册页面*/@GetMapping("/register")public String getRegisterPage(){return "/site/register";}/*** 根据注册页面提交的表单数据完成注册* @param model* @param user 注册页面提交的用户信息* @return 注册成功返回operate-result.html页面,否则返回register.html页面*/@PostMapping("/register")public String register(Model model, User user){Map<String, Object> map = userService.register(user);//如果map为空if(map==null || map.isEmpty()){model.addAttribute("msg","注册成功,我们已经向您的邮箱发送了一封激活邮件,请尽快激活!");model.addAttribute("target","/index");return "/site/operate-result";}else{model.addAttribute("usernameMsg",map.get("usernameMsg"));model.addAttribute("passwordMsg",map.get("passwordMsg"));model.addAttribute("emailMsg",map.get("emailMsg"));return "/site/register";}}
}
register.html:
<div class="main"><div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3"><h3 class="text-center text-info border-bottom pb-3">注 册</h3><form class="mt-5" method="post" th:action="@{/register}"><div class="form-group row"><label for="username" class="col-sm-2 col-form-label text-right">账号:</label><div class="col-sm-10"><input type="text"th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"th:value="${user!=null?user.username:''}"id="username" name="username" placeholder="请输入您的账号!" required><div class="invalid-feedback" th:text="${usernameMsg}">该账号已存在!</div></div></div><div class="form-group row mt-4"><label for="password" class="col-sm-2 col-form-label text-right">密码:</label><div class="col-sm-10"><input type="password"th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"th:value="${user!=null?user.password:''}"id="password" name="password" placeholder="请输入您的密码!" required><div class="invalid-feedback" th:text="${passwordMsg}">密码长度不能小于8位!</div></div></div><div class="form-group row mt-4"><label for="confirm-password" class="col-sm-2 col-form-label text-right">确认密码:</label><div class="col-sm-10"><input type="password" class="form-control"th:value="${user!=null?user.password:''}"id="confirm-password" placeholder="请再次输入密码!" required><div class="invalid-feedback">两次输入的密码不一致!</div></div></div><div class="form-group row"><label for="email" class="col-sm-2 col-form-label text-right">邮箱:</label><div class="col-sm-10"><input type="email"th:class="|form-control ${emailMsg!=null?'is-invalid':''}|"th:value="${user!=null?user.email:''}"id="email" name="email" placeholder="请输入您的邮箱!" required><div class="invalid-feedback" th:text="${emailMsg}">该邮箱已注册!</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></div>
</div>
operate-result.html:
<div class="main"><div class="container mt-5"><div class="jumbotron"><p class="lead" th:text="${msg}">您的账号已经激活成功,可以正常使用了!</p><hr class="my-4"><p>系统会在 <span id="seconds" class="text-danger">8</span> 秒后自动跳转,您也可以点此 <a id="target" th:href="@{${target}}" class="text-primary">链接</a>, 手动跳转!</p></div></div>
</div>
③ 激活账号:
点击激活邮件中的链接,访问服务器端的激活服务
public interface CommunityConstant {/*** 激活成功*/int ACTIVATION_SUCCESS = 0;/*** 重复激活*/int ACTIVATION_REPEAT = 1;/*** 激活失败*/int ACTIVATION_FAILURE = 2;
}
@Service
public class UserService implements CommunityConstant {/*** 激活账号* @param userId 用户id* @param code 激活码* @return 激活状态*/public int activation(int userId,String code){User user = userMapper.selectById(userId);if(user.getStatus()==1){//重复激活return ACTIVATION_REPEAT;}else if(user.getActivationCode().equals(code)){//激活成功userMapper.updateStatus(userId,1);return ACTIVATION_SUCCESS;}else{return ACTIVATION_FAILURE;}}
}
@Controller
public class LoginController implements CommunityConstant {/*** 处理激活账号的请求* @param model 存放数据并返回给operate-result页面* @param userId 用户id* @param code 用户激活码* @return operate-result页面*/@GetMapping("/activation/{userId}/{code}")public String activation(Model model,@PathVariable("userId") int userId,@PathVariable("code") String code){int result = userService.activation(userId, code);if(result==CommunityConstant.ACTIVATION_SUCCESS){model.addAttribute("msg","激活成功,您的账号已经可以正常使用了!");model.addAttribute("target","/login");}else if(result==CommunityConstant.ACTIVATION_REPEAT){model.addAttribute("msg","无效操作,该账号已经激活过了!");model.addAttribute("target","/index");}else {model.addAttribute("msg","激活失败,您提供的激活码不正确!");model.addAttribute("target","/index");}return "/site/operate-result";}
}
社区论坛P09 - 开发登录页面
@Service
public class UserService implements CommunityConstant {@Resourceprivate UserMapper userMapper;@Value("${server.servlet.context-path}")private String contextPath;@Resourceprivate LoginTicketMapper loginTicketMapper;/*** 登录* @param username 用户名* @param password 密码* @param expiredSeconds 凭证的过期时间,以及cookie的过期时间* @return*/public Map<String,Object> login(String username,String password,int expiredSeconds){//判断传入的参数是否为空Map<String,Object> map = new HashMap<>();if(StringUtils.isBlank(username)){map.put("usernameMsg","username不能为空");return map;}if(StringUtils.isBlank(password)){map.put("passwordMsg","password不能为空");return map;}//判断用户是否已经注册User user = userMapper.selectByName(username);if(user==null){map.put("usernameMsg","该账号不存在");return map;}//判断用户是否激活if(user.getStatus()==0){map.put("usernameMsg","该账号还没有激活");return map;}//验证密码if(!user.getPassword().equals(CommunityUtil.md5(password+user.getSalt()))){map.put("passwordMsg","账号密码不正确");return map;}//生成登录凭证LoginTicket loginTicket = new LoginTicket();loginTicket.setUserId(user.getId());loginTicket.setStatus(0);loginTicket.setExpired(new Date(System.currentTimeMillis()+expiredSeconds*1000));loginTicket.setTicket(CommunityUtil.generateUUID());//存入数据库中loginTicketMapper.insertLoginTicket(loginTicket);//将登录凭证的ticket放入map中,以便在controller层将其通过cookie响应给浏览器map.put("ticket",loginTicket.getTicket());return map;}/*** 登出:cookie中存放着ticket,将LoginTicket中的status改为1* @param ticket*/public void logout(String ticket){loginTicketMapper.updateStatus(ticket,1);}
}
@Controller
public class LoginController implements CommunityConstant {private Logger logger = LoggerFactory.getLogger(LoginController.class);@Autowiredprivate UserService userService;@Autowiredprivate Producer kaptchaProducer;@Value("${server.servlet.context-path}")private String contextPath;@PostMapping("/login")public String login(String username,String password,String code,boolean rememberme,Model model,HttpSession session,HttpServletResponse response){//验证验证码//从session中获取之前存的验证码String kaptcha = (String)session.getAttribute("kaptcha");if(StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)){model.addAttribute("codeMsg","验证码不正确");return "/site/login";}int expiredSeconds = rememberme ? REMEMBERME_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;//登录Map<String, Object> map = userService.login(username, password, expiredSeconds);if(map.containsKey("ticket")){// 登录成功,将ticket通过cookie响应给浏览器Cookie cookie = new Cookie("ticket",map.get("ticket").toString());cookie.setPath(contextPath);cookie.setMaxAge(expiredSeconds);response.addCookie(cookie);// 重定向到首页(重定向的浏览器地址栏变化,转发的浏览器地址栏不变)return "redirect:/index";}else{// 登录失败model.addAttribute("usernameMsg",map.get("usernameMsg"));model.addAttribute("passwordMsg",map.get("passwordMsg"));return "/site/login";}}@GetMapping("/logout")public String logout(@CookieValue("ticket") String ticket ){userService.logout(ticket);//现在在首页,重定向到登录页面,调用/login的方法return "redirect:/login";}
}
<!-- 内容 -->
<div class="main"><div class="container pl-5 pr-5 pt-3 pb-3 mt-3 mb-3"><h3 class="text-center text-info border-bottom pb-3">登 录</h3><form class="mt-5" method="post" th:action="@{/login}"><div class="form-group row"><label for="username" class="col-sm-2 col-form-label text-right">账号:</label><div class="col-sm-10"><!--name属性用于给服务器传值--><!--value用于登录失败重新回到login.html页面时显示的默认值--><input type="text" th:class="|form-control ${usernameMsg!=null?'is-invalid':''}|"name="username"th:value="${param.username}"id="username" placeholder="请输入您的账号!" required><div class="invalid-feedback" th:text="${usernameMsg}">该账号不存在!</div></div></div><div class="form-group row mt-4"><label for="password" class="col-sm-2 col-form-label text-right">密码:</label><div class="col-sm-10"><input type="password" th:class="|form-control ${passwordMsg!=null?'is-invalid':''}|"name="password"th:value="${param.password}"id="password" placeholder="请输入您的密码!" required><div class="invalid-feedback" th:text="${passwordMsg}">密码长度不能小于8位!</div> </div></div><div class="form-group row mt-4"><label for="verifycode" class="col-sm-2 col-form-label text-right">验证码:</label><div class="col-sm-6"><input type="text" th:class="|form-control ${codeMsg!=null?'is-invalid':''}|"name="code"id="verifycode" placeholder="请输入验证码!"><div class="invalid-feedback" th:text="${codeMsg}">验证码不正确!</div></div><div class="col-sm-4"><img th:src="@{/kaptcha}" id="kaptcha" style="width:100px;height:40px;" class="mr-2"/><a href="javascript:refresh_kaptcha();" class="font-size-12 align-bottom">刷新验证码</a></div></div> <div class="form-group row mt-4"><div class="col-sm-2"></div><div class="col-sm-10"><input type="checkbox"name="rememberme"id="remember-me" th:checked="${param.rememberme}"><label class="form-check-label" for="remember-me">记住我</label><a href="forget.html" class="text-danger float-right">忘记密码?</a></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> </div>
</div>
社区论坛P10 - 显示登录信息
登录之前显示的首页:
登录之后显示的用户信息:
@Component
public class LoginTicketInterceptor implements HandlerInterceptor {@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;/*** 在Controller之前执行** 在第一次登录时生成登录凭证LoginTicket存放在数据库,并将ticket通过Cookie存档在浏览器* 在没有登录时,首页显示的应该是登录,注册等信息* 在用户登录之后,应该显示用户的登录信息,去除登录,注册按钮** 所以应该拦截该项目下的所有请求:* 在请求的一开始查询登录用户* 在请求的过程中持有登录用户* 在模板视图上显示登录用户* 在请求结束后清除登录用户** 该项目下的请求一过来,就会被拦截器所拦截,* preHandle()方法在调用Controller层的该请求方法之前执行,* 比如登录请求,会在调用/login对应的请求方法之前执行*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//从cookie中获取键ticket对应的值String ticket = CookieUtil.getCookieValue(request, "ticket");//说明用户已经登录if(ticket!=null){//判断ticket的有效性LoginTicket loginTicket = userService.findLoginTicket(ticket);if(loginTicket!=null && loginTicket.getStatus()==0 && loginTicket.getExpired().after(new Date())){//在本次请求的一开始便拿到了User用户User user = userService.findUserById(loginTicket.getUserId());hostHolder.set(user);}}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {//在请求的过程中,将user存入ModelAndViewUser user = hostHolder.get();if(user!=null && modelAndView!=null){modelAndView.addObject("loginUser",user);}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {hostHolder.clear();}
}
public class CookieUtil {/*** 从请求所携带的Cookie中获取建name对应的Cookie值*/public static String getCookieValue(HttpServletRequest request,String name){//工具类中的参数需要做一个参数判断if (request == null || name == null) {throw new IllegalArgumentException("参数为空!");}Cookie[] cookies = request.getCookies();//对于数组,集合,对象都要做一个非空判断if(cookies!=null){for(Cookie cookie.getName:cookies){if(cookie.equals(name)){return cookie.getValue();}}}return null;}
}
@Component
public class HostHolder {// 由于浏览器对服务器是多对一的场景,每一个请求都是一个线程,因此服务器处在一个多线程的环境中// ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。// 通过get和set方法就可以得到当前线程对应的值。private ThreadLocal<User> userThreadLocal = new ThreadLocal<>();public void set(User user){userThreadLocal.set(user);}public User get(){return userThreadLocal.get();}public void clear(){userThreadLocal.remove();}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate AlphaInterceptor alphaInterceptor;@Autowiredprivate LoginTicketInterceptor loginTicketInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//只拦截登录和注册请求,不拦截静态资源registry.addInterceptor(alphaInterceptor).excludePathPatterns("/**/*.css","/**/*.js","/**/*.jpg","/**/*.png","/**/*.jpeg").addPathPatterns("/login","/register");//拦截一切请求,不拦截静态资源registry.addInterceptor(loginTicketInterceptor).excludePathPatterns("/**/*.css","/**/*.js","/**/*.jpg","/**/*.png","/**/*.jpeg");}
}
<div class="collapse navbar-collapse" id="navbarSupportedContent"><ul class="navbar-nav mr-auto"><li class="nav-item ml-3 btn-group-vertical"><a class="nav-link" th:href="@{/index}">首页</a></li><li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser!=null}"><a class="nav-link position-relative" href="site/letter.html">消息<span class="badge badge-danger">12</span></a></li><li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}"><a class="nav-link" th:href="@{/register}">注册</a></li><li class="nav-item ml-3 btn-group-vertical" th:if="${loginUser==null}"><a class="nav-link" th:href="@{/login}">登录</a></li><li class="nav-item ml-3 btn-group-vertical dropdown" th:if="${loginUser!=null}"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><img th:src="${loginUser.headerUrl}" class="rounded-circle" style="width:30px;"/></a><div class="dropdown-menu" aria-labelledby="navbarDropdown"><a class="dropdown-item text-center" href="site/profile.html">个人主页</a><a class="dropdown-item text-center" href="site/setting.html">账号设置</a><a class="dropdown-item text-center" th:href="@{/logout}">退出登录</a><div class="dropdown-divider"></div><span class="dropdown-item text-center text-secondary" th:utext="${loginUser.username}">nowcoder</span></div></li></ul>
</div>
社区论坛P11 - 检查登录状态
/*** @description 自定义注解,拦截器除了在配置类中配置拦截路径外,还可以使用自定义注解的方式** 对于一些接口方法,只有用户登录后才能访问,如果用户没有登录就要拦截这个请求,比如用户的账号设置请求* 将这个自定义注解加到需要被拦截的请求方法上即可*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoginRequired {
}
@Controller
@RequestMapping("/user")
public class UserController {/*** 账号设置页面只能用户登录后才能访问,没登录不能访问*/@LoginRequired@GetMapping("/setting")public String getSettingPage(){return "/site/setting";}
}
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {@Autowiredprivate HostHolder hostHolder;/*** 在请求的一开始就对加了@LoginRequired注解的请求方法进行拦截* @param handler 拦截的目标,这个目标可能为变量,可能为方法*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//如果拦截的目标是个方法if(handler instanceof HandlerMethod){//向上转型为HandlerMethodHandlerMethod handlerMethod = (HandlerMethod) handler;//获取到拦截目标方法(比如拦截到的getSettingPage()方法)Method method = handlerMethod.getMethod();LoginRequired annotation = method.getAnnotation(LoginRequired.class);//如果用户没有登录,重定向到登录页面//annotation!=null说明该方法上加了这个注解,用户必须登录后才能访问if(annotation!=null && hostHolder.get()==null){response.sendRedirect(request.getContextPath()+"/login");return false;}}return true;}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate LoginRequiredInterceptor loginRequiredInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//拦截一切请求,不拦截静态资源registry.addInterceptor(loginRequiredInterceptor).excludePathPatterns("/**/*.css","/**/*.js","/**/*.jpg","/**/*.png","/**/*.jpeg");}
}
社区论坛P12 - 发布帖子
public class CommunityUtil {/*** 异步请求时,向页面响应json数据,而不是刷新页面* @param code 响应码* @param message 提示消息* @param map 响应数据* @return*/public static String getJsonString(int code, String message, Map<String,Object> map){JSONObject jsonObject = new JSONObject();jsonObject.put("code",code);jsonObject.put("msg",message);if(map!=null){for(String key:map.keySet()){jsonObject.put(key,map.get(key));}}return jsonObject.toJSONString();}/*** 响应的json数据只有code* @param code* @return*/public static String getJsonString(int code){return getJsonString(code,null,null);}/*** 响应的json数据只有code和message* @param code* @param message* @return*/public static String getJsonString(int code,String message){return getJsonString(code,message,null);}
}
@Mapper
public interface DiscussPostMapper {/*** 发布帖子* @param discussPost 帖子实体类* @return 影响的行数*/int insertDiscussPost(DiscussPost discussPost);
}
<?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="insertFields">user_id,title,content,type,create_time,status,comment_count,score</sql><insert id="insertDiscussPost" parameterType="DiscussPost">insert into discuss_post(<include refid="insertFields"></include>)values (#{userId},#{title},#{content},#{type},#{createTime},#{status},#{commentCount},#{score})</insert>
</mapper>
@Service
public class DiscussPostService {@Autowiredprivate DiscussPostMapper discussPostMapper;public int addDiscussPost(DiscussPost discussPost){//对于service层传参为对象时,需要判空if(discussPost==null){throw new IllegalArgumentException("参数不能为空");}return discussPostMapper.insertDiscussPost(discussPost);}
}
@Controller
@RequestMapping("/discuss")
public class DiscussPostController {@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate HostHolder hostHolder;@PostMapping("/add")@ResponseBodypublic String addDiscussPost(String title,String content){//发布帖子这个功能是登录之后才能访问的接口,因此需要判断用户是否登录User user = hostHolder.get();if(user==null){return CommunityUtil.getJsonString(403,"你还没有登录哦");}//构造DiscussPostDiscussPost discussPost = new DiscussPost();discussPost.setTitle(title);discussPost.setContent(content);discussPost.setUserId(user.getId());discussPost.setCreateTime(new Date());discussPostService.addDiscussPost(discussPost);return CommunityUtil.getJsonString(200,"发布成功");}
}
$(function(){$("#publishBtn").click(publish);
});function publish() {$("#publishModal").modal("hide");// 获取标题和内容var title = $("#recipient-name").val();var content = $("#message-text").val();// 发送异步请求(POST)$.post(CONTEXT_PATH + "/discuss/add",{"title":title,"content":content},function(data) {data = $.parseJSON(data);// 在提示框中显示返回消息$("#hintBody").text(data.msg);// 显示提示框$("#hintModal").modal("show");// 2秒后,自动隐藏提示框setTimeout(function(){$("#hintModal").modal("hide");// 刷新页面if(data.code == 0) {window.location.reload();}}, 2000);});
}
社区论坛P13 - 帖子详情
@Mapper
public interface DiscussPostMapper {/*** 根据帖子的id查询帖子* @param id 帖子id* @return 一条帖子数据*/DiscussPost selectDiscussPostById(int id);
}
<?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,create_time,status,comment_count,score</sql><select id="selectDiscussPostById" resultType="DiscussPost">select <include refid="selectFields"></include>from discuss_post where id=#{id}</select></mapper>
@Service
public class DiscussPostService {@Autowiredprivate DiscussPostMapper discussPostMapper;public DiscussPost findDiscussPostById(int id){return discussPostMapper.selectDiscussPostById(id);}
}
@Controller
@RequestMapping("/discuss")
public class DiscussPostController {@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate UserService userService;/*** 点解帖子的标题会进入帖子详情页面,点击帖子详情时会带着discussPostId*/@GetMapping("/detail/{discussPostId}")public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){//帖子详情DiscussPost discussPost = discussPostService.findDiscussPostById(discussPostId);model.addAttribute("post",discussPost);//作者信息User user = userService.findUserById(discussPost.getUserId());model.addAttribute("user",user);return "/site/discuss-detail";}
}
<h6 class="mt-0 mb-3"><!--如果路径是相对路径,使用@,如果是变量使用$,如果是既有常量又有变量就用||--><a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a><span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span><span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}" >精华</span>
</h6>
<div class="container"><!-- 标题 --><h6 class="mb-4"><img src="http://static.nowcoder.com/images/img/icons/ico-discuss.png"/><span th:utext="${post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</span><div class="float-right"><button type="button" class="btn btn-danger btn-sm">置顶</button><button type="button" class="btn btn-danger btn-sm">加精</button><button type="button" class="btn btn-danger btn-sm">删除</button></div></h6><!-- 作者 --><div class="media pb-3 border-bottom"><a href="profile.html"><img th:src="${user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" ></a><div class="media-body"><div class="mt-0 text-warning" th:utext="${user.username}">寒江雪</div><div class="text-muted mt-3">发布于 <b th:text="${#dates.format(post.createTime,'yyyy-mm-dd HH:mm:ss')}">2019-04-15 15:32:18</b><ul class="d-inline float-right"><li class="d-inline ml-2"><a href="#" class="text-primary">赞 11</a></li><li class="d-inline ml-2">|</li><li class="d-inline ml-2"><a href="#replyform" class="text-primary">回帖 7</a></li></ul></div></div></div> <!-- 正文 --><div class="mt-4 mb-3 content" th:utext="${post.content}">金三银四的金三已经到了,你还沉浸在过年的喜悦中吗?如果是,那我要让你清醒一下了:目前大部分公司已经开启了内推,正式网申也将在3月份陆续开始,金三银四,春招的求职黄金时期已经来啦!!!再不准备,作为19应届生的你可能就找不到工作了。。。作为20届实习生的你可能就找不到实习了。。。现阶段时间紧,任务重,能做到短时间内快速提升的也就只有算法了,那么算法要怎么复习?重点在哪里?常见笔试面试算法题型和解题思路以及最优代码是怎样的?跟左程云老师学算法,不仅能解决以上所有问题,还能在短时间内得到最大程度的提升!!!</div>
</div>
社区论坛P14 - 显示评论
@Data
public class Comment {private int id;private int userId;private int entityType;private int entityId;private int targetId;private String content;private int status;private Date createTime;
}
@Mapper
public interface CommentMapper {/*** 根据实体类型分页查询所有评论* @param entityType 实体类型:评论、帖子* @param entityId 实体id* @param offset 其实行号* @param limit 每页显示的页码数* @return*/List<Comment> selectCommentsByEntity(int entityType,int entityId,int offset,int limit);/*** 查询评论下的回复数量* @param entityType* @param entityId* @return*/int selectCountByEntity(int entityType,int entityId);
}
<?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.CommentMapper"><sql id="selectFields">id,user_id,entity_type,entity_id,target_id,content,status,create_time</sql><select id="selectCommentsByEntity" resultType="Comment">select <include refid="selectFields"></include>from commentwhere status = 0and entity_type = #{entityType}and entity_id = #{entityId}order by create_time asclimit #{offset},#{limit}</select><select id="selectCountByEntity" resultType="int">select count(id) from commentwhere status=0and entity_type=#{entityType}and entity_id=#{entityId}</select></mapper>
@Service
public class CommentService {@Autowiredprivate CommentMapper commentMapper;public List<Comment> findCommentsByEntityType(int entityType,int entityId,int offset,int limit){return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);}public int findCountByEntityType(int entityType,int entityId){return commentMapper.selectCountByEntity(entityType,entityId);}
}
在帖子详情页面展示帖子详情时同时显示对应的评论:
@Api(value="discusspost",description = "提供帖子页面数据的增删改查")
@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate CommentService commentService;/*** 点解帖子的标题会进入帖子详情页面,点击帖子详情时会带着discussPostId*/@ApiOperation("帖子详情信息")@GetMapping("/detail/{discussPostId}")public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page){//帖子详情DiscussPost discussPost = discussPostService.findDiscussPostById(discussPostId);model.addAttribute("post",discussPost);//作者信息User user = userService.findUserById(discussPost.getUserId());model.addAttribute("user",user);//设置分页信息page.setPath("/discuss/detail/"+discussPostId);page.setLimit(5);page.setRows(discussPost.getCommentCount());//在显示帖子详情的同时,分页显示该帖子的评论,以及评论对应的回复//分页查询出所有的帖子列表List<Comment> commentList = commentService.findCommentsByEntityType(ENTITY_TYPE_POST, discussPost.getId(), page.getOffset(), page.getLimit());//页面需要展示每个帖子的评论内容和用户信息,但是comment表中只有user_idList<Map<String,Object>> commentVoList = new ArrayList<>();if(commentList!=null){for(Comment comment :commentList){Map<String,Object> commentVo = new HashMap<>();commentVo.put("comment",comment);commentVo.put("user",userService.findUserById(discussPost.getUserId()));//每个评论面还有回复,查询每个评论下面的所有回复List<Comment> replyList = commentService.findCommentsByEntityType(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);//页面需要展示回复的信息和回复用户的信息,以及对谁的回复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(comment.getId()));User targetUser = comment.getTargetId()==0 ? null :userService.findUserById(comment.getTargetId());replyVo.put("target",targetUser);replyVoList.add(replyVo);}}commentVo.put("replys",replyList);//每个评论的回复数量int replyCount = commentService.findCountByEntityType(ENTITY_TYPE_COMMENT, comment.getId());commentVo.put("replyCount",replyCount);commentVoList.add(commentVo);}}model.addAttribute("comments",commentList);return "/site/discuss-detail";}
}
使用Swagger工具进行测试:
① 导入相关注解:
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.7.0</version>
</dependency><!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.7.0</version>
</dependency>
② 编写配置:
package com.nowcoder.community.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;@Configuration
@EnableSwagger2
public class Swagger2Configuration {//com.newcoder.community包下面的所有加了@Controller注解类中的的接口方法都可以扫描到@Beanpublic Docket createRestApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.nowcoder.community")).paths(PathSelectors.any()).build();}private ApiInfo apiInfo() {return new ApiInfoBuilder().title("社区论坛文档").description("社区论坛API文档").version("1.0").build();}
}
③ 测试:http://localhost:8080/community/swagger-ui.html
④ 进入discuss-post-controller接口测试:
社区论坛P15 - 添加评论
package com.nowcoder.community.dao;@Mapper
public interface CommentMapper {/*** 增加评论* @param comment* @return*/int insertComment(Comment comment);
}
<?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.CommentMapper"><sql id="insertFields">user_id,entity_type,entity_id,target_id,content,status,create_time</sql><insert id="insertComment" parameterType="Comment">insert into comment (<include refid="insertFields"></include>)values (#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})</insert></mapper>
package com.nowcoder.community.dao;/*** 操作discuss_post表的接口*/
@Mapper
public interface DiscussPostMapper {/*** 更新一条帖子的评论数量* @param id* @param commentCount* @return*/int updateCommentCount(int id,int commentCount);
}
<?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"><update id="updateCommentCount">update discuss_post set comment_count=#{commentCount} where id=#{id}</update>
</mapper>
package com.nowcoder.community.service;@Service
public class CommentService implements CommunityConstant {/*** 添加一条评论,同时更新帖子的评论数量* @param comment* @return*/@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)public int addComment(Comment comment){if(comment==null){throw new IllegalArgumentException("参数异常");}//添加评论int rows = commentMapper.insertComment(comment);//如果是对帖子的评论,更新帖子的评论数量if(comment.getEntityType()== ENTITY_TYPE_POST){//查询帖子的评论数量int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());//更新帖子的评论数量discussPostMapper.updateCommentCount(comment.getEntityId(),count);}return rows;}
}
package com.nowcoder.community.controller;@Controller
@RequestMapping("/comment")
public class CommentController {@Autowiredprivate HostHolder hostHolder;@Autowiredprivate CommentService commentService;@PostMapping("/add/{discussPostId}")public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment){comment.setUserId(hostHolder.get().getId());comment.setStatus(0);comment.setCreateTime(new Date());commentService.addComment(comment);return "redirect:/discuss/detail/" + discussPostId;}
}
社区论坛P16 - 发送私信
@Data
public class Message {private int id;private int fromId;private int toId;private String conversationId;private String content;private int status;private Date createTime;
}
@Mapper
public interface MessageMapper {/*** 查询当前用户的会话列表,每个会话只显示最新的一条会话* @param userId 当前用户id* @param offset 起始行号* @param limit 每页显示的行号* @return*/List<Message> selectConversations(int userId,int offset,int limit);/*** 查询当前用户的会话数* @param userId* @return*/int selectConversationCount(int userId);/*** 查询某个会话包含的私信列表* @param conversationId* @param offset* @param limit* @return*/List<Message> selectLetters(String conversationId,int offset,int limit);/*** 查询某个会话包含的私信数量* @param conversationId* @return*/int selectLetterCount(String conversationId);/*** 查询某个用户的未读会话数量,如果加上conversationId就是单个会话的未读私信数量,否则是该用户所有的未读私信数量* @param userId* @param conversationId* @return*/int selectLetterUnreadCount(int userId,String conversationId);
}
<?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.MessageMapper"><sql id="selectFeilds">id, from_id, to_id, conversation_id, content, status, create_time</sql><select id="selectConversations" resultType="Message"><!--将所有会话中最新的会话排在最上面-->select <include refid="selectFeilds"></include> from messagewhere id in(<!--查询每个会话的最新会话-->select max(id) from messagewhere status != 2and from_id != 1and (from_id=#{userId} or to_id=#{userId})group by conversation_id)order by id desclimit #{offset},#{limit}</select><select id="selectConversationCount" resultType="int"><!--查询所有的会话数-->select count(m.maxid) from<!--查询每个会话的最新会话-->(select max(id) as maxid from messagewhere status != 2and from_id != 1and (from_id=#{userId} or to_id=#{userId})group by conversation_id) as m</select><select id="selectLetters" resultType="Message">select <include refid="selectFeilds"></include> from messagewhere status != 2and from_id != 1and conversation_id = #{conversationId}order by id desclimit #{offset},#{limit}</select><select id="selectLetterCount" resultType="int">select count(id) from messagewhere status != 2and from_id != 1and conversation_id = #{conversationId}</select>a<select id="selectLetterUnreadCount" resultType="int">select count(id) from messagewhere status != 2and from_id != 1and to_id = #{userId}<if test="conversationId!=null">and conversation_id = #{conversationId}</if></select>
</mapper>
@Service
public class MessageService {@Autowiredprivate MessageMapper messageMapper;public List<Message> findConversations(int userId, int offset, int limit){return messageMapper.selectConversations(userId,offset,limit);}public int findConversationCount(int userId){return messageMapper.selectConversationCount(userId);}public List<Message> findLetters(String conversationId,int offset,int limit){return messageMapper.selectLetters(conversationId,offset,limit);}public int findLetterCount(String conversationId){return messageMapper.selectLetterCount(conversationId);}public int findLetterUnreadCount(int userId,String conversationId){return messageMapper.selectLetterUnreadCount(userId,conversationId);}
}
package com.nowcoder.community.controller;@Controller
public class MessageController {@Autowiredprivate MessageService messageService;@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;/*** 私信列表* @param model* @param page* @return*/@ApiOperation("查询私信列表")@GetMapping("/letter/list")public String getLetterList(Model model, Page page){User user = hostHolder.get();//设置分页信息page.setPath("/letter/list");page.setRows(messageService.findConversationCount(user.getId()));page.setLimit(5);//查询所有会话List<Message> conversationList = messageService.findConversations(user.getId(), page.getOffset(), page.getLimit());//查询到的内容还不够,还需要做一些补充‘List<Map<String,Object>> conversationVoList = new ArrayList<>();if(conversationList!=null){for(Message message:conversationList){Map<String,Object> conversationVo = new HashMap<>();//会话conversationVo.put("conversation",message);//当前会话的未读消息数量conversationVo.put("unreadCount",messageService.findLetterUnreadCount(user.getId(),message.getConversationId()));//当前会话的消息数量conversationVo.put("letterCount",messageService.findConversationCount(user.getId()));int tagetId = user.getId()== message.getFromId()?message.getToId():message.getFromId();conversationVo.put("target",userService.findUserById(tagetId));conversationVoList.add(conversationVo);}}model.addAttribute("conversations",conversationVoList);//当前用户的所有未读消息数量int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(),null);model.addAttribute("letterUnreadCount",letterUnreadCount);return "/site/letter";}
}
社区论坛P17 - 私信详情
package com.nowcoder.community.controller;@Controller
public class MessageController {@Autowiredprivate MessageService messageService;@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@GetMapping("/letter/detail/{conversationId}")public String getLetterDetail(@PathVariable("conversationId")String conversationId,Page page,Model model){page.setLimit(5);page.setRows(messageService.findLetterCount(conversationId));page.setPath("/letter/detail/"+conversationId);List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());List<Map<String,Object>> letterVoList = new ArrayList<>();if(letterList!=null){for(Message message:letterList){Map<String,Object> letterVo = new HashMap<>();letterVo.put("letter",message);letterVo.put("fromUser",userService.findUserById(message.getFromId()));letterVoList.add(letterVo);}}model.addAttribute("letters",letterList);//来自xxx的消息,其中xxx即为targetmodel.addAttribute("target",getLetterTarget(conversationId));return "/site/letter-detail";}private User getLetterTarget(String conversationId) {String[] split = conversationId.split("_");int id0 = Integer.parseInt(split[0]);int id1 = Integer.parseInt(split[1]);if(hostHolder.get().getId()==id0){return userService.findUserById(id1);}else{return userService.findUserById(id0);}}
}
社区论坛P18 - 发送私信
package com.nowcoder.community.dao;@Mapper
public interface MessageMapper {/*** 发送私信* @param message* @return*/int insertMessage(Message message);/*** 将未读的私信设置为已读* @param ids* @param status* @return*/int updateStatus(List<Integer> ids,int status);
}
<?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.MessageMapper"><sql id="insertFields">from_id, to_id, conversation_id, content, status, create_time</sql><insert id="insertMessage" parameterType="Message">insert into message(<include refid="insertFields"></include>)values (#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})</insert><update id="updateStatus">update message set status = #{status}where id in(<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>)</update>
</mapper>
package com.nowcoder.community.service;@Service
public class MessageService {@Autowiredprivate MessageMapper messageMapper;public int addMessage(Message message){return messageMapper.insertMessage(message);}public int readMessage(List<Integer> ids){return messageMapper.updateStatus(ids,1);}
}
package com.nowcoder.community.controller;@Controller
public class MessageController {@Autowiredprivate MessageService messageService;@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@PostMapping("/letter/send")@ResponseBodypublic String sendLetter(String toName, String content){User toUser = userService.findByName(toName);if(toUser==null){return CommunityUtil.getJsonString(1,"目标用户不存在");}Message message = new Message();message.setFromId(hostHolder.get().getId());message.setToId(toUser.getId());message.setContent(content);//需要保证小号在前if(message.getFromId()<message.getToId()){message.setConversationId(message.getFromId()+"_"+message.getToId());}else{message.setConversationId(message.getToId()+"_"+message.getFromId());}message.setCreateTime(new Date());messageService.addMessage(message);return CommunityUtil.getJSONString(0);}
}
社区论坛P19 - 统一处理异常
① 出现异常后跳转到500页面:
package com.nowcoder.community.controller;@Controller
public class HomeController {@GetMapping("/error")public String getErrorPage(){return "/error/500";}
}
② 统一处理异常配置类:
package com.nowcoder.community.controller.advice;@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {private Logger logger = LoggerFactory.getLogger(ExceptionHandler.class);//统一处理Exception类型及其子类的异常@ExceptionHandler({Exception.class})public void exceptionHandler(Exception exception, HttpServletRequest request, HttpServletResponse response) throws IOException {//获取异常后记录日志logger.error("服务器出现异常,{}",exception.getMessage());//向页面响应数据,如果请求响应是json数据,否则跳转到500页面String xRequestedWith = request.getHeader("x-requested-with");if("XMLHttpRequest".equals(xRequestedWith)){//响应json数据response.setContentType("application/plain;charset=utf-8");PrintWriter writer = response.getWriter();writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));}else{//重定向到错误页面response.sendRedirect(request.getContextPath() + "/error");}}
}
社区论坛P20 - 统一记录日志
① 测试案例:
package com.nowcoder.community.aspect;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;//定义SpringAop组件
@Component
@Aspect
public class AlphaAspect {//声明切入点@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")public void pointCut(){}//定义通知,在方法的一开始执行@Before("pointCut()")public void before(){System.out.println("before");}//定义通知,在方法的最后执行@After("pointCut()")public void after(){System.out.println("after");}//定义通知,在方法返回值后执行@AfterReturning("pointCut()")public void afterReturning(){System.out.println("afterReturning");}//定义通知,在方法出现异常时执行@AfterThrowing("pointCut()")public void afterThrowing(){System.out.println("afterThrowing");}//环绕通知@Around("pointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {//在方法执行前执行System.out.println("around before");Object obj = joinPoint.proceed();// 在方法执行后执行System.out.println("around after");return obj;}
}
② 返回结果:
around before
before
around after
after
afterReturning
③ 在项目中使用aop记录日志:
package com.nowcoder.community.aspect;@Component
@Aspect
public class ServiceLogAspect {private static Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);//声明切入点,切入到哪些方法中@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")public void pointCut(){}//声明通知,切入到方法的具体哪些位置@Before("pointCut()")public void before(JoinPoint joinPoint){// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();String ip = request.getRemoteHost();String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));}
}
社区论坛P21 - Spring整合redis
① windows安装redis : https://github.com/microsoftarchive/redis/releases 版本:3.2.10
② 配置环境变量:将安装目录redis-cli.exe所在的文件夹配置在环境变量中
③ 利用命令行打开redis客户端:
④ Spring整合redis :
1、导包:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置application.properties:
# RedisProperties
spring.redis.database=11
spring.redis.host=localhost
spring.redis.port=6379
3、 配置redisTemplate的key和value序列化方式:
package com.nowcoder.community.config;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 设置key的序列化方式template.setKeySerializer(RedisSerializer.string());// 设置value的序列化方式template.setValueSerializer(RedisSerializer.json());// 设置hash的key的序列化方式template.setHashKeySerializer(RedisSerializer.string());// 设置hash的value的序列化方式template.setHashValueSerializer(RedisSerializer.json());template.afterPropertiesSet();return template;}
}
4、 测试Spring整合redis:
package com.nowcoder.community;@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void testStrings() {String redisKey = "test:count";redisTemplate.opsForValue().set(redisKey, 1);System.out.println(redisTemplate.opsForValue().get(redisKey));System.out.println(redisTemplate.opsForValue().increment(redisKey));System.out.println(redisTemplate.opsForValue().decrement(redisKey));}@Testpublic void testHashes() {String redisKey = "test:user";redisTemplate.opsForHash().put(redisKey, "id", 1);redisTemplate.opsForHash().put(redisKey, "username", "zhangsan");System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));System.out.println(redisTemplate.opsForHash().get(redisKey, "username"));}@Testpublic void testLists() {String redisKey = "test:ids";redisTemplate.opsForList().leftPush(redisKey, 101);redisTemplate.opsForList().leftPush(redisKey, 102);redisTemplate.opsForList().leftPush(redisKey, 103);System.out.println(redisTemplate.opsForList().size(redisKey));System.out.println(redisTemplate.opsForList().index(redisKey, 0));System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2));System.out.println(redisTemplate.opsForList().leftPop(redisKey));System.out.println(redisTemplate.opsForList().leftPop(redisKey));System.out.println(redisTemplate.opsForList().leftPop(redisKey));}@Testpublic void testSets() {String redisKey = "test:teachers";redisTemplate.opsForSet().add(redisKey, "刘备", "关羽", "张飞", "赵云", "诸葛亮");System.out.println(redisTemplate.opsForSet().size(redisKey));System.out.println(redisTemplate.opsForSet().pop(redisKey));System.out.println(redisTemplate.opsForSet().members(redisKey));}@Testpublic void testSortedSets() {String redisKey = "test:students";redisTemplate.opsForZSet().add(redisKey, "唐僧", 80);redisTemplate.opsForZSet().add(redisKey, "悟空", 90);redisTemplate.opsForZSet().add(redisKey, "八戒", 50);redisTemplate.opsForZSet().add(redisKey, "沙僧", 70);redisTemplate.opsForZSet().add(redisKey, "白龙马", 60);System.out.println(redisTemplate.opsForZSet().zCard(redisKey));System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒"));System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒"));System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2));}@Testpublic void testKeys() {redisTemplate.delete("test:user");System.out.println(redisTemplate.hasKey("test:user"));redisTemplate.expire("test:students", 10, TimeUnit.SECONDS);}// 批量发送命令,节约网络开销.@Testpublic void testBoundOperations() {String redisKey = "test:count";BoundValueOperations operations = redisTemplate.boundValueOps(redisKey);operations.increment();operations.increment();operations.increment();operations.increment();operations.increment();System.out.println(operations.get());}// 编程式事务@Testpublic void testTransaction() {Object result = redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations redisOperations) throws DataAccessException {String redisKey = "text:tx";// 启用事务redisOperations.multi();redisOperations.opsForSet().add(redisKey, "zhangsan");redisOperations.opsForSet().add(redisKey, "lisi");redisOperations.opsForSet().add(redisKey, "wangwu");System.out.println(redisOperations.opsForSet().members(redisKey));// 提交事务return redisOperations.exec();}});System.out.println(result);}
}
社区论坛P22 - 点赞
① 获取key的工具类:
public class RedisKeyUtil {private static final String SPLIT = ":";private static final String PREFIX_ENTITY_LIKE = "like:entity";//给某个实体点赞的key:like:entity:entityType:entityId-->set集合存放点赞人的userIdpublic static String getEntityLikeKey(int entityType,int entityId){return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;}
}
② service层:
package com.nowcoder.community.service;@Service
public class LikeService {@Autowiredprivate RedisTemplate redisTemplate;//点赞public void like(int userId,int entityType,int entityId){//获取keyString entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);//第一次点赞为点赞,第二次点赞为取消点赞,判断集中是否有该userIdif(redisTemplate.opsForSet().isMember(entityLikeKey,userId)){//取消点赞redisTemplate.opsForSet().remove(entityLikeKey,userId);}else{//点赞redisTemplate.opsForSet().add(entityLikeKey,userId);}}//查询某实体点赞的数量public long findEntityLikeCount(int entityType, int entityId){//获取keyString entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);return redisTemplate.opsForSet().size(entityLikeKey);}//查询登录用户对某实体的点赞状态public int findEntityLikeStatus(int userId,int entityType,int entityId){//获取keyString entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);return redisTemplate.opsForSet().isMember(entityLikeKey,userId) ? 1:0;}
}
③ Controller层 :LikeController()实现点赞功能
package com.nowcoder.community.controller;@Controller
public class LikeController {@Autowiredprivate LikeService likeService;@Autowiredprivate HostHolder hostHolder;@PostMapping("/like")@ResponseBodypublic String like(int entityType,int entityId){User user = hostHolder.getUser();//点赞likeService.like(user.getId(),entityType,entityId);//查询某实体点赞的数量long entityLikeCount = likeService.findEntityLikeCount(entityType, entityId);//点赞的状态int entityLikeStatus = likeService.findEntityLikeStatus(user.getId(), entityType, entityId);Map<String,Object> map = new HashMap<>();map.put("likeCount",entityLikeCount);map.put("likeStatus",entityLikeStatus);return CommunityUtil.getJSONString(0,null,map);}
}
④ 测试:http://localhost:8080/community/swagger-ui.html
⑤ 在首页显示点赞的数量:
package com.nowcoder.community.controller;@Controller
public class HomeController implements CommunityConstant {@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate UserService userService;@Autowiredprivate LikeService likeService;@RequestMapping(path = "/index", method = RequestMethod.GET)public String getIndexPage(Model model, Page page) {// 方法调用钱,SpringMVC会自动实例化Model和Page,并将Page注入Model.// 所以,在thymeleaf中可以直接访问Page对象中的数据.page.setRows(discussPostService.findDiscussPostRows(0));page.setPath("/index");List<DiscussPost> list = discussPostService.findDiscussPosts(0, page.getOffset(), page.getLimit());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";}
}
⑥ 在帖子详情页显示帖子的点赞数量和点赞状态:
如果用户登录了显示的是已赞+帖子目前拥有的点赞数量
如果用户没有登录无法点赞,显示的是赞+该帖子或评论已有的点赞数量:
package com.nowcoder.community.controller;@Controller
@RequestMapping("/discuss")
public class DiscussPostController implements CommunityConstant {@Autowiredprivate DiscussPostService discussPostService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;@Autowiredprivate CommentService commentService;@Autowiredprivate LikeService likeService;@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, post.getId());model.addAttribute("likeCount",likeCount);int likeStatus = hostHolder.getUser() == null ? 0 : likeService.findEntityLikeStatus(hostHolder.getUser().getId(), ENTITY_TYPE_POST, post.getId());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()));//回复的点赞数量和状态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);// 回复目标User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());replyVo.put("target", target);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";}
}
社区论坛P23 - 我收到的赞
① 某实体收到的赞key:
package com.nowcoder.community.util;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";//给某个实体点赞的key:like:entity:entityType:entityId-->set集合存放点赞人的userIdpublic static String getEntityLikeKey(int entityType,int entityId){return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;}//某实体收到的赞:like:user:userIdpublic static String getUserLikeKey(int userId){return PREFIX_USER_LIKE+SPLIT+userId;}
}
② 重构点赞方法,在点赞的同时记录某实体的点赞的数量,增加获取某用户点赞数量的方法:
package com.nowcoder.community.service;@Service
public class LikeService {@Autowiredprivate RedisTemplate redisTemplate;//点赞public void like(int userId,int entityType,int entityId,int entityUserId) {//因为是两次更新操作因此需要redis的事务处理redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);String userLikeKey = RedisKeyUtil.getUserLikeKey(entityUserId);//查询登录用户有没有对某实体点过赞Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);//开启事务operations.multi();if (isMember) {operations.opsForSet().remove(entityLikeKey, userId);operations.opsForValue().decrement(userLikeKey);} else {operations.opsForSet().add(entityLikeKey, userId);operations.opsForValue().increment(userLikeKey);}//执行事务return operations.exec();}});}//查询某实体点赞的数量public long findEntityLikeCount(int entityType, int entityId){//获取keyString entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);return redisTemplate.opsForSet().size(entityLikeKey);}//查询某人对对某实体的点赞状态public int findEntityLikeStatus(int userId,int entityType,int entityId){//获取keyString entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);return redisTemplate.opsForSet().isMember(entityLikeKey,userId) ? 1:0;}//查询某用户收到的赞public int getUserLikeCount(int userId){String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);Integer count =(Integer) redisTemplate.opsForValue().get(userLikeKey);return count==null?0:count.intValue();}
}
③ 开发个人主页:
package com.nowcoder.community.controller;@Controller
@RequestMapping("/user")
public class UserController {@Autowiredprivate LikeService likeService;@GetMapping("/profile/{userId}")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.getUserLikeCount(userId);model.addAttribute("likeCount",likeCount);return "/site/profile";}
}
社区论坛P24 - 关注和取消关注
① 构造关注和取消关注功能的key:
package com.nowcoder.community.util;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";//给某个实体点赞的key:like:entity:entityType:entityId-->set集合存放点赞人的userIdpublic static String getEntityLikeKey(int entityType,int entityId){return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;}//某实体收到的赞:like:user:userIdpublic static String getUserLikeKey(int userId){return PREFIX_USER_LIKE+SPLIT+userId;}//某用户关注的实体:followee:userId:entityType-->zset(entityId)public static String getFolloweeKey(int userId,int entityType){return PREFIX_FOLLOWEE+SPLIT+userId+SPLIT+entityType;}//某实体拥有的粉丝:follower:entityType:entityId-->zset(userId)public String getFollowerKey(int entityType,int entityId){return PREFIX_FOLLOWER+SPLIT+entityType+SPLIT+entityId;}
}
② Service层:
package com.nowcoder.community.service;@Service
public class FollowService {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate UserService userService;//关注public void followee(int userId,int entityType,int entityId){//我关注了某个实体,那么这个实体的关注者就会增加1redisTemplate.execute(new SessionCallback() {@Overridepublic Object execute(RedisOperations redisOperations) throws DataAccessException {String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);//开启事务redisOperations.multi();//某用户关注某实体redisOperations.opsForZSet().add(followeeKey,entityId,System.currentTimeMillis());//某实体关注的用户redisOperations.opsForZSet().add(followerKey,userId,System.currentTimeMillis());return redisOperations.exec();}});}//取消关注public void unfollowee(int userId,int entityType,int entityId){//我关注了某个实体,那么这个实体的关注者就会增加1redisTemplate.execute(new SessionCallback() {String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);@Overridepublic Object execute(RedisOperations redisOperations) throws DataAccessException {//开启事务redisOperations.multi();//某用户关注某实体redisOperations.opsForZSet().remove(followeeKey,entityId);//某实体关注的用户redisOperations.opsForZSet().remove(followerKey,userId);return redisOperations.exec();}});}
}
③ Controller层:
package com.nowcoder.community.controller;@Controller
public class FollowController {@Autowiredprivate HostHolder hostHolder;@Autowiredprivate FollowService followService;@PostMapping("/follow")@ResponseBodypublic String follow(int entityType,int entityId){User user = hostHolder.getUser();followService.followee(user.getId(),entityType,entityId);return CommunityUtil.getJSONString(0, "已关注!");}@PostMapping("/unfollow")@ResponseBodypublic String unfollow(int entityType,int entityId){User user = hostHolder.getUser();followService.unfollowee(user.getId(),entityType,entityId);return CommunityUtil.getJSONString(0, "已取消关注!");}
}
社区论坛P25 - 统计用户的粉丝数和关注数
别人的个人主页:
自己的个人主页,看不到关注按钮:
① Service层:
package com.nowcoder.community.service;@Service
public class FollowService {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate UserService userService;//查询实体的关注数public Long findFolloweeCount(int userId,int entityType){String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);return redisTemplate.opsForZSet().zCard(followeeKey);}//查询实体的粉丝数public Long findFollowerCount(int entityType,int entityId){String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);return redisTemplate.opsForZSet().zCard(followerKey);}//查询某用户是否关注某实体public boolean hasFollowed(int userId, int entityType, int entityId){String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);return redisTemplate.opsForZSet().score(followerKey,userId) != null;}
}
② Controller层:
package com.nowcoder.community.controller;@Controller
@RequestMapping("/user")
public class UserController implements CommunityConstant {@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate LikeService likeService;@Autowiredprivate FollowService followService;@GetMapping("/profile/{userId}")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.getUserLikeCount(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";}
}
社区论坛P26 - 关注列表和粉丝列表
① Service层:
package com.nowcoder.community.service;@Service
public class FollowService implements CommunityConstant {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate UserService userService;//分页查询某实体关注的人public List<Map<String, Object>> findFollowees(int userId, int offset, int limit){String followeeKey = RedisKeyUtil.getFolloweeKey(userId, ENTITY_TYPE_USER);//按分数倒叙取出一页的数据(按照索引下标区间取出)Set<Integer> entityIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, limit + offset - 1);if(entityIds==null){return null;}//遍历,获取用户和关注时间List<Map<String,Object>> list = new ArrayList<>();for(Integer entityid:entityIds){Map<String,Object> map = new HashMap<>();User user = userService.findUserById(entityid);map.put("user",user);Double score = redisTemplate.opsForZSet().score(followeeKey, entityid);map.put("followTime",new Date(score.longValue()));list.add(map);}return list;}//分页查询某用户关注的实体public List<Map<String, Object>> findFollowes(int userId, int offset, int limit){String followerKey = RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER, userId);//按分数倒叙取出一页的数据(按照索引下标区间取出)Set<Integer> entityIds = redisTemplate.opsForZSet().reverseRange(followerKey, offset, limit + offset - 1);if(entityIds==null){return null;}//遍历,获取用户和关注时间List<Map<String,Object>> list = new ArrayList<>();for(Integer entityid:entityIds){Map<String,Object> map = new HashMap<>();User user = userService.findUserById(entityid);map.put("user",user);Double score = redisTemplate.opsForZSet().score(followerKey, entityid);map.put("followTime",new Date(score.longValue()));list.add(map);}return list;}
}
② Controller层:
package com.nowcoder.community.controller;@Controller
public class FollowController implements CommunityConstant {@Autowiredprivate FollowService followService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;//分页查询关注实体的人@GetMapping("/followees/{userId}")public String getFollowees(@PathVariable("userId") int userId,Page page,Model model){User user = userService.findUserById(userId);if(user==null){throw new RuntimeException("该用户不存在");}//xxx关注的人model.addAttribute("user",user);//设置分页信息page.setLimit(5);page.setPath("/followees/"+userId);page.setRows((int)followService.findFolloweeCount(userId,ENTITY_TYPE_USER));List<Map<String, Object>> followeeList = followService.findFollowees(userId, page.getOffset(), page.getLimit());if(followeeList!=null){for(Map<String,Object> map:followeeList){User u = (User) map.get("user");//判断登录用户是否已关注boolean hasFollowed = hasFollowed(u.getId());map.put("hasFollowed",hasFollowed);}}//所有关注的用户model.addAttribute("users",followeeList);return "/site/follower";}//分页查询实体的粉丝@GetMapping("/followers/{userId}")public String getFollowers(@PathVariable("userId") int userId,Page page,Model model){User user = userService.findUserById(userId);if(user==null){throw new RuntimeException("用户不存在");}model.addAttribute("user",user);//设置分页信息page.setPath("/followers/"+userId);page.setRows((int)followService.findFollowerCount(ENTITY_TYPE_USER,userId));page.setLimit(5);List<Map<String, Object>> followerList = followService.findFollowers(userId, page.getOffset(), page.getLimit());if(followerList!=null){for(Map<String,Object> map:followerList){User u = (User) map.get("user");boolean hasFollowed = hasFollowed(u.getId());map.put("hasFollowed",hasFollowed);}}model.addAttribute("users",followerList);return "/site/follower";}private boolean hasFollowed(int userId) {if (hostHolder.getUser() == null) {return false;}return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);}
}
社区论坛P27 - 优化登录模块
1、使用redis存储验证码
① 构建存放验证码的key:
package com.nowcoder.community.util;public class RedisKeyUtil {private static final String SPLIT = ":";private static final String PREFIX_KAPTCHA = "kaptcha";//用户访问登录页面生成一个标识用户的凭证public static String getKaptchaKey(String owner){return PREFIX_KAPTCHA+SPLIT+owner;}
}
② 生成验证码以后,将验证码存放在redis中,同时将用户凭证通过cookie响应给浏览器端并保存:
package com.nowcoder.community.controller;@Controller
public class LoginController implements CommunityConstant {@Value("${server.servlet.context-path}")private String contextPath;@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping(path = "/kaptcha", method = RequestMethod.GET)public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {// 生成验证码String text = kaptchaProducer.createText();BufferedImage image = kaptchaProducer.createImage(text);// 将验证码存入session
// session.setAttribute("kaptcha", text);//生成首次访问登录页面的凭证,用于标识该用户String kaptchaOwner = CommunityUtil.generateUUID();//将凭证响应给浏览器并返回Cookie cookie = new Cookie("kaptchaOwner",kaptchaOwner);cookie.setPath(contextPath);cookie.setMaxAge(60);response.addCookie(cookie);//将验证码存放在redis中String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);redisTemplate.opsForValue().set(redisKey,text,60, TimeUnit.SECONDS);// 将突图片输出给浏览器response.setContentType("image/png");try {OutputStream os = response.getOutputStream();ImageIO.write(image, "png", os);} catch (IOException e) {logger.error("响应验证码失败:" + e.getMessage());}}
}
③ 从cookie中获取验证码凭证,并从redis中取出验证码并验证:
package com.nowcoder.community.controller;@Controller
public class LoginController implements CommunityConstant {@Value("${server.servlet.context-path}")private String contextPath;@Autowiredprivate RedisTemplate redisTemplate;@RequestMapping(path = "/login", method = RequestMethod.POST)public String login(String username, String password, String code, boolean rememberme,Model model,/* HttpSession session, */HttpServletResponse response,@CookieValue("kaptchaOwner") String kaptchaOwner) {// 检查验证码
// String kaptcha = (String) session.getAttribute("kaptcha");String kaptcha = null;if(StringUtils.isNoneBlank(kaptchaOwner)){String redisKey = RedisKeyUtil.getKaptchaKey(kaptchaOwner);kaptcha = (String) redisTemplate.opsForValue().get(redisKey);}if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {model.addAttribute("codeMsg", "验证码不正确!");return "/site/login";}// 检查账号,密码int expiredSeconds = rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;Map<String, Object> map = userService.login(username, password, expiredSeconds);if (map.containsKey("ticket")) {Cookie cookie = new Cookie("ticket", map.get("ticket").toString());cookie.setPath(contextPath);cookie.setMaxAge(expiredSeconds);response.addCookie(cookie);return "redirect:/index";} else {model.addAttribute("usernameMsg", map.get("usernameMsg"));model.addAttribute("passwordMsg", map.get("passwordMsg"));return "/site/login";}}
}
2、使用redis存储登录凭证:
① 存放登陆凭证的key :
package com.nowcoder.community.util;public class RedisKeyUtil {private static final String SPLIT = ":";private static final String PREFIX_TICKET = "ticket";public static String getTicketKey(String ticket){return PREFIX_TICKET+SPLIT+ticket;}
}
② 将登录凭证从数据库中改存在redis中:
package com.nowcoder.community.service;@Service
public class UserService implements CommunityConstant {@Autowiredprivate RedisTemplate redisTemplate;public Map<String, Object> login(String username, String password, int expiredSeconds) {Map<String, Object> map = new HashMap<>();// 空值处理if (StringUtils.isBlank(username)) {map.put("usernameMsg", "账号不能为空!");return map;}if (StringUtils.isBlank(password)) {map.put("passwordMsg", "密码不能为空!");return map;}// 验证账号User user = userMapper.selectByName(username);if (user == null) {map.put("usernameMsg", "该账号不存在!");return map;}// 验证状态if (user.getStatus() == 0) {map.put("usernameMsg", "该账号未激活!");return map;}// 验证密码password = CommunityUtil.md5(password + user.getSalt());if (!user.getPassword().equals(password)) {map.put("passwordMsg", "密码不正确!");return map;}// 生成登录凭证LoginTicket loginTicket = new LoginTicket();loginTicket.setUserId(user.getId());loginTicket.setTicket(CommunityUtil.generateUUID());loginTicket.setStatus(0);loginTicket.setExpired(new Date(System.currentTimeMillis() + expiredSeconds * 1000));
// loginTicketMapper.insertLoginTicket(loginTicket);String redisKey = RedisKeyUtil.getTicketKey(loginTicket.getTicket());redisTemplate.opsForValue().set(redisKey,loginTicket);map.put("ticket", loginTicket.getTicket());return map;}public void logout(String ticket) {
// loginTicketMapper.updateStatus(ticket, 1);String redisKey = RedisKeyUtil.getTicketKey(ticket);LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);loginTicket.setStatus(1);redisTemplate.opsForValue().set(redisKey,loginTicket);}public LoginTicket findLoginTicket(String ticket) {
// return loginTicketMapper.selectByTicket(ticket);String redisKey = RedisKeyUtil.getTicketKey(ticket);return (LoginTicket) redisTemplate.opsForValue().get(redisKey);}
}
3、使用redis缓存用户信息:
① 构建存储user信息的redis的key :
package com.nowcoder.community.util;public class RedisKeyUtil {private static final String SPLIT = ":";private static final String PREFIX_USER = "user";public static String getUserKey(int userId){return PREFIX_USER+SPLIT+userId;}
}
② 使用redis存储User:
-
取数据时,优先从缓存中取值
-
如果取不到,初始化缓存数据
-
当数据变更时,清楚缓存数据
package com.nowcoder.community.service;@Service
public class UserService implements CommunityConstant {@Autowiredprivate RedisTemplate redisTemplate;public User findUserById(int id) {User user = getCache(id);if(user==null){initCache(id);}return user;
// return userMapper.selectById(id);}public int activation(int userId, String code) {User user = userMapper.selectById(userId);if (user.getStatus() == 1) {return ACTIVATION_REPEAT;} else if (user.getActivationCode().equals(code)) {userMapper.updateStatus(userId, 1);//数据变更时,清除缓存clearCache(userId);return ACTIVATION_SUCCESS;} else {return ACTIVATION_FAILURE;}}public int updateHeader(int userId, String headerUrl) {
// return userMapper.updateHeader(userId, headerUrl);int count = userMapper.updateHeader(userId, headerUrl);clearCache(userId);return count;}//优先从缓存中取值,利用redis缓存用户信息public User getCache(int userId){String redisKey = RedisKeyUtil.getUserKey(userId);return (User)redisTemplate.opsForValue().get(redisKey);}//取不到时,初始化缓存public User initCache(int userId){User user = userMapper.selectById(userId);String redisKey = RedisKeyUtil.getUserKey(userId);redisTemplate.opsForValue().set(redisKey,user,3600, TimeUnit.SECONDS);return user;}//数据变更时,清除缓存public void clearCache(int userId){String redisKey = RedisKeyUtil.getUserKey(userId);redisTemplate.delete(redisKey);}
}
社区论坛P28 - Spring整合kafka
① http://kafka.apache.org/downloads下载kafka的kafka_2.12-2.3.0.tar
② 修改zookeeper.properties文件:
dataDir=D:/install/kafka/kafka/zookeeper/data
③ 修改server.properties文件:
log.dirs=D:/install/kafka/kafka/data
④ 在cmd窗口启动Zookeeper服务器:
D:\install\kafka\kafka>bin\windows\zookeeper-server-start.bat config\zookeeper.properties
⑤ 在cmd窗口启动kafka服务器:
D:\install\kafka\kafka>bin\windows\kafka-server-start.bat config\server.properties
⑥ 测试,重新开启一个cmd作为kafka生产者端:
# 创建主题
D:\install\kafka\kafka\bin\windows>kafka-topics.bat --create --bootstrap-server localhost:9092 --replication-factor 1 -partitions 1 --topic test# 查看对应服务器创建的主题
D:\install\kafka\kafka\bin\windows>kafka-topics.bat --list --bootstrap-server localhost:9092
test# 消息生产者发送消息
D:\install\kafka\kafka\bin\windows>kafka-console-producer.bat --broker-list localhost:9092 --topic test
>hello
⑦ 上个cmd窗口已被生产者占用,因此再开一个窗口作为消费者端消费消息:
D:\install\kafka\kafka\bin\windows>kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test --from-beginning
hello
⑧ 在项目中导入kafka依赖:
<dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId>
</dependency>
⑨ 配置kafka:
# KafkaProperties
spring.kafka.bootstrap-servers=localhost:9092
# 将配置文件consumer.properties中的test改成了community
spring.kafka.consumer.group-id=community-consumer-group
spring.kafka.consumer.enable-auto-commit=true
spring.kafka.consumer.auto-commit-interval=3000
⑩ 测试kafka:
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class KafkaTests {@Autowiredprivate KafkaProducer kafkaProducer;@Testpublic void testKafka() {kafkaProducer.sendMessage("test", "你好");kafkaProducer.sendMessage("test", "在吗");try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}}}@Component
class KafkaProducer {@Autowiredprivate KafkaTemplate kafkaTemplate;public void sendMessage(String topic, String content) {kafkaTemplate.send(topic, content);}
}@Component
class KafkaConsumer {@KafkaListener(topics = {"test"})public void handleMessage(ConsumerRecord record) {System.out.println(record.value());}
}
社区论坛P29 - 显示系统通知
需求分析:点赞,评论,关注分别为一个主题,当用户点赞、评论、关注某一个实体后需要给该实体发送一个系统通知
① 封装消息事件event,该消息中包含要发送的消息内容,及需要在页面中显示的数据:
public class Event {/*** 点赞、评论、关注各一个主题,当触发事件时将消息发送到对应的主题中*/private String topic;/*** 触发事件的用户id*/private int userId;/*** 实体类型,用户 niuke 点赞了你的回复, 点击查看 !(实体类型即为回复)*/private int entityType;private int entityId;private int entityUserId;/*** 其他数据*/private Map<String, Object> data = new HashMap<>();public String getTopic() {return topic;}public Event setTopic(String topic) {this.topic = topic;return this;}public int getUserId() {return userId;}public Event setUserId(int userId) {this.userId = userId;return this;}public int getEntityType() {return entityType;}public Event setEntityType(int entityType) {this.entityType = entityType;return this;}public int getEntityId() {return entityId;}public Event setEntityId(int entityId) {this.entityId = entityId;return this;}public int getEntityUserId() {return entityUserId;}public Event setEntityUserId(int entityUserId) {this.entityUserId = entityUserId;return this;}public Map<String, Object> getData() {return data;}public Event setData(String key, Object value) {this.data.put(key, value);return this;}
}
② kafka的消息生产者,将消息event发送到对应的主题中,在事件触发时调用:
@Component
public class EventProducer {@Autowiredprivate KafkaTemplate kafkaTemplate;// 处理事件public void fireEvent(Event event) {// 将事件发布到指定的主题kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));}}
③ kafka的消息消费者,从kafka的topic中获取到对应的消息event数据,然后封装成一个message数据插入数据库中:
public interface CommunityConstant {/*** 主题: 评论*/String TOPIC_COMMENT = "comment";/*** 主题: 点赞*/String TOPIC_LIKE = "like";/*** 主题: 关注*/String TOPIC_FOLLOW = "follow";/*** 系统用户ID*/int SYSTEM_USER_ID = 1;}
@Component
public class EventConsumer implements CommunityConstant {private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);@Autowiredprivate MessageService messageService;@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);}
}
④ 当用户点赞后,给点赞实体发送一个系统通知,封装消息所需要的数据,通过kafka的生产者将消息发送到点赞topic中:
@Controller
public class LikeController implements CommunityConstant {@Autowiredprivate LikeService likeService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate EventProducer eventProducer;@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);}return CommunityUtil.getJSONString(0, null, map);}
}
⑤ 用户评论后,给评论实体发送一个系统通知,封装消息所需要的数据,通过kafka的生产者将消息发送到评论topic中:
@Controller
@RequestMapping("/comment")
public class CommentController implements CommunityConstant {@Autowiredprivate CommentService commentService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate EventProducer eventProducer;@Autowiredprivate DiscussPostService discussPostService;@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);return "redirect:/discuss/detail/" + discussPostId;}
}
⑥ 用户关注后,给关注实体发送一个系统通知,封装消息所需要的数据,通过kafka的生产者将消息发送到关注topic中:
@Controller
public class FollowController implements CommunityConstant {@Autowiredprivate FollowService followService;@Autowiredprivate HostHolder hostHolder;@Autowiredprivate UserService userService;@Autowiredprivate EventProducer eventProducer;@RequestMapping(path = "/follow", method = RequestMethod.POST)@ResponseBodypublic String follow(int entityType, int entityId) {User user = hostHolder.getUser();followService.follow(user.getId(), entityType, entityId);// 触发关注事件Event event = new Event().setTopic(TOPIC_FOLLOW).setUserId(hostHolder.getUser().getId()).setEntityType(entityType).setEntityId(entityId).setEntityUserId(entityId);eventProducer.fireEvent(event);return CommunityUtil.getJSONString(0, "已关注!");}
}
社区论坛P30 - Spring整合elasticsearch
① 下载elasticsearch:https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-4-3
② 修改elasticsearch.yml文件:
cluster.name: nowcoderpath.data: D:/install/elasticsearch-6.4.3/datapath.logs: D:/install/elasticsearch-6.4.3/logs
③ 配置环境变量:D:\install\elasticsearch-6.4.3\bin
④ 下载ik分词器:https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v6.4.3
⑤ 解压缩,必须解压到D:\install\elasticsearch-6.4.3\plugins\ik目录下
⑥ 启动elasticsearch服务:点击elasticsearch.bat
⑦ 导入elasticsearch依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
⑧ 配置elasticsearch:
# ElasticsearchProperties
spring.data.elasticsearch.cluster-name=nowcoder
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
⑨ 测试:
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {}
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTests {@Autowiredprivate DiscussPostMapper discussMapper;@Autowiredprivate DiscussPostRepository discussRepository;@Autowiredprivate ElasticsearchTemplate elasticTemplate;@Testpublic void testInsert() {discussRepository.save(discussMapper.selectDiscussPostById(241));discussRepository.save(discussMapper.selectDiscussPostById(242));discussRepository.save(discussMapper.selectDiscussPostById(243));}@Testpublic void testInsertList() {discussRepository.saveAll(discussMapper.selectDiscussPosts(101, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(102, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(103, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(111, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(112, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(131, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(132, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(133, 0, 100));discussRepository.saveAll(discussMapper.selectDiscussPosts(134, 0, 100));}@Testpublic void testUpdate() {DiscussPost post = discussMapper.selectDiscussPostById(231);post.setContent("我是新人,使劲灌水.");discussRepository.save(post);}@Testpublic void testDelete() {// discussRepository.deleteById(231);discussRepository.deleteAll();}@Testpublic void testSearchByRepository() {SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content")).withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)).withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)).withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)).withPageable(PageRequest.of(0, 10)).withHighlightFields(new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")).build();// elasticTemplate.queryForPage(searchQuery, class, SearchResultMapper)// 底层获取得到了高亮显示的值, 但是没有返回.Page<DiscussPost> page = discussRepository.search(searchQuery);System.out.println(page.getTotalElements());System.out.println(page.getTotalPages());System.out.println(page.getNumber());System.out.println(page.getSize());for (DiscussPost post : page) {System.out.println(post);}}@Testpublic void testSearchByTemplate() {SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content")).withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC)).withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC)).withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)).withPageable(PageRequest.of(0, 10)).withHighlightFields(new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")).build();Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {@Overridepublic <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {SearchHits hits = response.getHits();if (hits.getTotalHits() <= 0) {return null;}List<DiscussPost> list = new ArrayList<>();for (SearchHit hit : hits) {DiscussPost post = new DiscussPost();String id = hit.getSourceAsMap().get("id").toString();post.setId(Integer.valueOf(id));String userId = hit.getSourceAsMap().get("userId").toString();post.setUserId(Integer.valueOf(userId));String title = hit.getSourceAsMap().get("title").toString();post.setTitle(title);String content = hit.getSourceAsMap().get("content").toString();post.setContent(content);String status = hit.getSourceAsMap().get("status").toString();post.setStatus(Integer.valueOf(status));String createTime = hit.getSourceAsMap().get("createTime").toString();post.setCreateTime(new Date(Long.valueOf(createTime)));String commentCount = hit.getSourceAsMap().get("commentCount").toString();post.setCommentCount(Integer.valueOf(commentCount));// 处理高亮显示的结果HighlightField titleField = hit.getHighlightFields().get("title");if (titleField != null) {post.setTitle(titleField.getFragments()[0].toString());}HighlightField contentField = hit.getHighlightFields().get("content");if (contentField != null) {post.setContent(contentField.getFragments()[0].toString());}list.add(post);}return new AggregatedPageImpl(list, pageable,hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());}});System.out.println(page.getTotalElements());System.out.println(page.getTotalPages());System.out.println(page.getNumber());System.out.println(page.getSize());for (DiscussPost post : page) {System.out.println(post);}}
}