Springboot和一些主流框架的整合样例

  1. 1. Springboot项目的创建
    1. 1.1 Springboot相关基本概念
    2. 1.2 Spring Initializer快速创建项目
    3. 1.3 前后端分离的后端接口样例
  2. 2. Springboot与其他框架的基本整合
    1. 2.1 整合Druid数据库连接池
      1. 2.1.1 Druid是什么
      2. 2.2.2 整合Druid
      3. 2.2.3 去除Druid的广告
    2. 2.2 整合Screw数据库表结构导出
      1. 2.2.1 Screw是什么
      2. 2.2.2 Screw的特点及支持
      3. 2.2.3 整合Screw
    3. 2.3 整合Swagger2接口文档
      1. 2.3.1 Swagger2是什么
      2. 2.3.2 整合Swagger2
      3. 2.3.3 Swagger2注解
      4. 2.3.4 导出离线接口文档
    4. 2.4 整合Sa-Token权限系统
      1. 2.4.1 Sa-Token是什么
      2. 2.4.2 整合Sa-Token
      3. 2.4.3 登录认证API
    5. 2.5 整合Shiro权限系统
      1. 2.5.1 Shiro是什么
      2. 2.5.2 整合Shiro
    6. 2.6 整合xxl-job分布式任务调度平台
      1. 2.6.1 xxl-job是什么
      2. 2.6.2 整合xxl-job
      3. 2.6.3 xxl-job的开发定时任务
      4. 2.6.4 xxl-job的服务器部署
    7. 2.7 整合MQTT客户端
      1. 2.7.1 MQTT是什么
      2. 2.7.2 使用Springboot开发MQTT客户端
      3. 2.7.3 测试开发的MQTT客户端
  3. 3. Springboot常见问题
    1. 3.1 解决跨域问题
      1. 3.1.1 跨域与CORS简介
      2. 3.1.2 实现全局跨域
    2. 3.2 统一接口响应格式
      1. 3.2.1 接口标准响应格式
      2. 3.2.2 实现统一响应格式
    3. 3.3 其他方便开发的依赖库
    4. 3.4 其他开发中的常见基本写法
    5. 3.5 其他常见报错问题
  4. 4. 常用的Java工具类
    1. 4.1 生成指定位数的随机密码
    2. 4.2 对txt进行等量拆分
    3. 4.3 将数据库数据以json格式导出到txt文件里
    4. 4.4 将某目录下所有文件的名称保存到txt里
    5. 4.5 按行读取txt文件内容
    6. 4.6 检查指定URL的HTTP请求状态码
    7. 4.7 生成指定位数的数字标号
    8. 4.8 按照指定长度拆分List
    9. 4.9 检查字符串中是否包含某子串
  5. 5. 参考资料

1. Springboot项目的创建

1.1 Springboot相关基本概念

Springboot:是简化Spring应用开发的一个框架,其核心理念是:“约定优于配置”。是整个Spring技术栈的一个大整合、JavaEE开发的一站式解决方案。

调用逻辑:控制层调业务层,业务层调数据层。

Springboot调用逻辑

基本概念:

(1)DAO(mapper),DAO= Data Acess Object, 数据持久层,对数据库进行持久化操作,负责跟数据库打交道。通常我们在DAO层里写接口,里面有与数据打交道的方法。SQL语句通常写在mapper文件里。

(2)Service,业务层或服务层,主要负责业务模块的逻辑应用设计。 Service层的实现,具体调用到已经定义的DAO接口,封装service层的业务逻辑有利于通用的业业务逻辑的独立性和重复利用性。如果把Dao层当作积木,那么Service层则是对积木的搭建。

(3)Controller, 负责具体的业务模块流程的控制。此层要调用Service层的接口去控制业务流程。

(4)Pojo 全称Plain Ordinary Java Object ,数据库实体类,有的地方也直接写成entity。也可以理解为domain,一般是跟数据库对应好的一个实体类。

(5)Bo ,bussiness object,表示业务对象的意思。bo就是把业务逻辑封装成一个对象,这个对象可以包括一个或多个对象。通过调用dao方法,结合Po或Vo进行业务操作。

(6)Vo ,value object表示值对象的也i是,通常用于业务层之间的数据传递。

(7)Po, persistant object, 代表持久层对象的意思,对应数据库中表的字段,可以理解为一个po就是数据库中的一条记录。

(8)Impl 全称是 implement, 实现的意思,主要用于实现接口。

1.2 Spring Initializer快速创建项目

IDEA支持使用Spring Initializer快速创建一个Spring Boot项目,选择我们需要的模块,向导会联网创建Spring Boot项目。

Step1:创建Spring Initializr工程,填写Project Metadata相关信息,根据需要选择模块(如Spring Web)

Step2:进去之后配置maven并拉取所需jar包,写个HelloController.java测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package springboot_project.springboot_01_helloworld_quick.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

@RequestMapping("/hello")
public String hello(){
return "Hello world!";
}

}

Step3:主程序已经生成好了,剩下我们只需要写自己的逻辑,resources文件夹中目录结构如下:

  • static:保存所有的静态资源(js css images)
  • templates:保存所有的模板页面(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持JSP页面),可以使用模板引擎(freemarker、thymeleaf)
  • application.properties:SpringBoot应用的配置文件,可以修改一些默认设置。

1.3 前后端分离的后端接口样例

这里先准备下数据,创建一个user表,并插入数据,sql如下:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`email` varchar(255) NOT NULL COMMENT '邮箱',
`password` varchar(255) NOT NULL COMMENT '密码',
`username` varchar(255) NOT NULL COMMENT '姓名',
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES ('1', '[email protected]', '123456', '张三');
INSERT INTO `user` VALUES ('2', '[email protected]', '234567', '李四');
INSERT INTO `user` VALUES ('3', '[email protected]', '345678', '王五');

首先创建一个springboot项目,然后在pom文件中引入相关依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?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.3.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.springboot</groupId>
<artifactId>springbootdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springbootdemo</name>
<description>Demo project for Spring Boot</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<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-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<!-- MySQL 连接驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<!--io工具类-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>

</project>

application.properties文件,添加配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
## 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/jdbc?useUnicode=true&characterEncoding=utf8&autoReconnect=true&failOverReadOnly=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

## Mybatis 配置
# 配置为 com.pancm.bean 指向实体类包路径。
mybatis.typeAliasesPackage=com.springboot.bean
# 配置为 classpath 路径下 mapper 包下,* 代表会扫描所有 xml 文件。
mybatis.mapperLocations=classpath\:mapper/*.xml

开始编写核心逻辑代码:

新建一个User.java实体类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.springboot.springbootdemo.bean;
public class User {
private long id;
private String email;
private String password;
private String username;
public long getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

然后再新建一个dao文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.springboot.springbootdemo.dao;

import com.springboot.springbootdemo.bean.User;
import org.apache.ibatis.annotations.*;
import org.springframework.data.repository.query.Param;
import java.util.List;

@Mapper
public interface UserDao {
/**
* 新增数据
*/
@Insert("insert into user(id,email,password,username) values (#{id},#{email},#{password},#{username})")
void addUser(User user);

/**
* 修改数据
*/
@Update("update user set username=#{username},password=#{password} where id=#{id}")
void updateUser(User user);

/**
* 删除数据
*/
@Delete("delete from user where id=#{id}")
void deleteUser(int id);

/**
* 根据查询数据
*
*/
@Select("select id,email,password,username from user where username=#{userName}")
User findByName(@Param("userName") String userName);

/**
* 查询所有数据
*/
@Select("select id,email,password,username FROM user")
List<User> findAll();
}

然后就是UserService和UserServiceImpl文件:

UserService文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.springboot.springbootdemo.service;

import com.springboot.springbootdemo.bean.User;

import java.util.List;

public interface UserService {
/**
* 新增用户
* @param user
* @return
*/
boolean addUser(User user);

/**
* 修改用户
* @param user
* @return
*/
boolean updateUser(User user);

/**
* 删除用户
* @param id
* @return
*/
boolean deleteUser(int id);

/**
* 根据名字查询用户信息
* @param userName
*/
User findUserByName(String userName);

/**
* 查询所有数据
* @return
*/
List<User> findAll();
}

UserServiceImpl文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.springboot.springbootdemo.service;
import com.springboot.springbootdemo.bean.User;
import com.springboot.springbootdemo.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;

@Override
public boolean addUser(User user) {
boolean flag=false;
try{
userDao.addUser(user);
flag=true;
}catch(Exception e){
e.printStackTrace();
}
return flag;
}

@Override
public boolean updateUser(User user) {
boolean flag=false;
try{
userDao.updateUser(user);
flag=true;
}catch(Exception e){
e.printStackTrace();
}
return flag;
}

@Override
public boolean deleteUser(int id) {
boolean flag=false;
try{
userDao.deleteUser(id);
flag=true;
}catch(Exception e){
e.printStackTrace();
}
return flag;
}

@Override
public User findUserByName(String userName) {
return userDao.findByName(userName);
}


@Override
public List<User> findAll() {
return userDao.findAll();
}
}

最后就是Controller文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.springboot.springbootdemo.controller;
import com.springboot.springbootdemo.bean.User;
import com.springboot.springbootdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping(value = "/do/user")
public class UserController {
@Autowired
private UserService userService;

@RequestMapping(value = "/user", method = RequestMethod.POST)
public boolean addUser(@RequestBody User user) {
System.out.println("新增数据:");
return userService.addUser(user);
}

@RequestMapping(value = "/user", method = RequestMethod.PUT)
public boolean updateUser(@RequestBody User user) {
System.out.println("更新数据:");
return userService.updateUser(user);
}

@RequestMapping(value = "/user", method = RequestMethod.DELETE)
public boolean delete(@RequestParam(value = "id", required = true) int Id) {
System.out.println("删除数据:");
return userService.deleteUser(Id);
}


@RequestMapping(value = "/user", method = RequestMethod.GET)
public User findByUserName(@RequestParam(value = "userName", required = true) String userName) {
System.out.println("查询数据:");
return userService.findUserByName(userName);
}

@RequestMapping(value = "/userAll", method = RequestMethod.GET)
public List<User> findByUserAge() {
System.out.println("查询所有数据:");
return userService.findAll();
}
}

然后启动项目,使用Postman进行测试即可。

2. Springboot与其他框架的基本整合

2.1 整合Druid数据库连接池

2.1.1 Druid是什么

Druid是阿里巴巴的一个开源项目,号称为监控而生的数据库连接池,在功能、性能、扩展性方面都超过其他例如DBCP、C3P0、BoneCP、Proxool、JBoss、DataSource等连接池,而且Druid已经在阿里巴巴部署了超过600个应用,通过了极为严格的考验。

Github项目地址:https://github.com/alibaba/druid

Druid Monitor

2.2.2 整合Druid

Step1:在我们项目的pom.xml文件中添加如下的依赖

1
2
3
4
5
6
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>

Step2:配置application.properties文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
## 数据源配置
# 表明使用Druid连接池
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# MySQL驱动
spring.datasource.driverClassName=com.mysql.jdbc.Driver
# 数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/eran?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
# 数据库用户名
spring.datasource.username=root
# 数据库密码
spring.datasource.password=root

## Druid连接池相关属性
#初始化时建立物理连接的个数。
spring.datasource.druid.initial-size=5
#最大连接池数量
spring.datasource.druid.max-active=20
#最小连接池数量
spring.datasource.druid.min-idle=5
#获取连接时最大等待时间,单位毫秒
spring.datasource.druid.max-wait=3000
#是否缓存preparedStatement,也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。
spring.datasource.druid.pool-prepared-statements=false
#要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
spring.datasource.druid.max-open-prepared-statements= -1
#配置检测可以关闭的空闲连接间隔时间
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置连接在池中的最小生存时间
spring.datasource.druid.min-evictable-idle-time-millis= 300000
spring.datasource.druid.max-evictable-idle-time-millis= 400000
#监控统计的stat,以及防sql注入的wall
spring.datasource.druid.filters= stat,wall
#Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
spring.datasource.druid.aop-patterns= com.xxx.xxx.service.*
#是否启用StatFilter默认值true
spring.datasource.druid.web-stat-filter.enabled= true
#添加过滤规则
spring.datasource.druid.web-stat-filter.url-pattern=/*
#忽略过滤的格式
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
#是否启用StatViewServlet默认值true
spring.datasource.druid.stat-view-servlet.enabled= true
#访问路径为/druid时,跳转到StatViewServlet
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
#是否能够重置数据
spring.datasource.druid.stat-view-servlet.reset-enable=false
#需要账号密码才能访问控制台,默认为root
spring.datasource.druid.stat-view-servlet.login-username=root
spring.datasource.druid.stat-view-servlet.login-password=root
#IP白名单
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
#IP黑名单(共同存在时,deny优先于allow)
spring.datasource.druid.stat-view-servlet.deny=

## Mybatis 配置
#配置为 com.pancm.bean 指向实体类包路径。
mybatis.typeAliasesPackage=com.springboot.bean
#配置为 classpath 路径下 mapper 包下,* 代表会扫描所有 xml 文件。
mybatis.mapperLocations=classpath\:mapper/*.xml

直接启动项目,浏览器访问localhost:8080/druid即可看到监控页面。

2.2.3 去除Druid的广告

如果使用的是阿里Druid的数据库连接池,那么会自带一个数据库的监控页面,但是其页面底部会有阿里的广告,如下图所示,并且在其最下方的作者申明中,有一个作者的链接,会直接到澳门赌场的页面,这是极其不友好的,因此需要进行去除。

Druid广告

在SpringBoot项目中编写一个RemoveDruidAdConfig配置类,进行监控页面广告的去除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import java.io.IOException;


/**
* 类名称: RemoveDruidAdConfig
* 类描述: 去除druid底部的广告配置类
*/

@Configuration
@ConditionalOnWebApplication
@AutoConfigureAfter(DruidDataSourceAutoConfigure.class)
@ConditionalOnProperty(name = "spring.datasource.druid.stat-view-servlet.enabled", havingValue = "true", matchIfMissing = true)
public class RemoveDruidAdConfig {


/**
* 方法名: removeDruidAdFilterRegistrationBean
* 方法描述: 除去页面底部的广告
* @param properties
* @return org.springframework.boot.web.servlet.FilterRegistrationBean
* @throws
*/
@Bean
public FilterRegistrationBean removeDruidAdFilterRegistrationBean(DruidStatProperties properties) {
// 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");

final String filePath = "support/http/resources/js/common.js";

//创建filter进行过滤
Filter filter = new Filter() {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
// 重置缓冲区,响应头不会被重置
response.resetBuffer();
// 获取common.js
String text = Utils.readFromResource(filePath);
// 正则替换banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text);
}

@Override
public void destroy() {
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}

再次启动项目,可以看到其底部的广告信息已经没有了。

原理说明:之所以底部有广告,是因为其引入的druid jar包的common.js中的内容,这段footer就是广告的来源。

1
2
3
4
5
6
var html ='<footer class="footer">'+
' <div class="container">'+
'<a href="https://render.alipay.com/p/s/taobaonpm_click/druid_banner_click" target="new"><img src="https://render.alipay.com/p/s/taobaonpm_click/druid_banner"></a><br/>' +
' powered by <a href="https://github.com/alibaba/" target="_blank">Alibaba</a> & sandzhang & <a href="http://melin.iteye.com/" target="_blank">melin</a> & <a href="https://github.com/shrekwang" target="_blank">shrek.wang</a>'+
' </div>'+
' </footer>';

在 RemoveDruidAdConfig 配置类中就是使用过滤器过滤common.js的请求,重新处理后用正则替换相关的广告代码片段。

2.2 整合Screw数据库表结构导出

2.2.1 Screw是什么

一句话简介:screw是一个简洁好用的数据库表结构文档生成器

Github项目地址:https://github.com/pingfangushi/screw

2.2.2 Screw的特点及支持

特点:[1] 简洁、轻量、设计良好 [2] 多数据库支持 [3] 多种格式文档 [4] 灵活扩展 [5] 支持自定义模板

数据库支持:MySQL、MariaDB、TIDB、Oracle、SqlServer、PostgreSQL、Cache DB

文档生成支持:html、md、word

screw-1

screw-2

2.2.3 整合Screw

官网提供了“普通方式”和“Maven插件”两种使用方式,本文只讲“普通方式”的使用方法,以最常见的MySQL和Oracle为例。

  • Step1:创建Springboot项目并配置好Maven环境

  • Step2:配置Maven依赖

    将以下代码添加到pom.xml文件中,然后打开Maven窗口,点击Lifecycle里的install安装依赖包

    注:screw核心的版本号填最新的,点此查看最新版本号

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <!-- screw核心 -->
    <dependency>
    <groupId>cn.smallbun.screw</groupId>
    <artifactId>screw-core</artifactId>
    <version>1.0.5</version>
    </dependency>
    <!-- HikariCP -->
    <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.4.5</version>
    </dependency>
    <!--mysql driver-->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
    </dependency>
    <!--oracle driver-->
    <dependency>
    <groupId>com.oracle.ojdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>19.3.0.0</version>
    </dependency>
    <dependency>
    <groupId>cn.easyproject</groupId>
    <artifactId>orai18n</artifactId>
    <version>12.1.0.2.0</version>
    </dependency>
  • Step3:编写数据库设计文档的生成代码

    在任意地方创建一个测试类ScrewConfig.java,编写数据库设计文档的生成代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    package com.example.screw;

    import cn.smallbun.screw.core.Configuration;
    import cn.smallbun.screw.core.engine.EngineConfig;
    import cn.smallbun.screw.core.engine.EngineFileType;
    import cn.smallbun.screw.core.engine.EngineTemplateType;
    import cn.smallbun.screw.core.execute.DocumentationExecute;
    import cn.smallbun.screw.core.process.ProcessConfig;
    import com.zaxxer.hikari.HikariConfig;
    import com.zaxxer.hikari.HikariDataSource;

    import javax.sql.DataSource;
    import java.util.ArrayList;

    public class ScrewConfig {
    /**
    * 数据库设计文档生成
    */
    static void documentGeneration() {
    //数据源
    HikariConfig hikariConfig = new HikariConfig();
    //数据库driver
    hikariConfig.setDriverClassName("oracle.jdbc.driver.OracleDriver"); //oracle
    //hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); //mysql
    //数据库URL
    hikariConfig.setJdbcUrl("jdbc:oracle:thin:@IP地址:端口:数据库名?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=CTT");
    //hikariConfig.setJdbcUrl("jdbc:mysql://IP地址:端口/数据库名?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=CTT");
    //数据库用户名
    hikariConfig.setUsername("root");
    //数据库用户密码
    hikariConfig.setPassword("r");
    //设置可以获取tables remarks信息
    hikariConfig.addDataSourceProperty("useInformationSchema", "true");
    hikariConfig.setMinimumIdle(2);
    hikariConfig.setMaximumPoolSize(5);
    DataSource dataSource = new HikariDataSource(hikariConfig);
    //生成配置
    EngineConfig engineConfig = EngineConfig.builder()
    //生成文件路径
    .fileOutputDir("../doc")
    //打开目录
    .openOutputDir(true)
    //文件类型 HTML、MD、WORD
    .fileType(EngineFileType.HTML)
    //生成模板实现
    .produceType(EngineTemplateType.freemarker)
    //自定义文件名称
    .fileName("demo").build();

    //忽略表
    ArrayList<String> ignoreTableName = new ArrayList<>();
    ignoreTableName.add("test_user");
    ignoreTableName.add("test_group");
    //忽略表前缀
    ArrayList<String> ignorePrefix = new ArrayList<>();
    ignorePrefix.add("test_");
    //忽略表后缀
    ArrayList<String> ignoreSuffix = new ArrayList<>();
    ignoreSuffix.add("_test");
    ProcessConfig processConfig = ProcessConfig.builder()
    //指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置
    //根据名称指定表生成
    .designatedTableName(new ArrayList<>())
    //根据表前缀生成
    .designatedTablePrefix(new ArrayList<>())
    //根据表后缀生成
    .designatedTableSuffix(new ArrayList<>())
    //忽略表名
    .ignoreTableName(ignoreTableName)
    //忽略表前缀
    .ignoreTablePrefix(ignorePrefix)
    //忽略表后缀
    .ignoreTableSuffix(ignoreSuffix).build();
    //配置
    Configuration config = Configuration.builder()
    //版本
    .version("v1.0")
    //数据源
    .dataSource(dataSource)
    //生成配置
    .engineConfig(engineConfig)
    //生成配置
    .produceConfig(processConfig)
    .build();
    //执行生成
    new DocumentationExecute(config).execute();
    }

    public static void main(String[] args) {
    documentGeneration();
    }
    }

    注:如果项目里的MySQL是5.x,驱动处则为com.mysql.jdbc.Driver

  • Step4:运行screw生成数据库设计文档

    直接运行ScrewConfig.java代码,即可生成数据库设计文档。

2.3 整合Swagger2接口文档

2.3.1 Swagger2是什么

前后端分离后,维护接口文档基本上是必不可少的工作。Swagger2是一个开源工具,可以在开发过程使用注解生成接口文档。这个接口文档可以直接在上面请求接口。

项目地址:https://github.com/swagger-api/swagger-ui

Swagger

2.3.2 整合Swagger2

创建一个Springboot项目,加入web依赖,然后再加入两个Swagger2相关的依赖,如下:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

Swagger2的配置也是比较容易的,配置类示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2Config {

@Bean
public Docket controllerApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("API接口文档")
.version("版本号:1.0")
.build())
.select()
.apis(RequestHandlerSelectors.basePackage("com.xxx.xxx.controller"))
.paths(PathSelectors.any())
.build();
}
}

此时启动项目,输入http://localhost:8080/swagger-ui.html即可看到接口文档。

说明:如果Swagger启动时报错Unable to scan documentation context default,无法显示Swagger文档,可能是因为开启了分组模式。

在 SwaggerConfig 中加入@Bean 注解,就是启用了分组模式,如果都没有 @Bean 就是默认模式。默认模式访问 /v2/api-docs 直接可以获取到所有的 json 数据。

另注:Swagger去掉basic-error-controller,在该配置文件后面再加一个bean即可。

1
2
3
4
5
6
7
8
@Bean
public Docket demoApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.regex("(?!/error.*).*"))
.build();
}

2.3.3 Swagger2注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Api:用在请求的类上,表示对类的说明
tags="说明该类的作用,可以在UI界面上看到的注解"
value="该参数没什么意义,在UI界面上也看到,所以不需要配置"

@ApiOperation:用在请求的方法上,说明方法的用途、作用
value="说明方法的用途、作用"
notes="方法的备注说明"

@ApiImplicitParams:用在请求的方法上,表示一组参数说明
@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
name:参数名
value:参数的汉字说明、解释
required:参数是否必须传
paramType:参数放在哪个地方
· header --> 请求参数的获取:@RequestHeader
· query --> 请求参数的获取:@RequestParam
· path(用于restful接口)--> 请求参数的获取:@PathVariable
· body(不常用)
· form(不常用)
dataType:参数类型,默认String,其它值dataType="Integer"
defaultValue:参数的默认值

@ApiResponses:用在请求的方法上,表示一组响应
@ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息
code:数字,例如400
message:信息,例如"请求参数没填好"
response:抛出异常的类

@ApiModel:用于响应类上,表示一个返回响应数据的信息
(这种一般用在post创建的时候,使用@RequestBody这样的场景,
请求参数无法使用@ApiImplicitParam注解进行描述的时候)
@ApiModelProperty:用在属性上,描述响应类的属性

2.3.4 导出离线接口文档

前面部署的Swagger接口文档是一个在线文档,需要启动项目才能查看,不够方便。可以通过 swagger2markup 将其导出离线版。

支持的格式很多,以下我只导出html、markdown、asciidoc三种格式,下图是html格式

Swagger离线版接口文档

Step1:首先在pom.xml导入以下4个依赖(严格按照这个版本号,不然可能会出错)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- swagger2导出离线接口文档 https://github.com/Swagger2Markup/swagger2markup -->
<dependency>
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>ch.netzwerg</groupId>
<artifactId>paleo-core</artifactId>
<version>0.10.2</version>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.9.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>

Step2:编写生成接口文档的工具类,先启动Appliation运行项目,再运行该工具类生成markdown、asciidoc格式的接口文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import io.github.swagger2markup.GroupBy;
import io.github.swagger2markup.Language;
import io.github.swagger2markup.Swagger2MarkupConfig;
import io.github.swagger2markup.Swagger2MarkupConverter;
import io.github.swagger2markup.builder.Swagger2MarkupConfigBuilder;
import io.github.swagger2markup.markup.builder.MarkupLanguage;
import org.junit.Test;
import java.net.URL;
import java.nio.file.Paths;

/**
* 生成各种格式的接口文档(需要先启动Appliation)
*/

public class ExportSwagger {

/**
* 生成Markdown格式文档--单文件版
* @throws Exception
*/
@Test
public void generateMarkdownDocsToFile() throws Exception {
// 输出Markdown到单文件
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.MARKDOWN)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();

Swagger2MarkupConverter.from(new URL("http://localhost:8080/v2/api-docs"))
.withConfig(config)
.build()
.toFile(Paths.get("src/main/resources/docs/markdown/interface-doc"));
}

/**
* 生成AsciiDocs格式文档--单文件版(用于转换HTML)
* @throws Exception
*/
@Test
public void generateAsciiDocsToFile() throws Exception {
// 输出Ascii到单文件
Swagger2MarkupConfig config = new Swagger2MarkupConfigBuilder()
.withMarkupLanguage(MarkupLanguage.ASCIIDOC)
.withOutputLanguage(Language.ZH)
.withPathsGroupedBy(GroupBy.TAGS)
.withGeneratedExamples()
.withoutInlineSchema()
.build();

Swagger2MarkupConverter.from(new URL("http://localhost:8080/v2/api-docs"))
.withConfig(config)
.build()
.toFile(Paths.get("src/main/resources/docs/asciidoc/interface-doc"));
}

}

Step3:再在pom.xml里添加asciidoc转html的Maven插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--将asciidoc格式的接口文档转换成html格式-->
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.6</version>
<configuration>
<!--asciidoc文件目录-->
<sourceDirectory>src/main/resources/docs/asciidoc</sourceDirectory>
<!---生成html的路径-->
<outputDirectory>src/main/resources/docs/html</outputDirectory>
<backend>html</backend>
<sourceHighlighter>coderay</sourceHighlighter>
<attributes>
<!--导航栏在左-->
<toc>left</toc>
<!--显示层级数-->
<toclevels>3</toclevels>
<!--自动打数字序号-->
<sectnums>true</sectnums>
</attributes>
</configuration>
</plugin>

注意资源路径要与生成的asciidoc路径相对应,然后双击下图选中的asciidoctor:process-asciidoc即可生成html格式的离线文档。

使用asciidoc转html的插件

2.4 整合Sa-Token权限系统

2.4.1 Sa-Token是什么

Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。

框架集成简单、开箱即用、API设计清爽,通过Sa-Token,你将以一种极其简单的方式实现系统的权限认证部分。

  • 登录认证 —— 单端登录、多端登录、同端互斥登录、七天内免登录
  • 权限认证 —— 权限认证、角色认证、会话二级认证
  • Session会话 —— 全端共享Session、单端独享Session、自定义Session
  • 踢人下线 —— 根据账号id踢人下线、根据Token值踢人下线
  • 账号封禁 —— 指定天数封禁、永久封禁、设定解封时间
  • 持久层扩展 —— 可集成Redis、Memcached等专业缓存中间件,重启数据不丢失
  • 分布式会话 —— 提供jwt集成、共享数据中心两种分布式会话方案
  • 微服务网关鉴权 —— 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证
  • 单点登录 —— 内置三种单点登录模式:无论是否跨域、是否共享Redis,都可以搞定
  • OAuth2.0认证 —— 基于RFC-6749标准编写,OAuth2.0标准流程的授权认证,支持openid模式
  • 二级认证 —— 在已登录的基础上再次认证,保证安全性
  • 独立Redis —— 将权限缓存与业务缓存分离
  • 临时Token验证 —— 解决短时间的Token授权问题
  • 模拟他人账号 —— 实时操作任意用户状态数据
  • 临时身份切换 —— 将会话身份临时切换为其它账号
  • 前后台分离 —— APP、小程序等不支持Cookie的终端
  • 同端互斥登录 —— 像QQ一样手机电脑同时在线,但是两个手机上互斥登录
  • 多账号认证体系 —— 比如一个商城项目的user表和admin表分开鉴权
  • 花式token生成 —— 内置六种Token风格,还可:自定义Token生成策略、自定义Token前缀
  • 注解式鉴权 —— 优雅的将鉴权与业务代码分离
  • 路由拦截式鉴权 —— 根据路由拦截鉴权,可适配restful模式
  • 自动续签 —— 提供两种Token过期策略,灵活搭配使用,还可自动续签
  • 会话治理 —— 提供方便灵活的会话查询接口
  • 记住我模式 —— 适配[记住我]模式,重启浏览器免验证
  • 密码加密 —— 提供密码加密模块,可快速MD5、SHA1、SHA256、AES、RSA加密
  • 全局侦听器 —— 在用户登录、注销、被踢下线等关键性操作时进行一些AOP操作
  • 开箱即用 —— 提供SpringMVC、WebFlux等常见web框架starter集成包,真正的开箱即用

项目地址:https://github.com/dromara/Sa-Token

官方文档:http://sa-token.dev33.cn/doc/index.html#/README (中文文档,写的很详细,一定要看!)

Sa-Token 认证流程图

2.4.2 整合Sa-Token

Step1:在 pom.xml 中添加依赖

1
2
3
4
5
6
<!-- Sa-Token 权限认证, 在线文档:http://sa-token.dev33.cn/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.26.0</version>
</dependency>

Step2:设置配置文件

你可以零配置启动项目,但同时你也可以在application.yml中增加如下配置,定制性使用框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
## Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: satoken
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: false
# token风格
token-style: uuid
# 是否输出操作日志
is-log: false

Step3:创建启动类

1
2
3
4
5
6
7
@SpringBootApplication
public class SaTokenDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenDemoApplication.class, args);
System.out.println("启动成功:Sa-Token配置如下:" + SaManager.getConfig());
}
}

Step4:创建测试Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/user/")
public class UserController {

// 测试登录,浏览器访问: http://localhost:8080/user/doLogin?username=zhang&password=123456
@RequestMapping("doLogin")
public String doLogin(String username, String password) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(username) && "123456".equals(password)) {
StpUtil.login(10001);
return "登录成功";
}
return "登录失败";
}

// 查询登录状态,浏览器访问: http://localhost:8080/user/isLogin
@RequestMapping("isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}

}

2.4.3 登录认证API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 标记当前会话登录的账号id 
StpUtil.login(Object id);

// 当前会话注销登录
StpUtil.logout();

// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();

// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin()

// 获取指定token对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);

// 获取当前`StpLogic`的token名称
StpUtil.getTokenName();

// 获取当前会话的token值
StpUtil.getTokenValue();

// 获取当前会话的token信息参数
StpUtil.getTokenInfo();

说明:TokenInfo参数详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"code": 200,
"msg": "ok",
"data": {
"tokenName": "satoken", // token名称
"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值
"isLogin": true, // 此token是否已经登录
"loginId": "10001", // 此token对应的LoginId,未登录时为null
"loginType": "login", // 账号类型标识
"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)
"sessionTimeout": 2591977, // User-Session剩余有效时间 (单位: 秒)
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒)
"tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒)
"loginDevice": "default-device" // 登录设备标识
},
}

2.5 整合Shiro权限系统

2.5.1 Shiro是什么

Shiro是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。借助 Shiro 易于理解的 API,您可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序。

项目地址:https://github.com/apache/shiro

2.5.2 整合Shiro

Step1:在pom.xml里添加如下依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>

Step2:创建 Realm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class MyRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
if (!"admin".equals(username)) {
throw new UnknownAccountException("账户不存在!");
}
return new SimpleAuthenticationInfo(username, "sa", getName());
}
}

Step3:配置 Shiro 基本信息

在 application.properties 中配置 Shiro 的基本信息

1
2
3
4
5
6
7
8
9
10
11
12
13
## Shiro配置
# 是否允许将sessionId 放到 cookie 中
shiro.sessionManager.sessionIdCookieEnabled=true
# 是否允许将 sessionId 放到 Url 地址拦中
shiro.sessionManager.sessionIdUrlRewritingEnabled=true
# 访问未获授权的页面时,默认的跳转路径
shiro.unauthorizedUrl=/unauthorizedurl
# 开启shiro
shiro.web.enabled=true
# 登录成功的跳转页面
shiro.successUrl=/index
# 登录页面
shiro.loginUrl=/login

Step4:配置ShiroConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {
@Bean
MyRealm myRealm() {
return new MyRealm();
}
@Bean
DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(myRealm());
return manager;
}
@Bean
ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
definition.addPathDefinition("/doLogin", "anon");
definition.addPathDefinition("/**", "authc");
return definition;
}
}

2.6 整合xxl-job分布式任务调度平台

2.6.1 xxl-job是什么

一个轻量级分布式任务调度框架 ,主要分为调度中心和执行器两部分,调度中心在启动初始化的时候,会默认生成执行器的RPC代理对象, 执行器项目启动之后,调度中心在触发定时器之后通过jobHandle来调用执行器项目里面的代码。

项目地址:https://github.com/xuxueli/xxl-job/

xxl-job的中文文档非常详细,强烈建议看官方文档操作。xxl-job官方文档

xxl-job

2.6.2 整合xxl-job

环境要求:Maven3+、Jdk1.8+、Mysql5.7+

Step1:去github把项目代码clone下来,下载安装Maven依赖,将建表sql导入MySQL。

1
2
3
4
5
6
doc:项目文档(含离线版中英文文档、架构说明ppt、数据库建表sql)
xxl-job-admin:调度中心
xxl-job-core:公共依赖(用Maven引入即可,这个用不到)
xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器)
:xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式;
:xxl-job-executor-sample-frameless:无框架版本;

注:sql里包含建库语句,如果已经有数据库了,把那句去掉即可。下面是关于数据表的说明:

1
2
3
4
5
6
7
8
- xxl_job_lock:任务调度锁表;
- xxl_job_group:执行器信息表,维护任务执行器信息;
- xxl_job_info:调度扩展信息表: 用于保存XXL-JOB调度任务的扩展信息,如任务分组、任务名、机器地址、执行器、执行入参和报警邮件等等;
- xxl_job_log:调度日志表: 用于保存XXL-JOB任务调度的历史信息,如调度结果、执行结果、调度入参、调度机器和执行器等等;
- xxl_job_log_report:调度日志报表:用户存储XXL-JOB任务调度日志的报表,调度中心报表功能页面会用到;
- xxl_job_logglue:任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
- xxl_job_user:系统用户表;

Step2:配置xxl-job-admin项目的application.properties文件(主要是修改数据库连接,再就是建议设置一下accessToken,其他配置参照官方文档即可),然后启动xxl-job-admin项目,Chrome打开http://localhost:port/xxl-job-admin地址,即可看到调度中心。

1
初始账号密码:admin/123456 (登录进去可以在系统里增删用户、修改密码)

Step3:配置xxl-job-executor-sample-springboot项目的application.properties文件,主要修改以下两项:

1
2
3
4
# 调度中心地址(就刚刚的xxl-job-admin启动地址)
xxl.job.admin.addresses = http://localhost:port/xxl-job-admin
# accessToken值(与调度中心的配置一致即可)
xxl.job.accessToken =

注:IP建议不填,执行器启动后,xxl-job-admin会自动检索,自动注册如果找得到IP,就说明执行器启动成功了,算是个验证吧。

另注:执行器可以整合进自己项目里(把SampleXxlJob和XxlJobConfig加进去即可),也可以单独建个项目,个人建议使用后者。因为可能定时任务模块已经开发好了,可以放到服务器里跑数据了,而项目还出于开发状态,还不便于部署。还有一个原因就是即便是使用shell模式,也必须在服务器放一个执行器(哪怕是空的)才能用,没有的话无法成功执行任务。

2.6.3 xxl-job的开发定时任务

下面运行一下官方提供的Demo,演示如何使用xxl-job开发定时任务,具体使用请查阅官方文档。

Step1:以SampleXxlJob.java的demoJobHandler()为例,这里定时任务代码已经写好了,如果要开发定时任务的话,就在这里面写具体业务逻辑(直接调用对应的Service即可)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class SampleXxlJob {
private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);

/**
* 简单任务示例(Bean模式)
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World."); // XxlJobHelper.log会记入到执行日志里

for (int i = 0; i < 5; i++) {
XxlJobHelper.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
// default success
}
}

另注:shell模式的话不用在这里开发,加任务的时候选择shell模式,加完之后直接在调度系统里写脚本即可。

Step2:打开调度中心,首先我们要在任务管理里添加任务,参照如下示例填写即可(官方提供的表里已有该数据),这时还不能执行

xxl-job任务配置

Step3:然后打开执行器管理,配置执行器,参照如下示例填写即可(官方提供的表里已有该数据)

xxl-job执行器管理

注:可以选择自动注册(由于我们在执行器里已经配置了调度中心地址),也可以选择手动导入,建议前者。配置成功后OnLine机器地址处便能查看到执行器地址,这个稍微有一点延迟,如果看不到的话说明之前配置的有问题,需要先去排查一下。

Step4:配置好了执行器之后再去任务管理处执行定时任务即可,执行结果可以在调度日志里查看(可以点开查看详细日志)

2.6.4 xxl-job的服务器部署

Step1:修改配置文件,对调度器和执行器分别用Maven插件打个jar包。

注:打包之前先执行mvn install,否则可能打出来的包小的离谱,可能是没有依赖,根本不能用。

另注:如果调度器和执行器部署到一个服务器上的话,调度器地址留localhost就行。

1
xxl.job.admin.addresses=http://localhost:8080/xxl-job-admin

Step2:将jar包上传到服务器上执行。

1
2
$ java -jar -Duser.timezone=GMT+8 xxl-job-admin.jar
$ java -jar -Duser.timezone=GMT+8 xxl-job-executor.jar

注:需要保证服务器上用的是jdk8,版本不一致的话执行器运行时会报错,如下图(本地跑的好好的jar放到服务器上就报错,排查了半天发现是服务器用的jdk11,换成jdk8就好了)

xxl-job因jdk版本不同部署报错

Step3:把启动项目的命令写个shell脚本,加开机自启。调度器加反向代理、HTTPS什么的,看你心情,弄不弄都无所谓了。

脚本的话,执行器和调度器要分开写,写一起的话只会执行第一个。还有个注意的点是,执行器启动要晚于调度器,因此我加了延时。

start_xxl_job_admin.sh

1
2
cd /myproject/xxl-job-admin
nohup java -jar -Duser.timezone=GMT+8 xxl-job-admin-2.1.6.RELEASE.jar > xxl-job-admin.log 2>&1 &

start_xxl_job_executor.sh

1
2
3
cd /myproject/xxl-job-executor
sleep 3m
nohup java -jar -Duser.timezone=GMT+8 executor-0.0.1-SNAPSHOT.jar > xxl-job-executor.log 2>&1 &

注意事项:

  • nohup加在一个命令的最前面,表示不挂断的运行命令。
  • -Duser.timezone=GMT+8表示采用东8区进行启动,否则会出现时间异常,调度器里的cron执行时间和日志记录都会受到影响。
  • 2>&1的意思是将标准错误(2)也定向到标准输出(1)的输出文件。
  • &加在一个命令的最后面,表示这个命令放在后台执行。

写好脚本之后,赋予其可执行权限,然后输入crontab -e命令,添加以下内容设置开机自启。

1
2
@reboot /myshell/start_xxl_job_admin.sh
@reboot /myshell/start_xxl_job_executor.sh

2.7 整合MQTT客户端

2.7.1 MQTT是什么

MQTT 是一个客户端服务端架构的发布/订阅模式的消息传输协议。 它的设计思想是轻巧、开放、简单、规范,因此易于实现。这些特点使得它对很多场景来说都是很好的选择,包括受限的环境如机器与机器的通信(M2M)以及物联网环境(IoT),这些场景要求很小的代码封装或者网络带宽非常昂贵。

MQTT 设计了 3 个 QoS 等级。

  • QoS 0:消息最多传递一次,如果当时客户端不可用,则会丢失该消息。
  • QoS 1:消息传递至少 1 次。
  • QoS 2:消息仅传送一次。

关于 MQTT 协议还不了解的话,可以先看一下这个手册: MQTT协议3.1.1中文翻译版

2.7.2 使用Springboot开发MQTT客户端

Step1:搭建 EMQX 消息服务器,准备MQTTX 客户端

对于 EMQX 消息服务器和 MQTTX 客户端的搭建及使用还不了解的话,见我的另一篇博客:VPS基本部署环境的搭建与配置

Step2:创建一个Springboot项目,使用Maven安装spring-integration-mqtt依赖:

1
2
3
4
5
6
<!--MQTT收发消息 https://docs.spring.io/spring-integration/reference/html/mqtt.html -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
<version>5.5.4</version>
</dependency>

Step3:配置文件及基本demo的代码如下:

application.yml

1
2
3
4
5
6
7
8
9
spring:
mqtt:
client-id: monitor
endpoint-url: tcp://ip:1883 # 这里配置EMQX消息服务器的地址
username: test
password: 666666
connection-timeout: 30
default-topic: defaultTopic
keep-alive-interval: 60

/config/MqttConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@Configuration
public class MqttConfig {
public static final String WILL_TOPIC = "willTopic";
public static final byte[] WILL_DATA = "offline".getBytes();

@Autowired
private MqttProperties mqttProperties;

/**
* MQTT 连接器选项设置
*
* @return {@link MqttConnectOptions}
*/
public MqttConnectOptions getMqttConnectOptions() {
MqttConnectOptions options = new MqttConnectOptions();
//设置是否清空 session
//true 表示每次连接到服务器都以新的身份连接
//false 表示服务器会保留客户端的连接记录
options.setCleanSession(true);
options.setUserName(mqttProperties.getUsername());
options.setPassword(mqttProperties.getPassword().toCharArray());
options.setServerURIs(mqttProperties.getEndpointUrl().split("[,]"));
options.setConnectionTimeout(mqttProperties.getConnectionTimeout());
//开启自动重连
options.setAutomaticReconnect(true);
//自动重连间隔时间,单位毫秒
options.setMaxReconnectDelay(5000);
options.setKeepAliveInterval(mqttProperties.getKeepAliveInterval());
//设置“遗嘱”消息的话题,若客户端和服务器之间的连接意外中断,服务器将发布客户端的“遗嘱”消息
options.setWill(WILL_TOPIC, WILL_DATA, 2, false);
return options;
}

/**
* MQTT客户端
*
* @return {@link org.springframework.integration.mqtt.core.MqttPahoClientFactory}
*/
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
factory.setConnectionOptions(getMqttConnectOptions());
return factory;
}

/**
* 发送通道
*
* @return
*/
@Bean
public MessageChannel mqttOutboundChannel() {
return new DirectChannel();
}

/**
* 配置Client,发送Topic
*
* @return
*/
@Bean
@ServiceActivator(inputChannel = "mqttOutboundChannel")
public MessageHandler mqttOutbound() {
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(mqttProperties.getClientId()+"_OutBound", mqttClientFactory());
messageHandler.setAsync(true);
messageHandler.setDefaultTopic(mqttProperties.getDefaultTopic());
return messageHandler;
}

/**
* 接收通道
*
* @return
*/
@Bean
public MessageChannel mqttInboundChannel() {
return new DirectChannel();
}

/**
* 配置Client,订阅Topic
*
* @return
*/
@Bean
public MessageProducer inbound() {
MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(mqttProperties.getClientId() + "_InBound", mqttClientFactory(),
"get_config/response"); // 这里配置订阅Topic,多个的话逗号分割即可
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(1);
adapter.setOutputChannel(mqttInboundChannel());
return adapter;
}

}

/config/MqttProperties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Data
@Component
@EnableConfigurationProperties(MqttProperties.class)
@ConfigurationProperties(prefix = "spring.mqtt")
public class MqttProperties {

/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* MQTT 的 TCP URL,多个URL用逗号分隔
*/
private String endpointUrl;
/**
* 客户端id
*/
private String clientId;
/**
* 默认的Topic
*/
private String defaultTopic;
/**
* 连接超时时长,单位为秒,默认为30
*/
private int connectionTimeout;
/**
* 会话心跳时间,单位为秒,默认为60
*/
private int keepAliveInterval;
}

/controller/MqttController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/mqtt")
public class MqttController {

@Autowired
private MqttSenderGateway mqttSenderService;

/**
* 获取配置参数
* @param map
*/
@PostMapping("/get_config/{id}/request")
public void readConfig(@RequestBody Map<String, Object> map, @PathVariable Integer id) {
String message = JSON.toJSONString(map.get("data"));
mqttSenderService.sendToMqtt(StrUtil.format("get_config/{}/request", id),1,message);
}

/service/MqttSenderGateway.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttSenderGateway {

/**
* 发送信息到MQTT服务器
*
* @param payload 消息主体
*/
void sendToMqtt(String payload);

/**
* 发送信息到MQTT服务器
*
* @param topic 主题
* @param payload 消息主体
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String payload);

/**
* 发送消息到MQTT服务器
*
* @param topic 主题
* @param qos 对消息的处理机制。
* 0 表示的是订阅者没收到消息不会再次发送,消息会丢失。<br>
* 1 表示的是会尝试重试,一直到接收到消息,但这种情况可能导致订阅者收到多次重复消息。<br>
* 2 多了一次去重的动作,确保订阅者收到的消息有一次。
* @param payload 消息主体
*/
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
}

/service/MqttSubscribe.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class MqttSubscribe {

public static final String MQTT_RECEIVED_TOPIC = "mqtt_receivedTopic";

@Autowired
private SaveDataService saveDataService;

@Bean
@ServiceActivator(inputChannel = "mqttInboundChannel")
public MessageHandler mqttInbound() {
return message -> {
String topic = (String) message.getHeaders().get(MQTT_RECEIVED_TOPIC);
String payload = (String) message.getPayload();
System.out.println(topic + ":" + payload);
MqttData document = JSONObject.parseObject(payload, MqttData.class);
System.out.println(document.getData());
};
}
}

2.7.3 测试开发的MQTT客户端

配置好 MQTTX 客户端,连接上 EMQX 消息服务器,分别测试收发消息的功能。

  • 发送消息:使用Postman发请求(JSON格式),查看 MQTTX 客户端是否接收到(不要忘了订阅该Topic)

  • 接收消息:使用 MQTTX 客户端发请求(JSON格式),查看代码里是否打印了输出(要和代码里配置的订阅Topic对应)

3. Springboot常见问题

3.1 解决跨域问题

3.1.1 跨域与CORS简介

现代浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。跨域HTTP请求是指A域上资源请求了B域上的资源,举例而言,部署在A机器上Nginx上的js代码通过ajax请求了部署在B机器Tomcat上的RESTful接口。

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。它通过服务器增加一个特殊的Header[Access-Control-Allow-Origin]来告诉客户端跨域的限制,如果浏览器支持CORS、并且判断Origin通过的话,就会允许XMLHttpRequest发起跨域请求。

3.1.2 实现全局跨域

创建一个配置类,返回一个新的WebMvcConfigurer Bean,并重写其提供的跨域请求处理的接口,目的是添加映射路径和具体的CORS配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class GlobalCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
//重写父类提供的跨域请求处理的接口
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//放行哪些原始域
.allowedOrigins("*")
//是否发送Cookie信息
.allowCredentials(true)
//放行哪些原始域(请求方式)
.allowedMethods("GET","POST", "PUT", "DELETE")
//放行哪些原始域(头部信息)
.allowedHeaders("*")
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.exposedHeaders("Header1", "Header2");
}
};
}
}

3.2 统一接口响应格式

3.2.1 接口标准响应格式

当前主流的 Web 应用开发通常采用前后端分离模式,前端和后端各自独立开发,然后通过数据接口沟通前后端,完成项目。

因此,定义一个统一的数据下发格式,有利于提高项目开发效率,减少各端开发沟通成本。

1
2
3
4
5
6
7
8
{
"code": 200,
"msg": "操作成功",
"data": {
"name": "zhangsan",
"email": "[email protected]"
}
}

3.2.2 实现统一响应格式

[1] 数据统一下发实体:ResponseBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Getter
@ToString
public class ResponseBean<T> {
private int code;
private String msg;
private T data;

// 成功操作
public static <E> ResponseBean<E> success(E data) {
return new ResponseBean<E>(ResultCode.SUCCESS, data);
}

// 失败操作
public static <E> ResponseBean<E> failure(E data) {
return new ResponseBean<E>(ResultCode.FAILURE, data);
}

// 设置为 private
private ResponseBean(ResultCode result, T data) {
this.code = result.code;
this.msg = result.msg;
this.data = data;
}

// 设置 private
private static enum ResultCode {
SUCCESS(200, "操作成功"),
FAILURE(500, "操作失败");

ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}

private int code;
private String msg;
}
}

[2] 转换器配置类:WebConfiguration.java

1
2
3
4
5
6
7
8
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}

[3] 数据下发拦截器:FormatResponseBodyAdvice.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
boolean isResponseBeanType = ResponseBean.class.equals(returnType.getParameterType());
// 如果返回的是 ResponseBean 类型,则无需进行拦截修改,直接返回即可
// 其他类型则拦截,并进行 beforeBodyWrite 方法进行修改
return !isResponseBeanType;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return ResponseBean.success(body);
}

@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseBean<String> handleException() {
return ResponseBean.failure("Error occured");
}
}

3.3 其他方便开发的依赖库

1
2
3
4
5
6
7
8
9
10
11
<!-- lombok 依赖 让代码更简洁 https://www.projectlombok.org/ -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Hutool 工具类库依赖 https://www.hutool.cn/docs/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<hutool.all.version>4.1.7</hutool.all.version>
</dependency>

3.4 其他开发中的常见基本写法

[1] 使用try-catch捕捉异常

1
2
3
4
5
try {
// 异常模块
} catch (Exception e) {
e.printStackTrace();
}

[2] 后端将Map转换成JSON字符串 及 前端将JSON字符串转换成JSON对象

1
2
后端将Map转换成JSON字符串:String strData = JSON.toJSONString(mapData); 
前端将JSON字符串转换成JSON对象:this.count = JSON.parse(strData).count;

[3] 将字符串先转成JSON字符串再转成Map

1
Map<String, Object> map = JSONObject.parseObject(JSON.toJSONString(result));

说明:将JSON字符串转成Map那步如果报错,请检查以下两点。

1)字符串内有斜杠转义导致。

在pom.xml里引入如下包:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>

再调用如下方法进行处理:

1
String str = StringEscapeUtils.unescapeJava(str);

2){}两边多了“”双引号导致。

1
String str = str.substring(1, str.length() - 1);  // 字符串去掉第一位和最后一位

[4] 将Object转成Map

1
Map<String, Object> map = JSON.parseObject(JSON.toJSONString(obj), Map.class)

[5] Mybatis的特殊符号替换

1
2
3
小于等于    a<=b                 a &lt;= b            a <![CDATA[<= ]]>b
大于等于 a>=b a &gt;= b a <![CDATA[>= ]]>b
不等于 a!=b <![CDATA[ <> ]]>b a <![CDATA[!= ]]>b

[6] 使用字符串模板代替字符串拼接

使用了第三方工具类库Hutool里的format方法

1
2
String template = "{}爱{},就像老鼠爱大米";
String str = StrUtil.format(template, "我", "你"); //str -> 我爱你,就像老鼠爱大米

[7] 请求路径的动态获取

在请求里用{}包裹需要动态获取的路径参数,如果下面需要用到它的话,在入参那里用@PathVariable Integer id获取即可。

1
2
@PostMapping("/g_config/{id}/request")   
public void getConfig(@RequestBody Map<String, Object> map, @PathVariable Integer id) {...}

[8] yml与properties互相转换

可借助 https://www.toyaml.com/index.html 在线工具进行转换。

[9] Map的遍历

1
2
3
map.entrySet().stream().forEach(x -> {
System.out.println("key: "+x.getKey()+", value: "+x.getValue());
});

[10] 将数组转成逗号分割的字符串

1
String arrayStr = String.join(",", array);   

[11] 去除字符串中的所有空格

1
result = result.replaceAll(" +",""); 

[12] 将图片进行base64编码

1
2
String path = "../image/test.jpeg";
String str = Base64.encode(new File(path));

3.5 其他常见报错问题

[1] pom.xml里spring-boot-maven-plugin爆红问题

该问题有多种可能导致的原因,都试一下吧。

  • 第一种可能:给它加上版本号,例如:<version>2.2.2.RELEASE</version>
  • 第二种可能:检查settings.xml是否配置正确。
  • 第三种可能:将Maven依赖仓库的地址使用IDEA默认的C:\Users\xxx\.m2\repository

注:看起来第三种可能最不靠谱,我他妈都被它整崩溃了,抱着死马当活马医的心态试了一下,没想到真的解决了!!!

[2] 主启动类运行报错Failed to load property source from location ‘classpath:/application.yml’

先校验yaml语法:http://www.yamllint.com/,如果没问题就是编码的事儿

方法一:File–>Settings–>File Encodings,三处编码设置为utf-8,重启项目

方法二: 删除application.yml文件中所有中文注释。

[3] 主启动类运行报错Command line is too long

报错信息:Error running ‘ServiceStarter’: Command line is too long. Shorten command line for ServiceStarter or also for Application default configuration.

解决办法:修改项目下 .idea\workspace.xml,找到标签<component name="PropertiesComponent">, 在标签里加一行 <property name="dynamic.classpath" value="true" />

[4] SpringBoot启动时报SilentExitException

以debug方式启动springboot之后,都会在SilentExitExceptionHandler类中的throw new SilentExitException()处终止,虽然不影响程序运行,但爆红令人感觉不爽。原因是用了热部署spring-boot-devtools。

解决办法:我选择去掉spring-boot-devtools依赖,使用Jrebel插件实现热部署。

[5] 使用@AutoWired注解出现Could not autowire. No beans of ‘xxxx’ type found 的错误提示

只是警告,不影响使用,但看着爆红不舒服。

解决办法 1)在注解上加上:@Autowired(required = false) 2)使用 @Resource 替换 @Autowired

[6] .properties出现\u7528中文乱码问题

其中’\u’表示Unicode编码,用工具类或者在线工具转换一下即可。

在线工具:Unicode与中文互转

[7] 调用接口出现 Content type ‘application/x-www-form-urlencoded;charset=UTF-8’ not supported报错

前端请求传JSON对象则后端使用@RequestParam;
前端请求传JSON对象的字符串则后端使用@RequestBody。

[8] 启动报错Field XXX required a bean of type XXX that could not be found.

可能性1:包结构的问题

项目启动时,只有@SpringBootApplication所在的包被扫描,而其他的controller和service以及mapper在其他的包里,所以并没有被扫描。解决方法是,把启动类放在外层包。

可能性2:没有自动注入导致

service类上面没有@service注解或者mapper上没有@Repository注解。但是这种情况比较少见,一般不会忘记。

可能性3:配置了Mybatis,但没有指定扫描的包。

在启动类上加上dao的路径:@MapperScan(value = "com.xxx.dao")

[9] 使用ConfigurationProperties注解时提示“Spring Boot Configuration Annotation Processor not configured”

解决办法:在pom.xml里添加以下依赖即可解决。

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

4. 常用的Java工具类

4.1 生成指定位数的随机密码

可以生成指定位数的随机密码,密码中包含大写字母、小写字母、数字和特殊字符中至少三种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

public class RandPassword {
public static final char[] allowedSpecialCharactors = {
'`', '~', '@', '#', '$', '%', '^', '&',
'*', '(', ')', '-', '_', '=', '+', '[',
'{', '}', ']', '\\', '|', ';', ':', '"',
'\'', ',', '<', '.', '>', '/', '?'};//密码能包含的特殊字符
private static final int letterRange = 26;
private static final int numberRange = 10;
private static final int spCharactorRange = allowedSpecialCharactors.length;
private static final Random random = new Random();
private int passwordLength;//密码的长度
private int minVariousType;//密码包含字符的最少种类

public RandPassword(int passwordLength, int minVariousType) {
if (minVariousType > CharactorType.values().length) minVariousType = CharactorType.values().length;
if (minVariousType > passwordLength) minVariousType = passwordLength;
this.passwordLength = passwordLength;
this.minVariousType = minVariousType;
}

public String generateRandomPassword() {
char[] password = new char[passwordLength];
List<Integer> pwCharsIndex = new ArrayList();
for (int i = 0; i < password.length; i++) {
pwCharsIndex.add(i);
}
List<CharactorType> takeTypes = new ArrayList(Arrays.asList(CharactorType.values()));
List<CharactorType> fixedTypes = Arrays.asList(CharactorType.values());
int typeCount = 0;
while (pwCharsIndex.size() > 0) {
int pwIndex = pwCharsIndex.remove(random.nextInt(pwCharsIndex.size()));//随机填充一位密码
Character c;
if (typeCount < minVariousType) {//生成不同种类字符
c = generateCharacter(takeTypes.remove(random.nextInt(takeTypes.size())));
typeCount++;
} else {//随机生成所有种类密码
c = generateCharacter(fixedTypes.get(random.nextInt(fixedTypes.size())));
}
password[pwIndex] = c.charValue();
}
return String.valueOf(password);
}

private Character generateCharacter(CharactorType type) {
Character c = null;
int rand;
switch (type) {
case LOWERCASE://随机小写字母
rand = random.nextInt(letterRange);
rand += 97;
c = new Character((char) rand);
break;
case UPPERCASE://随机大写字母
rand = random.nextInt(letterRange);
rand += 65;
c = new Character((char) rand);
break;
case NUMBER://随机数字
rand = random.nextInt(numberRange);
rand += 48;
c = new Character((char) rand);
break;
case SPECIAL_CHARACTOR://随机特殊字符
rand = random.nextInt(spCharactorRange);
c = new Character(allowedSpecialCharactors[rand]);
break;
}
return c;
}

public static void main(String[] args) {
System.out.println(new RandPassword(32, 4).generateRandomPassword());
}
}

enum CharactorType {
LOWERCASE,
UPPERCASE,
NUMBER,
SPECIAL_CHARACTOR
}

4.2 对txt进行等量拆分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class SplitTxt {

private static int count = 10; // 拆分个数
private static String inputFile = "H:/input.txt"; //输入文件
private static String outFile = "H:/output"; // 输出文件

public static void main(String[] args) {
try {
FileReader read = new FileReader(inputFile);
BufferedReader br = new BufferedReader(read);
String row;
List<FileWriter> flist = new ArrayList<FileWriter>();
for (int i = 0; i < count; i++) {
flist.add(new FileWriter(outFile + i + ".txt"));
}
int rownum = 1;
while ((row = br.readLine()) != null) {
flist.get(rownum % count).append(row + "\r\n");
rownum++;
}
for (int i = 0; i < flist.size(); i++) {
flist.get(i).close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

System.out.println("Txt file split end!");
}

}

4.3 将数据库数据以json格式导出到txt文件里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import com.alibaba.fastjson.JSON;
import java.io.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;

public class ExportData {

public static void exportdata(String file, String conent) {
BufferedWriter out = null;
try {
out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file, true)));
out.write(conent + "\r\n");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

public static void main(String[] args) {
Connection con;
String driver="com.mysql.jdbc.Driver";
String url="jdbc:mysql://127.0.0.1:3306/table?useUnicode=true&characterEncoding=utf-8&useSSL=false";
String user="root";
String password="123456";
try {
Class.forName(driver);
con = DriverManager.getConnection(url, user, password);
if (!con.isClosed()) {
System.out.println("数据库连接成功");
}
Statement statement = con.createStatement();

// 执行查询语句
String sql = "select id, name, city from user_info";
ResultSet resultSet = null;

try {
resultSet = statement.executeQuery(sql);
} catch (SQLException throwables) {
throwables.printStackTrace();
}

String id;
String name;
String city;

while (resultSet.next()) {
id = resultSet.getString("id");
name = resultSet.getString("name");
city = resultSet.getString("city");

// 转换成JSON
HashMap<String, String> map = new HashMap<String, String>();
map.put("id", id);
map.put("name", name);
map.put("city", city);
String strData = JSON.toJSONString(map);

System.out.println(strData);
System.out.println("开始写入数据");
exportdata(".\\output.txt",strData);
System.out.println("结束写入数据");

}

resultSet.close();
con.close();
System.out.println("数据库已关闭连接");
} catch (ClassNotFoundException e) {
System.out.println("数据库驱动没有安装");

} catch (SQLException e) {
System.out.println("数据库连接失败");
}
}
}

4.4 将某目录下所有文件的名称保存到txt里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;

public class GetFileName {

/**
* 递归读取文件路径下的所有文件
*
* @param path
* @param fileNameList
* @return
*/
public static ArrayList<String> readFiles(String path, ArrayList<String> fileNameList) {
File file = new File(path);
if (file.isDirectory()) {
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory()) {
readFiles(files[i].getPath(), fileNameList);
} else {
String filePath = files[i].getPath();
String fileName = filePath.substring(filePath.lastIndexOf("\\") + 1);
fileNameList.add(fileName);
}
}
} else {
String path1 = file.getPath();
String fileName = path1.substring(path1.lastIndexOf("\\") + 1);
fileNameList.add(fileName);
}
return fileNameList;
}

/**
* 将内容输出到(追加)txt文件保存
*
* @param content
* @throws IOException
*/
public static void outputToTxt(String content, String outputPath) throws IOException {
FileWriter fw = new FileWriter(outputPath, true); //追加内容
PrintWriter pw = new PrintWriter(fw);
pw.println(content);
pw.close();
fw.close();
pw.flush();
}

public static void main(String[] args) {
String filePath = "../data/fileDic";
String outputPath = "../data/fileName.txt";
try {
ArrayList<String> fileNameList = readFiles1(filePath, new ArrayList<String>());
System.out.println(fileNameList.size());
for (int i = 0; i < fileNameList.size(); i++) {
outputToTxt(fileNameList.get(i), outputPath);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

4.5 按行读取txt文件内容

1
2
3
4
5
6
7
8
9
FileInputStream inputStream = new FileInputStream("../data/fileName.txt");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str = null;
while((str = bufferedReader.readLine()) != null)
{
System.out.println(str);
}
inputStream.close();
bufferedReader.close();

4.6 检查指定URL的HTTP请求状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class CheckUrlCode {

/**
* 检查指定URL的HTTP请求状态码
* @param checkUrl
* @return
*/
public static int CheckUrlCode(String checkUrl) {
int httpCode = 0;
try {
URL u = new URL(checkUrl);
try {
HttpURLConnection uConnection = (HttpURLConnection) u.openConnection();
try {
uConnection.connect();
httpCode = uConnection.getResponseCode();
System.out.println(httpCode);
} catch (Exception e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
} catch (
MalformedURLException e) {
e.printStackTrace();
}
return httpCode;
}

}

4.7 生成指定位数的数字标号

1
2
3
4
5
6
7
8
9
/**
* 生成指定位数的数字标号
* 0:表示前面补0
* digit:表示保留数字位数
* d:表示参数为正数类型
*/
public static String fillString(int num, int digit) {
return String.format("%0"+digit+"d", num);
}

4.8 按照指定长度拆分List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 按照指定长度拆分list
* @param list
* @param groupSize
* @return
*/
public static List<List<String>> splitList(List<String> list , int groupSize){
int length = list.size();
// 计算可以分成多少组
int num = ( length + groupSize - 1 )/groupSize ; // TODO
List<List<String>> newList = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
// 开始位置
int fromIndex = i * groupSize;
// 结束位置
int toIndex = (i+1) * groupSize < length ? ( i+1 ) * groupSize : length ;
newList.add(list.subList(fromIndex,toIndex)) ;
}
return newList ;
}

4.9 检查字符串中是否包含某子串

1
2
3
4
5
6
7
8
9
10
11
/**
* 检查字符串中是否包含某子串
*/
public static boolean checkSubString(String wholeString, String subString){
boolean flag = false;
int result = wholeString.indexOf(subString);
if(result != -1){
flag = true;
}
return flag;
}

5. 参考资料

[1] 尚硅谷-使用向导快速创建Springboot项目 from Bilibili

[2] Springboot+Vue整合笔记【超详细】from 知乎

[3] Spring boot学习(四)Spring boot整合Druid from 掘金

[4] SpringBoot项目去除druid监控的阿里广告 from CSDN

[5] screw:简洁好用的数据库表结构文档生成器 from Github

[6] 实用!一键生成数据库文档,堪称数据库界的Swagger from 51CTO

[7] 超给力,一键生成数据库文档-数据库表结构逆向工程 from 博客园

[8] 使用 screw 数据库文档生成工具快速生成数据库文档 from Blilibili

[9] SpringBoot整合Swagger2,再也不用维护接口文档了!from CSDN

[10] Swagger2 注解 from Github

[11] Sa-Token–Java权限认证框架 from Github

[12] Spring Boot 整合 Shiro ,两种方式全总结! from 掘金

[13] springboot 解决跨域 from segmentfault

[14] Spring Boot - 统一数据下发接口格式 from 简书

[15] spring boot项目启动报错:Failed to load property source from location ‘classpath:/application.yml’ from CSDN

[16] Intellij IDEA运行报Command line is too long解法 from CSDN

[17] IDEA springboot “spring-boot-maven-plugin“报红问题的解决方法 from 博客园

[18] 关于@Slf4j中log爆红的问题 from CSDN

[19] 解决IntelliJ IDEA使用@AutoWired注解出现Could not autowire. No beans of ‘xxxx’ type found 的错误提示 from CSDN

[20] UTF-8编码下\u7528\u6237转换为中文汉字 from CSDN

[21] 将Swagger2文档导出为HTML或markdown等格式离线阅读 from segmentfault

[22] Swagger2 导出api文档(AsciiDocs、Markdown) from CSDN

[23] dao、pojo、service、controller、mapper、Impl、bo、vo、po、domain都是什么?from 每天进步一点点

[24] 分布式任务调度平台XXL-JOB from 官方文档

[25] Java定时任务框架对比 from AceKel

[26] XXL-JOB部署实战 from Bilibili

[27] springboot 启动报错Field XXX required a bean of type XXX that could not be found. from CSDN

[28] spring-boot如何去获取前端传递的参数 from 华为云

[29] 使用ConfigurationProperties注解时提示“Spring Boot Configuration Annotation Processor not configured” from 博客园

[30] 使用 Java 开发 MQTT 客户端 from CSDN

[31] OPC 数据采集服务,通过 MQTT 和 Kafka 落地到 Influxdb from Github

[32] MQTT QoS(服务质量)介绍 from emqx

[33] MQTT协议3.1.1中文翻译版,IoT,物联网 from Github

[34] Java递归读取文件路径下所有文件名称并保存为Txt文档 from CSDN

[35] Java发送Http请求并获取状态码 from CSDN

[36] springboot /v2/api-docs 无法访问 from CSDN

[37] SpringBoot Swagger去掉basic-error-controller from CSDN