第1章 初识SpringBoot ,开发社区首页
1.1 课程介绍
1.2 搭建开发环境
1.创建community项目
添加基本的依赖
pom.xml
<!--启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--web启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
编写配置文件
application.yml
server:
port: 8080
servlet:
context-path: /community
启动项目,输入http://localhost:8080/community
测试项目启动
2.导入数据库
评论表
CREATE TABLE `comment` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) DEFAULT NULL,
`entity_type` INT(11) DEFAULT NULL,
`entity_id` INT(11) DEFAULT NULL,
`target_id` INT(11) DEFAULT NULL,
`content` TEXT,
`status` INT(11) DEFAULT NULL,
`create_time` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_user_id` (`user_id`) /*!80000 INVISIBLE */,
KEY `index_entity_id` (`entity_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
帖子表
CREATE TABLE `discuss_post` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` VARCHAR(45) DEFAULT NULL,
`title` VARCHAR(100) DEFAULT NULL,
`content` TEXT,
`type` INT(11) DEFAULT NULL COMMENT '0-普通; 1-置顶;',
`status` INT(11) DEFAULT NULL COMMENT '0-正常; 1-精华; 2-拉黑;',
`create_time` TIMESTAMP NULL DEFAULT NULL,
`comment_count` INT(11) DEFAULT NULL,
`score` DOUBLE DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_user_id` (`user_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
登录凭证表
CREATE TABLE `login_ticket` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`user_id` INT(11) NOT NULL,
`ticket` VARCHAR(45) NOT NULL,
`status` INT(11) DEFAULT '0' COMMENT '0-有效; 1-无效;',
`expired` TIMESTAMP NOT NULL,
PRIMARY KEY (`id`),
KEY `index_ticket` (`ticket`(20))
) ENGINE=INNODB DEFAULT CHARSET=utf8;
消息表
CREATE TABLE `message` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`from_id` INT(11) DEFAULT NULL,
`to_id` INT(11) DEFAULT NULL,
`conversation_id` VARCHAR(45) NOT NULL,
`content` TEXT,
`status` INT(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;',
`create_time` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_from_id` (`from_id`),
KEY `index_to_id` (`to_id`),
KEY `index_conversation_id` (`conversation_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
用户表
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) DEFAULT NULL,
`password` VARCHAR(50) DEFAULT NULL,
`salt` VARCHAR(50) DEFAULT NULL,
`email` VARCHAR(100) DEFAULT NULL,
`type` INT(11) DEFAULT NULL COMMENT '0-普通用户; 1-超级管理员; 2-版主;',
`status` INT(11) DEFAULT NULL COMMENT '0-未激活; 1-已激活;',
`activation_code` VARCHAR(100) DEFAULT NULL,
`header_url` VARCHAR(200) DEFAULT NULL,
`create_time` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_username` (`username`(20)),
KEY `index_email` (`email`(20))
) ENGINE=INNODB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8;
3.使用Mybatis
导入依赖
pom.xml
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
编写配置文件
application.yml
spring:
# 数据源配置
datasource:
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/community?serverTimezone=UTC
type: com.zaxxer.hikari.HikariDataSource
hikari: # 连接池配置
maximum-pool-size: 15
minimum-idle: 5
idle-timeout: 30000
# mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.comunity.entity
configuration:
use-generated-keys: true # 自动生成主键
map-underscore-to-camel-case: true # 驼峰命名映射
编写实体类
/**
* 用户表
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String username;
private String password;
private String salt; //用来加密
private String email;
private int type;
private int status;
private String activationCode; //激活码
private String headerUrl;
private Date createTime;
}
UserMapper
@Mapper
public interface UserMapper {
// 通过id查询用户
User selectById(int id);
// 通过username查询用户
User selectByName(String username);
// 通过邮箱查询用户
User selectByEmail(String email);
// 新增用户
int insertUser(User user);
// 修改用户状态
int updateStatus(int id,int status);
// 修改用户头像
int updateHeader(int id,String headerUrl);
// 修改用户密码
int updatePassword(int id,String password);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.community.dao.UserMapper">
<sql id="selectFields">
id,username,password,salt,email,type,status,activation_code,header_url,create_time
</sql>
<sql id="insertFields">
username,password,salt,email,type,status,activation_code,header_url,create_time
</sql>
<select id="selectById" resultType="User">
select
<include refid="selectFields"></include>
from user
where id = #{id}
</select>
<select id="selectByName" resultType="User">
select
<include refid="selectFields"></include>
from user
where username = #{username}
</select>
<select id="selectByEmail" resultType="User">
select
<include refid="selectFields"></include>
from user
where email = #{email}
</select>
<!--mybatis自动生成id,通过keyProperty将id填入user 不然为空-->
<insert id="insertUser" parameterType="User" keyProperty="id">
insert into user(<include refid="insertFields"></include>)
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="updateHeader">
update user
set header_url=#{headerUrl}
where id = #{id}
</update>
<update id="updatePassword">
update user
set password=#{password}
where id = #{id}
</update>
</mapper>
编写测试类用来测试sql
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MapperTests {
@Autowired
private UserMapper userMapper;
}
查询
@Test
public void TestSelectUser() {
User user = userMapper.selectById(101);
System.out.println(user);
user= userMapper.selectByName("张三");
System.out.println(user);
user = userMapper.selectByEmail("1345313@sina.com");
System.out.println(user);
}
插入
@Test
public void TestInsertUser(){
User user = new User();
user.setUsername("李四");
user.setPassword("123456");
user.setSalt("abc");
user.setEmail("lisi@qq.com");
user.setHeaderUrl("");
user.setCreateTime(new Date());
int row = userMapper.insertUser(user);
System.out.println(row);
System.out.println(user.getId());
}
修改
@Test
public void TestUpdateUser(){
int row = userMapper.updateStatus(123, 1);
System.out.println(row);
row = userMapper.updateHeader(123, "123.com");
System.out.println(row);
row = userMapper.updatePassword(123, "23123");
System.out.println(row);
}
1.3 开发社区首页
1. 准备环境
导入静态资源
引入thymealeaf
pom.xml
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
关闭thymealeaf模板引擎
application.yml
# 关闭缓存
thymeleaf:
cache: false
2. 代码开发
实体类
DiscussPost
/**
* 帖子表
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DiscussPost {
private Integer id;
private Integer userId;
private String title;
private String content;
// 类型 0:普通 1:置顶
private Integer type;
// 状态 0:正常 1:精华 2:拉黑
private Integer status;
private Date createTime;
private Integer commentCount; // 评论数量
private double score;
}
数据访问层
DiscussPostMapper
@Mapper
public interface DiscussPostMapper {
// 根据用户id查询帖子列表,动态sql(也可以不使用userId)
List<DiscussPost> selectDiscussPosts(int userId,int offset,int limit);
// 查询数据行数 动态拼接条件且只有一个参数时 需要@Param
int selectDiscussPostRows(@Param("userId") int userId);
}
DiscussPostMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.community.dao.DiscussPostMapper">
<sql id="selectFields">
id,user_id,title,content,type,status,create_time,comment_count,score
</sql>
<select id="selectDiscussPosts" resultType="DiscussPost">
select
<include refid="selectFields"></include>
from discuss_post
where status!=2
<if test="userId!=0">
and user_id=#{userId}
</if>
order by type desc,create_time desc
limit #{offset},#{limit}
</select>
<select id="selectDiscussPostRows" resultType="int">
select count(id)
from discuss_post
where status!=2
<if test="userId!=0">
and user_id=#{userId}
</if>
</select>
</mapper>
测试sql
/**
* DiscussPostMapper
*/
@Test
public void testSelectPosts(){
List<DiscussPost> list = discussPostMapper.selectDiscussPosts(0, 0, 10);
for (DiscussPost discussPost : list) {
System.out.println(discussPost);
}
int rows = discussPostMapper.selectDiscussPostRows(0);
System.out.println(rows);
}
数据业务层
DiscussPostService
@Service
public class DiscussPostService {
@Autowired
private DiscussPostMapper discussPostMapper;
/**
* 查询帖子列表
* @param userId
* @param offset
* @param limit
* @return
*/
public List<DiscussPost> findDiscussPosts(int userId,int offset,int limit){
return discussPostMapper.selectDiscussPosts(userId, offset, limit);
}
/**
* 获取帖子行数
* @param userId
* @return
*/
public int findDiscussPostRows(int userId){
return discussPostMapper.selectDiscussPostRows(userId);
}
}
UserService
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 根据id查询用户信息
* @param id
* @return
*/
public User findUserById(int id){
return userMapper.selectById(id);
}
}
视图层
HomeController
@Controller
public class HomeController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private UserService userService;
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String GetIndexPage(Model model){
List<DiscussPost> list = discussPostService.findDiscussPosts(0, 0, 10);
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);
discussPosts.add(map);
}
}
model.addAttribute("discussPosts",discussPosts);
return "/index";
}
}
index.html
启动测试
输入http://localhost:8080/community/index
实现分页
Page
/**
* 封装分页相关信息
*/
@Data
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;
}
}
/**
* 获取当前页的起始页
*
* @return
*/
public int getOffset() {
// current * limit -limit
return (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代码
HomeController
@RequestMapping(path = "/index", method = RequestMethod.GET)
public String GetIndexPage(Model model, Page page){
// 方法调用前,SpringMVC会自动实力胡model和page,并将Page注入Model
// 所以,在thymealeaf中可以直接访问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);
discussPosts.add(map);
}
}
model.addAttribute("discussPosts",discussPosts);
return "/index";
}
将页面静态效果修改为动态效果
<!-- 分页 -->
<nav class="mt-5" th:if="${page.rows>0}" th:fragment="pagination">
<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.getFrom(),page.getTo())}">
<a class="page-link" th:href="@{${page.path}(current=${i})}" th:text="${i}">1</a>
</li>
<li th:class="|page-item ${page.current==page.getTotal()?'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>
第2章 SpringBoot实践,开发社区登录模块
1、发送邮件
启用SMTP服务
jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
编写配置文件
application.yml
这里邮件发送我用的是qq邮箱
工具类MailSender
/**
* 邮件发送
*/
@Component
public class MailClient {
// 声明log
private static final Logger logger= LoggerFactory.getLogger(MailClient.class);
@Autowired
private JavaMailSender mailSender;
// 发件人
@Value("${spring.mail.username}")
private String from;
/**
* 发送邮件
* @param to 收件人
* @param subject 标题
* @param content 内容
*/
public void sendMail(String to,String subject,String content){
try {
// 创建message对象
MimeMessage message =mailSender.createMimeMessage();
MimeMessageHelper helper =new MimeMessageHelper(message);
// 设置邮件信息
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content,true); // 可转换html标签
// 发送邮件
mailSender.send(helper.getMimeMessage());
} catch (MessagingException e) {
logger.error("发送邮件失败"+e.getMessage());
}
}
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class MailTests {
@Autowired
private MailClient mailClient;
@Test
public void testTextMail(){
mailClient.sendMail("@qq.com","test","welcome");
}
}
接收到的邮件效果
利用Html模板发送邮件
templates/mail/activation.html
引入thymeleaf,传入一个username参数
发送模板邮件测试
@Autowired
private TemplateEngine templateEngine;
@Test
public void testHtmlMail(){
Context context=new Context();
context.setVariable("username", "sunday");
String content=templateEngine.process("/mail/activation",context);
System.out.println(content);
mailClient.sendMail("@qq.com","html",content);
}
控制台输出
邮箱内容
2、开发注册功能
2.1 访问注册页面
编写LoginController来获取注册页面
LoginController
@Controller
public class LoginController {
/**
* 获取注册页面
* @return
*/
@RequestMapping(value = "/register",method = RequestMethod.GET)
public String getRegisterPage(){
return "/site/register";
}
}
使用thymeleaf对页面进行修改
静态资源路径
修改主页的头部信息
效果
点击注册进入注册页面
2.2 提交注册数据
准备工作
- 导入jar包
<!--commons-lang-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
- 配置网站域名
# community配置
community:
path:
domain: http://localhost:8080
- 编写工具类
CommunityUtil
用来生成随机字符串
public class CommunityUtil {
/**
* 生成随机字符串
* @return
*/
public static String generateUUID(){
return UUID.randomUUID().toString().replaceAll("-","");
}
/**
* MD5加密
* @param key
* @return
*/
public static String md5(String key){
// 空格 null 空串都为空
if (StringUtils.isBlank(key)){
return null;
}
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
代码开发
UserService
/**
* 用户注册
* @param user
* @return
*/
public Map<String, Object> register(User user) {
Map<String, Object> map = new HashMap<>();
// 1. 空值处理
if (user == null) {
throw new IllegalArgumentException("参数不能为空!");
}
if (StringUtils.isBlank(user.getUsername())) {
map.put("usernameMsg", "账号不能为空!");
return map;
}
if (StringUtils.isBlank(user.getPassword())) {
map.put("passwordMsg", "密码不能为空!");
return map;
}
if (StringUtils.isBlank(user.getEmail())) {
map.put("emailMsg", "邮箱不能为空!");
return map;
}
// 2.1 验证账号
User u = userMapper.selectByName(user.getUsername());
if (u != null) {
map.put("usernameMsg", "该账号已存在!");
return map;
}
// 2.2 验证邮箱
u = userMapper.selectByEmail(user.getEmail());
if (u != null) {
map.put("emailMsg", "该邮箱已被注册!");
return map;
}
// 3. 注册用户
// 获取随机字符串
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.setHeaderUrl(String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000))); // 随机头像
user.setCreateTime(new Date());
// 添加用户
userMapper.insertUser(user);
// 4.发送激活邮件
Context context = new Context();
context.setVariable("email", user.getEmail());
// http://localhost:8080/community/activation/101/code
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;
}
LoginController
/**
* 用户注册
* @param model
* @param user
* @return
*/
@RequestMapping(value = "/register", method = RequestMethod.POST)
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";
}
model.addAttribute("usernameMsg",map.get("usernameMsg"));
model.addAttribute("passwordMsg",map.get("passwordMsg"));
model.addAttribute("emailMsg",map.get("emailMsg"));
return "/site/register";
}
页面处理
发送验证码邮件页面 activation.html
注册成功中转页面 operate-result.html
注册页面 register.html
测试效果
测试是否可以注册已经注册过的账号
或者邮箱
注册成功页面跳转
成功发送邮件
2.3 激活注册账号
准备工作
编写常量类,来记录账号的状态,
/**
* 常量
*/
public interface CommunityConstant {
/**
* 激活成功
*/
int ACTIVATION_SUCCESS = 0;
/**
* 激活成功
*/
int ACTIVATION_REPEAT = 1;
/**
* 激活失败
*/
int ACTIVATION_FAILURE = 2;
}
使用常量接口时,需要先实现接口
代码开发
UserService
/**
* 激活账号
*
* @param userId
* @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;
}
}
LoginController
/**
* 获取登录页面
* @return
*/
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String getLoginPage(){
return "/site/login";
}
/**
* 激活账号
*
* @param model
* @param userId
* @param code
* @return
*/
// http://localhost:8080/community/activation/101/code
@RequestMapping(value = "/activation/{userId}/{code}", method = RequestMethod.GET)
public String activation(Model model, @PathVariable("userId") int userId, @PathVariable("code") String code) {
int result = userService.activation(userId, code);
if (result == ACTIVATION_SUCCESS) {
model.addAttribute("msg", "激活成功,你的账号已经可以正常使用了");
model.addAttribute("target", "/login");
} else if (result == ACTIVATION_REPEAT) {
model.addAttribute("msg", "无效操作,你的账号已激活");
model.addAttribute("target", "/index");
} else {
model.addAttribute("msg", "激活失败,激活码不正确");
model.addAttribute("target", "/index");
}
return "/site/operate-result";
}
修改登录页面login.html
在主页index.html
中需改登陆页面的路径
效果
点击登录,可以跳转到登录页面
点击邮件中的此链接
即可激活账号
3、生成验证码
准备工作
- 导入jar包
<!--kaptcha-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
- 编写Kaptcha配置
/**
* 验证码配置类
*/
@Configuration
public class KaptchaConfig {
@Bean
public Producer kaptchaProducer(){
Properties properties = new Properties();
// 图片
properties.setProperty("kaptcha.image.width","100");
properties.setProperty("kaptcha.image.height","40");
// 字体
properties.setProperty("kaptcha.textproducer.font.size","32");
properties.setProperty("kaptcha.textproducer.font.color","0,0,0");
// 内容
properties.setProperty("kaptcha.textproducer.char.string","0123456789ASCDEFGHIJKLMNOPQRSTUVWXYZ");
properties.setProperty("kaptcha.textproducer.char.length","4");
// 无干扰
properties.setProperty("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
DefaultKaptcha kaptcha = new DefaultKaptcha();
Config config = new Config(properties);
kaptcha.setConfig(config);
return kaptcha;
}
}
- LoginController
编写方法为前端页面提供图片验证码
/**
* 获取图片验证码
* @param response
* @param session
*/
@RequestMapping(value = "/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);
// 将图片输出给浏览器
response.setContentType("image/png"); // 响应类型
try {
ServletOutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
} catch (IOException e) {
logger.error("相应验证码失败:" + e.getMessage());
}
}
测试
输入localhost:8080/community/kaptcha
可以产看已经生成的验证码
4. 修改前端页面
login.html
刷新验证码
4、登录、退出功能
4.1 准备工作
实体类
LoginTicket
/**
* 登陆凭证表
*/
@Data
public class LoginTicket {
// 主键
private int id;
// 用户id
private int userId;
// 凭证
private String ticket;
// 状态 0:正常 1:过期
private int status;
// 过期时间
private Date expired;
}
4.2 登录
LoginTicketMapper
编写所需功能的Mapper接口
@Mapper
public interface LoginTicketMapper {
// 插入一个凭证
@Insert({
"insert into login_ticket(user_id,ticket,status,expired) ",
"values(#{userId},#{ticket},#{status},#{expired}) "
})
@Options(useGeneratedKeys = true,keyProperty = "id") // id自增,id为主键
int insertLoginTicket(LoginTicket loginTicket);
// 根据ticket查询
@Select({
"select id,user_id,ticket,status,expired",
"from login_ticket where ticket=#{ticket}"
})
LoginTicket selectByTicket(String ticket);
// 修改状态(相当于删除)
@Update({
"update login_ticket set status=#{status} ",
"where ticket=#{ticket}"
})
int updateStatus(String ticket,int status);
}
UserService
由于登录跟用户相关,所以直接写到UserService
中
/**
* 用户登录
*
* @param username
* @param password
* @param expiredSeconds
* @return
*/
public Map<String, Object> login(String username, String password, long 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);
map.put("ticket",loginTicket.getTicket());
return map;
}
注意:若过期事件为int类型,数据会造成溢出,数据库中的过期时间会与结果有所差别。
LoginController
/**
* 用户登录
* @param username
* @param password
* @param code
* @param rememberMe
* @param model
* @param session
* @param response
* @return
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(String username, String password, String code, boolean rememberMe,
Model model, HttpSession session, HttpServletResponse response) {
// 检查验证码
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 ? 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";
}
}
前端页面修改
login.html
4.3 退出
UserService
/**
* 退出
* @param ticket
*/
public void logout(String ticket){
loginTicketMapper.updateStatus(ticket,1);
}
LoginController
/**
* 退出
* @param ticket
* @return
*/
@RequestMapping(value = "/logout",method = RequestMethod.GET)
public String logout(@CookieValue("ticket") String ticket){
userService.logout(ticket);
return "redirect:/login";
}
5、显示登录信息
执行流程
代码实现
5.1 在请求开始时查询登录用户
UserService
/**
* 根据ticket查询登录凭证信息
* @param ticket
* @return
*/
public LoginTicket findLoginTicket(String ticket){
return loginTicketMapper.selectByTicket(ticket);
}
LoginTicketInterceptor
// controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.从cookie中获取ticket
String ticket = CookieUtil.getValue(request, "ticket");
// 2.查询凭证
if (ticket != null) {
LoginTicket loginTicket = userService.findLoginTicket(ticket);
// 凭证是否有效
if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
// 根据凭证id查询到用户
User user = userService.findUserById(loginTicket.getUserId());
// 在本次请求中持有用户
hostHolder.setUser(user);
}
}
return true;
}
5.2 在本次请求中持有用户数据
CookieUtil
public class CookieUtil {
public static String getValue(HttpServletRequest request, String name) {
if (request == null || name == null) {
throw new IllegalArgumentException("参数为空");
}
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie.getValue();
}
}
}
return null;
}
}
5.3 在模板视图上显示用户数据
LoginInterceptor
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//得到当前线程持有的user
User user = hostHolder.getUser();
System.out.println(user);
if (user != null && modelAndView != null) {
modelAndView.addObject("loginUser", user);
}
}
index.html
5.4 在请求结束时清理用户数据
UserService
//修改凭证
public void logout(String ticket){
loginTicketMapper.updateStatus(ticket,1);
}
LoginInterceptor
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清理数据
hostHolder.clear();
}
5.5 注册bean
WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.excludePathPatterns("/**/*.css","/**/*.js","/**/*.png","/**/*.jpg","/**/*.jpeg");//拦截所有请求
}
}
6、账号设置
6.1 访问账设置页面
创建UserController来获取setting页面
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "/setting",method = RequestMethod.GET)
public String getSettingPage(){
return "/site/setting";
}
}
修改前端页面
index.html
setting.html
效果
点击头像上的账号设置即可跳转到setting.html
页面
6.2 上传头像
UserController
/**
* 上传头像
*
* @param headerImage
* @param model
* @return
*/
@RequestMapping(value = "/upload", method = RequestMethod.POST)
public String uploadHeader(MultipartFile headerImage, Model model) {
if (headerImage == null) {
model.addAttribute("error", "你还没有选择图片");
// 回到设置页面
return "/site/setting";
}
// 得到原始文件名
String filename = headerImage.getOriginalFilename();
// 获取文件名后缀
String suffix = filename.substring(filename.lastIndexOf("."));
// 对后缀名进行判断
if (StringUtils.isBlank(suffix)) {
model.addAttribute("error", "文件格式不正确");
return "/site/setting";
}
// 生成随机文件名
filename = CommunityUtil.generateUUID() + suffix;
// 文件存储位置
File dest = new File(uploadPath);
// 如果文件目录不存在
if (!dest.exists()) {
// 自动生成文件夹
dest.mkdirs();
}
try {
// 存储文件
headerImage.transferTo(new File(dest +"/"+ filename));
} catch (IOException e) {
logger.error("上传文件失败" + e.getMessage());
throw new RuntimeException("上传文件失败,服务器发生异常!", e);
}
// 更新当前用户头像路径(web访问路径)
// http://localhost:8080/community/user/header/xxx.png
User user = hostHolder.getUser();
String headerUrl = domain + contextPath + "/user/header/" + filename;
userService.updateHeader(user.getId(), headerUrl);
//重定向回首页
return "redirect:/index";
}
需要注意的是:截取后缀名时,一定要注意截取的位置是否准确,jpg
还是.jpg
前端页面修改
setting.html
6.3 获取头像
UserController
/**
* 获取头像
*
* @param fileName
* @param response
*/
@RequestMapping(value = "/header/{fileName}", method = RequestMethod.GET)
public void getHeader(@PathVariable("fileName") String fileName, HttpServletResponse response) {
// 服务器存放的路径
fileName = uploadPath + "/" + fileName;
// 获取文件后缀
String suffix = fileName.substring(fileName.lastIndexOf(".")+1);
// 响应图片
response.setContentType("image/" + suffix);
try (
FileInputStream fis = new FileInputStream(fileName);
OutputStream os = response.getOutputStream();
) {
// 建立缓冲区
byte[] buffer = new byte[1024];
int b = 0;
while ((b = fis.read(buffer)) != -1) {
os.write(buffer, 0, b);
}
} catch (IOException e) {
logger.error("读取头像失败" + e.getMessage());
}
}
6.4 修改密码
UserService
我们需要获取到原来的密码、新密码、确认密码来进行修改密码以及判断。
//修改密码
public Map<String, Object> updatePassword(User user, String oldPassword, String newPassword, String checkPassword) {
Map<String, Object> map = new HashMap<>();
if (StringUtils.isBlank(oldPassword)) {
map.put("oldPasswordMsg", "原始密码不能为空");
return map;
}
if (StringUtils.isBlank(newPassword)) {
map.put("newPasswordMsg", "新的密码不能为空");
return map;
}
if (StringUtils.isBlank(checkPassword)) {
map.put("checkPasswordMsg", "两次密码不能为空");
return map;
}
if (!newPassword.equals(checkPassword)) {
map.put("checkPasswordMsg", "两次密码不一致");
return map;
}
//获取旧密码
oldPassword = CommunityUtil.md5(newPassword + user.getSalt());
if (!oldPassword.equals(user.getPassword())) {
map.put("oldPassword", "原始密码错误");
return map;
}
System.out.println("旧密码---"+oldPassword);
//获取新密码
newPassword=CommunityUtil.md5(newPassword+user.getSalt());
System.out.println("新密码---"+newPassword);
//修改密码
userMapper.updatePassword(user.getId(), newPassword);
return map;
}
UserController
//修改密码
@LoginRequired
@RequestMapping(value = "/updatePassword", method = RequestMethod.POST)
public String updatePassword(Model model, String oldPassword, String newPassword, String checkPassword) {
//获取user对象
User user = hostHolder.getUser();
//System.out.println("获得到的user:"+user);//d82c0e5494e88f041cab523fd31881b6
//System.out.println(newPassword);//123
Map<String, Object> map = userService.updatePassword(user, oldPassword, newPassword, checkPassword);
//判空
if (map == null || map.isEmpty()) {
return "redirect:/index";
} else {
System.out.println("进入报错区");
model.addAttribute("oldPasswordMsg", map.get("oldPasswordMsg"));
model.addAttribute("newPasswordMsg",map.get("newPasswordMsg"));
model.addAttribute("checkPasswordMsg",map.get("checkPasswordMsg"));
return "/site/setting";
}
}
修改前端页面
setting.html
7、检查登录状态
编写自定义注解LoginRequired
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
在需要拦截的方法上添加注解
编写拦截器
拦截只有loginRequired
注解的请求
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果拦截的是方法
if (handler instanceof HandlerMethod){
//转型
HandlerMethod handlerMethod= (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);
if (loginRequired!=null&&hostHolder.getUser()==null){
response.sendRedirect(request.getContextPath()+"/login");
return false;
}
}
return true;
}
}
在WebMvcConfig中注册拦截器
第3章 核心功能
1、敏感词过滤
前缀树
构建一颗前缀树
/**
* 1.定义前缀树
*/
private class TrieNode {
// 标志位
private boolean isKeywordEnd = false;
// 子节点 map key: 下级字符 value:下级节点
Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
// 获取子节点
public TrieNode getSubNodes(Character c) {
return subNodes.get(c);
}
// 添加子节点的方法
public void addSubNodes(Character c, TrieNode node) {
subNodes.put(c, node);
}
}
初始化前缀树
/**
* 2.初始化前缀树
*/
@PostConstruct
public void init() {
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while ((keyword = reader.readLine()) != null) {
// 添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感过滤器文件失败:" + e.getMessage());
}
}
检索敏感词并替换
/**
* 3.检索敏感词
*
* @param text 待过滤文本
* @return 过滤后文本
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
// 初始化指针
// 指针1
TrieNode tempNode = rootNode;
// 指针2
int begin = 0;
// 指针3
int position = 0;
// 返回的结果
StringBuilder sb = new StringBuilder();
// 开始过滤
while (position < text.length()) {
char c = text.charAt(position);
// 跳过符号
if (isSymbol(c)) {
// 若指针1处于root节点,将符号计入结果,指针2向下走一步
if (tempNode == rootNode) {
sb.append(c);
begin++;
}
// 无论符号在开头或中间,指针3都向下走一步
position++;
continue;// 进入下一轮循环
} else {
// 检查下级节点
tempNode = tempNode.getSubNodes(c);
if (tempNode == null) {
// 以begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一个位置
position = ++begin;
// 重新指向根节点
tempNode = rootNode;
} else if (tempNode.isKeywordEnd()) {
// 发现敏感词,将begin~position字符串替换掉
sb.append(REPLACEMENT);
// 进入下一个位置
begin = ++position;
// 重新指向根节点
tempNode = rootNode;
} else {
// 检查下一个节点
position++;
}
}
}
// 将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}
整体代码
SensitiveFilter
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
// 定义替换符
private static final String REPLACEMENT = "***";
// 定义根节点
private TrieNode rootNode = new TrieNode();
/**
* 2.初始化前缀树
*/
@PostConstruct
public void init() {
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while ((keyword = reader.readLine()) != null) {
// 添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感过滤器文件失败:" + e.getMessage());
}
}
/**
* 将敏感词添加到前缀树
*
* @param keyword
*/
private void addKeyword(String keyword) {
// 根节点
TrieNode tempNode = rootNode;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
// 获取子节点
TrieNode subNode = tempNode.getSubNodes(c);
// 判断子节点是否存在
if (subNode == null) {
// 初始化子节点
subNode = new TrieNode();
tempNode.addSubNodes(c, subNode);
}
// 如果已存在 指向子节点,进入下一轮循环
tempNode = subNode;
// 设置结束标识
if (i == keyword.length() - 1) {
tempNode.setKeywordEnd(true);
}
}
}
/**
* 3.检索敏感词
*
* @param text 待过滤文本
* @return 过滤后文本
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
// 初始化指针
// 指针1
TrieNode tempNode = rootNode;
// 指针2
int begin = 0;
// 指针3
int position = 0;
// 返回的结果
StringBuilder sb = new StringBuilder();
// 开始过滤
while (position < text.length()) {
char c = text.charAt(position);
// 跳过符号
if (isSymbol(c)) {
// 若指针1处于root节点,将符号计入结果,指针2向下走一步
if (tempNode == rootNode) {
sb.append(c);
begin++;
}
// 无论符号在开头或中间,指针3都向下走一步
position++;
continue;// 进入下一轮循环
} else {
// 检查下级节点
tempNode = tempNode.getSubNodes(c);
if (tempNode == null) {
// 以begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一个位置
position = ++begin;
// 重新指向根节点
tempNode = rootNode;
} else if (tempNode.isKeywordEnd()) {
// 发现敏感词,将begin~position字符串替换掉
sb.append(REPLACEMENT);
// 进入下一个位置
begin = ++position;
// 重新指向根节点
tempNode = rootNode;
} else {
// 检查下一个节点
position++;
}
}
}
// 将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}
/**
* 判断是否为符号
*
* @param c
* @return
*/
private boolean isSymbol(char c) {
// 0x2E80 ~ 0x9FFF 是东亚文字范围
return !CharUtils.isAsciiPrintable(c) && (c < 0x2E80 || c > 0x9FFF);
}
/**
* 1.定义前缀树
*/
private class TrieNode {
// 标志位
private boolean isKeywordEnd = false;
// 子节点 map key: 下级字符 value:下级节点
Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
// 获取子节点
public TrieNode getSubNodes(Character c) {
return subNodes.get(c);
}
// 添加子节点的方法
public void addSubNodes(Character c, TrieNode node) {
subNodes.put(c, node);
}
}
}
创建敏感词文件
sensitive-words.txt
赌博
嫖娼
吸毒
测试功能是否生效
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class SensitiveFilterTest {
@Autowired
private SensitiveFilter sensitiveFilter;
@Test
public void testSensitiveFilter(){
String text="我要吸毒,嫖娼,赌博";
text=sensitiveFilter.filter(text);
System.out.println(text);
text="我要※吸※毒※,※嫖※娼※,※赌※博※";
text=sensitiveFilter.filter(text);
System.out.println(text);
}
}
效果
2、发布帖子
点击主页的发布帖子,发送ajax请求
- 引入fastjson
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
- 在工具类中编写返回json对象方法
CommunityUtil
/**
* 返回json格式对象
* @param code
* @param msg
* @param map
* @return
*/
public static String getJSONString(int code, String msg, Map<String,Object> map){
JSONObject json = new JSONObject();
json.put("code",code);
json.put("msg",msg);
if(map!=null){
for (String key:map.keySet()){
json.put(key,map.get(key));
}
}
return json.toJSONString();
}
public static String getJSONString(int code, String msg){
return getJSONString(code,msg,null);
}
public static String getJSONString(int code){
return getJSONString(code,null,null);
}
- 业务开发
DiscussPostMapper
//增加帖子的方法
int insertDiscussPost(DiscussPost discussPost);
DiscussPostMapper.xml
<sql id="insertFields">
user_id,title,content,type,status,create_time,comment_count,score
</sql>
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values (#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
DiscussPostService
/**
* 新增帖子
* @param post
* @return
*/
public int addDiscussPost(DiscussPost post){
if(post==null){
throw new IllegalArgumentException("参数不能为空");
}
// 转义html标签
post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
post.setContent(HtmlUtils.htmlEscape(post.getContent()));
// 过滤敏感词
post.setTitle(sensitiveFilter.filter(post.getTitle()));
post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);
}
DiscussController
@Controller
@RequestMapping("/discuss")
public class DiscussPostController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(value = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if (user == null) {
return CommunityUtil.getJSONString(403, "你还没有登录");
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
// 报错的请求,将来统一处理
return CommunityUtil.getJSONString(0, "发布成功!");
}
}
- 处理前端页面
index.html
index.js
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
//获取标题和内容 id选择器
var title = $("#recipient-name").val();
var content = $("#message-text").val();
//发送异步请求
$.post(
//CONTEXT_PATH+"/discuss/add",
"/community/discuss/add",
{"title": title, "content": content},
function (data) {
console.log(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);
}
);
}
3、帖子详情
3.1 DiscussPostMapper
//查询帖子
DiscussPost selectDiscussPostById(int id);
DiscussPostMapper.xml
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id=#{id}
</select>
3.2 DiscussPostService
//根据ID查询帖子
public DiscussPost findDiscussPostById(int id){
return mapper.selectDiscussPostById(id);
}
3.3 DiscussPostController
//根据id查询帖子
@RequestMapping(value = "/detail/{discussPostId}",method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model){
//帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post",post);
//关联查询
User user =userService.findUserById(post.getUserId());
System.out.println(user);
model.addAttribute("user",user);
return "/site/discuss-detail";
}
3.4 index.html
index.html
discuss-detail.html
4、显示评论
4.1 数据层
Comment
/**
* 评论表
*/
@Data
public class Comment {
//主键
private int id;
//用户id 谁发布的
private int userId;
// 评论的目标,帖子或评论、用户。。。
private int entityType;
// 目标id 帖子
private int entityId;
// 目标
private int targetId;
// 内容
private String content;
//状态 0:可用 1:禁用
private int status;
//时间
private Date createTime;
}
CommentMapper
@Mapper
public interface CommentMapper {
// 根据实体查询评论
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
// 根据实体查询总数
int selectCountByEntity(int entityType,int entityId);
}
CommentMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.peng.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 comment
where status=0
and entity_type=#{entityType}
and entity_id=#{entityId}
order by create_time asc
limit #{offset},#{limit}
</select>
<select id="selectCountByEntity" resultType="int">
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
</mapper>
4.2 业务层
CommentService
@Service
public class CommentService {
@Autowired
private CommentMapper commentMapper;
// 根据实体查询评论
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
}
// 根据实体查询总数
public int findCommentCountByEntity(int entityType, int entityId) {
return commentMapper.selectCountByEntity(entityType, entityId);
}
}
4.3 表现层
DiscussPostController
/**
* 根据id查询帖子
*
* @param discussPostId
* @param model
* @return
*/
@RequestMapping(value = "/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);
// 评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/" + post.getId());
page.setRows(post.getCommentCount());
// 评论:给帖子的评论
// 回复:给帖子的评论
// 评论列表
List<Comment> commentList = commentService.
findCommentsByEntity(ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
// 返回结果
List<Map<String, Object>> commentVoList = new ArrayList<>();
if (commentList != null) {
for (Comment comment : commentList) {
// 一个评论的VO
Map<String, Object> commentVo = new HashMap<>();
// 添加评论
commentVo.put("comment", comment);
// 添加作者
commentVo.put("user", userService.findUserById(comment.getUserId()));
// 回复列表(评论的评论)
List<Comment> replyList = commentService
.findCommentsByEntity(ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
// 回复Vo列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
Map<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply", reply);
// 作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
// 回复的目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
// 存入集合
replyVoList.add(replyVo);
}
}
// 把回复存入commentVo
commentVo.put("replys", replyVoList);
System.out.println(replyVoList);
// 回复数量
int replyCount = commentService.findCommentCountByEntity(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
// 存入集合
commentVoList.add(commentVo);
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
}
index.html
discuss-detail.html
分页的复用
5、添加评论
5.1 数据层
CommentMapper
@Mapper
public interface CommentMapper {
int insertComment(Comment comment);
}
CommentMapper.xml
<insert id="insertComment" parameterType="Comment" keyProperty="id">
insert into comment(<include refid="insertFields"></include>)
values (#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</insert>
5.2 业务层
CommentService
// 添加评论
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public int insertComment(Comment comment) {
// 判空
if (comment == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 转义标签
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
// 过滤敏感词
comment.setContent(sensitiveFilter.filter(comment.getContent()));
// 添加评论
int rows = commentMapper.insertComment(comment);
// 更新评论数量
if (comment.getEntityType() == ENTITY_TYPE_POST) {
// 获取原来的评论数量
int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
// 更新评论数量
discussPostService.updateCommentCount(comment.getEntityId(), count);
}
return rows;
}
5.3 表现层
CommentController
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
/**
* 添加评论
* @param discussPostId
* @param comment
* @return
*/
@RequestMapping(value = "/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.insertComment(comment);
return "redirect:/discuss/detail/"+discussPostId;
}
}
discuss-detail.html
6、私信列表
6.1 私信列表
数据持久层
Message
/**
* 私信表
*/
@Data
public class Message {
private int id;
private int fromId;
private int toId;
private String conversationId;
private String content;
// 0:未读 1:已读 2:删除
private int status;
private Date createTime;
}
MessageMapper
@Mapper
public interface MessageMapper {
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
List<Message> selectConversations(int userId,int offset,int limit);
// 查询当前用户的会话数量
int selectConversationCount(int userId);
// 查询某个会话所包含的私信列表
List<Message> selectLetters(String conversationId,int offset,int limit);
// 查询某个会话所包含的私信数量
int selectLetterCount(String conversationId);
// 查询未读私信的数量
int selectLetterUnreadCount(int userId,String conversationId);
}
MessageMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.community.dao.MessageMapper">
<sql id="selectFields">
id,from_id,to_id,conversation_id,content,status,create_time
</sql>
<select id="selectConversations" resultType="Message">
select
<include refid="selectFields"></include>
from message
where id in(
SELECT MAX(id) FROM message
WHERE STATUS!=2 AND from_id!=1 AND(from_id = #{userId} or to_id=#{userId})
GROUP BY conversation_id
)
order by id desc
limit #{offset},#{limit}
</select>
<select id="selectConversationCount" resultType="int">
select count(m.maxid)
from (
SELECT MAX(id) maxid
FROM message
WHERE STATUS != 2
AND from_id != 1
AND (from_id = #{userId} or to_id = #{userId})
GROUP BY conversation_id
) as m
</select>
<select id="selectLetters" resultType="Message">
select
<include refid="selectFields"></include>
from message
where status!=2 and from_id !=1 and conversation_id =#{conversationId}
order by id desc
limit #{offset},#{limit}
</select>
<select id="selectLetterCount" resultType="int">
select count(id)
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
</select>
<select id="selectLetterUnreadCount" resultType="int">
select count(id)
from message
where status=0 and from_id!=1 and to_id=#{userId}
<if test="conversationId!=null">
and conversation_id =#{conversationId}
</if>
</select>
</mapper>
测试mapper.xml文件中的sql是否有误
/**
* MessageMapper
*/
@Test
public void testSelectLetters(){
List<Message> messages = messageMapper.selectConversations(112, 0, 10);
for (Message message : messages) {
System.out.println(message);
}
System.out.println("------------------");
int count = messageMapper.selectConversationCount(112);
System.out.println("数量为:"+count);
System.out.println("------------------");
List<Message> messageList = messageMapper.selectLetters("112_114", 0, 10);
for (Message message : messageList) {
System.out.println(message);
}
System.out.println("***************");
int count1 = messageMapper.selectLetterCount("112_114");
System.out.println("数量为:"+count1);
int count2 = messageMapper.selectLetterUnreadCount(112, "114_112");
System.out.println(count2);
}
数据业务层
MessageService
@Service
public class MessageService {
@Autowired
private MessageMapper messageMapper;
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信
public List<Message> findConversations(int userId,int offset,int limit){
return messageMapper.selectConversations(userId, offset, limit);
}
// 查询当前用户的会话数量
public int findConversationCount(int useId){
return messageMapper.selectConversationCount(useId);
}
// 查询某个会话所包含的私信列表
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);
}
}
数据表现层
MessageController
@Controller
public class MessageController {
@Autowired
private MessageService messageService;
@Autowired
private HostHolder hostHolder;
@Autowired
private UserService userService;
/**
* 私信列表
*
* @param model
* @param page
* @return
*/
@RequestMapping(value = "/letter/list", method = RequestMethod.GET)
public String getLetterList(Model model, Page page) {
User user = hostHolder.getUser();
// 分页信息
page.setLimit(5);
page.setPath("/letter/list");
page.setRows(messageService.findConversationCount(user.getId()));
// 会话列表
List<Message> conversationList = messageService
.findConversations(user.getId(), page.getOffset(), page.getLimit());
List<Map<String, Object>> conversations = new ArrayList<>();
if (conversationList != null) {
for (Message message : conversationList) {
Map<String, Object> map = new HashMap<>();
map.put("conversation", message);
map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
map.put("letterCount", messageService.findLetterCount(message.getConversationId()));
int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
map.put("target", userService.findUserById(targetId));
conversations.add(map);
}
}
model.addAttribute("conversations", conversations);
// 查询未读消息数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
return "/site/letter";
}
}
index.html
点击消息页面跳转到letter.html
私信列表页面
letter.html
引入thymeleaf
模板以及静态资源路径的绑定后
返回的js
<script>
function back() {
location.href = CONTEXT_PATH + "/letter/list";
}
</script>
6.2 私信详情
MessageController
/**
* 私信详情
* @param conversationId
* @param page
* @param model
* @return
*/
@RequestMapping(value = "/letter/detail/{conversationId}", method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {
// 分页信息
page.setLimit(5);
page.setPath("/letter/detail/" + conversationId);
page.setRows(messageService.findLetterCount(conversationId));
// 获取私信信息
List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
List<Map<String, Object>> letters = new ArrayList<>();
if (letterList != null) {
for (Message message : letterList) {
Map<String, Object> map = new HashMap<>();
map.put("letter", message);
map.put("fromUser", userService.findUserById(message.getFromId()));
letters.add(map);
}
}
model.addAttribute("letters", letters);
// 私信目标
model.addAttribute("target", getLetterTarget(conversationId));
return "/site/letter-detail";
}
/**
* 获取目标对象
*
* @param conversationId
* @return
*/
private User getLetterTarget(String conversationId) {
String[] ids = conversationId.split("_");
int id0 = Integer.parseInt(ids[0]);
int id1 = Integer.parseInt(ids[1]);
if (hostHolder.getUser().getId() == id0) {
return userService.findUserById(id1);
} else {
return userService.findUserById(id0);
}
}
letter.html
letter-detail.html
7、发送私信
7.1 发送私信
数据持久层
MessageMapper
//添加私信
int insertMessage(Message message);
//修改消息的状态
int updateStatus(List<Integer> ids,int status);
MessageMapper.xml
<insert id="insertMessage" keyProperty="id" 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>
数据业务层
MessageService
public int addMessage(Message message){
message.setContent(HtmlUtils.htmlEscape(message.getContent()));
message.setContent(sensitiveFilter.filter(message.getContent()));
return messageMapper.insertMessage(message);
}
public int readMessage(List<Integer> ids){
return messageMapper.updateStatus(ids,1);
}
数据表现层
MessageController
@RequestMapping(value = "/letter/send", method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName, String content) {
User target = userService.findUserByName(toName);
System.out.println("target--------------"+target);
if (target == null) {
return CommunityUtil.getJsonString(1, "用户不存在");
}
Message message = new Message();
message.setFromId(hostHolder.getUser().getId());
message.setToId(target.getId());
if (message.getFromId() < message.getToId()) {
message.setConversationId(message.getFromId() + "_" + message.getToId());
} else {
message.setConversationId(message.getToId() + "_" + message.getFromId());
}
message.setContent(content);
message.setCreateTime(new Date());
messageService.addMessage(message);
System.out.println("controller执行了");
return CommunityUtil.getJsonString(0);
}
letter.js
$(function(){
$("#sendBtn").click(send_letter);
$(".close").click(delete_msg);
});
function send_letter() {
$("#sendModal").modal("hide");
var toName=$("#recipient-name").val();
var content=$("#message-text").val();
$.post(
//CONTEXT_PATH+"/letter/send",
"/community/letter/send",
{"toName":toName,"content":content},
function (data){
data=$.parseJSON(data);
if (data.code==0){
$("hintBody").text("发送成功!");
}else {
$("hintBody").text(data.msg);
}
$("#hintModal").modal("show");
setTimeout(function(){
$("#hintModal").modal("hide");
location.reload();
}, 2000);
}
)
}
function delete_msg() {
// TODO 删除数据
$(this).parents(".media").remove();
}
7.2 设置已读
MessageController
/**
* 获取未读消息id
* @param letterList
* @return
*/
private List<Integer> getLetterIds(List<Message> letterList) {
List<Integer> ids = new ArrayList<>();
if (letterList != null) {
for (Message message : letterList) {
if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {
ids.add(message.getId());
}
}
}
return ids;
}
8、统一异常处理
将error文件夹放在templates
文件下,并引入thymeleaf
模板
获取错误页面
HomeController
@RequestMapping(value = "/error",method = RequestMethod.GET)
public String getErrorPage(){
return "/error/500";
}
捕捉controller异常
ExceptionAdvice
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
// 日志
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
@ExceptionHandler({Exception.class})
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.error("服务器发生异常:"+e.getMessage());
for (StackTraceElement element : e.getStackTrace()) {
logger.error(element.toString());
}
// 如果是异步请求就返回json字符串,否则返回错误页面
String xRequestWith = request.getHeader("x-request-with");
if("XMLHttpRequest".equals(xRequestWith)){
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer=response.getWriter();
writer.write(CommunityUtil.getJSONString(1,"服务器异常!"));
}else{
response.sendRedirect(request.getContextPath()+"/error");
}
}
}
9、统一记录日志
把所有的业务组件进行记录日志,在业务组件调用的一开始进行记录
导包
<!--aspect-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
ServiceLogAspect
@Component
@Aspect
public class ServiceLogAspect {
// 日志
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.example.community.service.*.*(..))")
public void pointcut(){
}
@Before("pointcut()")
public void before(JoinPoint joinPoint){
// 用户。。。在。。。访问。。。
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));
}
}
效果
第3章 Redis 一站式高性能存储方案
常用基本命令
select 1 # 选择数据库
flushdb # 清空数据库
keys * # 获取所有数据
type key # 查看数据类型
exists key # 当前key是否存在
expire key second # 几秒后删除
del key # 删除key
strings
set key value # 存值
get key # 取值
incr key # 增加value值
decr key # 减少value值
hashs
hset key field value # 存值 键值对
hget key field # 取值
lists
lpush key value1 value2 valu3 # 存值(入栈)
llen key # 列表长度
lindex key index # 对应索引的值
lrange key star end # 范围性取值
rpop key # 出栈(右侧)
sets
sadd key value1 value2 value3 # 存值
scard key # 集合中的数量
spop key # 随机抽取一个值 弹出, 抽奖
smembers key # 遍历当前集合
zsets
zadd key score member socre member # 存值
zcard key # 数量
zscore key member # 取值(分数)
zrank key member # 返回有序集合中指定成员的索引
zrange key star end # 遍历有序集合
1、Spring整合Redis
1.1 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1.2 配置Redis
在spring对redis的自动配置中,默认redis 的key为Object类型。我们需要将redis的key设置为String类型的
application.yml
# redis
spring:
redis:
database: 11
host: localhost
port: 6379
1.3 Redis序列化
将redis中key的类型转化为string
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 实例化
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(factory);
// 设置key序列化方式
redisTemplate.setKeySerializer(RedisSerializer.string());
// 设置普通的value序列化方式
redisTemplate.setValueSerializer(RedisSerializer.json());
// 设置hash的key的序列化
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化
redisTemplate.setHashValueSerializer(RedisSerializer.json());
// 设置生效
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
1.4 测试redis
import com.example.community.CommunityApplication;
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.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.TimeUnit;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = CommunityApplication.class)
public class RedisTests {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
/**
* string
*/
@Test
public void testRedis(){
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));
}
/**
* hash
*/
@Test
public void testHashes(){
String redisKey="test:user";
redisTemplate.opsForHash().put(redisKey,"id",1);
redisTemplate.opsForHash().put(redisKey,"name","张三");
System.out.println(redisTemplate.opsForHash().get(redisKey, "id"));
System.out.println(redisTemplate.opsForHash().get(redisKey, "name"));
}
/**
* list
*/
@Test
public void testList(){
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));
}
/**
* set
*/
@Test
public 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));
}
/**
* sortedSets
*/
@Test
public void testZSet(){
String redisKey="test:students";
redisTemplate.opsForZSet().add(redisKey,"唐僧",10);
redisTemplate.opsForZSet().add(redisKey,"孙悟空",99);
redisTemplate.opsForZSet().add(redisKey,"猪八戒",80);
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, -1));
}
/**
* key是否存在
*/
@Test
public void testKeys(){
redisTemplate.delete("test:user");
System.out.println(redisTemplate.hasKey("test:user"));
redisTemplate.expire("test:teachers",10, TimeUnit.SECONDS);
}
/**
* 多吹访问同一个key
*/
@Test
public void testBoundOperations(){
String redisKey="test:count";
BoundValueOperations<String, Object> operations = redisTemplate.boundValueOps(redisKey);
operations.increment();
operations.increment();
System.out.println(operations.get());
}
}
1.5 redis中的事务
概念
- redis事务本质是一组命令的集合。事务支持一次性执行多个命令,一个事务中所有命令都会被序列化。
- 在事务执行过程中,会按照顺序串行化执行队列中的命令,其他科幻的提交的命令请求不会插入到事务执行命令序列中。
- 简单来说:redis事务就是一次性、顺序性、排他性、的执行一个队列中的一系列命令。
不存在隔离级别
- 批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
不能保证原子性
- Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。
事务执行的三个阶段
- 开始事务
- 命令入队
- 执行事务
相关命令
- watch key1 key2 … : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
- multi : 标记一个事务块的开始( queued )
- exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
- discard : 取消事务,放弃事务块中的所有命令
- unwatch : 取消watch对所有key的监控
简单使用
/**
* 编程式事务
*/
@Test
public void testTransactional(){
Object obj=redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
String redisKey = "test:tx";
// 开启事务
operations.multi();
operations.opsForSet().add(redisKey,"张三");
operations.opsForSet().add(redisKey,"李四");
operations.opsForSet().add(redisKey,"王五");
// 此时事务未被提交,所以为空[]
System.out.println(operations.opsForSet().members(redisKey));
// 提交事务
return operations.exec();
}
});
System.out.println(obj);//[1, 1, 1, [王五, 张三, 李四]]
}
2、点赞
2.1 点赞
- 编写保存redisKey的工具类
RedisKeyUtil
/**
* 生成需要的Key
*/
public class RedisKeyUtil {
private static final String SPLIT = ":"; // 分隔符
private static final String PREFIX_ENTITY_LIKE = "like:entity"; //点赞实体
// 某个实体的赞 like:entity:entityType:entityId -> set(userId)
public static String getEntityLikeKey(int entityType,int entityId){
return PREFIX_ENTITY_LIKE+SPLIT+entityType+SPLIT+entityId;
}
}
- 在业务层创建
LikeService
用来创建使用redis的方法
LikeService
@Service
public class LikeService {
@Autowired
private RedisTemplate redisTemplate;
// 点赞
public void like(int userId, int entityType, int entityId) {
// 获取key
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
// 查看是否已存在集合中
Boolean isMember = redisTemplate.opsForSet().isMember(entityLikeKey, userId);
if (isMember) {
redisTemplate.opsForSet().remove(entityLikeKey, userId);
} else {
redisTemplate.opsForSet().add(entityLikeKey, userId);
}
}
// 查询某实体点赞的数量
public long findEntityLikeCount(int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().size(entityLikeKey);
}
// 查询某人对某实体的点赞状态
public int findEntityLikeStatus(int userId, int entityType, int entityId) {
String entityLikeKey = RedisKeyUtil.getEntityLikeKey(entityType, entityId);
return redisTemplate.opsForSet().isMember(entityLikeKey, userId) ? 1 : 0;
}
}
- 实现点赞功能
LikeController
@Controller
public class LikeController {
@Autowired
private LikeService likeService;
@Autowired
private HostHolder hostHolder;
/**
* 点赞功能
* @param entityType
* @param entityId
* @return
*/
@RequestMapping(value = "/like",method = RequestMethod.POST)
@ResponseBody
public String like(int entityType,int entityId){
User user =hostHolder.getUser();
// 点赞
likeService.like(user.getId(),entityType,entityId);
// 数量
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);
return CommunityUtil.getJSONString(0,null,map);
}
}
disscuss-detail.html
作者
回帖列表
回复列表
discuss.js 记得引入js
function like(btn, entityType, entityId) {
$.post(
CONTEXT_PATH + "/like",
{entityType, entityId},
function (data) {
data = $.parseJSON(data);
if (data.code == 0) {
$(btn).children("i").text(data.likeCount);
$(btn).children("b").text(data.likeStatus == 1 ? '已赞' : '赞');
} else {
alert(data.msg);
}
}
)
}
2.2 首页点赞数量
HomeContoller
index.html
2.3 详情页点赞数量
DiscussPostController
帖子点赞数量以及状态
回帖点赞数量以及状态
回复点赞数量以及状态
3、我收到的赞
3.1 重构点赞功能
编写用户赞的key,用来记录点赞数量
RedisKeyUtil
private static final String PREFIX_USER_LIKE = "like:user";
// 某用户的赞 like:user:userId -> int
public static String getUserLikeKey(int userId){
return PREFIX_USER_LIKE+SPLIT+userId;
}
在likeService
中加入用户点赞数量的方法,由于两个方法要么同时成功,要么同时失败,所以利用编程式事务,entityUserId
为被点赞id,userId
为点赞者id
将LikeController
的点赞功能中添加一个EntityUserId
参数
在帖子详情discuss-detail
页面添加一个entityUserId
参数
discuss.js
3.2 开发个人主页
LikeService
// 查询某个用户获得的赞的数量
public int findUserLikeCount(int userId) {
// 获取key
String userLikeKey = RedisKeyUtil.getUserLikeKey(userId);
// 查询数量
Integer count = (Integer) redisTemplate.opsForValue().get(userLikeKey);
return count == null ? 0 : count.intValue();
}
UserController
/**
* 个人主页
* @param userId
* @param model
* @return
*/
@RequestMapping(value = "/profile/{userId}",method = RequestMethod.GET)
public String getProfilePage(@PathVariable("userId") int userId, Model model){
User user = userService.findUserById(userId);
if(user==null){
throw new RuntimeException("该用户不存在");
}
// 用户
model.addAttribute("user",user);
// 点赞数量
int likeCount = likeService.findUserLikeCount(userId);
model.addAttribute("likeCount",likeCount);
return "/site/profile";
}
index.html
点击头部栏头像的下拉框的个人主页即可跳转
点击帖子列表中的头像即可跳转
discuss-detail.html
点击作者头像跳转到主页
点击评论中头像跳转主页
profile.html
引入thymeleaf模板
4、关注、取消关注
4.1关注、取消关注
RedisKeyUtil
//关注
private static final String PREFIX_FOLLOWEE = "followee"; //关注目标
private static final String PREFIX_FOLLOWER = "follower"; //粉丝
// 某个关注的实体
// followee:userId:entityType -> zset(entityId,now)
public static String getFolloweeKey(int userId, int entityType) {
return PREFIX_FOLLOWEE + SPLIT + userId + SPLIT + entityType;
}
// 某个用户拥有的粉丝
// follower:entityType:entityId -> zset(userId,now)
public static String getFollowerKey(int entityType, int entityId) {
return PREFIX_FOLLOWER + SPLIT + entityType + SPLIT + entityId;
}
FollowService
// 关注、取消关注
@Service
public class FollowService {
@Autowired
private RedisTemplate redisTemplate;
// 关注
public void follow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityId);
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 unfollow(int userId, int entityType, int entityId) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
String followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityId);
String followerKey = RedisKeyUtil.getFollowerKey(entityType, entityId);
redisOperations.multi();
redisOperations.opsForZSet().remove(followeeKey, entityId);
redisOperations.opsForZSet().remove(followerKey, userId);
return redisOperations.exec();
}
});
}
}
FollowController
// 关注、取消关注
@Controller
public class FollowController {
@Autowired
private FollowService followService;
@Autowired
private HostHolder hostHolder;
// 关注
@RequestMapping(value = "/follow", method = RequestMethod.POST)
@ResponseBody
public String follow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.follow(user.getId(), entityId, entityType);
return CommunityUtil.getJsonString(0, "已关注!");
}
// 取消关注
@RequestMapping(value = "/unfollow", method = RequestMethod.POST)
@ResponseBody
public String unfollow(int entityType, int entityId) {
User user = hostHolder.getUser();
followService.unfollow(user.getId(), entityId, entityType);
return CommunityUtil.getJsonString(0, "已取消关注!");
}
}
profile.html
点击按钮进行关注、取消关注
profile.js
$(function(){
$(".follow-btn").click(follow);
});
function follow() {
var btn = this;
var entityId = $(btn).prev().val();
if($(btn).hasClass("btn-info")) {
// 关注TA
$.post(
CONTEXT_PATH+"/follow",
{"entityType":3,"entityId":entityId},
function (data){
data = $.parseJSON(data)
if(data.code == 0){
window.location.reload();
}else{
alert(data.msg)
}
}
)
// $(btn).removeClass("btn-info").addClass("btn-secondary");
} else {
// 取消关注
$.post(
CONTEXT_PATH+"/unfollow",
{"entityType":3,"entityId":entityId},
function (data){
data = $.parseJSON(data)
if(data.code == 0){
window.location.reload();
}else{
alert(data.msg)
}
}
)
// $(btn).removeClass("btn-secondary").addClass("btn-info");
}
}
4.2 用户的关注数、粉丝数
FollowService
// 查询某个用户关注的实体数量
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 followeeKey = RedisKeyUtil.getFolloweeKey(userId, entityType);
return redisTemplate.opsForZSet().score(followeeKey, entityId) != null;
}
}
UserController
profile.html
5、关注列表与粉丝列表
5.1 业务层
FollowService
// 查询某用户关注的人
public List<Map<String,Object>> findFollowees(int userId,int offset,int limit){
String followeeKey=RedisKeyUtil.getFolloweeKey(userId,ENTITY_TYPE_USER);
Set<Integer> targetIds = redisTemplate.opsForZSet().reverseRange(followeeKey, offset, offset + limit - 1);
if (targetIds==null){
return null;
}
List<Map<String,Object>> list=new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String,Object> map=new HashMap<>();
User user = userService.findUserById(targetId);
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(followeeKey, targetId);
map.put("followTime",new Date(score.longValue()));
System.out.println(new Date(score.longValue()));
list.add(map);
}
return list;
}
// 查询某用户的粉丝
public List<Map<String,Object>> findFollowers(int userId,int offset,int limit){
String followerKey=RedisKeyUtil.getFollowerKey(ENTITY_TYPE_USER,userId);
Set<Integer> targetIds=redisTemplate.opsForZSet().reverseRange(followerKey,offset,offset+limit-1);
if (targetIds==null){
return null;
}
List<Map<String,Object>> list=new ArrayList<>();
for (Integer targetId : targetIds) {
Map<String,Object> map=new HashMap<>();
User user = userService.findUserById(targetId);
map.put("user",user);
Double score = redisTemplate.opsForZSet().score(followerKey, targetId);
map.put("followTime",new Date(score.longValue()));
System.out.println(new Date(score.longValue()));
list.add(map);
}
return list;
}
5.2 表现层
FollowController
// 用户关注的人
@RequestMapping(value = "/followees/{userId}", method = RequestMethod.GET)
public String getFollowees(@PathVariable("userId") int userId, Page page, Model model) {
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("该用户不存在!");
}
model.addAttribute("user", user);
// 分页
page.setLimit(5);
page.setPath("/followees/" + userId);
page.setRows((int) followService.findFolloweeCount(userId, ENTITY_TYPE_USER));
List<Map<String, Object>> userList = followService.findFollowees(userId, page.getOffset(), page.getLimit());
if (userList != null) {
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);
return "/site/followee";
}
private boolean hasFollowed(int userId) {
if (hostHolder.getUser() == null) {
return false;
}
return followService.hasFollowed(hostHolder.getUser().getId(), ENTITY_TYPE_USER, userId);
}
// 粉丝
@RequestMapping(value = "/followers/{userId}", method = RequestMethod.GET)
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.setLimit(5);
page.setPath("/followers/" + userId);
page.setRows((int) followService.findFollowerCount(ENTITY_TYPE_USER, userId));
List<Map<String, Object>> userList = followService.findFollowers(userId, page.getOffset(), page.getLimit());
if (userList != null) {
for (Map<String, Object> map : userList) {
User u = (User) map.get("user");
map.put("hasFollowed", hasFollowed(u.getId()));
}
}
model.addAttribute("users", userList);
return "/site/follower";
}
profile.html
follower.html
followee.html
与follower.html
一样
6、优化登录模块
6.1 使用Redis存储验证码
1. 设置验证码的KeyRedisKeyUtil
//验证码
private static final String PREFIX_KAPTCHA = "kaptcha";
// 验证码
public static String getCaptchaKey(String owner) {
return PREFIX_KAPTCHA + SPLIT + owner;
}
2. 将原来的session存储改为存入redisLoginController
获取图片验证码
// 1.获取随机字符串
String kaptchaOwner = CommunityUtil.generateUUID();
// 2.验证码的归属
Cookie cookie = new Cookie("kaptchaOwner", kaptchaOwner);
cookie.setMaxAge(60);// 生效时间
cookie.setPath(contextPath);// 生效路径
response.addCookie(cookie);
// 3.将验证码存入redis
String redisKey = RedisKeyUtil.getkaptchaKey(kaptchaOwner);
// key: redisKey value :text 60秒有效
redisTemplate.opsForValue().set(redisKey,text,60, TimeUnit.SECONDS);
用户登录
// 1.检查验证码
String kaptcha=null;
// 2.判断随机字符串是否存在
if(StringUtils.isNotBlank(kaptchaOwner)){
// 如果存在,则获取redis中的验证码
String redisKey = RedisKeyUtil.getkaptchaKey(kaptchaOwner);
kaptcha= (String) redisTemplate.opsForValue().get(redisKey);
}
4. 测试
成功存入redis
6.2 使用Redis存储登陆凭证
1. 设置登陆凭证的key
private static final String PREFIX_TICKET = "ticket"; // 登陆凭证
// 登录凭证
public static String getLoginTicket(String ticket) {
return PREFIX_TICKET + SPLIT + ticket;
}
2. 将原来关于登录凭证的方法置为不可用
LoginTicketMapper
@Deprecated 不推荐使用
@Mapper
@Deprecated
public interface LoginTicketMapper {
}
3. 利用redis修改原来的方法
用户登录
// loginTicketMapper.insertLoginTicket(loginTicket);
// 将登陆凭证存入redis
String redisKey = RedisKeyUtil.getLoginTicket(loginTicket.getTicket());
// redis会把对象序列化为一个json
redisTemplate.opsForValue().set(redisKey,loginTicket);
退出
/**
* 退出
*
* @param ticket
*/
public void logout(String ticket) {
// 1.获取key
String redisKey = RedisKeyUtil.getLoginTicket(ticket);
// 2.获取登录凭证
LoginTicket loginTicket = (LoginTicket) redisTemplate.opsForValue().get(redisKey);
// 3.修改状态
loginTicket.setStatus(1);
// 4.重新存入
redisTemplate.opsForValue().set(redisKey,loginTicket);
}
在退出时有一个小小的bug,如果在退出之前,redis中没有存入ticket,则会报错(因为可能上次登陆后未退出,然后利用redis进行登录凭证的重构)
查询登录凭证
/**
* 根据ticket查询登录凭证信息
*
* @param ticket
* @return
*/
public LoginTicket findLoginTicket(String ticket) {
// 1.获取key
String redisKey = RedisKeyUtil.getLoginTicket(ticket);
// 2.获取登录凭证
return (LoginTicket) redisTemplate.opsForValue().get(redisKey);
}
4. 测试
6.3 使用redis缓存用户信息
1. 设置用户信息的key
private static final String PREFIX_USER ="user"; // 用户信息
// user
public static String getUserKey(int userId){
return PREFIX_USER+SPLIT+userId;
}
2. 利用redis重构
UserService
封装三个关于redis的方法
// 1.优先去缓存中取值
public User getCache(int userId){
String redisKey=RedisKeyUtil.getUserKey(userId);
User user= (User) redisTemplate.opsForValue().get(redisKey);
return user;
}
// 2。取不到时初始化缓存数据
public User initCache(int userId){
// 获取 user
User user=userMapper.selectById(userId);
String redisKey=RedisKeyUtil.getUserKey(userId);
// 将user存入redis缓存
redisTemplate.opsForValue().set(redisKey,user);
return user;
}
// 3.数据变更时清楚缓存数据
public void clearCache(int userId){
// 获取key
String redisKey=RedisKeyUtil.getUserKey(userId);
// 删除key
redisTemplate.delete(redisKey);
}
重构查询代码
// 根据id查找user
public User findUserById(int id) {
//return userMapper.selectById(id);
// 1.先去缓存中取数据
User user = getCache(id);
if (user==null){
// 2.初始化user
user = initCache(id);
}
return user;
}
//激活码
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);
// 清理redis缓存
clearCache(userId);
return ACTIVATION_SUCCESS;
} else {
return ACTIVATION_FAILURE;
}
}
//更新用户头像
public int updateHeader(int userId, String headerUrl) {
//return userMapper.updateHeader(userId, headerUrl);
int rows = userMapper.updateHeader(userId, headerUrl);
clearCache(userId);
return rows;
}
}
3. 测试