一,技术介绍
技术选型 | 功能说明 |
---|---|
springboot | 是一种基于 Spring 框架的快速开发应用程序的框架,它的主要作用是简化 Spring 应用程序的配置和开发,同时提供一系列开箱即用的功能和组件,如内置服务器、数据访问、安全、监控等,使开发者可以更加高效地构建和部署应用程序 |
Maven | 快速的引入jar包进行开发,自动构建部署 |
tomcat | web服务器,快速部署发布web 服务。Tomcat是一个开源的Java Servlet容器,它可以运行Java Servlet和JavaServer Pages(JSP)应用程序。作为一个Web服务器,它可以处理HTTP请求和响应,并将它们传递给Java Servlet和JSP应用程序进行处理。 |
Thymeleaf | Thymeleaf是一种Java模板引擎,它可以将HTML、XML、JavaScript等文件转换为可执行的模板。在开发Web应用程序时,通常会使用Tomcat作为Web服务器,而Thymeleaf可以作为模板引擎来生成动态的Web页面。因此,Thymeleaf和Tomcat可以一起使用来构建动态Web应用程序。 |
junit | 单元测试框架 |
mybatis | 将Java对象与关系数据库进行映射,实现数据的持久化操作。mybatis的mapper文件是储存sql语句的一个xml文件,他替代了JDBC在类中写多条语句的问题,简化了步骤。 |
redis | 用作缓存。它的读写速度非常快,每秒可以处理超过10万次读写操作。高并发访问数据时直接走内存,和直接查询数据库相比,redis的高效性、快速性优势明显 |
mysql | 关系型数据库 |
Spring、Spring Boot和Spring Cloud的关系
我们可以这样理解:正是由于 IoC (控制反转,把创建好的对象给Spring进行管理)和 AOP(面向切面编程,不修改源代码的情况下进行功能增加) 这两个强大的功能才有了强大的轻量级开源JavaEE框架 Spring;Spring 生态不断地发展才有了 Spring Boot;Spring Boot 开发、部署的简化,使得 Spring Cloud 微服务治理方案彻底落地。
Spring Boot 在 Spring Cloud 中起到了承上启下的作用:
- Springboot 将原有的 xml 配置,简化为 java 注解
- 使用 IDE 可以很方便的搭建一个 springboot 项目,选择对应的 maven 依赖,简化Spring应用的初始搭建以及开发过程
- springboot 有内置的 tomcat 服务器,可以 jar 形式启动一个服务,可以快速部署发布 web 服务
- springboot 使用 starter 依赖自动完成 bean 配置,解决 bean 之间的冲突,并引入相关的 jar 包
Mybatis和Redis缓存的区别
Mybatis和Redis缓存的区别在于:
- Mybatis缓存是基于内存的,而Redis缓存是基于磁盘的。即Mybatis缓存是在应用程序内部实现的,而Redis缓存是在外部服务器上实现的,这意味着Redis缓存可以在多个应用程序之间共享,而Mybatis缓存只能在单个应用程序实例中使用。
- Mybatis缓存是局部缓存,只能缓存查询结果,而Redis缓存可以缓存任何类型的数据,包括对象、列表、哈希表等。
- Mybatis缓存是默认开启的,但需要手动配置,而Redis缓存需要安装和配置Redis服务器。
- Mybatis缓存是基于时间和空间的限制,而Redis缓存可以设置过期时间和最大内存使用量。
二,项目介绍
本项目是一个基于SpringBoot的用户权限管理系统,主要实现用户的注册、登录、角色管理、权限管理等功能。
数据库设计
表设计 | 作用 |
---|---|
用户表 | 用户表(user)用于存储系统中的用户信息,包括用户ID、用户名、密码、邮箱、手机号等字段。 |
角色表 | 角色表(role)用于存储系统中的角色信息,包括角色ID、角色名称、角色描述等字段 |
权限表 | 权限表(permission)用于存储系统中的权限信息,包括权限ID、权限名称、权限描述等字段。 |
用户角色关联表 | 用户角色关联表(user_role)用于存储系统中用户与角色之间的关系,包括用户ID、角色ID等字段。 |
角色权限关联表 | 角色权限关联表(role_permission)用于存储系统中角色与权限之间的关系,包括角色ID、权限ID等字段。 |
功能模块
用户注册:提供用户注册功能。
用户登录:提供用户登录功能。
角色管理:提供角色的增删改查功能。
权限管理:提供权限的增删改查功能。
用户角色管理:提供用户与角色的关联管理功能。
角色权限管理:提供角色与权限的关联管理功能。
代码结构
SpringBootRedis 工程项目结构如下:controller - Controller 层dao - 数据操作层model - 实体层service - 业务逻辑层Application - 启动类resources 资源文件夹application.properties - 应用配置文件,应用启动会自动读取配置generatorConfig.xml - mybatis 逆向生成配置(这里不是本文只要重点,所以不进行介绍)mapper 文件夹StudentMapper.xml - mybatis 关系映射 xml 文件
三,项目实现
1,配置依赖
pom.xml依赖
demospringboot\pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.15-SNAPSHOT</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>demospringboot</artifactId><version>0.0.1-SNAPSHOT</version><name>demospringboot</name><description>Demo project for Spring Boot</description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-commons</artifactId></dependency><!-- 添加mybatis依赖 --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.1</version></dependency><!-- 添加redis依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!-- 添加mysql依赖 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version><scope>runtime</scope><!-- MySQL5.x时,请使用5.x的连接器(cmd执行mysql -V确定)<groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.45</version>--></dependency><!-- 添加thymeleaf依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- 添加swagger依赖 --><dependency><groupId>com.spring4all</groupId><artifactId>swagger-spring-boot-starter</artifactId><version>1.9.0.RELEASE</version></dependency><!-- 添加junit依赖 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.20</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build><repositories><repository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url><snapshots><enabled>false</enabled></snapshots></repository><repository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></repository></repositories><pluginRepositories><pluginRepository><id>spring-milestones</id><name>Spring Milestones</name><url>https://repo.spring.io/milestone</url><snapshots><enabled>false</enabled></snapshots></pluginRepository><pluginRepository><id>spring-snapshots</id><name>Spring Snapshots</name><url>https://repo.spring.io/snapshot</url><releases><enabled>false</enabled></releases></pluginRepository></pluginRepositories></project>
application.properties配置
demospringboot\src\main\resources\application.properties
:
# mysql 指定使用的数据库
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql5: spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 执行初始化sql
spring.datasource.initialize=true
spring.datasource.schema=classpath:schema.sql# redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=100ms# 默认线程池
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.shutdown.await-termination=false
spring.task.execution.shutdown.await-termination-period=
spring.task.execution.thread-name-prefix=task-# mybatis 指定mapper xml映射文件
mybatis.mapper-locations=classpath:mybatis/*.xml
# 打印mybatis的执行sql
# mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl# swagger
swagger.title=spring-boot-starter-swagger
swagger.description=Starter for swagger 2.x
swagger.version=1.4.0.RELEASE
swagger.license=Apache License, Version 2.0
swagger.licenseUrl=https://www.apache.org/licenses/LICENSE-2.0.html
swagger.termsOfServiceUrl=https://github.com/xxx/spring-boot-starter-swagger
swagger.contact.name=didi
swagger.contact.url=http://blog.xxx.com
swagger.contact.email=xxx.com
swagger.base-package=com.example.demospringboot
swagger.base-path=/**spring.mvc.pathmatch.matching-strategy=ant_path_matcher
2,数据库初始化
在前面的application.properties的sql配置中,我们指定了会自动创建mydatabase,并且指定了初始化sql文件:
spring.datasource.initialize=true
spring.datasource.schema=classpath:schema.sql
对应的demospringboot\src\main\resources\schema.sql
主要用来初始化设计的5张表,sql语句如下:
-- 测试表初始化
drop database if exists mydatabase;
create database if not exists mydatabase character set utf8;
use mydatabase;
drop table if exists user;
drop table if exists role;
drop table if exists permission;
drop table if exists user_role;
drop table if exists role_permission;-- 用户表
CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',`username` varchar(32) NOT NULL COMMENT '用户名',`password` varchar(64) NOT NULL COMMENT '密码',PRIMARY KEY (`id`),UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';-- 角色表
CREATE TABLE `role` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`name` varchar(32) NOT NULL COMMENT '角色名称',`description` varchar(128) DEFAULT NULL COMMENT '角色描述',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';-- 权限表
CREATE TABLE `permission` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '权限ID',`name` varchar(32) NOT NULL COMMENT '权限名称',`description` varchar(128) DEFAULT NULL COMMENT '权限描述',`create_time` datetime NOT NULL COMMENT '创建时间',`update_time` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';-- 用户角色关联表
CREATE TABLE `user_role` (`user_id` int(11) NOT NULL COMMENT '用户ID',`role_id` int(11) NOT NULL COMMENT '角色ID',PRIMARY KEY (`user_id`,`role_id`),KEY `role_id` (`role_id`),CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';-- 角色权限关联表
CREATE TABLE `role_permission` (`role_id` int(11) NOT NULL COMMENT '角色ID',`permission_id` int(11) NOT NULL COMMENT '权限ID',PRIMARY KEY (`role_id`,`permission_id`),KEY `permission_id` (`permission_id`),CONSTRAINT `role_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,CONSTRAINT `role_permission_ibfk_2` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';INSERT INTO user VALUES(1,'admin','123456');
INSERT INTO user VALUES(2,'admin2','123456');
INSERT INTO user VALUES(3,'guanyu','1234');
INSERT INTO user VALUES(4,'zhangsan','1235');
然后主类实现CommandLineRunner run接口,执行initDatabase进行初始化:
进行如下调用:
package com.example.demospringboot;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.cache.annotation.EnableCaching;import com.example.demospringboot.workmanager.WorkManager;
import com.example.demospringboot.workmanager.AsyncWorkManager;import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import com.spring4all.swagger.EnableSwagger2Doc;@EnableCaching
@EnableAsync
@EnableSwagger2Doc
@SpringBootApplication
// 需要指定扫描的类,并在配置文件指定mybatis.mapper-locations为对应的xml路径
@MapperScan(value = {"com.example.demospringboot.dao"})
public class DemospringbootApplication implements CommandLineRunner {@Autowiredprivate DataSource dataSource;public static void main(String[] args) {SpringApplication.run(DemospringbootApplication.class, args);}@Overridepublic void run(String... strings) throws SQLException {initDatabase();}private void initDatabase() throws SQLException {System.out.println("======== 自动初始化数据库开始 ========");Resource initData = new ClassPathResource("schema.sql");Connection connection = null;try {connection = dataSource.getConnection();ScriptUtils.executeSqlScript(connection, initData);} catch (SQLException e) {throw new RuntimeException(e);} finally {if (connection != null) {connection.close();}}System.out.println("======== 自动初始化数据库结束 ========");}
}
如上我们通过DataSource.getConnection()总是从datasource或连接池返回一个新的连接,并通过ScriptUtils.executeSqlScript执行了我们的sql脚本。需要注意如果开发者没有手工释放这连接(显式调用 Connection.close() 方法),则这个连接将永久被占用(处于 active 状态),造成连接泄漏!
2,数据库增删改查接口实现
首先,我们基于mabatis+redis+mysql实现一个user类数据库增删改查的基本功能。
实体类bean.User实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis
package com.example.demospringboot.bean;import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;@Data
@NoArgsConstructor
public class User implements Serializable {private int id;private String username;private String password;
}
dao层,定义UserMapper接口:
package com.example.demospringboot.dao;import com.example.demospringboot.bean.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;@Repository
@CacheConfig(cacheNames = "users")
public interface UserMapper {User findUserById(@Param("id") int id);User findUserByName(@Param("username") String username);String findPassword(String username);@CacheableList<User> findAllUsers();void deleteUserById(@Param("id") int id);void deleteAllUsers();int insertUser(@Param("user") User user);void updateUserPassword(@Param("user") User user);
}
对应的demospringboot\src\main\resources\mybatis\UserMapper.xml:
<?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属性,和实际的mapper文件一致-->
<mapper namespace="com.example.demospringboot.dao.UserMapper"><select id="findUserById" resultType="com.example.demospringboot.bean.User">select * from t_user where id = #{id}</select><select id="findUserByName" resultType="com.example.demospringboot.bean.User">select * from t_user where username = #{username}</select><select id="findAllUsers" resultType="com.example.demospringboot.bean.User">select * from t_user</select><delete id="deleteAllUsers" >delete from t_user</delete><delete id="deleteUserById" parameterType="int">delete from t_user where id=#{id}</delete><insert id="insertUser" parameterType="com.example.demospringboot.bean.User">insert into t_user(id,username,password) values(#{user.id},#{user.username},#{user.password})</insert><update id="updateUserPassword" parameterType="com.example.demospringboot.bean.User">update t_user set password=#{user.password} where id=#{user.id}</update>
</mapper>
主启动类:
package com.example.demospringboot;import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Repository;import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.cache.annotation.EnableCaching;
import com.spring4all.swagger.EnableSwagger2Doc;@EnableCaching
@EnableAsync
@EnableSwagger2Doc
@SpringBootApplication
// 需要指定扫描的类,并在配置文件指定mybatis.mapper-locations为对应的xml路径
@MapperScan(value = {"com.example.demospringboot.dao"})
public class DemospringbootApplication {public static void main(String[] args) {SpringApplication.run(DemospringbootApplication.class, args);}}
3,Web页面接口实现
用户注册和登录
我们基于springboot内嵌的tomcat,用Thymeleaf 模版实现一个存储用户账号密码的web界面。在UsreController中设计如下接口:
接口 | 作用 |
---|---|
@GetMapping(value = {“/register”}) public String registerPage() | 返回注册界面html |
@GetMapping(value = {“/login”}) public String loginPage() | 返回登录界面html |
@GetMapping(value = {“/success”}) public String UserPage(HttpSession session, Model model) | 返回登录以后的界面html |
@PostMapping(“register”) public String Register(User user, Model model) | 提交注册信息进行注册 |
@PostMapping(“login”) public String login(User user, HttpSession session, Model model) | 提交登录信息进行登录 |
对应的3个html放置在demospringboot\src\main\resources\templates\目录下:
success.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Thymeleaf Spring Boot Example</title>
</head>
<body>
<h1 th:text="'Welcome,'+${user}+'!'"></h1>
<a>You have successfully logged in !</a>
</body>
</html>
login.html
访问http://localhost:8080/login时返回login.html页面。
点击登录按钮,通过html的form表单method="post" th:action="@{/login}
跳到Controller的@PostMapping("login")
,查询成功后重定向到@GetMapping("success")
,返回success.html
点击注册按钮,通过html的form表单method="get" th:action="@{/register}
跳到Controller的@GetMapping("register")
,返回registe.html页面。
<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>登录注册界面</title><link rel="stylesheet" href="../static/style.css">
</head><body>
<!-- 整体布局 -->
<div class="container right-panel-active"><!-- 登录框 --><div class="container_from container_signin"><form class="form" id="form" method="post" th:action="@{/login}"><h2 class="form_title">欢迎登录</h2><div class="row"><span>用户名:</span><input type="text" name="username" placeholder="请输入您的账号" class="input"></div><div class="row"><span>密 码:</span><input type="password" name="password" placeholder="请输入您的密码" class="input"></div><div class="row"><span th:text="${msg}"></span></div><input type="submit" class="btn" value="登录"/></form><form class="form" method="get" id="form1" th:action="@{/register}"><label id="register" class="form-label" >没有账号?请点击<input class="btn" type="submit" value="注册"/></form></div>
</div>
<script src="../static/login.js"></script>
</body>
</html>
register.html
填入信息后点击注册,通过html的form表单method="post" th:action="@{/register}
跳到Controller的@PostMapping("register")
,进行注册
<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>登录注册界面</title><link rel="stylesheet" href="../static/style.css">
</head><body>
<!-- 整体布局 -->
<div class="container right-panel-active"><!-- 注册框 --><div class="container_from container_signup"><form class="from" method="post" id="from" th:action="@{/register}"><h2 class="form_title">注册账号</h2><div class="row"><span>用户名:</span><input type="text" id="username" name="username" placeholder="请输入账号" class="input"></div><div class="row"><span>密 码:</span><input type="password" name="password" placeholder="请输入密码" class="input"></div><!-- 提示注册信息${tip} --><div class="row"><span th:text="${tip}"></span></div><input class="btn" type="submit" value="注册"/></form></div>
</div>
<script src="../static/login.js"></script>
</body>
</html>
实现userController:
package com.example.demospringboot.controller;import com.example.demospringboot.bean.User;
import com.example.demospringboot.dao.UserMapper;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.ui.Model;import javax.servlet.http.HttpSession;@Controller
public class UserController {@AutowiredUserMapper userService;@GetMapping(value = {"/login"})public String loginPage() {// 返回login.htmlreturn "login";}@GetMapping("register")public String registerPage() {// 返回register.htmlreturn "register";}@PostMapping("register")public String Register(User user, Model model) {try {User userName = userService.findUserByName(user.getUsername());//没有用户可以进行注册if (userName == null) {if (user.getPassword().equals("") || user.getUsername().equals("")) {model.addAttribute("tip", "请填写信息");return "register";} else {int ret = userService.insertUser(user);if (ret > 0) {model.addAttribute("tip", "注册成功,请返回登录页面进行登录");}return "register";}} else {model.addAttribute("tip", "用户已存在,请返回登录页面进行登录");return "register";}} catch (Exception e) {e.printStackTrace();return e.getMessage();}}@PostMapping("login")public String login(User user, HttpSession session, Model model) {try {//先查找一下有没有该账号User userReturn = userService.findUserByName(user.getUsername());if (userReturn != null) {//如果有账号则判断账号密码是否正确if (userReturn.getPassword().equals(user.getPassword())) {//添加到session保存起来session.setAttribute("loginUser", user);//重定向到@GetMapping("success")return "redirect:/success";} else {//如果密码错误,则提示输入有误model.addAttribute("msg", "账号或者密码有误");return "login";}} else {model.addAttribute("msg", "账号或者密码有误");return "login";}} catch (Exception e) {e.printStackTrace();return e.getMessage();}}@GetMapping("success")public String UserPage(HttpSession session, Model model) {User loginUser = (User)session.getAttribute("loginUser");if (loginUser != null) {model.addAttribute("user", loginUser.getUsername());// 返回success.htmlreturn "success";} else {model.addAttribute("msg", "请登录");return "login";}}}
界面效果如下:
step3:多线程task
首先,实现两个UserService和AsyncUserService两个服务接口:
接口:
package com.example.demospringboot.service;public interface UserService {void checkUserStatus();
}
package com.example.demospringboot.service;public interface AsyncUserService {void checkUserStatus();
}
对应实现:
package com.example.demospringboot.service.impl;import com.example.demospringboot.bean.User;
import com.example.demospringboot.service.UserService;
import com.example.demospringboot.dao.UserMapper;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;@Slf4j
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic void checkUserStatus() {List<User> AllUsers = userMapper.findAllUsers();for (User u : AllUsers) {// System.out.println(ThreadUtils.getThreadName() + ": " + u);log.info("{}", u);}};
}
package com.example.demospringboot.service.impl;import com.example.demospringboot.task.AsyncTasks;
import com.example.demospringboot.service.AsyncUserService;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class AsyncUserServiceImpl implements AsyncUserService {@Autowiredprivate AsyncTasks asyncTasks;@Overridepublic void checkUserStatus() {asyncTasks.doTaskOne("1");asyncTasks.doTaskOne("2");asyncTasks.doTaskOne("3");};
}
用到的task类如下:
package com.example.demospringboot.task;import com.example.demospringboot.utils.ThreadUtils;import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;import java.util.Random;
import java.util.concurrent.CompletableFuture;@Slf4j
@Component
public class AsyncTasks {public static Random random = new Random();// @Async注解中的参数就是异步任务的线程池@Async("taskExecutor")public CompletableFuture<String> doTaskOne(String taskNo){log.info("开始任务:{}", taskNo);long start = System.currentTimeMillis();ThreadUtils.sleepUtil(random.nextInt(10000));long end = System.currentTimeMillis();log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);return CompletableFuture.completedFuture("任务完成");}}
(1)异步任务通过方法上的@Async("taskExecutor")
和启动类的@EnableAsync
注解实现,@Async
中的参数指定了异步任务使用的的线程池。调用异步方法时不会等待方法执行完,调用即过,被调用方法在自己的线程池中奔跑。
(2)多线程执行的返回值是Future类型或void。Future是非序列化的,微服务架构中有可能传递失败。spring boot推荐使用的CompletableFuture来返回异步调用的结果。
用到的thread工具类如下:
package com.example.demospringboot.utils;import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Repository;import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;@Repository
public class ThreadUtils {public static final int MAX_POOL_SIZE = 2;public static final String EXECUTOR_POOL_PREFIX = "exe-" + MAX_POOL_SIZE + "-";public static final String ASYNC_EXECUTOR_POOL_PREFIX = "async-exe-" + MAX_POOL_SIZE + "-";public static final String ASYNC_TASK_POOL_PREFIX = "async-task-" + MAX_POOL_SIZE + "-";// 自定义AsyncTask线程池@Beanpublic ThreadPoolTaskExecutor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(MAX_POOL_SIZE);executor.setMaxPoolSize(MAX_POOL_SIZE);executor.setQueueCapacity(MAX_POOL_SIZE);executor.setKeepAliveSeconds(0);executor.setThreadNamePrefix(ASYNC_TASK_POOL_PREFIX);// 如果添加到线程池失败,那么主线程会自己去执行该任务executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return executor;}// 启动Executor的线程池public static ThreadPoolTaskExecutor getThreadPool(String threadNamePrefix) {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(MAX_POOL_SIZE);executor.setMaxPoolSize(MAX_POOL_SIZE);executor.setQueueCapacity(MAX_POOL_SIZE);executor.setKeepAliveSeconds(0);executor.setThreadNamePrefix(threadNamePrefix);executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize();return executor;}public static void sleepUtil(long millis) {try {Thread.sleep(millis);} catch (InterruptedException e) {System.out.println(e);}}
}
线程池用的是ThreadPoolTaskExecutor 。Executor 顾名思义是专门用来处理多线程相关的一个接口,所有线程相关的类都实现了这个接口,里面有一个execute()方法,用来执行线程,线程池主要提供一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁的额外开销,提高了响应的速度。
ThreadPoolTaskExecutor是对ThreadPoolExecutor进行了封装处理,是spring core包中提供的,而ThreadPoolExecutor是JDK中的JUC。
参数说明:
- corePoolSize:核心线程数
- queueCapacity:任务队列容量(阻塞队列)
- maxPoolSize:最大线程数
- keepAliveTime:线程空闲时间
- rejectedExecutionHandler:任务拒绝处理器
异步任务会先占用核心线程,核心线程满了其他任务进入队列等待;在缓冲队列也满了之后才会申请超过核心线程数的线程来进行处理。当线程数已经达到maxPoolSize,且队列已满,线程池可以调用这四个策略处理:- AbortPolicy策略:默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
- DiscardPolicy策略:如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
- DiscardOldestPolicy策略:如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
- CallerRunsPolicy策略:如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。
- 也可以自己实现RejectedExecutionHandler接口,可自定义处理器
为了控制异步任务的并发不影响到应用的正常运作,我们必须要对线程池做好相应的配置,防止资源的过渡使用。需考虑好默认线程池的配置和多任务情况下的线程池隔离。
上述服务我们就用不同线程池的两个WorkManager进行管理:
package com.example.demospringboot.workmanager;import com.example.demospringboot.service.UserService;
import com.example.demospringboot.utils.ThreadUtils;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;@Slf4j
@Component
public class WorkManager {private static final ThreadPoolTaskExecutor EXECUTOR_POOL =ThreadUtils.getThreadPool(ThreadUtils.EXECUTOR_POOL_PREFIX);@Autowiredprivate UserService userService;public void startExecutor() {EXECUTOR_POOL.execute(new Executor(userService));}static class Executor implements Runnable {private UserService userService;public Executor(UserService userService) {this.userService = userService;}@Overridepublic void run() {while (true) {userService.checkUserStatus();// sleep 1sThreadUtils.sleepUtil(1000L);}}}
}
package com.example.demospringboot.workmanager;import com.example.demospringboot.service.AsyncUserService;
import com.example.demospringboot.utils.ThreadUtils;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;@Slf4j
@Component
public class AsyncWorkManager {private static final ThreadPoolTaskExecutor ASYNC_EXECUTOR_POOL =ThreadUtils.getThreadPool(ThreadUtils.ASYNC_EXECUTOR_POOL_PREFIX);@Autowiredprivate AsyncUserService asyncUserService;public void startSyncExecutor() {ASYNC_EXECUTOR_POOL.execute(new AsyncExecutor(asyncUserService));}static class AsyncExecutor implements Runnable {private AsyncUserService asyncUserService;public AsyncExecutor(AsyncUserService asyncUserService) {this.asyncUserService = asyncUserService;}@Overridepublic void run() {while (true) {asyncUserService.checkUserStatus();// sleep 1sThreadUtils.sleepUtil(1000L);}}}
}
主类如下:
package com.example.demospringboot;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;import org.springframework.cache.annotation.EnableCaching;import com.example.demospringboot.workmanager.WorkManager;
import com.example.demospringboot.workmanager.AsyncWorkManager;@EnableCaching
@EnableAsync
@SpringBootApplication
@MapperScan(value = {"com.example.demospringboot.dao"})
public class DemospringbootApplication implements CommandLineRunner {@Autowiredprivate WorkManager workManager;@Autowiredprivate AsyncWorkManager asyncWorkManager;public static void main(String[] args) {SpringApplication.run(DemospringbootApplication.class, args);}@Overridepublic void run(String... strings) {//workManager.startExecutor();asyncWorkManager.startSyncExecutor();}
}
主启动类实现了CommandLineRunner 接口,会直接执行run方法。
我们在其中调用了WorkManager的startExecutor方法,用线程池execute方法启动了对应线程类的run方法。
test
package com.example.demospringboot;import com.example.demospringboot.dao.UserMapper;
import com.example.demospringboot.bean.User;
import com.example.demospringboot.task.AsyncTasks;import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.CompletableFuture;
import org.springframework.cache.CacheManager;import java.util.List;@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback(value = false)
public class DemospringbootApplicationTests {@Autowired()private UserMapper userMapper;@Autowiredprivate CacheManager cacheManager;@Testpublic void testUserMapper() throws Exception {// deleteAllUsersuserMapper.deleteAllUsers();// insertUser 插入2条User user = new User();user.setId(100);user.setUsername("Jacky");user.setPassword("1000");userMapper.insertUser(user);user.setId(200);user.setUsername("Mike");user.setPassword("2000");userMapper.insertUser(user);// findUserByIduser = userMapper.findUserById(100);Assert.assertEquals("Jacky", user.getUsername());// updateUserPassworduser.setPassword("1500");userMapper.updateUserPassword(user);Assert.assertEquals("1500", user.getPassword());// deleteUserByIduserMapper.deleteUserById(100);// findAllUsersList<User> AllUsers = userMapper.findAllUsers();for (User u : AllUsers) {System.out.println(u);}//Assert.assertEquals(1, AllUsers.size());System.out.println("CacheManager type : " + cacheManager.getClass());}@Autowiredprivate AsyncTasks asyncTasks;@Testpublic void testTasks() throws Exception {long start = System.currentTimeMillis();// 线程池1CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");// 线程池2CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6");// 一起执行CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();long end = System.currentTimeMillis();log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");}}