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

1,技术介绍

技术选型 功能说明
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 关系型数据库

1.1, 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 包

1.2,Mybatis和Redis缓存的区别

Mybatis和Redis缓存的区别在于:

  • Mybatis缓存是基于内存的,而Redis缓存是基于磁盘的。即Mybatis缓存是在应用程序内部实现的,而Redis缓存是在外部服务器上实现的,这意味着Redis缓存可以在多个应用程序之间共享,而Mybatis缓存只能在单个应用程序实例中使用。
  • Mybatis缓存是局部缓存,只能缓存查询结果,而Redis缓存可以缓存任何类型的数据,包括对象、列表、哈希表等。
  • Mybatis缓存是默认开启的,但需要手动配置,而Redis缓存需要安装和配置Redis服务器。
  • Mybatis缓存是基于时间和空间的限制,而Redis缓存可以设置过期时间和最大内存使用量。

2,项目结构

复制代码
 SpringBootRedis 工程项目结构如下:

  controller - Controller 层

  dao - 数据操作层

  model - 实体层

  service - 业务逻辑层

  Application - 启动类

  resources 资源文件夹

    application.properties - 应用配置文件,应用启动会自动读取配置

    generatorConfig.xml - mybatis 逆向生成配置(这里不是本文只要重点,所以不进行介绍)

    mapper 文件夹

      StudentMapper.xml - mybatis 关系映射 xml 文件

3,项目实现

配置

demospringboot\src\main\resources\application.properties:

c 复制代码
# 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

demospringboot\pom.xml:

java 复制代码
<?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>


		<!-- 添加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>

step0:数据库初始化

在前面的application.properties的sql配置中,我们指定了会自动创建mydatabase,并且指定了初始化sql:

java 复制代码
spring.datasource.initialize=true
spring.datasource.schema=classpath:schema.sql

对应的demospringboot\src\main\resources\schema.sqlsql语句如下

java 复制代码
drop database if exists mydatabase;

create database if not exists mydatabase character set utf8;

use mydatabase;

drop table if exists t_user;

CREATE TABLE `t_user` (
    `id` int NOT NULL AUTO_INCREMENT COMMENT '用户id',
    `username` varchar(100) DEFAULT NULL,
    `password` varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';


INSERT INTO t_user VALUES(1,'admin','123456');
INSERT INTO t_user VALUES(2,'admin2','123456');
INSERT INTO t_user VALUES(3,'guanyu','1234');
INSERT INTO t_user VALUES(4,'zhangsan','1235');
INSERT INTO t_user VALUES(5,'lisi','1236');
INSERT INTO t_user VALUES(6,'wangwu','1237');
INSERT INTO t_user VALUES(7,'sunquan','1238');
INSERT INTO t_user VALUES(8,'sunwukong','1239');
INSERT INTO t_user VALUES(9,'zhubajie','1239');

然后主类实现CommandLineRunner run接口,执行initDatabase进行初始化:

进行如下调用:

java 复制代码
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;

@EnableCaching
@EnableAsync
@SpringBootApplication
// 需要指定扫描的类,并在配置文件指定mybatis.mapper-locations为对应的xml路径
@MapperScan(value = {"com.example.demospringboot.dao"})
public class DemospringbootApplication implements CommandLineRunner {
	@Autowired
	private DataSource dataSource;

	public static void main(String[] args) {
		SpringApplication.run(DemospringbootApplication.class, args);
	}

	@Override
	public 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 状态),造成连接泄漏!

step1:数据库增删改查接口

首先,我们基于mabatis+redis+mysql实现一个user类数据库增删改查的基本功能。

实体类bean.User实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis

java 复制代码
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接口:

java 复制代码
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);
    @Cacheable
    List<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:

java 复制代码
<?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属性,和实际的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>

主启动类:

java 复制代码
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;

@EnableCaching
@EnableAsync
@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);
    }

}

step2:Web页面

然后,我们基于springboot内嵌的tomcat,用Thymeleaf 模版实现一个存储用户账号密码的web界面如下:

实现userController:

java 复制代码
package com.example.demospringboot.controller;

import com.example.demospringboot.bean.User;
import com.example.demospringboot.dao.UserMapper;

import com.sun.org.apache.bcel.internal.generic.ARETURN;
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 {
    @Autowired
    UserMapper userService;

    // 访问http://localhost:8080/login时返回login.html页面
    @GetMapping(value = {"/login"})
    public String loginPage() {
        return "login";
    }

    //注册用户
    @GetMapping("gotoregister")
    public String register2Page(HttpSession session, Model model) {
        // 返回register.html
        return "register";
    }

    @PostMapping("register")
    public String RegisterUser(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 loginSuccess(User user, HttpSession session, Model model) {
        try {
            //先查找一下有没有该账号
            User userName = userService.findUserByName(user.getUsername());
            if (userName != null) {
                //如果有账号则判断账号密码是否正确
                String password = userService.findPassword(user.getUsername());
                if (password.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 successPage(HttpSession session, Model model) {
        User loginUser = (User)session.getAttribute("loginUser");
        if (loginUser != null) {
            model.addAttribute("user", loginUser.getUsername());
            // 返回success.html
            return "success";
        } else {
            model.addAttribute("msg", "请登录");
            return "login";
        }
    }
}

访问http://localhost:8080/login时返回login.html页面。

点击注册,通过html的form表单th:action="@{/gotoregister}跳到Controller的@GetMapping("gotoregister"),返回registe.html页面。

点击登录,通过html的form表单th:action="@{/login}跳到Controller的@PostMapping("login"),查询成功后重定向到@GetMapping("success"),返回success.html

对应的html放置在demospringboot\src\main\resources\templates\目录下:

login.html:

java 复制代码
<!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="@{/gotoregister}">
            <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:

java 复制代码
<!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>

success.html:

java 复制代码
<!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>

step3:多线程task

首先,实现两个UserService和AsyncUserService两个服务接口:

接口:

java 复制代码
package com.example.demospringboot.service;

public interface UserService {
    void checkUserStatus();
}
java 复制代码
package com.example.demospringboot.service;

public interface AsyncUserService {
    void checkUserStatus();
}

对应实现:

java 复制代码
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 {

    @Autowired
    private UserMapper userMapper;

    @Override
    public void checkUserStatus() {
        List<User> AllUsers = userMapper.findAllUsers();
        for (User u : AllUsers) {
           // System.out.println(ThreadUtils.getThreadName() + ": " + u);
            log.info("{}", u);
        }
    };
}
java 复制代码
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 {

    @Autowired
    private AsyncTasks asyncTasks;

    @Override
    public void checkUserStatus() {
        asyncTasks.doTaskOne("1");
        asyncTasks.doTaskOne("2");
        asyncTasks.doTaskOne("3");
    };
}

用到的task类如下:

java 复制代码
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工具类如下:

java 复制代码
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线程池
    @Bean
    public 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进行管理:

java 复制代码
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);

    @Autowired
    private 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;
        }

        @Override
        public void run() {
            while (true) {
                userService.checkUserStatus();
                // sleep 1s
                ThreadUtils.sleepUtil(1000L);
            }
        }
    }
}
java 复制代码
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);

    @Autowired
    private 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;
        }

        @Override
        public void run() {
          while (true) {
                asyncUserService.checkUserStatus();
               // sleep 1s
               ThreadUtils.sleepUtil(1000L);
           }
        }
    }
}

主类如下:

java 复制代码
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 {
    @Autowired
    private WorkManager workManager;

    @Autowired
    private AsyncWorkManager asyncWorkManager;

    public static void main(String[] args) {
        SpringApplication.run(DemospringbootApplication.class, args);
    }

    @Override
    public 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;

    @Autowired
    private CacheManager cacheManager;

    @Test
    public void testUserMapper() throws Exception {
        // deleteAllUsers
        userMapper.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);

        // findUserById
        user = userMapper.findUserById(100);
        Assert.assertEquals("Jacky", user.getUsername());

        // updateUserPassword
        user.setPassword("1500");
        userMapper.updateUserPassword(user);
        Assert.assertEquals("1500", user.getPassword());

        // deleteUserById
        userMapper.deleteUserById(100);

        // findAllUsers
        List<User> AllUsers = userMapper.findAllUsers();
        for (User u : AllUsers) {
            System.out.println(u);
        }
        //Assert.assertEquals(1, AllUsers.size());

        System.out.println("CacheManager type : " + cacheManager.getClass());

    }


    @Autowired
    private AsyncTasks asyncTasks;


    @Test
    public void testTasks() throws Exception {
        long start = System.currentTimeMillis();

        // 线程池1
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");

        // 线程池2
        CompletableFuture<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) + "毫秒");
    }

}
相关推荐
L.EscaRC11 分钟前
Lua语言知识与应用解析
java·python·lua
S7777777S17 分钟前
easyExcel单元格动态合并示例
java·excel
间彧20 分钟前
什么是Region多副本容灾
后端
爱敲代码的北21 分钟前
WPF容器控件布局与应用学习笔记
后端
爱敲代码的北22 分钟前
XAML语法与静态资源应用
后端
清空mega23 分钟前
从零开始搭建 flask 博客实验(5)
后端·python·flask
爱敲代码的北27 分钟前
UniformGrid 均匀网格布局学习笔记
后端
刘个Java28 分钟前
对接大疆上云api---实现直播效果
java
用户95451568116230 分钟前
== 和 equals 区别及使用方法组件封装方法
java
hashiqimiya33 分钟前
html的input的required
java·前端·html