Spring Boot实践八--用户管理系统

一,技术介绍

技术选型功能说明
springboot是一种基于 Spring 框架的快速开发应用程序的框架,它的主要作用是简化 Spring 应用程序的配置和开发,同时提供一系列开箱即用的功能和组件,如内置服务器、数据访问、安全、监控等,使开发者可以更加高效地构建和部署应用程序
Maven快速的引入jar包进行开发,自动构建部署
tomcatweb服务器,快速部署发布web 服务。Tomcat是一个开源的Java Servlet容器,它可以运行Java Servlet和JavaServer Pages(JSP)应用程序。作为一个Web服务器,它可以处理HTTP请求和响应,并将它们传递给Java Servlet和JSP应用程序进行处理。
ThymeleafThymeleaf是一种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>&emsp;码:</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>&emsp;码:</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) + "毫秒");}}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/80984.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[MyBatis系列②]Dao层开发的两种方式

目录 1、传统开发 1.1、代码 1.2、存在的问题 2、代理开发 2.1、开发规范 2.2、代码 ⭐mybatis系列①&#xff1a;增删改查 1、传统开发 传统的mybatis开发中&#xff0c;是在数据访问层实现相应的接口&#xff0c;在实现类中用"命名空间.id"的形式找到对应的映…

U盘被分成了4个盘要怎么合并

原来是做为系统盘的&#xff0c;然后有一大概小半年没用&#xff0c;今天一看它自己分成了四个盘。 并且我一插入电脑就提示我格式化 其实根本不需要任何工具&#xff0c;操作前最好把U盘数据备份一下 首先把你的U盘插在电脑上 方法一 U盘被分成四个分区的原因有以下几种可…

基于OpenCV实战(基础知识一)

目录 简介 1.计算机眼中的图像 2.图片的读取、显示与保存 3.视频的读取与显示 简介 OpenCV是一个流行的开源计算机视觉库&#xff0c;由英特尔公司发起发展。它提供了超过2500个优化算法和许多工具包&#xff0c;可用于灰度、彩色、深度、基于特征和运动跟踪等的图像处理和…

让家长自己动手,轻松查询分班信息

暑假即将结束&#xff0c;老师们面临着制作分班信息查询系统的重要任务。今天给大家推荐的这个工具对于老师们来说非常重要&#xff0c;因为它可以提供给家长和学生们方便快捷的查询服务。 分班信息查询系统可以让家长和学生们通过系统查询自己所在的班级、同班同学的信息以及班…

SpringSecurity原理

最近在研究SpringSecurity&#xff0c;肝了好多天&#xff0c;算是有点收获&#xff0c;在这里分享下 SpringSecurity是什么&#xff1f; SpringSecurity是一个强大的可高度定制的认证和授权框架&#xff0c;对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为J…

PyTorch学习笔记(十六)——利用GPU训练

一、方式一 网络模型、损失函数、数据&#xff08;包括输入、标注&#xff09; 找到以上三种变量&#xff0c;调用它们的.cuda()&#xff0c;再返回即可 if torch.cuda.is_available():mynn mynn.cuda() if torch.cuda.is_available():loss_function loss_function.cuda(…

elementui表格嵌套上传文件直传到oss服务器(表单上传)

提示&#xff1a;记录项目中遇到的问题&#xff0c;仅供参考 文章目录 前言一、vue代码二、js接口请求代码 前言 项目需求是在表格中嵌套一个上传图片的功能&#xff0c;并且回显选择的图片和已上传的图片&#xff0c;再通过点击操作列中上传按钮才开始上传&#xff0c;使用的…

如何提高企业生产效率与安全性?设备报修管理系统有什么用?

随着现代工业技术的不断发展&#xff0c;企业生产设备变得越来越复杂&#xff0c;出现故障的可能性也随之增加。设备故障不仅会降低企业的生产效率&#xff0c;还可能导致生产安全事故的发生。为了更好地管理维护生产设备&#xff0c;提高生产效率和安全性&#xff0c;本文将向…

区块链碎碎念

现在的区块链早已过了跑马圈地的时代&#xff0c;现在还按照以前承接项目的方式做区块链只能是越来越艰难。经过几年的技术沉淀&#xff0c;做区块链项目的公司都已经没落的七七八八了。 区块链不是一个能够快速显现盈利能力的行业&#xff0c;相反这个行业目前的模式还是处于…

缓存穿透、缓存击穿和缓存雪崩

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱发博客的嗯哼&#xff0c;爱好Java的小菜鸟 &#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&#x1f44d;一下博主哦 &#x1f4dd;社区论坛&#xff1a;希望大家能加入社区共同进步…

快速指南:使用Termux SFTP通过远程进行文件传输——”cpolar内网穿透“

文章目录 1. 安装openSSH2. 安装cpolar3. 远程SFTP连接配置4. 远程SFTP访问4. 配置固定远程连接地址 SFTP&#xff08;SSH File Transfer Protocol&#xff09;是一种基于SSH&#xff08;Secure Shell&#xff09;安全协议的文件传输协议。与FTP协议相比&#xff0c;SFTP使用了…

DAY24

题目一 啊 看着挺复杂 其实很简单 第一种方法 就是纵轴是怪兽编号 横轴是能力值 看看能不能打过 逻辑很简单 看看能不能打得过 打过的就在花钱和直接打里面取小的 打不过就只能花钱 这种方法就导致 如果怪兽的能力值很大 那么我们就需要很大的空间 所以引出下一种做法 纵…