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

  1. 1. Springboot项目的创建
    1. 1.1 Springboot相关基本概念
    2. 1.2 Spring Initializer快速创建项目
  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 导出离线接口文档
      5. 2.3.5 Swagger的knife4j UI
      6. 2.3.6 Swagger添加权限验证
    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的服务器部署
  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 自动生成mapper和pojo文件
      1. 3.3.1 通过Database工具自动生成pojo文件
      2. 3.3.2 使用mybatis-generator自动生成mapper和pojo文件
    4. 3.4 方便开发的依赖库
    5. 3.5 常用的正则表达式
    6. 3.6 从Enum枚举获取Map列表
    7. 3.7 将文本转换为ASCII艺术字
    8. 3.8 工具类读取.properties配置文件
    9. 3.9 Springboot的包外配置
    10. 3.10 Spring Security 框架的相关问题
      1. 3.10.1 通过body接收数据
      2. 3.10.2 多次登录失败后账户锁定
      3. 3.10.3 登录登出接口添加Swagger
    11. 3.11 SpringBoot实现自定义注解
      1. 3.11.1 Java元注解
      2. 3.11.2 Spring AOP的实现
    12. 3.12 用配置项设置定时任务是否开启
    13. 3.13 yml与properties的互相转换
    14. 3.14 业务系统增加日志审计模块
    15. 3.15 常见报错及警告问题的解决方案
  4. 4. 常用的Java工具类及代码示例
    1. 4.1 常用工具类
      1. 4.1.2 检查指定URL的HTTP请求状态码
      2. 4.1.2 生成指定位数的数字标号
      3. 4.1.3 按照指定长度拆分List
      4. 4.1.4 检查字符串中是否包含某子串
      5. 4.1.5 按照value对Map排序
      6. 4.1.6 本地和网络图片与Base64互转
      7. 4.1.7 将网络文件下载到本地的指定文件夹下
      8. 4.1.8 数学计算工具类
      9. 4.1.9 日期时间获取转换工具类
      10. 4.1.10 截取指定长度的字符串
      11. 4.1.11 MD5通用工具类
      12. 4.1.12 AES加密解密工具类
      13. 4.1.13 RSA加密解密工具类
      14. 4.1.14 下划线与驼峰转换工具类
      15. 4.1.15 将一个列表分割成多个等长度的子列表
      16. 4.1.16 Object解析转换工具类
      17. 4.1.17 发送HTTP请求工具类
      18. 4.1.18 请求接口上传及下载文件工具类
      19. 4.1.19 将base64转成MultipartFile类型
      20. 4.1.20 判断数值是否在某字符串区间内
      21. 4.1.21 中文乱码恢复
      22. 4.1.22 删除html标签工具类
      23. 4.1.23 使用jfreechart绘制图表
      24. 4.1.24 基于zxing库的二维码生成及解析工具类
      25. 4.1.25 邮件发送工具类
      26. 4.1.26 身份证号校验工具类
      27. 4.1.27 敏感信息脱敏工具类
    2. 4.2 常见基本写法
      1. 4.2.1 常见基本语法
      2. 4.2.2 类型转换遍历
      3. 4.2.3 文件及目录操作
    3. 4.3 完整代码示例
      1. 4.3.1 生成指定位数的随机密码
      2. 4.3.2 对txt进行等量拆分
      3. 4.3.3 将数据库数据以json格式导出到txt里
      4. 4.3.4 将某目录下所有文件的名称保存到txt里
      5. 4.3.5 Excel的上传导入及导出下载
      6. 4.3.6 Excel的本地导出
      7. 4.3.7 将文件夹打包压缩后下载
      8. 4.3.8 递归统计项目的代码行数
      9. 4.3.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应用的配置文件,可以修改一些默认设置。

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
60
61
62
63
## 数据源配置
# 表明使用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.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=123456
#初始化时建立物理连接的个数。
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("password");
    //设置可以获取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.3.5 Swagger的knife4j UI

如果觉得原生Swagger UI太丑,可以将其换成knife4j UI,配置与之完全相同,只需要添加如下依赖即可。

1
2
3
4
5
6
<!--Knife4J版的Swagger UI-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>

注意,使用Knife4j2.0.6及以上的版本,SpringBoot的版本必须大于等于2.2.x。

knife4j-UI

2.3.6 Swagger添加权限验证

pom.xml引入依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>

application.properties添加配置:

1
2
3
4
5
6
# If this is set to true, the swagger page is hidden
swagger.production=false
# This setting is true to enable account password verification
swagger.basic.enable=true
swagger.basic.username=admin
swagger.basic.password=123456

SwaggerConfig.java添加注解:

1
@EnableSwaggerBootstrapUI

页面验证效果:

Swagger权限校验

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来调用执行器项目里面的代码。

xxl-job架构

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

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

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");
}
};
}
}

注:如果你的SpringBoot>=2.4.0,请将上述代码的.allowedOrigins替换成.allowedOriginPatterns,否则跨域配置会出错。

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 自动生成mapper和pojo文件

如果你只是想生成pojo文件,那么使用IDEA自带的Database工具即可。如果你除了想生成pojo文件之外还想生成mapper文件,那就需要用到mybatis-generator插件了。

3.3.1 通过Database工具自动生成pojo文件

Step1:使用IDEA自带的Database工具填写好信息连接数据库。

Step2:打开连接,选择需要映射生成的POJO类的表,右键,选择Scripted Extensions,然后选择Generate POJOs.groovy,选择生成POJO类的存放位置,点击OK。然后修改一下生成代码里的package路径即可。

3.3.2 使用mybatis-generator自动生成mapper和pojo文件

Step1:在maven工程当中的resource下面,创建generatorConfig.xml文件。

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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<!--mysql 连接数据库jar 这里选择自己本地位置-->
<classPathEntry location="C:\Users\xxx\.m2\repository\mysql\mysql-connector-java\5.1.39\mysql-connector-java-5.1.39.jar" />
<context id="testTables" targetRuntime="MyBatis3">
<commentGenerator>
<!-- 是否去除自动生成的注释 -->
<property name="suppressAllComments" value="true" />
</commentGenerator>
<!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/testdb" userId="root"
password="123456">
</jdbcConnection>
<!-- 默认false,把JDBC DECIMAL和NUMERIC类型解析为Integer,为true时解析为java.math.BigDecimal -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<!-- targetProject:生成pojo类的位置 -->
<javaModelGenerator targetPackage="com.test.demo.pojo"
targetProject="src/main/java">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false" />
<!-- 从数据库返回的值被清理前后的空格 -->
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- targetProject:mapper映射文件生成的位置-->
<sqlMapGenerator targetPackage="mapper"
targetProject="src/main/resources">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false" />
</sqlMapGenerator>
<!-- targetPackage:mapper接口生成的位置 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.test.demo.mapper"
targetProject="src/main/java">
<!-- enableSubPackages:是否让schema作为包的后缀 -->
<property name="enableSubPackages" value="false" />
</javaClientGenerator>
<!-- 指定数据库表 -->
<table schema="" tableName="sys_code" domainObjectName="" enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false">
</table>
</context>
</generatorConfiguration>

说明:

[1] "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"的字体变成了红色,鼠标放上去显示URI is not registerd。

解决办法:鼠标点击红色字,然后Intelli出现小红灯,选择Fetch external resource即可解决。

[2] mysql连接数据库的jar包去maven仓库里找到它的本地位置即可,然后填写数据库连接信息。

[3] pojo、mapper以及映射的xml的位置,根据自己的需求填写,最后指定一下数据库表即可。

Step2:配置pom.xml文件,在plugins里增加以下内容:

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
<!--mybatis逆向工程-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<!--配置文件的位置-->
<configurationFile>src/main/resources/mybatis-generator.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
</plugin>

Step3:打开IDEA的Maven面板,点击Plugins下mybatis-generator的mybatis-generator:generate按钮生成即可。

注:出现BUILD SUCCESS即生成成功,出现No appenders could be found for logger无视即可,生成的文件需要根据需求进行修改。

3.4 方便开发的依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  <!-- 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>
<!-- fastjson Java对象与JSON互转依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

3.5 常用的正则表达式

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
/**
* 正则表达式
*/
public class RegexpConstants {

/**
* 正则表达式:验证用户名
*/
public static final String REGEX_USERNAME = "^[a-zA-Z]\\w{5,20}$";

/**
* 正则表达式:验证密码
*/
public static final String REGEX_PASSWORD = "^[a-zA-Z0-9]{6,20}$";

/**
* 正则表达式:验证手机号
*/
public static final String REGEX_MOBILE = "^(1[0-9])\\d{9}$";

/**
* 正则表达式:验证邮箱
*/
public static final String REGEX_EMAIL = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";

/**
* 正则表达式:验证汉字
*/
public static final String REGEX_CHINESE = "^[\u4e00-\u9fa5],{0,}$";

/**
* 正则表达式:验证身份证
*/
public static final String REGEX_ID_CARD = "(^\\d{18}$)|(^\\d{15}$)";

/**
* 正则表达式:验证URL
*/
public static final String REGEX_URL = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";

/**
* 正则表达式:验证IP地址
*/
public static final String REGEX_IP_ADDR = "(25[0-5]|2[0-4]\\d|[0-1]\\d{2}|[1-9]?\\d)";

/**
* 正则表达式:验证数字类型逗号分隔
*/
public static final String REGEX_COMMA_SEPARATED = "^[0-9]+(,[0-9]+)*$";

}

3.6 从Enum枚举获取Map列表

枚举:

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
public enum EducateStatusEnum {

/**
* 学生 学业状态
*/
IN_STUDYING((short) 1, "在读"),
UNDERGRADUATE((short) 2, "肆业"),
SUSPENDED((short) 3, "休学"),
DROPPED((short) 4, "辍学"),
QUITED((short) 5, "退学"),
GRADUATED((short) 6, "已毕业"),
;

public short code;
public String name;

EducateStatusEnum(Short code, String name) {
this.code = code;
this.name = name;
}

public int getCode() {
return this.code;
}

public String getName() {
return this.name;
}

public static EducateStatusEnum findEnumByCode(Integer code) {
for (EducateStatusEnum statusEnum : EducateStatusEnum.values()) {
if (statusEnum.getCode() == code) {
return statusEnum;
}
}
throw new IllegalArgumentException("code is not support");
}

public static EducateStatusEnum findEnumByName(String name) {
for (EducateStatusEnum statusEnum : EducateStatusEnum.values()) {
if (statusEnum.getName().equals(name)) {
return statusEnum;
}
}
throw new IllegalArgumentException("name is not support");
}

}

工具类:

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
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;

/**
* 将枚举转换为list类型
*/
public class EnumListUtil {

private static String ENUM_CLASSPATH="java.lang.Enum";

public static List<Map<String, Object>> enumToMapList(Class<?> enumClass) {
List<Map<String, Object>> resultList= new ArrayList<>();
if (!ENUM_CLASSPATH.equals(enumClass.getSuperclass().getCanonicalName())) {
return resultList;
}
// 获取所有public方法
Method[] methods = enumClass.getMethods();
List<Field> fieldList = new ArrayList<>();
//1.通过get方法提取字段
Arrays.stream(methods)
.map(Method::getName)
.filter(
methodName -> methodName.startsWith("get") && !"getDeclaringClass".equals(methodName) && !"getClass".equals(methodName)
).forEachOrdered(methodName -> {
try {
Field field = enumClass.getDeclaredField(StringUtils.uncapitalize(methodName.substring(3)));
if (null != field) {
fieldList.add(field);
}
} catch (NoSuchFieldException | SecurityException e) {
e.printStackTrace();
}
});

//2.将字段作为key,逐一把枚举值作为value 存入list
if (CollectionUtils.isEmpty(fieldList)) {
return resultList;
}

Enum<?>[] enums = (Enum[]) enumClass.getEnumConstants();
for (Enum<?> anEnum : enums) {
Map<String, Object> map = new HashMap<>(fieldList.size());
for (Field field : fieldList) {
field.setAccessible(true);
try {
// 向map集合添加字段名称 和 字段值
map.put(field.getName(), field.get(anEnum));
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
// 将Map添加到集合中
resultList.add(map);
}
return resultList;
}

public static void main(String[] args) {
// 转换枚举类成Map列表
System.out.println(enumToMapList(EducateStatusEnum.class));
}

}

3.7 将文本转换为ASCII艺术字

有时我们会在加一个banner,项目启动时会展示项目的一些信息。有个现成的开源库,可以将指定文本转换为对应的ASCII艺术字。

1
2
3
4
5
<dependency>
<groupId>com.github.lalyos</groupId>
<artifactId>jfiglet</artifactId>
<version>0.0.8</version>
</dependency>

使用方法:

1
2
String asciiArt= FigletFont.convertOneLine("Hello");
System.out.println(asciiArt);

效果展示:

1
2
3
4
5
_   _          _   _        
| | | | ___ | | | | ___
| |_| | / _ \ | | | | / _ \
| _ | | __/ | | | | | (_) |
|_| |_| \___| |_| |_| \___/

3.8 工具类读取.properties配置文件

Springboot项目我们通常使用@value注解获取.properties配置文件的数据,而一些独立运行的工具类,无法通过该注解得到所需要的值,对此我们可以使用如下方式来获取:

1
2
3
4
5
6
7
8
9
// 读取properties文件
Properties properties = new Properties();
// 方式一:本机和服务器部署均可用,但读不了其他子模块的
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new ClassPathResource("application.properties").getInputStream()));
// 方式二:仅本机可用,服务器部署会找不到路径
// BufferedReader bufferedReader = new BufferedReader(new FileReader("web_manage/src/main/resources/application.properties"));
properties.load(bufferedReader);
// 获取key对应的value值
System.out.println(properties.getProperty("spring.datasource.username"));

3.9 Springboot的包外配置

Springboot读取核心配置文件(application.properties)的优先级依次为:Jar包同级目录的config目录、Jar包同级目录、classPath(即resources目录)的config目录、classpath目录。

上面是springboot默认去拿自己的核心配置文件的优先级,还有一种最高优先级的方式是项目启动时通过命令的方式指定项目加载核心配置文件,命令如下

1
$ java –jar -Dspring.config.location=xxx/xxx/xxxx.properties xxxx.jar

如果Spring Boot在优先级更高的位置找到了配置,那么它会无视优先级更低的配置。

3.10 Spring Security 框架的相关问题

Spring Security 基于Spring 框架,提供了一套web应用安全性的完整解决方案。权限管理系统一般包含两大核心模块:认证模块(Authentication)和鉴权模块(Authorization)。

  • 认证:认证模块负责验证用户身份的合法性,生成认证令牌,并保存到服务端会话中。

  • 鉴权:鉴权模块负责从服务端会话内获取用户身份信息,与访问的资源进行权限比对。

官方给出的Spring Security的核心架构图如下:

Spring-Security核心架构

核心架构解读:

  • AuthenticationManager:负责认证管理,解析用户登录信息,读取用户、角色、权限信息进行认证,认证结果被回填到Authentication,保存在SecurityContext。

  • AccessDecisionManager:负责鉴权投票表决,汇总投票器的结果,实现一票通过(默认)、多票通过、一票否决策略。

  • SecurityInterceptor:负责权限拦截,包括Web URL拦截和方法调用拦截。通过ConfigAttributes获取资源的描述信息,借助于AccessDecisionManager进行鉴权拦截。

  • SecurityContext:安全上下文,保存认证结果。提供了全局上下文、线程继承上下文、线程独立上下文(默认)三种策略。

  • Authentication:认证信息,保存用户的身份标示、权限列表、证书、认证通过标记等信息。

  • SecuredResource:被安全管控的资源,如Web URL、用户、角色、自定义领域对象等。

  • ConfigAttributes:资源属性配置,描述安全管控资源信息,为SecurityInterceptor提供拦截逻辑的输入。

3.10.1 通过body接收数据

需求描述:项目过安全检测的时候,不允许在参数中探测到username、password等敏感信息,即便它是密文的,因此打算将整个入参都加密成一个无规则字符串,通过body直接传输。

问题描述:在使用 Spring Security 的时候,发现用 body 传参的话,后台不能获取到数据,查看UsernamePasswordAuthenticationFilter 源码发现,是直接从 Request 获取的,而不是从 RequestBody 中获取的。

1
2
3
4
5
6
7
8
//获取密码
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
//获取用户名
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}

解决办法:重写 UsernamePasswordAuthenticationFilter 类,配置自定义过滤器

新建 UserAuthenticationFilter.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
import com.alibaba.fastjson.JSONObject;
import com.intyt.vrim.framework.util.AesUtility;
import org.apache.commons.io.IOUtils;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
* 自定义过滤器,重写 UsernamePasswordAuthenticationFilter,从body获取参数
*/
public class UserAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

private ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();

@Override
protected String obtainPassword(HttpServletRequest request) {
String password = this.getBodyParams(request).get(SPRING_SECURITY_FORM_PASSWORD_KEY);
if(!StringUtils.isEmpty(password)){
return password;
}
return super.obtainPassword(request);
}

@Override
protected String obtainUsername(HttpServletRequest request) {
String username = this.getBodyParams(request).get(SPRING_SECURITY_FORM_USERNAME_KEY);
if(!StringUtils.isEmpty(username)){
return username;
}
return super.obtainUsername(request);
}

/**
* 获取body参数 body中的参数只能获取一次
* @param request
* @return
*/
private Map<String,String> getBodyParams(HttpServletRequest request){
Map<String,String> bodyParams = threadLocal.get();
if(bodyParams == null){
bodyParams = new HashMap<>();
}
try (InputStream is = request.getInputStream()) {
String bodyString = IOUtils.toString(is, String.valueOf(StandardCharsets.UTF_8));
// 对接收到的 bodyString进行解密(此代码略)
Map<String, Object> tempMap = JSONObject.parseObject(bodyString);
bodyParams.put("password",tempMap.get("password").toString());
bodyParams.put("username",tempMap.get("username").toString());
} catch (IOException e) {
}
threadLocal.set(bodyParams);

return bodyParams;
}

}

修改 WebSecurityConfig.java

改动一:在 protected void configure(HttpSecurity httpSecurity) throws Exception 里配置自定义过滤器。

1
2
// 配置自定义过滤器,增加 post json 支持
httpSecurity.addFilterAt(UserAuthenticationFilterBean(), UsernamePasswordAuthenticationFilter.class);

改动二:添加如下方法。

1
2
3
4
5
6
7
private UserAuthenticationFilter UserAuthenticationFilterBean() throws Exception {
UserAuthenticationFilter userAuthenticationFilter = new UserAuthenticationFilter();
userAuthenticationFilter.setAuthenticationManager(super.authenticationManager());
userAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
userAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
return userAuthenticationFilter;
}

注:authenticationSuccessHandler、authenticationFailureHandler分别是登录成功、失败的处理类,此处略。

3.10.2 多次登录失败后账户锁定

[1] 实现多次登录失败锁定的原理

一般来说实现这个需求,我们需要针对每一个用户记录登录失败的次数nLock和锁定账户的到期时间releaseTime。具体你是把这2个信息存储在mysql、还是文件中、还是redis中等等,完全取决于你对你所处的应用架构适用性的判断。具体的实现逻辑无非就是:

  • 登陆失败之后,从存储中将nLock取出来加1。
  • 如果nLock大于登陆失败阈值(比如3次),则将nLock=0,然后设置releaseTime为当前时间加上锁定周期。通过setAccountNonLocked(false)告知Spring Security该登录账户被锁定。
  • 如果nLock小于等于1,则将nLock再次存起来。
  • 在一个合适的时机,将锁定状态重置为setAccountNonLocked(true)。

这是一种非常典型的实现方式,可以使用开源的ratelimitj来实现。它的功能主要是为API访问进行限流,也就是说可以通过制定规则限制API接口的访问频率。那恰好登录验证接口也是API的一种啊,我们正好也需要限制它在一定的时间内的访问次数。

[2] 多次登录失败锁定的具体实现

首先需要将ratelimitj通过maven引入到项目里来。这里使用的是内存存储的版本,还有redis存储的版本,可以根据自己的实际情况选用。

1
2
3
4
5
<dependency>
<groupId>es.moki.ratelimitj</groupId>
<artifactId>ratelimitj-inmemory</artifactId>
<version>0.4.1</version>
</dependency>

CheckLockUtility.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
import es.moki.ratelimitj.core.limiter.request.RequestLimitRule;
import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter;
import es.moki.ratelimitj.inmemory.request.InMemorySlidingWindowRequestRateLimiter;

import java.util.Collections;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
* 多次登录失败账号锁定工具类
*/
public class AccountLockUtility {

// 规则定义:10分钟之内3次错误机会,达到就触发账号锁定
private static Set<RequestLimitRule> rules = Collections.singleton(RequestLimitRule.of(10, TimeUnit.MINUTES,3));
private static RequestRateLimiter limiter = new InMemorySlidingWindowRequestRateLimiter(rules);

// 计数器加1,并判断该用户是否已经到了触发了锁定规则
public static boolean checkLock(String userId) {
return limiter.overLimitWhenIncremented(userId);
}

// 重置锁定
public static void resetLock(String userId){
limiter.resetLimit(userId);
}
}

在AuthenticationSuccessHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
//从request.getSession中获取登录用户
String userId = request.getSession().getId();
boolean reachLimit = AccountLockUtility.checkLock(userId);
if(reachLimit) {
try {
... // 返回“多次登录失败,账户已被锁定,请10分钟后重试”的错误消息
} catch (Exception e) {
e.printStackTrace();
}
}else{
AccountLockUtility.resetLock(userId); // 重置锁定
... // 编写登录成功的业务逻辑
}

AuthenticationFailureHandler里分别编写账户锁定校验。

1
2
3
4
5
6
7
8
9
10
11
12
//从request.getSession中获取登录用户
String userId = request.getSession().getId();
boolean reachLimit = AccountLockUtility.checkLock(userId);
if(reachLimit) {
try {
... // 返回“多次登录失败,账户已被锁定,请10分钟后重试”的错误消息
} catch (Exception e) {
e.printStackTrace();
}
}else{
... // 编写登录失败的业务逻辑
}

3.10.3 登录登出接口添加Swagger

情景描述:有些场景我们需要手动将接口添加到Swagger中,比如非SpringMVC注解暴露接口(如定义在filter中),无法通过这种注解方式生成api接口文档。SpringSecurity的用户名密码登录接口,就是在filter中进行了拦截,因此在Swagger中看不到该登录接口,这样在平时的开发测试中,非常不方便。

解决思路:通过实现swagger提供的插件ApiListingScannerPlugin,可以手动将接口添加到swagger文档里。

代码实现:SpringSecurityApis.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
96
97
98
99
100
101
102
103
104
105
106
107
import com.fasterxml.classmate.TypeResolver;
import org.apache.commons.compress.utils.Sets;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.Operation;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ApiListingScannerPlugin;
import springfox.documentation.spi.service.contexts.DocumentationContext;
import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;


/**
* 由于 Spring Security 的登录、登出接口是通过Filter实现,导致 Swagger 无法获取其信息。
* 这里手动将登录、登出接口注册到Swagger中,在Swagger-UI才能展示,方便调用。
*/
@Component
public class SpringSecurityApis implements ApiListingScannerPlugin {

/**
* Implement this method to manually add ApiDescriptions
* 实现此方法可手动添加ApiDescriptions
*
* @param context - Documentation context that can be used infer documentation context
* @return List of {@link ApiDescription}
* @see ApiDescription
*/
@Override
public List<ApiDescription> apply(DocumentationContext context) {

// 额外添加登录接口的文档
Operation loginOperation = new OperationBuilder(new CachingOperationNameGenerator())
.method(HttpMethod.POST)
.summary("系统登录")
// 接收参数格式
.consumes(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
// 返回参数格式
.produces(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
.tags(Sets.newHashSet("系统管理"))
.uniqueId("login")
.parameters(Collections.singletonList(
new ParameterBuilder()
.description("用户名与密码使用AES加密后得到的字符串")
.type(new TypeResolver().resolve(String.class))
.name("aes_str")
.parameterType("body")
.parameterAccess("access")
.required(true)
.modelRef(new ModelRef("text"))
.build()
))
.responseMessages(Collections.singleton(
new ResponseMessageBuilder().code(200).message("请求成功")
.responseModel(new ModelRef(
"xyz.gits.boot.common.core.response.RestResponse")
).build()))
.build();

ApiDescription loginApiDescription = new ApiDescription("auth", "/auth/login", "登录接口",
Collections.singletonList(loginOperation), false);

// 额外添加退出登录接口的文档
Operation logoutOperation = new OperationBuilder(new CachingOperationNameGenerator())
.method(HttpMethod.POST)
.summary("系统退出登录")
// 接收参数格式
.consumes(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
// 返回参数格式
.produces(Sets.newHashSet(MediaType.APPLICATION_JSON_VALUE))
.parameters(Collections.emptyList())
.uniqueId("logout")
.tags(Sets.newHashSet("系统管理"))
.responseMessages(Collections.singleton(
new ResponseMessageBuilder().code(200).message("请求成功")
.responseModel(new ModelRef(
"xyz.gits.boot.common.core.response.RestResponse")
).build()))
.build();

ApiDescription logoutApiDescription = new ApiDescription("auth", "/auth/logout", "退出登录接口",
Collections.singletonList(logoutOperation), false);

return Arrays.asList(loginApiDescription, logoutApiDescription);

}

/**
* 是否使用此插件
*
* @param documentationType swagger文档类型
* @return true 启用
*/
@Override
public boolean supports(DocumentationType documentationType) {
return DocumentationType.SWAGGER_2.equals(documentationType);
}

}

3.11 SpringBoot实现自定义注解

3.11.1 Java元注解

Java中提供了四种元注解,用来注解其他的注解,当我们创建自定义注解时,就需要使用元注解来标注我们定义的注解,以标识自定义注解的一些属性信息。@Target,@Retention,@Documented,@Inherited,这四种元注解都位于java.lang.annotation包下。

@Target

用来指定当前创建的注解作用于什么地方,如@Target({ElementType.TYPE})标识自定义注解可以作用于类、解耦或枚举类型上。注解取值范围为枚举常量ElementType的所有值,有:

  • ElementType.TYPE:用于类、接口或枚举声明
  • ElementType.FIELD:用于字段声明
  • ElementType.METHOD:用于方法声明
  • ElementType.PARAMETER:用于正式的参数声明
  • ElementType.CONSTRUCTOR:用于构造函数声明
  • ElementType.LOCAL_VARIABLE:用于局部变量声明
  • ElementType.ANNOTATION_TYPE:用于注解类型声明
  • ElementType.PACKAGE:用于包声明

@Retention

@Retention注解代表了一个注解的生命周期,其值对应了注解会留存到什么时候,如编译阶段、运行阶段等。使用时与RetentionPolicy枚举常量配合,如@Retention({RetentionPolicy.Runtime}),所有取值有:

  • @Retention({RetentionPolicy.SOURCE}):表示定义注解指保存在源码级别,编译时会忽略。
  • @Retention({RetentionPolicy.CLASS}):表示注解将会在编译时保存在类文件中,运行时不保留。
  • @Retention({RetentionPolicy.Runtime}):表示注解在编译时和运行时都存在,且可以被反向读取。

@Documented

使用@Documented注解标注的自定义注解会被添加在JavaDoc中。

@Inherited

如果父类使用了被@Inherited注解标注的自定义注解,那么其子类会继承自定义注解。

3.11.2 Spring AOP的实现

理解了Java中元注解的代表含义,那么我们可以使用这些元注解来实现自定义注解。

Step1:创建自定义注解

创建一个自定义注解,根据需要添加元注解并指定枚举值

1
2
3
4
5
6
7
import java.lang.annotation.*;

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}

Step2:实现自定义注解逻辑

[1] 定义切面类

[2] 定义切点,并以自定义注解为切入

[3] 定义使用注解的方法需要附加的通知类型和内容

具体的注解逻辑实现代码为:

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.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
* 实现注解逻辑
* @Aspect 定义切面类
* @Component 注册为容器组件
*/
@Aspect
@Component
public class TestAnnotationImpl {

//定义切点
@Pointcut("@annotation(com.yoyo.admin.common.test.annotation.TestAnnotation)")
private void cut(){
}

@Before("cut()")
public void before(){
System.out.println("自定义注解前置通知!");
}
@After("cut()")
public void after(){
System.out.println("自定义注解后置通知!");
}
}

Step3:使用自定义注解

注解定义完成并将注解的具体逻辑实现后,我们就可以在controller控制层来使用我们自定义的注解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.yoyo.admin.common.test.annotation.TestAnnotation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/api/web")
public class TestController {

/**
* 使用自定义注解测试
* @return
*/
@TestAnnotation
@RequestMapping("/testAop")
public String hello(){
System.out.println("hello world!");
return "hello world!";
}

}

我们为方法使用了自定义注解后,访问方法地址,会发现在方法输出内容的前、后出现了自定义注解的内容,这就说明我们的注解发生了作用,为方法添加了前置通知和后置通知。

1
2
3
自定义注解前置通知!
hello world!
自定义注解后置通知!

3.12 用配置项设置定时任务是否开启

在 application.properties 文件里添加配置项

1
2
# periodical-task settings
settings.task.enabled=false

在定时任务的文件的类上添加如下两个注解

1
2
3
@EnableScheduling
// 配置文件读取是否开启定时任务(与havingValue值相同才会开启)
@ConditionalOnProperty(prefix = "settings.task", name = "enabled", havingValue = "true")

3.13 yml与properties的互相转换

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

方案二:使用工具类将 yml 转换为 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
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import lombok.Data;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Stream;


/**
* Yaml 配置文件转 Properties 配置文件工具类
*/
public class YmlTool {

private static final String lineSeparator = "\n";

/**
* 将 yml 字符串化为 properties字符串
*
* @param yml
* @return
*/
public static String yamlStr2PropStr(String yml) {
List<YmlNode> nodeList = getNodeList(yml);
// 去掉多余数据,并打印
String str = printNodeList(nodeList);
return str;
}

/**
* 将 yml 文件转化为 properties 文件
*
* @param ymlFileName 工程根目录下(非resources目录)的 yml 文件名称(如:abc.yml)
* @return List<Node> 每个Nyml 文件中每行对应解析的数据
*/
public static List<YmlNode> yamlFile2PropFile(String ymlFileName) {
if (ymlFileName == null || !ymlFileName.endsWith(".yml")) {
throw new RuntimeException("请输入yml文件名称!!");
}
File ymlFile = new File(ymlFileName);
if (!ymlFile.exists()) {
throw new RuntimeException("工程根目录下不存在 " + ymlFileName + "文件!!");
}
String fileName = ymlFileName.split(".yml", 2)[0];
// 获取文件数据
String yml = read(ymlFile);
List<YmlNode> nodeList = getNodeList(yml);
// 去掉多余数据,并打印
String str = printNodeList(nodeList);
// 将数据写入到 properties 文件中
String propertiesName = fileName + ".properties";
File file = new File(propertiesName);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
try (FileWriter writer = new FileWriter(file)) {
writer.write(str);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
return nodeList;
}

/**
* 将yml转化为porperties文件,并获取转化后的Map键值对
*
* @param ymlFileName 工程根目录下的 yml 文件名称
* @return 转化后的 porperties 文件键值对Map
*/
public static Map<String, String> yamlFile2Map(String ymlFileName) {
Map<String, String> map = new HashMap<>();
List<YmlNode> list = yamlFile2PropFile(ymlFileName);
String s = printNodeList(list);
String[] lines = s.split(lineSeparator);
Stream.of(lines).forEach(line -> {
String[] split = line.split("=");
map.put(split[0], split[1]);
});
return map;
}

public static Map<String, String> yamlStr2Map(String yaml) {
Map<String, String> map = new HashMap<>();
List<YmlNode> list = getNodeList(yaml);
String s = printNodeList(list);
String[] lines = s.split(lineSeparator);
Stream.of(lines).forEach(line -> {
String[] split = line.split("=");
map.put(split[0], split[1]);
});
return map;
}

private static String read(File file) {
if (Objects.isNull(file) || !file.exists()) {
return "";
}
try (FileInputStream fis = new FileInputStream(file)) {
byte[] b = new byte[(int) file.length()];
fis.read(b);
return new String(b, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
return "";
}

private static String printNodeList(List<YmlNode> nodeList) {
StringBuilder sb = new StringBuilder();
for (YmlNode node : nodeList) {
if (node.getLast().equals(Boolean.FALSE)) {
continue;
}
if (node.getEmptyLine().equals(Boolean.TRUE)) {
sb.append(lineSeparator);
continue;
}
// 判断是否有行级注释
if (node.getHeadRemark().length() > 0) {
String s = "# " + node.getHeadRemark();
sb.append(s).append(lineSeparator);
continue;
}
// 判断是否有行末注释 (properties中注释不允许末尾注释,故而放在上面)
if (node.getTailRemark().length() > 0) {
String s = "# " + node.getTailRemark();
sb.append(s).append(lineSeparator);
}
//
String kv = node.getKey() + "=" + node.getValue();
sb.append(kv).append(lineSeparator);
}
return sb.toString();
}

private static List<YmlNode> getNodeList(String yml) {
String[] lines = yml.split(lineSeparator);
List<YmlNode> nodeList = new ArrayList<>();
Map<Integer, String> keyMap = new HashMap<>();
Set<String> keySet = new HashSet<>();
for (String line : lines) {
YmlNode node = getNode(line);
if (node.getKey() != null && node.getKey().length() > 0) {
int level = node.getLevel();
if (level == 0) {
keyMap.clear();
keyMap.put(0, node.getKey());
} else {
int parentLevel = level - 1;
String parentKey = keyMap.get(parentLevel);
String currentKey = parentKey + "." + node.getKey();
keyMap.put(level, currentKey);
node.setKey(currentKey);
}
}
keySet.add(node.getKey() + ".");
nodeList.add(node);
}
// 标识是否最后一级
for (YmlNode each : nodeList) {
each.setLast(getNodeLast(each.getKey(), keySet));
}
return nodeList;
}

private static boolean getNodeLast(String key, Set<String> keySet) {
if (key.isEmpty()) {
return true;
}
key = key + ".";
int count = 0;
for (String each : keySet) {
if (each.startsWith(key)) {
count++;
}
}
return count == 1;
}

private static YmlNode getNode(String line) {
YmlNode node = new YmlNode();
// 初始化默认数据(防止NPE)
node.setEffective(Boolean.FALSE);
node.setEmptyLine(Boolean.FALSE);
node.setHeadRemark("");
node.setKey("");
node.setValue("");
node.setTailRemark("");
node.setLast(Boolean.FALSE);
node.setLevel(0);
// 空行,不处理
String trimStr = line.trim();
if (trimStr.isEmpty()) {
node.setEmptyLine(Boolean.TRUE);
return node;
}
// 行注释,不处理
if (trimStr.startsWith("#")) {
node.setHeadRemark(trimStr.replaceFirst("#", "").trim());
return node;
}
// 处理值
String[] strs = line.split(":", 2);
// 拆分后长度为0的,属于异常数据,不做处理
if (strs.length == 0) {
return node;
}
// 获取键
node.setKey(strs[0].trim());
// 获取值
String value;
if (strs.length == 2) {
value = strs[1];
} else {
value = "";
}
// 获取行末备注
String tailRemark = "";
if (value.contains(" #")) {
String[] vs = value.split("#", 2);
if (vs.length == 2) {
value = vs[0];
tailRemark = vs[1];
}
}
node.setTailRemark(tailRemark.trim());
node.setValue(value.trim());
// 获取当前层级
int level = getNodeLevel(line);
node.setLevel(level);
node.setEffective(Boolean.TRUE);
return node;
}

private static int getNodeLevel(String line) {
if (line.trim().isEmpty()) {
return 0;
}
char[] chars = line.toCharArray();
int count = 0;
for (char c : chars) {
if (c != ' ') {
break;
}
count++;
}
return count / 2;
}

public static void main(String[] args) {
// 把 application.yml 放在项目根目录(而不是resources目录),转换完会在根目录生成对应的 application.properties 文件
yamlFile2PropFile("application.yml");
}

}

@Data
class YmlNode {

/**
* 层级关系
*/
private Integer level;
/**
* 键
*/
private String key;
/**
* 值
*/
private String value;
/**
* 是否为空行
*/
private Boolean emptyLine;
/**
* 当前行是否为有效配置
*/
private Boolean effective;
/**
* 头部注释(单行注释)
*/
private String headRemark;
/**
* 末尾注释
*/
private String tailRemark;
/**
* 是否为最后一层配置
*/
private Boolean last;
}

3.14 业务系统增加日志审计模块

在项目里添加 logger 模块,然后在具体的接口加上注解,即可实现操作日志审计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
logger
├── annotation
│   ├── CreateLog.java
│   ├── DeleteLog.java
│   └── UpdateLog.java
├── domain
│   ├── LogContentObject.java
│   ├── OperationLog.java
│   └── OperationLogRepository.java
├── enumeration
│   ├── OperationType.java
│   └── OperationTypeConverter.java
└── service
├── OperationLogService.java
└── TerminalService.java

注:这里仅仅是封装代码部分,还要结合具体业务实体类和接口注解才能实现此功能,import com.yoyo.admin.xxx的代码此处略去。

./annotation/CreateLog.java

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreateLog {

String target();

String operation() default "新增";
}

./annotation/DeleteLog.java

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DeleteLog {
String target();

String operation() default "删除";
}

./annotation/UpdateLog.java

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UpdateLog {
String target();

String operation() default "更新";
}

./domain/LogContentObject.java

1
2
3
4
5
6
7
8
9
10
11
12
import lombok.Data;

import java.io.Serializable;

@Data
public class LogContentObject implements Serializable {

private String name;

private Object data;

}

./domain/OperationLog.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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import com.yoyo.admin.common.logger.enumeration.OperationType;
import com.yoyo.admin.common.logger.enumeration.OperationTypeConverter;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;

import javax.persistence.*;
import java.util.Date;

/**
* 操作日志
*/
@Data
@Entity
@ApiModel(value = "OperationLog", description = "操作日志")
@Table(name = "operation_log")
@org.hibernate.annotations.Table(appliesTo = "operation_log", comment = "操作日志表")
public class OperationLog {

/**
* 操作日志id
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@ApiModelProperty(value = "操作日志id")
@Column(name = "id", columnDefinition = "bigint COMMENT '操作日志id'")
private Long id;

/**
* 操作类型id
*/
@ApiModelProperty(value = "操作类型id")
@Column(name = "operation_type_id", columnDefinition = "bigint COMMENT '操作类型id'")
@Convert(converter = OperationTypeConverter.class)
private OperationType operationType;

/**
* 终端名称
*/
@ApiModelProperty(value = "终端名称")
@Column(name = "terminal_name", length = 20, columnDefinition = "varchar(255) COMMENT '终端名称'")
private String terminalName;

/**
* web用户Id
*/
@ApiModelProperty(value = "web用户Id")
@Column(name = "web_user_id", columnDefinition = "bigint COMMENT 'web用户Id'")
private Long webUserId;

/**
* 小程序/公众号用户Id
*/
@ApiModelProperty(value = "小程序/公众号用户Id")
@Column(name = "wechat_user_id", length = 50, columnDefinition = "varchar(50) COMMENT '小程序/公众号用户Id'")
private String wechatUserId;

/**
* 用户姓名
*/
@ApiModelProperty(value = "用户姓名")
@Column(name = "user_name", length = 50, columnDefinition = "varchar(50) COMMENT '用户姓名'")
private String userName;

/**
* ip地址
*/
@ApiModelProperty(value = "ip地址")
@Column(name = "ip_address", length = 50, columnDefinition = "varchar(50) COMMENT 'ip地址'")
private String ipAddress;

/**
* 具体操作,由注解传来
*/
@ApiModelProperty(value = "具体操作,由注解传来")
@Column(name = "operation", length = 50, columnDefinition = "varchar(50) COMMENT '具体操作,由注解传来'")
private String operation;

/**
* 操作内容
*/
@ApiModelProperty(value = "操作内容")
@Column(name = "content", columnDefinition = "text COMMENT '操作内容'")
private String content;

/**
* 操作对象
*/
@ApiModelProperty(value = "操作对象")
@Column(name = "target", length = 50, columnDefinition = "varchar(50) COMMENT '操作对象'")
private String target;

/**
* 备注
*/
@ApiModelProperty(value = "备注")
@Column(name = "remark", length = 1024, columnDefinition = "varchar(1024) COMMENT '备注'")
private String remark;

/**
* 创建时间
*/
@CreationTimestamp
@ApiModelProperty(value = "创建时间")
@Column(name = "create_time", updatable = false, columnDefinition = "datetime COMMENT '创建时间'")
private Date createTime;

}

./domain/OperationLogRepository.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
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

/**
* 操作日志
*/
public interface OperationLogRepository extends JpaRepository<OperationLog, Long>, JpaSpecificationExecutor<OperationLog> {

/**
* 根据id获取操作日志
* @param id
* @return
*/
OperationLog findFirstById(Long id);

/**
* 获取系统全部操作日志
* @param terminalNames
* @return
*/
@Query("select o.target from OperationLog o where o.terminalName in ?1 group by o.target")
List<String> findAllTargets(List<String> terminalNames);
}

./enumeration/OperationType.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
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 操作类型
*/
public enum OperationType {
Create(1, "新增"), Update(2, "更新"), Delete(3, "删除"),
Login(4, "登录"), Logout(5, "登出");

private final Integer id;
private final String name;

OperationType(Integer id, String name) {
this.id = id;
this.name = name;
}

@JsonValue
public Integer getId() {
return id;
}

public String getName() {
return name;
}

/**
* 按id获取
* @param id
* @return
*/
@JsonCreator
public static OperationType getById(Integer id) {
if (id == null) {
return null;
}
for (OperationType item : OperationType.values()) {
if (item.getId().equals(id)) {
return item;
}
}
return null;
}

/**
* 获取列表
* @return
*/
public static List<Map<String, Object>> listAll() {
List<Map<String, Object>> list = new ArrayList<>();
for (OperationType item : OperationType.values()) {
Map<String, Object> map = new HashMap<>();
map.put("id", item.getId());
map.put("name", item.getName());
list.add(map);
}
return list;
}

/**
* 解析从body中提取的操作类型id参数为操作类型
*
* @param idObject id对象
* @param isNecessary 是否是必填,一般新建时为true,更新时为false
* @return 操作类型
* @throws RuntimeException 用于返回给前端的错误提示
*/
public static OperationType parseBodyPlatformParam(Object idObject, boolean isNecessary) throws RuntimeException {
if (idObject == null) {
if (isNecessary) {
throw new RuntimeException("未指定操作类型id");
} else {
return null;
}
}
int id;
try {
id = Integer.parseInt(idObject.toString());
} catch (Exception ex) {
throw new RuntimeException("指定的操作类型id格式不正确");
}
OperationType operationType = getById(id);
if (operationType == null) {
throw new RuntimeException("指定的操作类型id不正确");
}
return operationType;
}
}

./enumeration/OperationTypeConverter.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
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

/**
* 操作类型转换
*/
@Converter
public class OperationTypeConverter implements AttributeConverter<OperationType, Integer> {

@Override
public Integer convertToDatabaseColumn(OperationType enumItem) {
// 这里要加上判空,不然实体类的相应字段为空时会报错
if (enumItem == null) {
return null;
}
return enumItem.getId();
}

@Override
public OperationType convertToEntityAttribute(Integer id) {
if (id == null) {
return null;
}
return OperationType.getById(id);
}
}

./service/OperationLogService.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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yoyo.admin.common.domain.Dept;
import com.yoyo.admin.common.domain.Role;
import com.yoyo.admin.common.domain.User;
import com.yoyo.admin.common.logger.annotation.DeleteLog;
import com.yoyo.admin.common.utils.PageData;
import com.yoyo.admin.common.logger.annotation.CreateLog;
import com.yoyo.admin.common.logger.annotation.UpdateLog;
import com.yoyo.admin.common.logger.domain.LogContentObject;
import com.yoyo.admin.common.logger.domain.OperationLog;
import com.yoyo.admin.common.logger.domain.OperationLogRepository;
import com.yoyo.admin.common.logger.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import java.io.File;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.text.SimpleDateFormat;
import java.util.*;

/**
* 操作日志
*/
@Service
@Slf4j
public class OperationLogService {

private OperationLogRepository operationLogRepository;
private TerminalService terminalService;
private final ObjectMapper _objectMapper = new ObjectMapper();
private final SimpleDateFormat _simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Autowired
public void setOperationLogRepository(OperationLogRepository operationLogRepository) {
this.operationLogRepository = operationLogRepository;
}

@Autowired
public void setTerminalService(TerminalService terminalService) {
this.terminalService = terminalService;
}

/**
* 按id获取操作日志
* @param id
* @return
*/
public OperationLog get(Long id) {
return operationLogRepository.findFirstById(id);
}

/**
* 操作日志查询
* @param operationType
* @param terminalNames
* @param longUserId
* @param stringUserId
* @param userName
* @param ipAddress
* @param target
* @param operation
* @param beginTime
* @param endTime
* @param remark
* @param page
* @param pageSize
* @return
*/
public PageData<OperationLog> list(OperationType operationType, List<String> terminalNames, Long longUserId, String stringUserId, String userName,
String ipAddress, String target, String operation, Date beginTime, Date endTime, String remark, int page, int pageSize) {
Specification<OperationLog> specification = (Specification<OperationLog>) (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicateList = new ArrayList<>();
if (operationType != null) {
predicateList.add(criteriaBuilder.isNotNull(root.get("operationType")));
predicateList.add(criteriaBuilder.equal(root.get("operationType"), operationType));
}
if (terminalNames != null && terminalNames.size() > 0) {
predicateList.add(criteriaBuilder.isNotNull(root.get("terminalName")));
CriteriaBuilder.In<String> in = criteriaBuilder.in(root.get("terminalName"));
terminalNames.forEach(in::value);
predicateList.add(in);
}
if (longUserId != null) {
predicateList.add(criteriaBuilder.isNotNull(root.get("longUserId")));
predicateList.add(criteriaBuilder.equal(root.get("longUserId"), longUserId));
}
if (stringUserId != null && !stringUserId.isEmpty()) {
predicateList.add(criteriaBuilder.isNotNull(root.get("stringUserId")));
predicateList.add(criteriaBuilder.equal(root.get("stringUserId"), stringUserId));
}
if (userName != null && !userName.isEmpty()) {
predicateList.add(criteriaBuilder.isNotNull(root.get("userName")));
predicateList.add(criteriaBuilder.like(root.get("userName"), "%" + userName + "%"));
}
if (ipAddress != null && !ipAddress.isEmpty()) {
predicateList.add(criteriaBuilder.isNotNull(root.get("ipAddress")));
predicateList.add(criteriaBuilder.like(root.get("ipAddress"), "%" + ipAddress + "%"));
}
if (target != null && !target.isEmpty()) {
predicateList.add(criteriaBuilder.isNotNull(root.get("target")));
predicateList.add(criteriaBuilder.equal(root.get("target"), target));
}
if (operation != null && !operation.isEmpty()) {
predicateList.add(criteriaBuilder.isNotNull(root.get("operation")));
predicateList.add(criteriaBuilder.like(root.get("operation"), "%" + operation + "%"));
}
if (beginTime != null) {
predicateList.add(criteriaBuilder.greaterThanOrEqualTo(root.get("createTime"), beginTime));
}
if (endTime != null) {
predicateList.add(criteriaBuilder.lessThanOrEqualTo(root.get("createTime"), endTime));
}
if (remark != null && !remark.isEmpty()) {
predicateList.add(criteriaBuilder.isNotNull(root.get("remark")));
predicateList.add(criteriaBuilder.like(root.get("remark"), "%" + remark + "%"));
}
Predicate[] predicates = new Predicate[predicateList.size()];
return criteriaBuilder.and(predicateList.toArray(predicates));
};
Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
Page<OperationLog> specialities = operationLogRepository.findAll(specification, PageRequest.of(page - 1, pageSize, sort));
return new PageData<>(specialities, page, pageSize);
}

/**
* 新增操作日志
* @param joinPoint
* @param webUserId
* @param wechatUserId
* @param userName
* @param ipAddress
*/
public void create(JoinPoint joinPoint, Long webUserId, String wechatUserId, String userName, String ipAddress) {
Method method = getMethodByName(joinPoint.getTarget(), joinPoint.getSignature().getName());
if (method == null) {
return;
}
Annotation annotation = getOperationLogAnnotationFromMethod(method);
OperationType operationType = null;
String target = null;
String operation = null;
StringBuilder remark = new StringBuilder();
if (annotation != null) {
operationType = getOperationTypeByAnnotation(annotation);
target = getTargetFromAnnotation(annotation);
operation = getOperationFromAnnotation(annotation);
}
OperationLog operationLog = new OperationLog();
operationLog.setOperationType(operationType);
operationLog.setTerminalName(terminalService.getTerminalName());
operationLog.setWebUserId(webUserId);
operationLog.setWechatUserId(wechatUserId);
operationLog.setUserName(userName);
operationLog.setIpAddress(ipAddress);
operationLog.setOperation(operation);
operationLog.setContent(packageParamToJson(method, joinPoint.getArgs(), remark));
operationLog.setTarget(target);
operationLog.setRemark(remark.toString());
operationLogRepository.save(operationLog);
}

/**
* 获取已存在的所有target
* @param terminalNames
* @return
*/
public List<String> listTarget(List<String> terminalNames) {
return operationLogRepository.findAllTargets(terminalNames);
}

/**
* @param object 对象
* @param methodName 需要的方法的方法名
* @return 方法
*/
public Method getMethodByName(Object object, String methodName) {
Class<?> objectClass = object.getClass();
Method[] methods = objectClass.getDeclaredMethods();
for (int i = 0; i <= methods.length - 1; i++) {
Method method = methods[i];
if (method.getName().equals(methodName)) {
return method;
}
}
return null;
}

/**
* 根据方法获取操作日志注解
* @param method
* @return
*/
private Annotation getOperationLogAnnotationFromMethod(Method method) {
Annotation[] annotations = method.getDeclaredAnnotations();
for (int i = 0; i <= annotations.length - 1; i++) {
Annotation annotation = annotations[i];
Class<?> clazz = annotation.annotationType();
if (clazz.equals(CreateLog.class) || clazz.equals(UpdateLog.class) || clazz.equals(DeleteLog.class)) {
return annotation;
}
}
return null;
}

/**
* 根据操作日志注解判断操作类型
* @param annotation
* @return
*/
private OperationType getOperationTypeByAnnotation(Annotation annotation) {
if (annotation.annotationType().equals(CreateLog.class)) {
return OperationType.Create;
}
if (annotation.annotationType().equals(UpdateLog.class)) {
return OperationType.Update;
}
if (annotation.annotationType().equals(DeleteLog.class)) {
return OperationType.Delete;
}
return null;
}

/**
* 从注解中获取target
* @param annotation
* @return
*/
private String getTargetFromAnnotation(Annotation annotation) {
if (annotation.annotationType().equals(CreateLog.class)) {
CreateLog createLog = (CreateLog) annotation;
return createLog.target();
}
if (annotation.annotationType().equals(UpdateLog.class)) {
UpdateLog updateLog = (UpdateLog) annotation;
return updateLog.target();
}
if (annotation.annotationType().equals(DeleteLog.class)) {
DeleteLog deleteLog = (DeleteLog) annotation;
return deleteLog.target();
}
return null;
}

/**
* 从注解中获取operation
* @param annotation
* @return
*/
private String getOperationFromAnnotation(Annotation annotation) {
if (annotation.annotationType().equals(CreateLog.class)) {
CreateLog createLog = (CreateLog) annotation;
return createLog.operation();
}
if (annotation.annotationType().equals(UpdateLog.class)) {
UpdateLog updateLog = (UpdateLog) annotation;
return updateLog.operation();
}
if (annotation.annotationType().equals(DeleteLog.class)) {
DeleteLog deleteLog = (DeleteLog) annotation;
return deleteLog.operation();
}
return null;
}

/**
* 将指定方法的参数打包成json
* @param method
* @param args
* @param remark
* @return
*/
private String packageParamToJson(Method method, Object[] args, StringBuilder remark) {
Map<String, Object> map = new HashMap<>();
Parameter[] parameters = method.getParameters();
if (parameters.length == args.length) {
for (int i = 0; i <= parameters.length - 1; i++) {
LogContentObject contentObject = simplifyParameter(parameters[i].getName(), args[i], 0, remark);
if (contentObject != null && contentObject.getName() != null) {
map.put(contentObject.getName(), contentObject.getData());
}
}
}
try {
return _objectMapper.writeValueAsString(map);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}

private LogContentObject simplifyParameter(String paramName, Object data, Integer deep, StringBuilder remark) {
if (data == null || deep > 3) {
return null;
}
LogContentObject contentObject = new LogContentObject();
contentObject.setName(paramName);
//如果是基本类型,直接返回
if (isBaseType(data)) {
contentObject.setData(data);
return contentObject;
}
//如果是枚举类型,返回枚举名
if (data.getClass().isEnum()) {
contentObject.setData(((Enum<?>) data).name());
return contentObject;
}
//如果是列表,处理其中的每一个元素
if (data instanceof List) {
List<Object> list = new ArrayList<>();
List<?> dataList = (List<?>) data;
String name = paramName;
for (int i = 0; i <= dataList.size() - 1; i++) {
LogContentObject listLogContentObject = simplifyParameter(null, dataList.get(i), deep + 1, remark);
if (listLogContentObject == null) {
continue;
}
if (listLogContentObject.getName() != null) {
name = listLogContentObject.getName();
}
list.add(listLogContentObject.getData());
}
contentObject.setName(name);
contentObject.setData(list);
return contentObject;
}
//如果是文件,直接返回字符串"文件"
if (data instanceof InputStream || data instanceof File) {
contentObject.setData("文件");
return contentObject;
}
if (data instanceof Role) {
contentObject.setName("角色");
contentObject.setData(simplifyRole((Role) data, remark));
return contentObject;
}
if (data instanceof User) {
contentObject.setName("用户");
contentObject.setData(simplifyUser((User) data, remark));
return contentObject;
}
if (data instanceof Dept) {
contentObject.setName("部门");
contentObject.setData(simplifyDept((Dept) data));
return contentObject;
}
//如果是其他类型,返回数据为一个Map,内部保存字段名和字段值
Map<String, Object> map = new HashMap<>();
Field[] fields = data.getClass().getDeclaredFields();
for (int i = 0; i <= fields.length - 1; i++) {
LogContentObject filedLogContentObject = null;
Field field = fields[i];
field.setAccessible(true);
try {
filedLogContentObject = simplifyParameter(field.getName(), field.get(data), deep + 1, remark);
} catch (IllegalAccessException ex) {
log.error(ex.getMessage());
}
if (filedLogContentObject != null) {
map.put(filedLogContentObject.getName(), filedLogContentObject.getData());
}
}
contentObject.setData(map);
return contentObject;
}

private Map<String, Object> simplifyRole(Role role, StringBuilder remark) {
Map<String, Object> map = new HashMap<>();
if (role != null) {
map.put("id", role.getId());
map.put("部门类型编号", role.getCode());
map.put("部门类型名称", role.getName());
remark.append(role.getName());
}
return map;
}

private Map<String, Object> simplifyUser(User user, StringBuilder remark) {
Map<String, Object> map = new HashMap<>();
if (user != null) {
map.put("用户id", user.getId());
map.put("用户姓名", user.getName());
map.put("部门id", user.getDept() != null ? user.getDept().getId() : null);
remark.append(user.getName()).append("(").append(user.getUsername()).append(")").append(" ");
}
return map;
}

private Map<String, Object> simplifyDept(Dept dept) {
Map<String, Object> map = new HashMap<>();
if (dept != null) {
map.put("id", dept.getId());
map.put("名称", dept.getName());
map.put("上级部门", dept.getParentDept() != null ? dept.getParentDept().getName() : null);
}
return map;
}

private static boolean isBaseType(Object object) {
if (object instanceof Boolean) {
return true;
}
if (object instanceof Character) {
return true;
}
if (object instanceof Byte) {
return true;
}
if (object instanceof Short) {
return true;
}
if (object instanceof Integer) {
return true;
}
if (object instanceof Long) {
return true;
}
if (object instanceof Float) {
return true;
}
if (object instanceof Double) {
return true;
}
if (object instanceof Date) {
return true;
}
return object instanceof String;
}

}

./service/TerminalService.java

1
2
3
4
5
6
7
8
9
import org.springframework.stereotype.Component;

/**
* 获取终端名
*/
@Component
public interface TerminalService {
String getTerminalName();
}

3.15 常见报错及警告问题的解决方案

[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–>Editor–>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>

[10] 启动报错 Exception encountered during context initialization …… Error creating bean with name ‘xxxController’ …

拖到最右边查看具体报错,我是因为复制mapper的xml代码时忘记修改id导致的。

[11] AES的256位密钥加解密报 java.security.InvalidKeyException: Illegal key size or default parameters 异常

报错原因:

1
在我们安装JRE的目录下有这样一个文件夹:%JAVE_HOME%\jre\lib\security,其中包含有两个.jar文件:“local_policy.jar ”和“US_export_policy.jar”。JRE中自带的“local_policy.jar ”和“US_export_policy.jar”是支持128位密钥的加密算法,而当我们要使用256位密钥算法的时候,已经超出它的范围,无法支持,所以才会报:“java.security.InvalidKeyException: Illegal key size or default parameters”的异常。

解决办法:

方法一:下载单独策略文件,替换原来的文件。

其对应的JCE下载地址为:http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html

下载完后,解压,将其中的“local_policy.jar ”和“US_export_policy.jar”两个文件替换掉自己%JAVE_HOME%\jre\lib\security文件夹下对应的原文件。

方法二:将jdk升级到JDK1.8.0-161以上版本,就不需要单独策略文件了。

[12] Null value was assigned to a property of primitive type setter

无法将null值分配给基本类型,这个异常常会出现在往数据库插入数据时。如果数据库中的字段可以为null,那么实体类中相应的属性应该是一个包装类,如Integer,Long等,而不应该是基本类型int,long。

[13] 打开swagger页面时,出现 java.lang.NumberFormatException: For input string 警告

报错原因:这是由于实体类使用@ApiModelProperty时,example属性没有赋值导致的,在AbstractSerializableParameter的getExample方法中会将数值属性的example的转换数值类返回,example的默认值是””,因此当example没有赋值时,会出现上面的异常。

解决办法:排除swagger2的annotations和models依赖,然后再引入1.5.21版本的annotations和models依赖即可解决。

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
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<!--排除swagger2的annotations和models依赖,然后再引入1.5.21版本的annotations和models依赖-->
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--引入1.5.21版本的annotations和models依赖-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>

[14] JPA实体类不能同时使用多个 fetch = FetchType.EAGER 的问题

错误情景:org.hibernate.loader.MultipleBagFetchException:不能同时获取多个包

1
2
3
4
5
6
7
@OneToMany(mappedBy = "consultaSQL", orphanRemoval = true, fetch = FetchType.EAGER,
cascade = CascadeType.ALL)
private List<ParametroSQL> parametros;

@OneToMany(mappedBy = "consulta", orphanRemoval = true, fetch = FetchType.EAGER,
cascade = CascadeType.ALL)
private List<Contato> contatos;

解决办法:改成 @LazyCollection(LazyCollectionOption.FALSE) 即可

1
2
3
4
5
6
7
8
9
@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(mappedBy = "consultaSQL", orphanRemoval = true,
cascade = CascadeType.ALL)
private List<ParametroSQL> parametros;

@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(mappedBy = "consulta", orphanRemoval = true,
cascade = CascadeType.ALL)
private List<Contato> contatos;

[15] springboot自动注入出现Consider defining a bean of type ‘xxx‘ in your configuration问题

解决办法:将接口与对应的实现类放在与application启动类的同一个目录或者他的子目录下,这样注解就可以被扫描到了。

[16] springboot启动报错“java:程序包XXX不存在”,但实际上程序包都存在

解决办法:编译问题导致的,把 target 包删了,再重新运行即可。

[17] Elasticsearch日期格式的存储与查询问题

问题1:实体类声明是以Date形式存储,但存入ES库却变成了Long类型。

解决办法:在实体类上添加@JSONField(format=”yyyy-MM-dd HH:mm:ss”)即可解决。

1
2
3
@ApiModelProperty(value = "采集时间")
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date collectionTime;

问题2:时间范围查询没有得到预期结果(原因1是ES默认存储的是UTC时间,比北京时间晚8h;原因2是因为被分词处理了)

解决办法:原因1建议直接传入String类型的时间进行查询,原因2精确匹配要加上.keyword(不加是对查询内容做分词处理之后再匹配,加了是直接对查询结果做精确匹配)

1
2
3
4
5
6
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
if(beginTime != null && endTime != null) {
boolQueryBuilder.filter(QueryBuilders.rangeQuery("collectionTime.keyword")
.gte(beginTimeStr)
.lte(endTimeStr);
}

[18] 项目启动时Cannot find template location(s): [classpath:/templates/] (please add some templates, check your FreeMarker configuration, or set spring.freemarker.checkTemplateLocation=false) 警告

application.properties添加如下配置,即可隐藏此警告。

1
spring.freemarker.checkTemplateLocation=false

[19] 项目启动时警告Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used

在config目录下添加如下配置文件即可解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import io.undertow.server.DefaultByteBufferPool;
import io.undertow.websockets.jsr.WebSocketDeploymentInfo;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

/**
* 解决启动时警告 Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
*/
@Component
public class CustomizationBean implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
@Override
public void customize(UndertowServletWebServerFactory factory) {
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(false, 1024));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);
});
}
}

[20] 编译项目时出现“sun.misc.BASE64Decoder是内部专用 API, 可能会在未来发行版中删除”的警告

将如下两行代码替换成最后一行的写法,替代sun.misc.BASE64Decoder的API即可。

1
2
3
// BASE64Decoder decoder = new BASE64Decoder();
// byte[] bytes = decoder.decodeBuffer(base64str);
byte[] bytes = Base64.decodeBase64(base64str);

4. 常用的Java工具类及代码示例

4.1 常用工具类

4.1.2 检查指定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
/**
* 检查指定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.1.2 生成指定位数的数字标号

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.1.3 按照指定长度拆分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.1.4 检查字符串中是否包含某子串

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;
}

4.1.5 按照value对Map排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 按照value对Map排序
* @param map
* @param isAsc 是否升序排序
* @return ArrayList 有序存储着Map的元素
*/
public static ArrayList<Map.Entry<String, Double>> sortMapByValue(Map<String, Double> map, final boolean isAsc) {
//把Map转换为List
List<Map.Entry<String, Double>> entries = new ArrayList<>(
map.entrySet());
//利用Collections.sort进行排序
Collections.sort(entries, new Comparator<Map.Entry<String, Double>>() {
@Override
public int compare(Map.Entry<String, Double> obj1,
Map.Entry<String, Double> obj2) {
if(isAsc) {
return obj1.getValue().compareTo(obj2.getValue());
} else {
return obj2.getValue().compareTo(obj1.getValue());
}
}
});
return (ArrayList<Map.Entry<String, Double>>) entries;
}

4.1.6 本地和网络图片与Base64互转

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
96
97
98
99
100
101
102
103
104
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;

public class ImageToBase64Utils {

private static String strNetImageToBase64;
private static String strLocalImageToBase64;

/**
* 网络图片转换Base64的方法
* @param netImagePath
* @return
*/
public static String networkImageToBase64(String netImagePath) {
final ByteArrayOutputStream data = new ByteArrayOutputStream();
try {
// 创建URL
URL url = new URL(netImagePath);
final byte[] by = new byte[1024];
// 创建链接
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(100);
InputStream is = conn.getInputStream();
// 将内容读取内存中
int len = -1;
while ((len = is.read(by)) != -1) {
data.write(by, 0, len);
}
// 对字节数组Base64编码
strNetImageToBase64 = new String(Base64.encodeBase64(data.toByteArray()));
System.out.println("网络图片转换Base64:" + strNetImageToBase64);
// 关闭流
is.close();

} catch (IOException e) {
e.printStackTrace();
}
return strNetImageToBase64;
}

/**
* 本地图片转换Base64的方法
* @param imgPath
* @return
*/
public static String localImageToBase64(String imgPath) {
byte[] data = null;
// 读取图片字节数组
try {
InputStream in = Files.newInputStream(Paths.get(imgPath));
data = new byte[in.available()];
in.read(data);
in.close();
} catch (IOException e) {
e.printStackTrace();
}
strLocalImageToBase64 = new String(Base64.encodeBase64(Objects.requireNonNull(data)));
// 返回Base64编码过的字节数组字符串
System.out.println("本地图片转换Base64:" + strLocalImageToBase64);
return strLocalImageToBase64;
}


/**
* base64字符串转换成图片
*
* @param imgStr base64字符串
* @param imgFilePath 图片存放路径
* @return
*/
public static boolean Base64ToImage(String imgStr, String imgFilePath) {

// 图像数据判空校验
if (StringUtils.isEmpty(imgStr)){
return false;
}
try {
// Base64解码
byte[] b =Base64.decodeBase64(imgStr);
for (int i = 0; i < b.length; ++i) {
// 调整异常数据
if (b[i] < 0) {
b[i] += 256;
}
}
OutputStream out = Files.newOutputStream(Paths.get(imgFilePath));
out.write(b);
out.flush();
out.close();
return true;
} catch (Exception e) {
return false;
}
}

}

4.1.7 将网络文件下载到本地的指定文件夹下

依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

代码:

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
/**
* 将网络文件下载到本地的指定文件夹下
* @param url
* @param directory
* @param fileName
*/
public static void downloadFileToLocal(String url, String directory, String fileName) {
URL source;
try {
source = new URL(url);
File destination = FileUtils.getFile(directory, fileName);
FileUtils.copyURLToFile(source, destination);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 创建目录
* @param destDirName
* @return
*/
public static boolean createDir(String destDirName) {
File dir = new File(destDirName);
if (dir.exists()) {
System.out.println("创建目录" + destDirName + "失败,目标目录已经存在");
return false;
}
if (!destDirName.endsWith(File.separator)) {
destDirName = destDirName + File.separator;
}
//创建目录
if (dir.mkdirs()) {
System.out.println("创建目录" + destDirName + "成功!");
return true;
} else {
System.out.println("创建目录" + destDirName + "失败!");
return false;
}
}

4.1.8 数学计算工具类

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import java.math.BigDecimal;

/**
* 数学计算工具类
*/
public class MathUtils {

// 默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
// PI的取值精度
public static double PI = 3.1415926535897932384626;

/**
* 提供精确的加法运算
* @param v1 被加数
* @param v2 加数
* @return v1加v2
*/
public static double add(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}

/**
* 提供精确的减法运算
* @param v1
* @param v2
* @return v1减v2
*/
public static double sub(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2).doubleValue();
}

/**
* 提供精确的乘法运算
* @param v1
* @param v2
* @return v1乘v2
*/
public static double mul(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2).doubleValue();
}

/**
* 提供相对精确的除法运算,当发生除不尽的情况,精确到小数点后10位
* @param v1
* @param v2
* @return v1除v2
*/
public static double div(double v1, double v2) {
if(DEF_DIV_SCALE < 0){
throw new IllegalArgumentException(" the scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, DEF_DIV_SCALE, BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 计算两点之间的距离
* @param point1
* @param point2
* @return
*/
public static double distanceToPoint(double[] point1, double[] point2) {
double x1 = point1[0];
double y1 = point1[1];
double x2 = point2[0];
double y2 = point2[1];
double lineLength = 0;
lineLength = Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
return lineLength;
}

/**
* 点到直线的最短距离的判断
* @param start (x1,y1)
* @param end (x2,y2)
* @param point (x0,y0)
* @return
*/
public static double distanceToSegment(double[] start, double[] end, double[] point) {
double space = 0;
double a, b, c;
a = distanceToPoint(start, end); // 线段的长度
b = distanceToPoint(start, point); // (x1,y1)到点的距离
c = distanceToPoint(end, point); // (x2,y2)到点的距离
if (c+b == a) { //点在线段上
space = 0;
return space;
}
if (a <= 0.000001) { //不是线段,是一个点
space = b;
return space;
}
if (c * c >= a * a + b * b) { //组成直角三角形或钝角三角形,(x1,y1)为直角或钝角
space = b;
return space;
}
if (b * b >= a * a + c * c) { //组成直角三角形或钝角三角形,(x2,y2)为直角或钝角
space = c;
return space;
}
// 组成锐角三角形,则求三角形的高
double p = (a + b + c) / 2; // 半周长
double s = Math.sqrt(p * (p - a) * (p - b) * (p - c)); // 海伦公式求面积
space = 2 * s / a; // 返回点到线的距离(利用三角形面积公式求高)
return space;
}

public static void main(String[] args) {
double[] start = {0,2};
double[] end = {2,0};
double[] point = {0,0};
System.out.println(distanceToSegment(start, end, point));
}

}

4.1.9 日期时间获取转换工具类

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
import cn.hutool.core.date.DateUtil;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

/**
* 日期时间工具类
*/
public class DateTimeUtils{

private static final List<String> FORMATS = new ArrayList<>();

static {
FORMATS.add("yyyy-MM-dd HH:mm:ss");
FORMATS.add("yyyy-MM-dd");
FORMATS.add("yyyy-MM-dd HH:mm");
FORMATS.add("yyyy-MM");
FORMATS.add("yyyy/MM/dd HH:mm:ss");
FORMATS.add("yyyy/MM/dd");
FORMATS.add("yyyy/MM/dd HH:mm");
FORMATS.add("yyyy/MM");
FORMATS.add("yyyyMMdd");
FORMATS.add("yyyyMM");
FORMATS.add("yyyy-MM-dd HH:mm:ss.SSS");
FORMATS.add("yyyy/MM/dd HH:mm:ss.SSS");
}

/**
* 将字符串按指定格式解析成日期
* @param dateStr
* @param format
* @return
*/
public static Date parseDate(String dateStr, String format) {
Date date = null;
try {
DateFormat dateFormat = new SimpleDateFormat(format);
date = dateFormat.parse(dateStr);
} catch (Exception ex) {
ex.printStackTrace();
}
return date;
}

/**
* 将日期按指定格式解析成字符串
* @param date
* @param format
* @return
*/
public static String parseStr(Date date, String format) {
String dateStr = null;
try {
DateFormat dateFormat = new SimpleDateFormat(format);
dateStr = dateFormat.format(date);
} catch (Exception ex) {
ex.printStackTrace();
}
return dateStr;
}

/**
* 自动将字符串解析成日期
* @param source
* @return
*/
public static Date autoParseDate(String source) {
if (source == null) {
return null;
}
String dateStr = source.trim();
if (dateStr.isEmpty()) {
return null;
}
if (dateStr.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(0));
}
if (dateStr.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(1));
}
if (dateStr.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(2));
}
if (dateStr.matches("^\\d{4}-\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(3));
}
if (dateStr.matches("^\\d{4}/\\d{1,2}/\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(4));
}
if (dateStr.matches("^\\d{4}/\\d{1,2}/\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(5));
}
if (dateStr.matches("^\\d{4}/\\d{1,2}/\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(6));
}
if (dateStr.matches("^\\d{4}/\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(7));
}
if (dateStr.matches("^\\d{4}\\d{1,2}\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(8));
}
if (dateStr.matches("^\\d{4}\\d{1,2}$")) {
return parseDate(dateStr, FORMATS.get(9));
}
if (dateStr.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}.\\d{1,3}$")) {
return parseDate(dateStr, FORMATS.get(10));
}
if (dateStr.matches("^\\d{4}/\\d{1,2}/\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}.\\d{1,3}$")) {
return parseDate(dateStr, FORMATS.get(4));
}
try {
long timestamp = Long.parseLong(dateStr);
return new Date(timestamp);
} catch (Exception ex) {
return null;
}
}

/**
* 指定年份的当前日期
* @param date
* @param yearNumber
* @return
*/
public static Date setYear(Date date, Integer yearNumber) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.YEAR, yearNumber);
return calendar.getTime();
}

/**
* 获取当年的开始时间
* @param date
* @return
*/
public static Date getYearStart(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_YEAR, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
return calendar.getTime();
}

/**
* 获取当年的结束时间
* @param date
* @return
*/
public static Date getYearEnd(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.MONTH, 11);
calendar.set(Calendar.DAY_OF_MONTH, 31);
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 29);
return calendar.getTime();
}

/**
* 获取今天零时
* @return
*/
public static Date todayBegin() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
return calendar.getTime();
}

/**
* 获取今天中午12时
* @return
*/
public static Date todayNoon() {
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.set(Calendar.HOUR_OF_DAY, 12);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
return calendar.getTime();
}

/**
* 获取指定日期零时
* @param date
* @return
*/
public static Date dayBegin(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
return calendar.getTime();
}

/**
* 获取指定日期23时59分29秒
* @param date
* @return
*/
public static Date dayEnd(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
return calendar.getTime();
}

/**
* 获取明天
* @param date
* @return
*/
public static Date tomorrow(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, 1);
return calendar.getTime();
}

/**
* 获取一天前
* @param date
* @return
*/
public static Date yesterday(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, -1);
return calendar.getTime();
}

/**
* 获取相隔指定秒的日期
* @param date
* @param seconds
* @return
*/
public static Date secondAdd(Date date, Integer seconds) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.SECOND, seconds);
return calendar.getTime();
}

/**
* 获取相隔指定分钟的日期
* @param date
* @param minutes
* @return
*/
public static Date minuteAdd(Date date, Integer minutes) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, minutes);
return calendar.getTime();
}

/**
* 获取相隔指定小时的日期
* @param date
* @param hours
* @return
*/
public static Date hourAdd(Date date, Integer hours) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.HOUR, hours);
return calendar.getTime();
}

/**
* 获取相隔指定天数的日期
* @param date
* @param days
* @return
*/
public static Date dayAdd(Date date, Integer days) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DATE, days);
return calendar.getTime();
}

/**
* 获取相隔指定月数的日期
* @param date
* @param months
* @return
*/
public static Date monthAdd(Date date, Integer months) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MONTH, months);
return calendar.getTime();
}

/**
* 获取相隔指定年数的日期
* @param date
* @param years
* @return
*/
public static Date yearAdd(Date date, Integer years) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.YEAR, years);
return calendar.getTime();
}

/**
* 获取一周前
* @param date
* @return
*/
public static Date lastWeek(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.WEEK_OF_YEAR, -1);
return calendar.getTime();
}

/**
* 获取一个月前
* @param date
* @return
*/
public static Date lastMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MONTH, -1);
return calendar.getTime();
}

/**
* 获取季度
* @param date
* @return
*/
public static Integer getSeason(Date date) {
Calendar calendar = Calendar.getInstance();
int season = 0;
calendar.setTime(date);
int month = calendar.get(Calendar.MONTH);
switch (month) {
case Calendar.JANUARY:
case Calendar.FEBRUARY:
case Calendar.MARCH:
season = 1;
break;
case Calendar.APRIL:
case Calendar.MAY:
case Calendar.JUNE:
season = 2;
break;
case Calendar.JULY:
case Calendar.AUGUST:
case Calendar.SEPTEMBER:
season = 3;
break;
case Calendar.OCTOBER:
case Calendar.NOVEMBER:
case Calendar.DECEMBER:
season = 4;
break;
default:
break;
}
return season;
}

/**
* 计算时间差
* @param date1
* @param date2
* @return
*/
public static String getDiffTime(Date date1, Date date2, String formatType){

Long milliseconds = date1.getTime() - date2.getTime(); // 相差的毫秒值
long nd = 1000 * 24 * 60 * 60;// 一天的毫秒数
long nh = 1000 * 60 * 60;// 一小时的毫秒数
long nm = 1000 * 60;// 一分钟的毫秒数
long ns = 1000;// 一秒钟的毫秒数

long day = milliseconds / nd; // 计算相差多少天
long hour = milliseconds % nd / nh; // 计算相差剩余多少小时
long min = milliseconds % nd % nh / nm; // 计算相差剩余多少分钟
long sec = milliseconds % nd % nh % nm / ns; // 计算相差剩余多少秒
long hourAll = milliseconds / nh; // 计算相差多少小时
long min2 = milliseconds / nm; // 计算相差多少分钟

String diffTime = null;
if(Objects.equals(formatType, "day")){
diffTime = day + "天" + hour + "小时" + min + "分钟" + sec + "秒";
}else if(Objects.equals(formatType, "hour")){
diffTime = hourAll + "小时" + min + "分钟" + sec + "秒";
}else if(Objects.equals(formatType, "minute")){
diffTime = min2 + "分钟" + sec + "秒";
}

return diffTime;
}

/**
* 获取默认统计时间间隔,用于mysql
* @param beginTime
* @param endTime
* @return
*/
public static String getDefaultCountInterval(Date beginTime, Date endTime) {
Calendar calendar = Calendar.getInstance();
//默认以天为单位统计
String interval = "%Y-%m%-%d";
if (beginTime == null || endTime == null) {
return interval;
}
Date tempTime;
//如果时间间隔大于4周,以周为单位统计
calendar.setTime(beginTime);
calendar.add(Calendar.WEEK_OF_YEAR, 4);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "%X年第%V周";
}
//如果时间间隔大于6个月,以月为单位统计
calendar.setTime(beginTime);
calendar.add(Calendar.MONTH, 6);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "%Y年%c月";
}
//如果时间间隔大于3年,以年为单位统计
calendar.setTime(beginTime);
calendar.add(Calendar.YEAR, 3);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "%Y年";
}
return interval;
}

/**
* 自动判定聚合interval,用于es
* @param beginTime
* @param endTime
* @return
*/
public static String getDefaultAggregationInterval(Date beginTime, Date endTime) {
Calendar calendar = Calendar.getInstance();
String interval = "year";
if (beginTime == null || endTime == null) {
return interval;
}
Date tempTime;
//如果时间间隔大于3天
calendar.setTime(beginTime);
calendar.add(Calendar.DAY_OF_YEAR, 3);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "day";
}
//如果时间间隔大于3周
calendar.setTime(beginTime);
calendar.add(Calendar.WEEK_OF_YEAR, 3);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "week";
}
//如果时间间隔大于3个月
calendar.setTime(beginTime);
calendar.add(Calendar.MONTH, 3);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "month";
}
//如果时间间隔大于12个月
calendar.setTime(beginTime);
calendar.add(Calendar.MONTH, 12);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "quarter";
}
//如果时间间隔大于6年
calendar.setTime(beginTime);
calendar.add(Calendar.YEAR, 5);
tempTime = calendar.getTime();
if (tempTime.getTime() < endTime.getTime()) {
interval = "year";
}
return interval;
}

public static void main(String[] args) {

// 获取当前时间 2022-02-15 09:55:01
Date date = DateUtil.date();
System.out.println(date);

// 获取当前时间 Tue Feb 15 09:43:55 CST 2022
Date dateCST = new Date();
System.out.println(dateCST);

// 获取当前时间戳 1644890101230
Long timestamp = Calendar.getInstance().getTimeInMillis();
System.out.println(timestamp);

// 将时间戳转成字符串 2022-02-15 09:55:01
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String timestampStr = format.format(timestamp);
System.out.println(timestampStr);

// 将字符串按指定格式解析成日期 Tue Feb 15 09:43:55 CST 2022
System.out.println(parseDate("2022-02-15 09:43:55","yyyy-MM-dd HH:mm:ss"));

// 将日期按指定格式解析成字符串 2022-02-15 09:55:01
System.out.println(parseStr(date,"yyyy-MM-dd HH:mm:ss"));

// 自动将字符串解析成日期 Tue Feb 15 09:43:55 CST 2022
System.out.println(autoParseDate("2022-02-15 09:43:55"));

// 指定年份的当前日期 Tue Feb 15 09:43:55 CST 2022
System.out.println(setYear(date,2022));

// 获取当年开始时间 Sat Jan 01 00:00:00 CST 2022
System.out.println(getYearStart(date));

// 获取当年结束时间 Sat Dec 31 23:59:29 CST 2022
System.out.println(getYearEnd(date));

// 获取当日零时 Tue Feb 15 00:00:00 CST 2022
System.out.println(todayBegin());

// 获取当日12时 Tue Feb 15 12:00:00 CST 2022
System.out.println(todayNoon());

// 获取指定日期0时 Tue Feb 15 00:00:00 CST 2022
System.out.println(dayBegin(date));

// 获取指定日期23时59分29秒 Tue Feb 15 23:59:59 CST 2022
System.out.println(dayEnd(date));

// 获取明天 Wed Feb 16 09:55:01 CST 2022
System.out.println(tomorrow(date));

// 获取一天前 Mon Feb 14 09:55:01 CST 2022
System.out.println(yesterday(date));

// 获取相隔指定分钟的日期 Mon Feb 15 09:56:01 CST 2022
System.out.println(minuteAdd(date,1));

// 获取相隔指定小时的日期 Mon Feb 15 10:55:01 CST 2022
System.out.println(hourAdd(date,1));

// 获取相隔指定天数的日期 Wed Feb 16 09:55:01 CST 2022
System.out.println(dayAdd(date,1));

// 获取相隔指定月数的日期 Sat Jan 15 09:55:01 CST 2022
System.out.println(monthAdd(date,-1));

// 获取相隔指定年数的日期 Tue Feb 15 09:55:01 CST 2022
System.out.println(yearAdd(date,0));

// 获取一周前 Tue Feb 08 09:55:01 CST 2022
System.out.println(lastWeek(date));

// 获取一月前 Sat Jan 15 09:55:01 CST 2022
System.out.println(lastMonth(date));

// 获取季度 1
System.out.println(getSeason(date));

// 获取时间差 2天1小时10分钟1秒
System.out.println(getDiffTime(dateCST, parseDate("2022-02-13 08:33:54","yyyy-MM-dd HH:mm:ss"), "day"));

// 获取默认统计时间间隔,用于mysql %X年第%V周
System.out.println(getDefaultCountInterval(date,dayAdd(date,100)));

// 自动判定聚合interval,用于es month
System.out.println(getDefaultAggregationInterval(date,dayAdd(date,100)));

}
}

4.1.10 截取指定长度的字符串

1
2
3
4
5
6
7
8
9
10
/**
* 截取指定长度的字符串
*
* @param str 原字符串
* @param len 长度
* @return 如果str为null,则返回null;如果str长度小于len,则返回str;如果str的长度大于len,则返回截取后的字符串
*/
public static String subStrByStrAndLen(String str, int len) {
return null != str ? str.substring(0, str.length() > len ? len : str.length()) : null;
}

4.1.11 MD5通用工具类

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
import org.apache.commons.codec.digest.DigestUtils;

/**
* MD5通用类
*/
public class Md5Util {

/**
* MD5方法
*
* @param text 明文
* @param key 密钥
* @return 密文
* @throws Exception
*/
public static String md5(String text, String key) throws Exception {
// 加密后的字符串
String encodeStr = DigestUtils.md5Hex(text + key);
return encodeStr;
}

/**
* MD5验证方法
*
* @param text 明文
* @param key 密钥
* @param md5 密文
* @return true/false
* @throws Exception
*/
public static boolean verify(String text, String key, String md5) throws Exception {
// 根据传入的密钥进行验证
String md5Text = md5(text, key);
if (md5Text.equalsIgnoreCase(md5)) {
System.out.println("MD5验证通过");
return true;
}
return false;
}

}

4.1.12 AES加密解密工具类

AES加密为最常见的对称加密算法(微信小程序的加密传输就是用的这个加密算法)。对称加密算法也就是加密和解密用相同的密钥,具体的加密流程如下图:

AES加密传输流程

AES加密解密工具类代码如下:

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.Base64;

/**
* AES加密工具类
*/
public class AesUtility {

/**
* 偏移量 AES 为16bytes. DES
*/
public static final String VIPARA = "0845762876543456";

/**
* 编码方式
*/
public static final String CODE_TYPE = "UTF-8";

/**
* 填充类型
*/
public static final String AES_TYPE = "AES/ECB/PKCS5Padding";

/**
* 字符补全
*/
private static final String[] CONSULT = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B",
"C", "D", "E", "F", "G"};

/**
* AES加密
*
* @param cleartext 明文
* @param aesKey 密钥
* @return 密文
*/
public static String encrypt(String cleartext, String aesKey) {
// 加密方式: AES128(CBC/PKCS5Padding) + Base64
try {
if ("AES/ECB/NoPadding".equals(AES_TYPE)) {
cleartext = completionCodeFor16Bytes(cleartext);
}
aesKey = md5(aesKey);
System.out.println(aesKey);
// 两个参数,第一个为私钥字节数组, 第二个为加密方式 AES或者DES
SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(), "AES");
// 实例化加密类,参数为加密方式,要写全
// PKCS5Padding比PKCS7Padding效率高,PKCS7Padding可支持IOS加解密
Cipher cipher = Cipher.getInstance(AES_TYPE);
// 初始化,此方法可以采用三种方式,按加密算法要求来添加。
// (1)无第三个参数
// (2)第三个参数为SecureRandom random = new SecureRandom();中random对象,随机数。(AES不可采用这种方法)
// (3)采用此代码中的IVParameterSpec 加密时使用:ENCRYPT_MODE; 解密时使用:DECRYPT_MODE;
// CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
cipher.init(Cipher.ENCRYPT_MODE, key);
// 加密操作,返回加密后的字节数组,然后需要编码。主要编解码方式有Base64, HEX,
// UUE,7bit等等。此处看服务器需要什么编码方式
byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
Base64.Encoder encoder = Base64.getMimeEncoder();
return encoder.encodeToString(encryptedData);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 解密
*
* @param encrypted
* @return
*/
public static String decrypt(String encrypted, String aesKey) {
try {
aesKey = md5(aesKey);
Base64.Decoder decoder = Base64.getMimeDecoder();
byte[] byteMi = decoder.decode(encrypted);
SecretKeySpec key = new SecretKeySpec(aesKey.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(AES_TYPE);
// 与加密时不同MODE:Cipher.DECRYPT_MODE
// CBC类型的可以在第三个参数传递偏移量zeroIv,ECB没有偏移量
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decryptedData = cipher.doFinal(byteMi);
String content = new String(decryptedData, CODE_TYPE);
if ("AES/ECB/NoPadding".equals(AES_TYPE)) {
content = resumeCodeOf16Bytes(content);
}
return content;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

/**
* 补全字符
* @param str
* @return
* @throws UnsupportedEncodingException
*/
public static String completionCodeFor16Bytes(String str) throws UnsupportedEncodingException {
int num = str.getBytes(CODE_TYPE).length;
int index = num % 16;
// 进行加密内容补全操作, 加密内容应该为 16字节的倍数, 当不足16*n字节是进行补全, 差一位时 补全16+1位
// 补全字符 以 $ 开始,$后一位代表$后补全字符位数,之后全部以0进行补全;
if (index != 0) {
StringBuilder stringBuilder = new StringBuilder(str);
if (16 - index == 1) {
stringBuilder.append("$").append(CONSULT[16 - 1]).append(addStr(16 - 1 - 1));
} else {
stringBuilder.append("$").append(CONSULT[16 - index - 1]).append(addStr(16 - index - 1 - 1));
}
str = stringBuilder.toString();
}
return str;
}

/**
* 追加字符
* @param num
* @return
*/
public static String addStr(int num) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < num; i++) {
stringBuilder.append("0");
}
return stringBuilder.toString();
}

/**
* 还原字符(进行字符判断)
* @param str
* @return
*/
public static String resumeCodeOf16Bytes(String str) {
int indexOf = str.lastIndexOf("$");
if (indexOf == -1) {
return str;
}
String trim = str.substring(indexOf + 1, indexOf + 2).trim();
int num = 0;
for (int i = 0; i < CONSULT.length; i++) {
if (trim.equals(CONSULT[i])) {
num = i;
}
}
if (num == 0) {
return str;
}
return str.substring(0, indexOf).trim();
}

/**
* md5
* @param dateString
* @return
* @throws Exception
*/
public static String md5(String dateString) throws Exception {
byte[] digest = MessageDigest.getInstance("md5").digest(dateString.getBytes(CODE_TYPE));
StringBuilder md5code = new StringBuilder(new BigInteger(1, digest).toString(16));
// 如果生成数字未满32位,需要前面补0
for (int i = 0; i < 32 - md5code.length(); i++) {
md5code.insert(0, "0");
}
return md5code.toString();
}

public static String sampleEncrypt(String clearText, String aesKey) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
Cipher cipher = Cipher.getInstance(AES_TYPE);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(aesKey.getBytes(CODE_TYPE), "AES"));
byte[] b = cipher.doFinal(clearText.getBytes(CODE_TYPE));
return Base64.getMimeEncoder().encodeToString(b);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}

public static String sampleDecrypt(String encrypted, String aesKey) {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128);
Cipher cipher = Cipher.getInstance(AES_TYPE);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey.getBytes(CODE_TYPE), "AES"));
byte[] b = cipher.doFinal(Base64.getMimeDecoder().decode(encrypted));
return new String(b, CODE_TYPE);
} catch (Exception ex) {
ex.printStackTrace();
return null;
}
}

public static void main(String[] args) {

// AES加密算法
String encrypt = sampleEncrypt("{\"username\":\"admin\",\"password\":\"12345678\"}", "bbbvccc22cabdcbaf399dffff48604fv");
System.out.println("result:" + encrypt);
String decrypt = sampleDecrypt("/C3gUT4IKEBdf70O7sjqupwZSRhNKiArQAynSu3+LoCKmiFIb963ZIV2QlbzuQWW", SessionHolder.getWebAesKey());
System.out.println(decrypt);
}

}

4.1.13 RSA加密解密工具类

RSA加密算法是一种非对称加密算法,所谓非对称,就是指该算法加密和解密使用不同的密钥,即使用加密密钥进行加密、解密密钥进行解密。在RAS算法中,加密密钥PK是公开信息,而解密密钥SK是需要保密的。加密算法E和解密算法D也都是公开的。虽然解密密钥SK是由公开密钥PK决定的,由于无法计算出大数n的欧拉函数phi(N),所以不能根据PK计算出SK。

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import org.apache.commons.codec.binary.Base64;

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
* RSA工具类
*/
public class RsaUtility {

/**
* 使用公钥字符串加密
*
* @param plainText 明文
* @param publicKeyString 公钥字符串
* @return 密文
* @throws Exception 异常
*/
public static String encryptByPublicString(String plainText, String publicKeyString) throws Exception {
PublicKey publicKey = getPublicKeyByString(publicKeyString);
return encrypt(plainText, publicKey);
}

/**
* 使用私钥字符串解密
*
* @param cipherText 密文
* @param privateKeyString 私钥字符串
* @return 明文
* @throws Exception 异常
*/
public static String decryptByPrivateString(String cipherText, String privateKeyString) throws Exception {
PrivateKey privateKey = getPrivateKeyByString(privateKeyString);
return decrypt(cipherText, privateKey);
}

/**
* 使用私钥字符串签名
*
* @param plainText 明文
* @param privateKeyString 私钥字符串
* @return 签名
* @throws Exception 异常
*/
public static String signByPrivateString(String plainText, String privateKeyString) throws Exception {
PrivateKey privateKey = getPrivateKeyByString(privateKeyString);
return sign(plainText, privateKey);
}

/**
* 使用公钥字符串验签
*
* @param plainText 明文
* @param signature 签名
* @param publicKeyString 公钥字符串
* @return 是否通过签名验证
* @throws Exception 异常
*/
public static boolean verifyByPublicString(String plainText, String signature, String publicKeyString) throws Exception {
PublicKey publicKey = getPublicKeyByString(publicKeyString);
return verify(plainText, signature, publicKey);
}

/**
* 生成2048位的RSA密钥对
*
* @return 密钥对
* @throws Exception 异常
*/
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048, new SecureRandom());
return generator.generateKeyPair();
}

/**
* 从RSA密钥对中获取私钥字符串
*
* @param keyPair RSA密钥对
* @return 私钥字符串
*/
public static String getPrivateKeyString(KeyPair keyPair) {
return getPrivateKeyString(keyPair.getPrivate());
}

public static String getPrivateKeyString(PrivateKey privateKey) {
return Base64.encodeBase64String(privateKey.getEncoded());
}

/**
* 从RSA密钥对中获取公钥字符串
*
* @param keyPair RSA密钥对
* @return 公钥字符串
*/
public static String getPublicKeyString(KeyPair keyPair) {
return Base64.encodeBase64String(keyPair.getPublic().getEncoded());
}

/**
* 将私钥字符串还原为私钥
*
* @param privateKeyString 私钥字符串
* @return 私钥
* @throws Exception 异常
*/
public static PrivateKey getPrivateKeyByString(String privateKeyString) throws Exception {
byte[] keyBytes = Base64.decodeBase64(privateKeyString);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}

/**
* 将公钥字符串还原为公钥
*
* @param publicKeyString 公钥字符串
* @return 公钥
* @throws Exception 异常
*/
public static PublicKey getPublicKeyByString(String publicKeyString) throws Exception {
byte[] keyBytes = Base64.decodeBase64(publicKeyString);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(keySpec);
}

/**
* RSA加密
*
* @param plainText 明文
* @param publicKey 公钥
* @return 密文
* @throws Exception 异常
*/
public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.encodeBase64String(cipherText);
}

/**
* RSA解密
*
* @param cipherText 密文
* @param privateKey 私钥
* @return 明文
* @throws Exception 异常
*/
public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception {
byte[] bytes = Base64.decodeBase64(cipherText);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(cipher.doFinal(bytes), StandardCharsets.UTF_8);
}

/**
* RSA签名
*
* @param plainText 明文
* @param privateKey 私钥
* @return 签名
* @throws Exception 异常
*/
public static String sign(String plainText, PrivateKey privateKey) throws Exception {
Signature privateSignature = Signature.getInstance("SHA256withRSA");
privateSignature.initSign(privateKey);
privateSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
byte[] signedBytes = privateSignature.sign();
return Base64.encodeBase64String(signedBytes);
}

/**
* RSA验签
*
* @param plainText 明文
* @param signature 签名
* @param publicKey 公钥
* @return 是否通过验证
* @throws Exception 异常
*/
public static boolean verify(String plainText, String signature, PublicKey publicKey) throws Exception {
Signature publicSignature = Signature.getInstance("SHA256withRSA");
publicSignature.initVerify(publicKey);
publicSignature.update(plainText.getBytes(StandardCharsets.UTF_8));
byte[] signedBytes = Base64.decodeBase64(signature);
return publicSignature.verify(signedBytes);
}

public static void main(String[] args) throws Exception {

// 生成2048位密钥对
KeyPair keyPair = generateKeyPair();
System.out.println("=====公钥=====");
System.out.println(keyPair.getPublic());
System.out.println("=====私钥=====");
System.out.println(keyPair.getPrivate());

// RSA公钥加密
System.out.println("=====测试RSA公钥加密=====");
String testText = "测试文本";
String encryptText = encrypt(testText, keyPair.getPublic());
System.out.println(encryptText);

// RSA私钥解密
System.out.println("=====测试RSA私钥解密=====");
String decryptText = decrypt(encryptText, keyPair.getPrivate());
System.out.println(decryptText);

}

}

4.1.14 下划线与驼峰转换工具类

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
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* 驼峰字符串工具类
*/
public class CamelTool {

/**
* 下划线转驼峰
* @param underscore
* @return
*/
public static String underscoreToCamel(String underscore){
String[] ss = underscore.split("_");
if(ss.length ==1){
return underscore;
}
StringBuffer sb = new StringBuffer();
sb.append(ss[0]);
for (int i = 1; i < ss.length; i++) {
sb.append(upperFirstCase(ss[i]));
}
return sb.toString();
}

/**
* 驼峰转下划线
* @param camelCase
* @return
*/
public static String camelToUnderscore(String camelCase){
Pattern humpPattern = Pattern.compile("[A-Z]");
Matcher matcher = humpPattern.matcher(camelCase);
StringBuffer sb = new StringBuffer();
while(matcher.find()){
matcher.appendReplacement(sb, "_"+matcher.group(0).toLowerCase());
}
matcher.appendTail(sb);
return sb.toString();
}

/**
* 首字母 转小写
* @param str
* @return
*/
private static String lowerFirstCase(String str) {
char[] chars = str.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}

/**
* 首字母 转大写
* @param str
* @return
*/
private static String upperFirstCase(String str) {
char[] chars = str.toCharArray();
chars[0] -= 32;
return String.valueOf(chars);
}

public static void main(String[] args) {
// 下划线转驼峰
String camelStr = underscoreToCamel("cteate_time");
System.out.println(camelStr);
// 驼峰转下划线
String underscoreStr = camelToUnderscore("cteateTime");
System.out.println(underscoreStr);
}

}

4.1.15 将一个列表分割成多个等长度的子列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 将一个列表分割成多个等长度的子列表
* @param targ
* @param mList
* @return
*/
private static List<List<String>> getSubList(int targ, List<String> mList) {
List<List<String>> list=new ArrayList<List<String>>();
int m=mList.size()/targ;
for (int i=0;i<m;i++){
list.add (mList.subList(i * targ, i * targ +targ)) ;
}
if (mList.size()%targ!=0){
list.add (mList.subList(m * targ,mList.size() )) ;
}
return list;
}

4.1.16 Object解析转换工具类

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import java.util.*;

/**
* Object类型转换工具类
*/
public class ObjectConvertUtils {

/**
* 将Object转换成Integer
* @param object
* @return
*/
public static Integer parseInteger(Object object) {
if (object == null) {
return null;
}
try {
return Integer.parseInt(object.toString());
} catch (NumberFormatException ex) {
return null;
}
}

/**
* 将Object转换成Long
* @param object
* @return
*/
public static Long parseLong(Object object) {
if (object == null) {
return null;
}
try {
return Long.parseLong(object.toString());
} catch (NumberFormatException ex) {
return null;
}
}

/**
* 将Object转换成Long或默认值
* @param object
* @param defaultValue
* @return
*/
public static Long parseLongOrDefault(Object object, Long defaultValue) {
if (object == null) {
return defaultValue;
}
try {
return Long.parseLong(object.toString());
} catch (NumberFormatException ex) {
return defaultValue;
}
}

/**
* 将Object转换成Double
* @param object
* @return
*/
public static Double parseDouble(Object object) {
if (object == null) {
return null;
}
try {
return Double.parseDouble(object.toString());
} catch (NumberFormatException ex) {
return null;
}
}

/**
* 将Object转换成Float
* @param object
* @return
*/
public static Float parseFloat(Object object) {
if (object == null) {
return null;
}
try {
return Float.parseFloat(object.toString());
} catch (NumberFormatException ex) {
return null;
}
}

/**
* 将Object转换成Date
* @param object
* @return
*/
public static Date getAsDate(Object object) {
if (object instanceof Date) {
return (Date) object;
} else {
return null;
}
}

/**
* 将Object转换成String
* @param object
* @return
*/
public static String parseString(Object object) {
if (object == null) {
return null;
} else {
return object.toString();
}
}

/**
* 将非空Object转换成String
* @param object
* @return
*/
public static String getAsStringExceptEmpty(Object object) {
if (object != null && !object.toString().isEmpty()) {
return object.toString();
} else {
return null;
}
}

/**
* 将Object转换成List<Long>
* @param object
* @return
*/
public static List<Long> parseLongList(Object object) {
if (!(object instanceof List)) {
return null;
}
List<?> tmpList = (List<?>) object;
List<Long> longList = new ArrayList<>();
if (tmpList.size() > 0) {
try {
tmpList.forEach(v -> {
longList.add(Long.parseLong(v.toString()));
});
} catch (NumberFormatException ex) {
ex.printStackTrace();
}
}
return longList;
}

/**
* 将Object转换成List<Map<String, Object>>
* @param object
* @return
*/
public static List<Map<String, Object>> parseMapList(Object object) {
if (!(object instanceof List)) {
return null;
}
List<?> objectList = (List<?>) object;
List<Map<String, Object>> list = new ArrayList<>();
for (int i = 0; i <= objectList.size() - 1; i++) {
Object o = objectList.get(i);
if (!(o instanceof Map)) {
continue;
}
Map<?, ?> objectMap = (Map<?, ?>) o;
Map<String, Object> map = new HashMap<>();
for (Map.Entry<?, ?> entry : objectMap.entrySet()) {
map.put(entry.getKey().toString(), entry.getValue());
}
list.add(map);
}
return list;
}

/**
* 将各种类型的是否转成0或1
* @param object
* @return
*/
public static Integer parseYesOrNoToInteger(Object object) {
if (object == null) {
return null;
}
if ("1".equals(object.toString()) || "是".equals(object.toString()) || "yes".equals(object.toString().toLowerCase())) {
return 1;
}
if ("0".equals(object.toString()) || "否".equals(object.toString()) || "no".equals(object.toString().toLowerCase())) {
return 0;
}
return null;
}

}

4.1.17 发送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
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

/**
* 发送HTTP请求工具类
*/
public class HttpRequest {

private final Logger log = LoggerFactory.getLogger(HttpRequest.class);

/**
* 向指定URL发送GET方法的请求
*
* @param url 发送请求的URL
* @return URL 所代表远程资源的响应结果
*/
public static String get(String url) throws Exception {
HttpURLConnection httpURLConnection = getHttpURLConnectionForSimpleRequest(url);
httpURLConnection.setRequestMethod("GET");
// 建立实际的连接
httpURLConnection.connect();
return getHttpURLConnectionResponse(httpURLConnection);
}

/**
* 向指定 URL 发送POST方法的请求
*
* @param url 发送请求的 URL
* @param body 请求参数,请求参数应该是Json的形式。
* @return 所代表远程资源的响应结果
*/
public static String post(String url, String body) {
try {
return post(url, body, null);
} catch (Exception ex) {
return ex.getMessage();
}
}

/**
* 向指定 URL 发送POST方法的请求(Token身份验证)
*
* @param url 发送请求的 URL
* @param body 请求参数,请求参数应该是Json的形式。
* @param token 身份验证使用的token
* @return 远程资源的响应结果
* @throws Exception 异常,包括远程资源返回的异常
*/
public static String post(String url, String body, String token) throws Exception {
OutputStream outputStream = null;
byte[] writeBytes = body.getBytes(StandardCharsets.UTF_8);
String response;
try {
HttpURLConnection httpURLConnection = getHttpURLConnectionForJson(url);
// 设置文件长度
httpURLConnection.setRequestProperty("Content-Length", String.valueOf(writeBytes.length));
if (token != null && !token.isEmpty()) {
httpURLConnection.setRequestProperty("Authorization", token);
}
httpURLConnection.setRequestMethod("POST");
// 发送数据
outputStream = httpURLConnection.getOutputStream();
outputStream.write(writeBytes);
outputStream.flush();
response = getHttpURLConnectionResponse(httpURLConnection);
} catch (Exception ex) {
if (outputStream != null) {
outputStream.close();
}
throw ex;
}
return response;
}

/**
* 以form的方式发送post请求
*
* @param url 发送请求的 URL
* @param params 请求参数,以&连接的字符串
* @return 远程资源的响应结果
* @throws Exception 异常,包括远程资源返回的异常
*/
public static String postForm(String url, String params) throws Exception {
OutputStream outputStream = null;
byte[] writeBytes = params.getBytes(StandardCharsets.UTF_8);
String response;
try {
HttpURLConnection httpURLConnection = getHttpURLConnectionForSimpleRequest(url);
httpURLConnection.setRequestMethod("POST");
httpURLConnection.setDoOutput(true);
//发送数据
outputStream = httpURLConnection.getOutputStream();
outputStream.write(writeBytes);
outputStream.flush();
response = getHttpURLConnectionResponse(httpURLConnection);
} catch (Exception ex) {
if (outputStream != null) {
outputStream.close();
}
throw ex;
}
return response;
}

/**
* 向指定 URL 发送PUT方法的请求
*
* @param url 发送请求的 URL
* @param body 请求参数,请求参数应该是Json的形式。
* @return 所代表远程资源的响应结果
*/
public static String put(String url, String body) throws Exception {
OutputStream outputStream = null;
byte[] writeBytes = body.getBytes(StandardCharsets.UTF_8);
String response = "";
try {
HttpURLConnection httpURLConnection = getHttpURLConnectionForJson(url);
//设置文件长度
httpURLConnection.setRequestProperty("Content-Length", String.valueOf(writeBytes.length));
httpURLConnection.setRequestMethod("PUT");
//发送数据
outputStream = httpURLConnection.getOutputStream();
outputStream.write(writeBytes);
outputStream.flush();
response = getHttpURLConnectionResponse(httpURLConnection);
} catch (Exception ex) {
if (outputStream != null) {
outputStream.close();
}
throw ex;
}
return response;
}

/**
* 向指定URL发送DELETE方法的请求
*
* @param url 发送请求的URL
* @return URL 所代表远程资源的响应结果
*/
public static String delete(String url) throws Exception {
HttpURLConnection httpURLConnection = getHttpURLConnectionForSimpleRequest(url);
httpURLConnection.setRequestMethod("DELETE");
httpURLConnection.connect();//建立实际的连接
return getHttpURLConnectionResponse(httpURLConnection);
}

/**
* 为简单请求准备的url连接
* @param urlString
* @return
* @throws Exception
*/
private static HttpURLConnection getHttpURLConnectionForSimpleRequest(String urlString) throws Exception {
URL url = new URL(urlString);
// 打开和URL之间的连接
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
// 设置超时
httpURLConnection.setConnectTimeout(3000);
httpURLConnection.setReadTimeout(6000);
// 设置通用的请求属性
httpURLConnection.setRequestProperty("accept", "*/*");
return httpURLConnection;
}

/**
* 为application/json准备的url连接
* @param urlString
* @return
* @throws Exception
*/
private static HttpURLConnection getHttpURLConnectionForJson(String urlString) throws Exception {
URL url = new URL(urlString);
// 打开和URL之间的tcp连接
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
// 默认为false 发送post请求必须设置setDoOutput(true)
httpURLConnection.setDoOutput(true);
// 默认为true 所以不设置也可以
httpURLConnection.setDoInput(true);
// 连接超时
httpURLConnection.setConnectTimeout(3000);
// 读取超时
httpURLConnection.setReadTimeout(6000);
// 设置请求头
httpURLConnection.setRequestProperty("accept", "*/*");
// 设置请求头
httpURLConnection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
return httpURLConnection;
}

/**
* 获取response
* @param httpURLConnection
* @return
* @throws Exception
*/
private static String getHttpURLConnectionResponse(HttpURLConnection httpURLConnection) throws Exception {
StringBuilder response = new StringBuilder();
int code = httpURLConnection.getResponseCode();
InputStream inputStream;
if (code >= 200 && code < 300) {
inputStream = httpURLConnection.getInputStream();
} else {
inputStream = httpURLConnection.getErrorStream();
}
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = bufferedReader.readLine()) != null) {
response.append(line);
}
bufferedReader.close();
if (code < 400) {
return response.toString();
} else {
throw new Exception(response.toString());
}
}

}

4.1.18 请求接口上传及下载文件工具类

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;

import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
* 请求接口上传及下载文件工具类
*/
public class HttpFileApiRequestUtils {

/**
* 以POST上传文件的方式请求接口
* @param urlStr
* @param textMap
* @param fileMap
* @param contentType 没有传入文件类型默认采用application/octet-stream
* contentType非空采用filename匹配默认的图片类型
* @return 返回response数据
*/
public static String queryApiUsePostByFile(String urlStr, Map<String, String> textMap, Map<String, String> fileMap, String contentType) {

String res = "";
HttpURLConnection conn = null;
// boundary就是request头和上传文件内容的分隔符
String BOUNDARY = "---------------------------123821742118716";
try {
URL url = new URL(urlStr);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(30000);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("User-Agent","Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
conn.setRequestProperty("Content-Type","multipart/form-data; boundary=" + BOUNDARY);
OutputStream out = new DataOutputStream(conn.getOutputStream());
// text
if (textMap != null) {
StringBuffer strBuf = new StringBuffer();
Iterator iter = textMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
String inputName = (String) entry.getKey();
String inputValue = (String) entry.getValue();
if (inputValue == null) {
continue;
}
strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
strBuf.append(inputValue);
}
out.write(strBuf.toString().getBytes());
}
// file
if (fileMap != null) {
Iterator iter = fileMap.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
String inputName = (String) entry.getKey();
String inputValue = (String) entry.getValue();
if (inputValue == null) {
continue;
}
File file = new File(inputValue);
String filename = file.getName();

//没有传入文件类型,同时根据文件获取不到类型,默认采用application/octet-stream
contentType = new MimetypesFileTypeMap().getContentType(file);
//contentType非空采用filename匹配默认的图片类型
if(!"".equals(contentType)){
if (filename.endsWith(".png")) {
contentType = "image/png";
}else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg") || filename.endsWith(".jpe")) {
contentType = "image/jpeg";
}else if (filename.endsWith(".gif")) {
contentType = "image/gif";
}else if (filename.endsWith(".ico")) {
contentType = "image/image/x-icon";
}
}
if (contentType == null || "".equals(contentType)) {
contentType = "application/octet-stream";
}
StringBuffer strBuf = new StringBuffer();
strBuf.append("\r\n").append("--").append(BOUNDARY).append("\r\n");
strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"; filename=\"" + filename + "\"\r\n");
strBuf.append("Content-Type:" + contentType + "\r\n\r\n");
out.write(strBuf.toString().getBytes());
DataInputStream in = new DataInputStream(new FileInputStream(file));
int bytes = 0;
byte[] bufferOut = new byte[1024];
while ((bytes = in.read(bufferOut)) != -1) {
out.write(bufferOut, 0, bytes);
}
in.close();
}
}
byte[] endData = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();
out.write(endData);
out.flush();
out.close();
// 读取返回数据
StringBuffer strBuf = new StringBuffer();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line = null;
while ((line = reader.readLine()) != null) {
strBuf.append(line).append("\n");
}
res = strBuf.toString();
reader.close();
reader = null;
} catch (Exception e) {
System.out.println("发送POST请求出错。" + urlStr);
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
conn = null;
}
}
return res;
}

/**
* 以POST方式请求接口并返回文件下载到指定目录
* @param url
* @param bodyJson
* @param downloadPath
* @param extName
*/
public static void queryApiUsePostReturnFile(String url, String bodyJson, String downloadPath, String extName) {
FileOutputStream out = null;
String resultStr = HttpUtil.post(url, bodyJson);
InputStream inputStream = new ByteArrayInputStream(resultStr.getBytes());
try {
// 转化为byte数组
byte[] data = getByteData(inputStream);
// 建立文件存储目录
File file = new File(downloadPath + "/" + DateUtil.formatDate(new Date()));
if (!file.exists()) {
file.mkdirs();
}
// 修改文件名(文件名不能是中文)
String fileName = IdUtil.simpleUUID() + extName;
File res = new File(file + File.separator + fileName);
// 写入输出流
out = new FileOutputStream(res);
out.write(data);
}catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭输出流
try {
if (null != out){
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 从输入流中获取字节数组
* @param in
* @return
* @throws IOException
*/
private static byte[] getByteData(InputStream in) throws IOException {
byte[] b = new byte[1024];
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
while ((len = in.read(b)) != -1) {
bos.write(b, 0, len);
}
if(null!=bos){
bos.close();
}
return bos.toByteArray();
}

public static void main(String[] args) throws IOException {

// 测试1:以POST上传文件的方式请求接口
String url1 = "API URL";
String filePath = "本地图片路径";
Map<String, String> textMap = new HashMap<>();
// 设置form入参(可选)
textMap.put("param", "test");
// 设置文件路径
Map<String, String> fileMap = new HashMap<>();
fileMap.put("image", filePath);
String contentType = "";
String ret = queryApiUsePostByFile(url1, textMap, fileMap, contentType);
System.out.println(ret);

// 测试2:以POST方式请求接口并返回文件下载到指定目录
String url2 = "API URL";
String downloadPath = "./downloads";
Map<String, Object> body = new HashMap<>();
body.put("param", "test");
String bodyJson = JSON.toJSONString(body);
String extName = ".pdf";
queryApiUsePostReturnFile(url2, bodyJson, downloadPath, extName);

}


}

4.1.19 将base64转成MultipartFile类型

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
import org.apache.commons.codec.binary.Base64;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;


/**
* base64转MultipartFile的工具类
*/
public class Base64DecodedMultipartFileUtils implements MultipartFile {

private final byte[] imgContent;
private final String header;

public Base64DecodedMultipartFileUtils(byte[] imgContent, String header) {
this.imgContent = imgContent;
this.header = header.split(";")[0];
}

@Override
public String getName() {
return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
}

@Override
public String getOriginalFilename() {
return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
}

@Override
public String getContentType() {
return header.split(":")[1];
}

@Override
public boolean isEmpty() {
return imgContent == null || imgContent.length == 0;
}

@Override
public long getSize() {
return imgContent.length;
}

@Override
public byte[] getBytes() throws IOException {
return imgContent;
}

@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(imgContent);
}

@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
new FileOutputStream(dest).write(imgContent);
}

public static MultipartFile base64ToMultipart(String base64) {
try {
String[] baseStrs = base64.split(",");
byte[] b = Base64.decodeBase64(base64);

for (int i = 0; i < b.length; ++i) {
if (b[i] < 0) {
b[i] += 256;
}
}
return new Base64DecodedMultipartFileUtils(b, baseStrs[0]);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}


public static void main(String[] args) {
String base64 = ImageToBase64Utils.networkImageToBase64("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png");
System.out.println(base64);
MultipartFile multipartFile = Base64DecodedMultipartFileUtils.base64ToMultipart("data:image/jpg;base64,"+base64);
System.out.println(multipartFile.getName());
}

}

注:输入的base64字符串注意带上类似于"data:image/jpg;base64,"的前缀。

4.1.20 判断数值是否在某字符串区间内

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;


/**
* 判断一个数是否在某数学区间内
*/
public class IntervalUtil {

/**
* 判断data_value是否在interval区间范围内
* @param dataValue 数值类型的
* @param interval 正常的数学区间,包括无穷大等,如:(1,3)、>5%、(-∞,6]、(125%,135%)U(70%,80%)
* @return true:表示data_value在区间interval范围内,false:表示data_value不在区间interval范围内
*/
public static boolean isInTheInterval(String dataValue, String interval) {
//将区间和data_value转化为可计算的表达式
String formula = getFormulaByAllInterval(dataValue,interval,"||");
ScriptEngine jse = new ScriptEngineManager().getEngineByName("JavaScript");
try {
//计算表达式
return (Boolean) jse.eval(formula);
} catch (Exception t) {
return false;
}
}

/**
* 将所有阀值区间转化为公式:如
* [75,80) =》 dateValue < 80 && dateValue >= 75
* (125%,135%)U(70%,80%) =》 (dateValue < 1.35 && dateValue > 1.25) || (dateValue < 0.8 && dateValue > 0.7)
* @param dateValue
* @param interval 形式如:(125%,135%)U(70%,80%)
* @param connector 连接符 如:") || ("
*/
public static String getFormulaByAllInterval(String dateValue, String interval, String connector) {
StringBuffer buff = new StringBuffer();
for(String limit:interval.split("U")){ //如:(125%,135%)U (70%,80%)
buff.append("(").append(getFormulaByInterval(dateValue, limit," && ")).append(")").append(connector);
}
String allLimitInvel = buff.toString();
int index = allLimitInvel.lastIndexOf(connector);
allLimitInvel = allLimitInvel.substring(0,index);
return allLimitInvel;
}

/**
* 将整个阀值区间转化为公式:如
* 145) =》 dateValue < 145
* [75,80) =》 dateValue < 80 && dateValue >= 75
* @param dateValue
* @param interval 形式如:145)、[75,80)
* @param connector 连接符 如:&&
*/
public static String getFormulaByInterval(String dateValue, String interval, String connector) {
StringBuffer buff = new StringBuffer();
for(String halfInterval:interval.split(",")){//如:[75,80)、≥80
buff.append(getFormulaByHalfInterval(halfInterval, dateValue)).append(connector);
}
String limitInvel = buff.toString();
int index = limitInvel.lastIndexOf(connector);
limitInvel = limitInvel.substring(0,index);
return limitInvel;
}

/**
* 将半个阀值区间转化为公式:如
* 145) =》 dateValue < 145
* ≥80% =》 dateValue >= 0.8
* [130 =》 dateValue >= 130
* <80% =》 dateValue < 0.8
* @param halfInterval 形式如:145)、≥80%、[130、<80%
* @param dateValue
* @return dateValue < 145
*/
public static String getFormulaByHalfInterval(String halfInterval, String dateValue) {
halfInterval = halfInterval.trim();
if(halfInterval.contains("∞")){ //包含无穷大则不需要公式
return "1 == 1";
}
StringBuffer formula = new StringBuffer();
String data = "";
String opera = "";
if(halfInterval.matches("^([<>≤≥\\[\\(]{1}(-?\\d+.?\\d*\\%?))$")){ //表示判断方向(如>)在前面 如:≥80%
opera = halfInterval.substring(0,1);
data = halfInterval.substring(1);
}else{//[130、145)
opera = halfInterval.substring(halfInterval.length()-1);
data = halfInterval.substring(0,halfInterval.length()-1);
}
double value = dealPercent(data);
formula.append(dateValue).append(" ").append(opera).append(" ").append(value);
String a = formula.toString();
//转化特定字符
return a.replace("[", ">=").replace("(", ">").replace("]", "<=").replace(")", "<").replace("≤", "<=").replace("≥", ">=");
}

/**
* 去除百分号,转为小数
* @param str 可能含百分号的数字
* @return
*/
public static double dealPercent(String str){
double d = 0.0;
if(str.contains("%")){
str = str.substring(0,str.length()-1);
d = Double.parseDouble(str)/100;
}else{
d = Double.parseDouble(str);
}
return d;
}

public static void main(String[] args) {
System.out.println(isInTheInterval("6.1", "(-∞,6]"));
}

}

4.1.21 中文乱码恢复

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
import java.io.UnsupportedEncodingException;

public class EncodeConvert {

private static String[] charsetArr = {"UTF-8","GB18030","GB2312","GBK","Windows-1252","ISO8859-1"};

public static void testAllCharset(String text) throws UnsupportedEncodingException {
if (text == null || text.length() == 0) {
System.out.println("文本不能为空");
return;
}

System.out.println("假设当前编码 假设原始编码 编码后的内容");
printSeparator();

for (String curCharset : charsetArr) {
byte[] btArr = text.getBytes(curCharset);
for (String originCharset : charsetArr) {
if (originCharset.equals(curCharset)) {
continue;
}
String encodeText = new String(btArr,originCharset);
printTable(curCharset, originCharset, encodeText);
}
printSeparator();
}
}

private static void printSeparator() {
System.out.println("--------------------------------------------------------");
}

private static void printTable(String curCharset, String originCharset, String encodeText) {
System.out.print(curCharset);
for (int i = 0; i < 15 - curCharset.length(); i++) {
System.out.print(" ");
}
System.out.print("| " + originCharset);
for (int i = 0; i < 16 - originCharset.length(); i++) {
System.out.print(" ");
}
System.out.println("| " + encodeText);
}

public static void main(String[] args) throws UnsupportedEncodingException {
//测试乱码
testAllCharset("浣犲ソ");
}
}

4.1.22 删除html标签工具类

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
/**
* 删除html标签
*/
public static String delHtmlTag(String htmlStr) {
//定义script的正则表达式
String regExScript = "<script[^>]*?>[\\s\\S]*?<\\/script>";
//定义style的正则表达式
String regExStyle = "<style[^>]*?>[\\s\\S]*?<\\/style>";
//定义HTML标签的正则表达式
String regExHtml = "<[^>]+>";
//定义HTML标签的正则表达式
String regExHtml2 = "\\{[^\\}]+\\}";
Pattern pScript = Pattern.compile(regExScript, Pattern.CASE_INSENSITIVE);
Matcher mScript = pScript.matcher(htmlStr);
//过滤script标签
htmlStr = mScript.replaceAll("");
Pattern pStyle = Pattern.compile(regExStyle, Pattern.CASE_INSENSITIVE);
Matcher mStyle = pStyle.matcher(htmlStr);
//过滤style标签
htmlStr = mStyle.replaceAll("");
Pattern pHtml = Pattern.compile(regExHtml, Pattern.CASE_INSENSITIVE);
Matcher mHtml = pHtml.matcher(htmlStr);
//过滤html标签
htmlStr = mHtml.replaceAll("");
Pattern pHtml2 = Pattern.compile(regExHtml2, Pattern.CASE_INSENSITIVE);
Matcher mHtml2 = pHtml2.matcher(htmlStr);
//过滤html标签
htmlStr = mHtml2.replaceAll("");
//返回文本字符串
return htmlStr.trim();
}

4.1.23 使用jfreechart绘制图表

引入jfreechart依赖

1
2
3
4
5
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.0</version>
</dependency>

绘制代码:

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.labels.ItemLabelAnchor;
import org.jfree.chart.labels.ItemLabelPosition;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.*;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.renderer.category.StandardBarPainter;
import org.jfree.chart.renderer.xy.StandardXYBarPainter;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.chart.ui.TextAnchor;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;

import java.awt.*;
import java.io.File;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
* 使用jfreechart绘制图表(折线图/饼图)
*/
@SuppressWarnings("rawtypes")
public class ChartUtils {

private static String NO_DATA_MSG = "暂无数据";
private static Font FONT = new Font("宋体", Font.PLAIN, 20);
public static Color[] CHART_COLORS = {
new Color(31,129,188), new Color(92,92,97), new Color(144,237,125), new Color(255,188,117),
new Color(153,158,255), new Color(255,117,153), new Color(253,236,109), new Color(128,133,232),
new Color(158,90,102),new Color(255, 204, 102) };// 颜色

static{
setChartTheme();
}
/**
* 中文主题样式 解决乱码
*/
public static void setChartTheme() {
// 设置中文主题样式 解决乱码
StandardChartTheme chartTheme = new StandardChartTheme("CN");
// 设置标题字体
chartTheme.setExtraLargeFont(FONT);
// 设置图例的字体
chartTheme.setRegularFont(FONT);
// 设置轴向的字体
chartTheme.setLargeFont(FONT);
chartTheme.setSmallFont(FONT);
chartTheme.setTitlePaint(new Color(51, 51, 51));
chartTheme.setSubtitlePaint(new Color(85, 85, 85));

chartTheme.setLegendBackgroundPaint(Color.WHITE);// 设置标注
chartTheme.setLegendItemPaint(Color.BLACK);//
chartTheme.setChartBackgroundPaint(Color.WHITE);

Paint[] OUTLINE_PAINT_SEQUENCE = new Paint[] { Color.WHITE };
// 绘制器颜色源
DefaultDrawingSupplier drawingSupplier = new DefaultDrawingSupplier(CHART_COLORS, CHART_COLORS, OUTLINE_PAINT_SEQUENCE,
DefaultDrawingSupplier.DEFAULT_STROKE_SEQUENCE, DefaultDrawingSupplier.DEFAULT_OUTLINE_STROKE_SEQUENCE,
DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE);
chartTheme.setDrawingSupplier(drawingSupplier);

chartTheme.setPlotBackgroundPaint(Color.WHITE);// 绘制区域
chartTheme.setPlotOutlinePaint(Color.WHITE);// 绘制区域外边框
chartTheme.setLabelLinkPaint(new Color(8, 55, 114));// 链接标签颜色
chartTheme.setLabelLinkStyle(PieLabelLinkStyle.CUBIC_CURVE);

chartTheme.setAxisOffset(new RectangleInsets(5, 12, 5, 12));
chartTheme.setDomainGridlinePaint(new Color(192, 208, 224));// X坐标轴垂直网格颜色
chartTheme.setRangeGridlinePaint(new Color(192, 192, 192));// Y坐标轴水平网格颜色

chartTheme.setBaselinePaint(Color.WHITE);
chartTheme.setCrosshairPaint(Color.BLUE);// 不确定含义
chartTheme.setAxisLabelPaint(new Color(51, 51, 51));// 坐标轴标题文字颜色
chartTheme.setTickLabelPaint(new Color(67, 67, 72));// 刻度数字
chartTheme.setBarPainter(new StandardBarPainter());// 设置柱状图渲染
chartTheme.setXYBarPainter(new StandardXYBarPainter());// XYBar 渲染

chartTheme.setItemLabelPaint(Color.black);
chartTheme.setThermometerPaint(Color.white);// 温度计

ChartFactory.setChartTheme(chartTheme);
}

/**
* 必须设置文本抗锯齿
*/
public static void setAntiAlias(JFreeChart chart) {
chart.setTextAntiAlias(false);

}

/**
* 设置图例无边框,默认黑色边框
*/
public static void setLegendEmptyBorder(JFreeChart chart) {
chart.getLegend().setFrame(new BlockBorder(Color.WHITE));
}

/**
* 提供静态方法:获取报表图形1:饼状图
* @param title 标题
* @param datas 数据
* @param
*/
public static void createPiePort(String title,Map<String,Double> datas,String url){
try {
// 如果不使用Font,中文将显示不出来
DefaultPieDataset pds = new DefaultPieDataset();

// 获取迭代器:
Set<Entry<String, Double>> set = datas.entrySet();
Iterator iterator=(Iterator) set.iterator();
Entry entry=null;
while(iterator.hasNext()){
entry=(Entry) iterator.next();
pds.setValue(entry.getKey().toString(),Double.parseDouble(entry.getValue().toString()));
}
/**
* 生成一个饼图的图表
* 分别是:显示图表的标题、需要提供对应图表的DateSet对象、是否显示图例、是否生成贴士以及是否生成URL链接
*/
JFreeChart chart = ChartFactory.createPieChart(title, pds, true, false, true);
setPieRender((PiePlot) chart.getPlot());

//将内存中的图片写到本地硬盘
org.jfree.chart.ChartUtils.saveChartAsPNG(new File(url), chart,800,500);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 提供静态方法:获取报表图形3:折线图
* @param title 标题
* @param datas 数据
*/
public static void createLinePort(String title,Map<String,Double> datas,String xName,String yName,String url){
try {
//种类数据集
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
//获取迭代器:
Set<Entry<String, Double>> set = datas.entrySet();
Iterator iterator=(Iterator) set.iterator();
Entry entry=null;
while(iterator.hasNext()){
entry=(Entry) iterator.next();
dataset.setValue(Double.parseDouble(entry.getValue().toString()),//y
title, //名称
entry.getKey().toString()); //x
}
//创建折线图,折线图分水平显示和垂直显示两种
JFreeChart chart = ChartFactory.createLineChart(title, xName, yName, dataset,//2D折线图
PlotOrientation.VERTICAL,
false, // 是否显示图例(对于简单的柱状图必须是false)
true, // 是否生成工具
true);// 是否生成URL链接
//得到绘图区
setLineRender((CategoryPlot)chart.getPlot(),true,true);
org.jfree.chart.ChartUtils.saveChartAsPNG(new File(url), chart, 1000,600);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 设置折线图样式
*
* @param plot
* @param isShowDataLabels 是否显示数据标签
*/
public static void setLineRender(CategoryPlot plot, boolean isShowDataLabels, boolean isShapesVisible) {
plot.setNoDataMessage(NO_DATA_MSG);
plot.setInsets(new RectangleInsets(10, 10, 0, 10), false);
LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
renderer.setDefaultStroke(new BasicStroke(1.5F));
if (isShowDataLabels) {
renderer.setDefaultItemLabelsVisible(true);
renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator(StandardCategoryItemLabelGenerator.DEFAULT_LABEL_FORMAT_STRING,
NumberFormat.getInstance()));
renderer.setDefaultPositiveItemLabelPosition(new ItemLabelPosition(ItemLabelAnchor.OUTSIDE1, TextAnchor.BOTTOM_CENTER));// weizhi
}
renderer.setDefaultShapesVisible(isShapesVisible);// 数据点绘制形状
setXAixs(plot);
setYAixs(plot);
}

/**
* 设置饼状图渲染
*/
public static void setPieRender(Plot plot) {
plot.setNoDataMessage(NO_DATA_MSG);
plot.setInsets(new RectangleInsets(10, 10, 5, 10));
PiePlot piePlot = (PiePlot) plot;
piePlot.setInsets(new RectangleInsets(0, 0, 0, 0));
piePlot.setCircular(true);// 圆形
// piePlot.setSimpleLabels(true);// 简单标签
piePlot.setLabelGap(0.01);
piePlot.setInteriorGap(0.05D);
piePlot.setLegendItemShape(new Rectangle(10, 10));// 图例形状
piePlot.setIgnoreNullValues(true);
piePlot.setLabelBackgroundPaint(null);// 去掉背景色
piePlot.setLabelShadowPaint(null);// 去掉阴影
piePlot.setLabelOutlinePaint(null);// 去掉边框
piePlot.setShadowPaint(null);
// 0:category 1:value:2 :percentage
piePlot.setLabelGenerator(new StandardPieSectionLabelGenerator("{0}:{2}"));// 显示标签数据
}

/**
* 设置类别图表(CategoryPlot) X坐标轴线条颜色和样式
*
* @param
*/
public static void setXAixs(CategoryPlot plot) {
Color lineColor = new Color(31, 121, 170);
plot.getDomainAxis().setCategoryLabelPositions(CategoryLabelPositions.UP_45);
plot.getDomainAxis().setAxisLinePaint(lineColor); // X坐标轴颜色
plot.getDomainAxis().setTickMarkPaint(lineColor); // X坐标轴标记|竖线颜色

}

/**
* 设置类别图表(CategoryPlot) Y坐标轴线条颜色和样式 同时防止数据无法显示
*
* @param
*/
public static void setYAixs(CategoryPlot plot) {
Color lineColor = new Color(31, 121, 170);
ValueAxis axis = plot.getRangeAxis();
axis.setAxisLinePaint(lineColor);// Y坐标轴颜色
axis.setTickMarkPaint(lineColor);// Y坐标轴标记|竖线颜色
// false隐藏Y刻度
axis.setAxisLineVisible(true);
axis.setTickMarksVisible(true);
// Y轴网格线条
plot.setRangeGridlinePaint(new Color(192, 192, 192));
plot.setRangeGridlineStroke(new BasicStroke(1));

plot.getRangeAxis().setUpperMargin(0.1);// 设置顶部Y坐标轴间距,防止数据无法显示
plot.getRangeAxis().setLowerMargin(0.1);// 设置底部Y坐标轴间距

}

/**
* 设置XY图表(XYPlot) X坐标轴线条颜色和样式
*
* @param
*/
public static void setXY_XAixs(XYPlot plot) {
Color lineColor = new Color(31, 121, 170);
plot.getDomainAxis().setAxisLinePaint(lineColor);// X坐标轴颜色
plot.getDomainAxis().setTickMarkPaint(lineColor);// X坐标轴标记|竖线颜色
}

/**
* 设置XY图表(XYPlot) Y坐标轴线条颜色和样式 同时防止数据无法显示
*
* @param
*/
public static void setXY_YAixs(XYPlot plot) {
Color lineColor = new Color(192, 208, 224);
ValueAxis axis = plot.getRangeAxis();
axis.setAxisLinePaint(lineColor);// X坐标轴颜色
axis.setTickMarkPaint(lineColor);// X坐标轴标记|竖线颜色
// 隐藏Y刻度
axis.setAxisLineVisible(false);
axis.setTickMarksVisible(false);
// Y轴网格线条
plot.setRangeGridlinePaint(new Color(192, 192, 192));
plot.setRangeGridlineStroke(new BasicStroke(1));
plot.setDomainGridlinesVisible(false);

plot.getRangeAxis().setUpperMargin(0.12);// 设置顶部Y坐标轴间距,防止数据无法显示
plot.getRangeAxis().setLowerMargin(0.12);// 设置底部Y坐标轴间距

}

public static void main(String[] args) {

// 绘制饼图
Map<String, Double> map = new HashMap<String, Double>();
map.put("天使", (double) 1000);
map.put("雄兵连", (double) 700);
map.put("太阳之光", (double) 600);
map.put("辅助", (double) 400);
createPiePort("投票结果", map,"/Users/yoyo/Temp/pieChart.jpg");

// 绘制折线图
Map<String, Double> map1 = new HashMap<String, Double>();
map1.put("2020-02-03", (double) 700);
map1.put("2020-02-04", (double) 1000);
map1.put("2020-02-05", (double) 600);
map1.put("2020-02-06", (double) 400);
map1.put("2020-02-07", (double) 4000);
map1.put("2020-02-08", (double) 1200);
map1.put("2020-02-09", (double) 800);
createLinePort("近7日金额",map1,"日期","金额(元)","/Users/yoyo/Temp/lineChart.jpg");

}

}

4.1.24 基于zxing库的二维码生成及解析工具类

引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Google ZXing 操作二维码,生成二维码必备 https://mvnrepository.com/artifact/com.google.zxing/core -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<!--Google ZXing 操作二维码,解析二维码必备,如果不解析二维码可以不导该包-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>

生成二维码:

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
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.EnumMap;

/**
* 在指定路径生成一张二维码,内容类型为字符串,可以是普通文本或者是超链接
*
* 生成二维码的步骤
* 1.根据Zxingjar包生成二维码,这只是生成二维码,并不是生成二维码图片
* 2.创建一张图片,大小和二维码的大小一致。
* 3.二维码可以看成是二维数组,所以遍历二维码,然后把二维码的每一个点添加到图片上,添加的时候,判断是黑色点,还是白色点。
* encode.get(i,j)?BLACK:WHITE
* 4.最后把真正的二维码图片生成到指定路径即可。
*/
public class EncodeQRCodeUtils {

//0x:代表是16进制,ff:代表完全不透明度,其余6位代表颜色值(000000:代表黑色)
//定义二维码的颜色
private static final int BLACK = 0xff000000;
//定义二维码的背景颜色
private static final int WHITE = 0xffffffff;

//二维码格式参数集合
private static final EnumMap<EncodeHintType,Object> hints =
new EnumMap<EncodeHintType, Object>(EncodeHintType.class);

//静态代码块初始化参数,在类加载的时候就初始化,而且在类的生命周期内只初始化一次
static {
/*
* 二维码的纠错级别(排错率),4个级别: L(7%)、M(15%)、Q (25%)、H(30%)
* 最高级别为:H
* 纠错信息同样存储在二维码中,纠错级别越高,纠错信息占用的空间越多,那么能存储的有用信息就越少;
*/
hints.put(EncodeHintType.ERROR_CORRECTION,ErrorCorrectionLevel.L);
// 二维码边界空白大小:1,2,3,4(4为默认,最大)
hints.put(EncodeHintType.MARGIN,1);
//设置二维码存储内容的字符集
hints.put(EncodeHintType.CHARACTER_SET,"UTF-8");
}

public static void createZXingCodeSaveToDisk(String content,int width,int height,String savePath,String imageType){
try {
BitMatrix encode = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, hints);
//获得二维码的宽高
int codeWidth = encode.getWidth();
int codeHeight = encode.getHeight();
//创建图片
BufferedImage image = new BufferedImage(codeWidth, codeHeight, BufferedImage.TYPE_INT_RGB);
//把二维码里面的信息写到图片里面
for (int i = 0; i < codeWidth; i++) {
for (int j = 0; j < codeHeight; j++) {
image.setRGB(i,j,encode.get(i,j)?BLACK:WHITE);
}
}
//保存图片到指定位置
File file = new File(savePath);
if(!file.exists()){
//文件不存在则创建
file.createNewFile();
}
ImageIO.write(image,imageType,file);
//打印一句话,给个提示是否成功
System.out.println("二维码生成成功");
} catch (Exception e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
// 将字符串"hello world"存储在二维码中
createZXingCodeSaveToDisk("hello world",300,300,"./text.jpeg","JPEG");
// 将我的博客地址存储在二维码中
createZXingCodeSaveToDisk("https://www.eula.club/",300,300,"./blog.jpeg","JPEG");
}

}

解析二维码:

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
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.EnumMap;

/**
* 该类主要解析二维码
* 步骤:
* 1.读取二维码图片
* 2.读取二维码图片中的二维码
* 3.读取二维码中的信息
*/
public class DecodeQRCodeUtils {

//二维码格式参数集合
private static final EnumMap<DecodeHintType,Object> HINTS = new EnumMap<DecodeHintType, Object>(DecodeHintType.class);

static {
//设置解析二维码后信息的字符集
HINTS.put(DecodeHintType.CHARACTER_SET,"UTF-8");
}

/**
* 解析二维码
* @param path 二维码图片路径
* @return 二维码中的文本内容
*/
public static String decodeQRCodeForPath(String path){
//该文件对象映射二维码图片
File file = new File(path);
if(file.exists()){
try {
return decodeQRCodeStreamForStream(new FileInputStream(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
}
return null;
}

/**
* 解析二维码
* @param ins 读取二维码图片的流
* @return 二维码中的文本内容
*/
public static String decodeQRCodeStreamForStream(InputStream ins) {
if(ins != null){
try {
//将读取二维码图片的流转为图片对象
BufferedImage image = ImageIO.read(ins);
BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(image);
HybridBinarizer binarizer = new HybridBinarizer(source);
BinaryBitmap binaryBitmap = new BinaryBitmap(binarizer);
MultiFormatReader reader = new MultiFormatReader();
Result result = reader.decode(binaryBitmap, HINTS);
//返回二维码中的文本内容
String content = result.getText();
System.out.println("二维码解析成功");
return content;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
return null;
}

public static void main(String[] args) {
String myName = decodeQRCodeForPath("./text.jpeg");
System.out.println(myName);
String myBlog = decodeQRCodeForPath("./blog.jpeg");
System.out.println(myBlog);
}

}

4.1.25 邮件发送工具类

引入依赖:

1
2
3
4
5
<!-- 邮箱通知 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
## JavaMailSender config
# smtp.163.com,smtp.qq.com,(smtp.gmail.com is unavailable)
spring.mail.host=smtp.163.com
spring.mail.username=[email protected]
# use authorization code
spring.mail.password=your_authorization_code
spring.mail.port=465
spring.mail.default-encoding=utf-8
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.protocol=smtp
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.socketFactory.port=465
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
# mail debug
spring.mail.properties.mail.debug=false
# check connectivity at startup
spring.mail.test-connection=false

注:这里的密码要去申请单独的授权码,而不是你的邮箱普通登录密码,另外gmail邮箱我没有测试通过,总是连不上服务器。

邮件发送封装:

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
96
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

@Service
public class MailService {

private static final Logger logger = LoggerFactory.getLogger(MailService.class);

@Autowired
private JavaMailSender javaMailSender;

@Value("${spring.mail.username}")
private String sender;

/**
* 发送普通邮件
*
* @param to
* @param subject
* @param content
*/
public void sendSimpleMailMessge(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(sender);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
try {
javaMailSender.send(message);
logger.info("send success.................");
} catch (Exception e) {
logger.error("发送简单邮件时发生异常!", e);
}
}

/**
* 发送 HTML 邮件
*
* @param to 收件人
* @param subject 主题
* @param content 内容(html)
*/
public void sendHtmlMessge(String to, String subject, String content) {
MimeMessage message = javaMailSender.createMimeMessage();
try {
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(sender);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
javaMailSender.send(message);
logger.info("send success.................");
} catch (MessagingException e) {
logger.error("发送MimeMessge时发生异常!", e);
}
}

/**
* 发送带附件的邮件
*
* @param to 收件人
* @param subject 主题
* @param content 内容
* @param filePath 附件路径
*/
public void sendAttachmentMessge(String to, String subject, String content, String filePath) {
MimeMessage message = javaMailSender.createMimeMessage();
try {
// true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(sender);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
FileSystemResource file = new FileSystemResource(filePath);
String fileName = file.getFilename();
helper.addAttachment(fileName, file);
javaMailSender.send(message);
logger.info("send success.................");
} catch (MessagingException e) {
logger.error("发送带附件的MimeMessge时发生异常!", e);
}
}

}

4.1.26 身份证号校验工具类

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import java.util.HashMap;

/**
* 身份证号校验工具类
* 第一代身份证15位,第二代身份证18位
*/
public class IdCardVerifyUtils {

private static String _codeError;

// wi =2(n-1)(mod 11)
final static int[] wi = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2, 1};
// verify digit
final static int[] vi = {1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2};
private static int[] ai = new int[18];
private static String[] _areaCode = {"11", "12", "13", "14", "15", "21", "22"
, "23", "31", "32", "33", "34", "35", "36", "37", "41", "42", "43", "44"
, "45", "46", "50", "51", "52", "53", "54", "61", "62", "63", "64", "65", "71", "81", "82", "91"};
private static HashMap<String, Integer> dateMap;
private static HashMap<String, String> areaCodeMap;

static {
dateMap = new HashMap<String, Integer>();
dateMap.put("01", 31);
dateMap.put("02", null);
dateMap.put("03", 31);
dateMap.put("04", 30);
dateMap.put("05", 31);
dateMap.put("06", 30);
dateMap.put("07", 31);
dateMap.put("08", 31);
dateMap.put("09", 30);
dateMap.put("10", 31);
dateMap.put("11", 30);
dateMap.put("12", 31);
areaCodeMap = new HashMap<String, String>();
for (String code : _areaCode) {
areaCodeMap.put(code, null);
}
}

// 验证身份证位数,15位和18位身份证
public static boolean verifyLength(String code) {
int length = code.length();
if (length == 15 || length == 18) {
return true;
} else {
_codeError = "错误:输入的身份证号不是15位和18位的";
return false;
}
}

// 判断地区码
public static boolean verifyAreaCode(String code) {
String areaCode = code.substring(0, 2);
if (areaCodeMap.containsKey(areaCode)) {
return true;
} else {
_codeError = "错误:输入的身份证号的地区码(1-2位)[" + areaCode + "]不符合中国行政区划分代码规定(GB/T2260-1999)";
return false;
}
}

// 判断月份和日期
public static boolean verifyBirthdayCode(String code) {
// 验证月份
String month = code.substring(10, 12);
boolean isEighteenCode = (18 == code.length());
if (!dateMap.containsKey(month)) {
_codeError = "错误:输入的身份证号" + (isEighteenCode ? "(11-12位)" : "(9-10位)") + "不存在[" + month + "]月份,不符合要求(GB/T7408)";
return false;
}
// 验证日期
String dayCode = code.substring(12, 14);
Integer day = dateMap.get(month);
String yearCode = code.substring(6, 10);
Integer year = Integer.valueOf(yearCode);

// 非2月的情况
if (day != null) {
if (Integer.valueOf(dayCode) > day || Integer.valueOf(dayCode) < 1) {
_codeError = "错误:输入的身份证号" + (isEighteenCode ? "(13-14位)" : "(11-13位)") + "[" + dayCode + "]号不符合小月1-30天大月1-31天的规定(GB/T7408)";
return false;
}
}
// 2月的情况
else {
// 闰月的情况
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
if (Integer.valueOf(dayCode) > 29 || Integer.valueOf(dayCode) < 1) {
_codeError = "错误:输入的身份证号" + (isEighteenCode ? "(13-14位)" : "(11-13位)") + "[" + dayCode + "]号在" + year + "闰年的情况下未符合1-29号的规定(GB/T7408)";
return false;
}
}
// 非闰月的情况
else {
if (Integer.valueOf(dayCode) > 28 || Integer.valueOf(dayCode) < 1) {
_codeError = "错误:输入的身份证号" + (isEighteenCode ? "(13-14位)" : "(11-13位)") + "[" + dayCode + "]号在" + year + "平年的情况下未符合1-28号的规定(GB/T7408)";
return false;
}
}
}
return true;
}

// 验证身份除了最后位其他的是否包含字母
public static boolean containsAllNumber(String code) {
String str = "";
if (code.length() == 15) {
str = code.substring(0, 15);
} else if (code.length() == 18) {
str = code.substring(0, 17);
}
char[] ch = str.toCharArray();
for (int i = 0; i < ch.length; i++) {
if (!(ch[i] >= '0' && ch[i] <= '9')) {
_codeError = "错误:输入的身份证号第" + (i + 1) + "位包含字母";
return false;
}
}
return true;
}

public static String getCodeError() {
return _codeError;
}

// 验证18位校验码,校验码采用ISO 7064:1983,MOD 11-2 校验码系统
public static boolean verifyMOD(String code) {
String verify = code.substring(17, 18);
if ("x".equals(verify)) {
code = code.replaceAll("x", "X");
verify = "X";
}
String verifyIndex = getVerify(code);
if (verify.equals(verifyIndex)) {
return true;
}
_codeError = "错误:输入的身份证号最末尾的数字验证码错误";
return false;
}

// 获得校验位
public static String getVerify(String eightcardid) {
int remaining = 0;

if (eightcardid.length() == 18) {
eightcardid = eightcardid.substring(0, 17);
}
if (eightcardid.length() == 17) {
int sum = 0;
for (int i = 0; i < 17; i++) {
String k = eightcardid.substring(i, i + 1);
ai[i] = Integer.parseInt(k);
}
for (int i = 0; i < 17; i++) {
sum = sum + wi[i] * ai[i];
}
remaining = sum % 11;
}
return remaining == 2 ? "X" : String.valueOf(vi[remaining]);
}

// 15位转18位身份证
public static String uptoeighteen(String fifteencardid) {
String eightcardid = fifteencardid.substring(0, 6);
eightcardid = eightcardid + "19";
eightcardid = eightcardid + fifteencardid.substring(6, 15);
eightcardid = eightcardid + getVerify(eightcardid);
return eightcardid;
}

// 验证身份证
public static boolean verify(String idcard) {
_codeError = "";
// 验证身份证位数,15位和18位身份证
if (!verifyLength(idcard)) {
return false;
}
// 验证身份除了最后位其他的是否包含字母
if (!containsAllNumber(idcard)) {
return false;
}
// 如果是15位的就转成18位的身份证
String eifhteencard = "";
if (idcard.length() == 15) {
eifhteencard = uptoeighteen(idcard);
} else {
eifhteencard = idcard;
}
// 验证身份证的地区码
if (!verifyAreaCode(eifhteencard)) {
return false;
}
// 判断月份和日期
if (!verifyBirthdayCode(eifhteencard)) {
return false;
}
// 验证18位校验码,校验码采用ISO 7064:1983,MOD 11-2 校验码系统
if (!verifyMOD(eifhteencard)) {
return false;
}
return true;
}

}

4.1.27 敏感信息脱敏工具类

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import org.apache.commons.lang3.StringUtils;

/**
* 字段脱敏工具类
*/
public class SensitiveFieldUtils {

/**
* @param userName 名字
* @return 脱敏结果
*/
public static String chineseName(String userName) {
if (StringUtils.isEmpty(userName)) {
return "";
}
String name = StringUtils.left(userName, 1);
return StringUtils.rightPad(name, StringUtils.length(userName), "*");
}

/**
* @param idCard 身份证号
* @return 脱敏结果
*/
public static String idCard(String idCard) {
if (StringUtils.isEmpty(idCard)) {
return "";
}
String id = StringUtils.right(idCard, 4);
return StringUtils.leftPad(id, StringUtils.length(idCard), "*");
}

/**
* @param phone 手机号
* @return 脱敏结果
*/
public static String telephone(String phone) {
if (StringUtils.isEmpty(phone)) {
return "";
}
return StringUtils.left(phone, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(phone, 4), StringUtils.length(phone), "*"), "***"));
}

/**
* @param address 地址信息
* @param sensitiveSize 敏感信息长度
* @return 脱敏结果
*/
public static String address(String address, int sensitiveSize) {
if (StringUtils.isBlank(address)) {
return "";
}
int length = StringUtils.length(address);
return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");
}

/**
* @param email 邮箱
* @return 脱敏结果
*/
public static String email(String email) {
if (StringUtils.isBlank(email)) {
return "";
}
int index = StringUtils.indexOf(email, "@");
if (index <= 1) {
return email;
} else {
return StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));
}
}


/**
* @param cardNum 银行卡号
* @return 脱敏结果
*/
public static String bankCard(String cardNum) {
if (StringUtils.isBlank(cardNum)) {
return "";
}
return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "******"));
}

public static void main(String[] args) {

// 测试姓名脱敏(张**)
String name = chineseName("张三丰");
System.out.println("name = " + name);

// 测试身份证号脱敏(**************7812)
String idCard = idCard("123456781234567812");
System.out.println("idCard = " + idCard);

// 测试手机号脱敏(186****0000)
String telephone = telephone("18600000000");
System.out.println("telephone = " + telephone);

// 测试地址脱敏(天津市滨海新区*********)
String address = address("天津市滨海新区经济开发区第三大街", 9);
System.out.println("address = " + address);

// 测试邮箱脱敏(t***@163.com)
String email = email("[email protected]");
System.out.println("email = " + email);

// 测试银行卡号脱敏(622316******6887)
String bankCard = bankCard("6223165905596887");
System.out.println("bankCard = " + bankCard);

}

}

4.2 常见基本写法

4.2.1 常见基本语法

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

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

[2] 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

[3] 出现异常时数据库回滚

增删改接口都要加这个异常时数据库回滚,在service的接口处加如下注解即可。

1
@Transactional(rollbackFor = Exception.class)

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

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

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

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

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

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

[6] 按行读取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();

[7] Google Guava的HashBasedTable类库

Google Guava的HashBasedTable类库可以实现相当于两个key的Map的数据结构,下面为基本使用示例。

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
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;

// 创建
Table<String, String, Float> weightTable = HashBasedTable.create();

// 新增
weightTable.put("aaa", "bbb", 1.0f);
weightTable.put("ccc", "ddd", 2.0f);
weightTable.put("ddd", "eee", 3.0f);

// 获取
float weight = weightTable.get("aaa", "bbb");

// 包含
boolean exist = weightTable.contains("aaa", "bbb")

// 遍历
Set<Table.Cell<String,String,String>> tableSet = weightTable.cellSet();
Iterator<Table.Cell<String,String,String>> it = tableSet.iterator();
while (it.hasNext()){
Table.Cell<String,String,String> data = it.next();
String rowCompany = data.getRowKey();
String columnId = data.getColumnKey();
float valueName = data.getValue();
System.out.println("行号:"+rowCompany+",列号:"+columnId+",值:"+valueName);
}

[8] Hutool的Tree工具类库

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
// 构建Tree
List<TreeNode<String>> nodeList = CollUtil.newArrayList();

nodeList.add(new TreeNode<>("1", "0", "系统管理", 5));
nodeList.add(new TreeNode<>("11", "1", "用户管理", 222222));
nodeList.add(new TreeNode<>("111", "11", "用户添加", 0));
nodeList.add(new TreeNode<>("2", "0", "店铺管理", 1));
nodeList.add(new TreeNode<>("21", "2", "商品管理", 44));
nodeList.add(new TreeNode<>("221", "2", "商品管理2", 2));

//配置
TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
// 最大递归深度
treeNodeConfig.setDeep(3);

//转换器
List<Tree<String>> treeNodes = TreeUtil.build(nodeList, "0", treeNodeConfig,
(treeNode, tree) -> {
tree.setId(treeNode.getId());
tree.setParentId(treeNode.getParentId());
tree.setWeight(treeNode.getWeight());
tree.setName(treeNode.getName());
// 扩展属性 ...
tree.putExtra("extraField", 666);
tree.putExtra("other", new Object());
});

[9] 将文件转换成输出流提供给前端下载

将输入流中的数据循环写入到响应输出流中,而不是一次性读取到内存,通过响应输出流输出到前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param path 指想要下载的文件的路径
* @param response
* @功能描述 下载文件:将输入流中的数据循环写入到响应输出流中,而不是一次性读取到内存
*/
@RequestMapping("/downloadLocal")
public void downloadLocal(String path, HttpServletResponse response) throws IOException {
// 读到流中
InputStream inputStream = new FileInputStream(path);// 文件的存放路径
response.reset();
response.setContentType("application/octet-stream");
String filename = new File(path).getName();
response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));
ServletOutputStream outputStream = response.getOutputStream();
byte[] b = new byte[1024];
int len;
//从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1
while ((len = inputStream.read(b)) > 0) {
outputStream.write(b, 0, len);
}
inputStream.close();
}

[10] 读取JSON文件并转换

需要fastjson依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>

读取JSON文件工具类

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
/**
* 读取json文件,返回json串
* @param fileName
* @return
*/
public static String readJsonFile(String fileName) {
String jsonStr = "";
try {
File jsonFile = new File(fileName);
FileReader fileReader = new FileReader(jsonFile);

Reader reader = new InputStreamReader(new FileInputStream(jsonFile), "utf-8");
int ch = 0;
StringBuffer sb = new StringBuffer();
while ((ch = reader.read()) != -1) {
sb.append((char) ch);
}

fileReader.close();
reader.close();
jsonStr = sb.toString();
return jsonStr;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}

调用工具类读取并转换成List<Map<String, Object>>

1
2
3
String path = "./test.json";
String s = CommonUtils.readJsonFile(path);
List<Map<String, Object>> result = (List<Map<String, Object>>) JSONArray.parse(s);

[11] 价格折扣计算

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据总价与活动价计算折扣
* @param totalPrice
* @param activityPrice
* @return
*/
public static String calPriceDiscount(int totalPrice, int activityPrice) {
BigDecimal totalPriceBigDecimal = new BigDecimal(totalPrice);
BigDecimal activityPriceBigDecimal = new BigDecimal(activityPrice);
double num = activityPriceBigDecimal.divide(totalPriceBigDecimal, 10, BigDecimal.ROUND_HALF_UP).doubleValue();
DecimalFormat df=new DecimalFormat("0.0");
return df.format(num*10);
}

[12] 生成数值列表

1
2
3
4
5
6
7
8
9
10
11
/**
* 生成数值列表
* @param start
* @param end
* @return
*/
public static List<Integer> generateOrdinalList(int start, int end) {
List<Integer> range = IntStream.rangeClosed(start, end)
.boxed().collect(Collectors.toList());
return range;
}

[13] 计算百分比 并保留两位小数

1
2
3
4
5
6
7
// 计算百分比 并保留两位小数
int num = 3, val = 5;
System.out.println(String.format("%.2f", (float) num / (float) val * 100) + "%");

// 直接对double类型四舍五入保留两位小数
double changeRate = 0.3456789
System.out.println(Math.round(changeRate * 100) / 100.0);

[14] 转换成中文大写数字

1
2
3
4
// 大小写转换,转换为大写只能精确到分(小数点儿后两位),之后的数字会被忽略。
double a = 67556.32;
String digitUppercase = Convert.digitToChinese(a);
System.out.println(digitUppercase); // 结果为:"陆万柒仟伍佰伍拾陆元叁角贰分"

[15] 计时器的实现

1
2
3
4
5
6
7
8
// 计时器
TimeInterval timer = DateUtil.timer();
//---------------------------------
//-------这是执行过程
//---------------------------------
System.out.println(timer.interval()); //花费毫秒数
System.out.println(timer.intervalRestart()); //返回花费的毫秒数,并重置开始时间
System.out.println(timer.intervalMinute()); //花费分钟数

4.2.2 类型转换遍历

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

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

[2] 将字符串转换成Map

方式一:泛型的反序列化,使用 TypeReference 传入类型信息

1
Map<String, Object> map = JSON.parseObject(resultStr, new TypeReference<Map<String, Object>>(){});

方式二:将字符串先转成JSON字符串再转成Map

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

说明:将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);  // 字符串去掉第一位和最后一位

[3] 将字符串转成List<Map<String,Object>>

1
List<Map<String, Object>> mapList = JSON.parseObject(jsonStr, new TypeReference<List<Map<String, Object>>>() {});

注:需要fastjson依赖,用于Java对象与JSON互转。

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

[4] Object相关转换

1)将Object转成Map

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

注:需要fastjson依赖,用于Java对象与JSON互转。

2)将Object转成float(其他基本类型同理)

1
float a = Float.parseFloat(obj.toString());  

3)将Object转成boolean

1
boolean result = (Boolean)ResultMap.get("result");

4)将Object转成List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 将Object转List
* @param obj
* @param <T>
* @return
*/
public static <T> List<T> objectToList(Object obj, Class<T> clazz) {
List<T> result = new ArrayList<T>();
if(obj instanceof List<?>)
{
for (Object o : (List<?>) obj)
{
result.add(clazz.cast(o));
}
return result;
}
return null;
}

// 调用示例
List<Object> testList = objectToList((obj), Object.class);

[5] Map的遍历

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

[6] 字符串处理

1
2
3
4
5
// 将数组转成逗号分割的字符串
String arrayStr = String.join(",", array);

// 去除字符串中的所有空格
String result = result.replaceAll(" +","");

[7] JSON字符串的转义与反转义

1
2
3
4
5
// 转义
StringEscapeUtils.escapeJson()

// 反转义
StringEscapeUtils.unescapeJson()

注:如果出现“常量字符串过长”的问题,File -> Settings -> Build,Execution,Deployment -> Compiler -> 将 javac 改成 eclipse 即可。

[8] 逗号分割的字符串与List互转

1
2
3
4
5
6
7
8
9
// 将逗号分割的字符串转List
String str1 = "a,b,c";
List<String> list = Arrays.asList(str1.split(","));
System.out.println(list);
// 将List转成逗号分割的字符串
String str2 = Joiner.on(",").join(list);
System.out.println(str2);
// 逗号分隔字符串转List<Long>,再将List转Set
Set<Long> stateSet = new HashSet<>(Arrays.asList(state.split(",")).stream().map(s -> Long.parseLong(s.trim())).collect(Collectors.toList()));

[9] List 快速取最大值和最小值

1
2
double max = Collections.max(list);
double min = Collections.min(list);

[10] 过滤出符合条件的List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 存放过滤结果的列表
List<SysCode> result;

// 创建测试数据
ArrayList<String> codes = new ArrayList<>();
codes.add("SEX");
ArrayList<String> values = new ArrayList<>();
values.add("1");

// 使用lambda表达式(java8支持)过滤出结果并放到result列表里
result = sysCodeList.stream()
.filter((SysCode b) -> codes.contains(b.getCode()))
.filter((SysCode b) -> values.contains(b.getValue()))
.collect(Collectors.toList());

System.out.println(result);

[11] Map与实体类的互相转换

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
/**
* 使用BeanMap将实体类转成Map
* @param bean
* @param <T>
* @return
*/
public static <T> Map<String, Object> beanToMap(T bean) {
Map<String, Object> map = Maps.newHashMap();
if (bean != null) {
BeanMap beanMap = BeanMap.create(bean);
for (Object key : beanMap.keySet()) {
map.put(key + "", beanMap.get(key));
}
}
return map;
}

/**
* 将 map 转成实体类
* @param clazz
* @param map
* @param <T>
* @return
*/
public static <T> T convertMapToBean(Class<T> clazz, Map<String, Object> map) {
T obj = null;
try {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
// 创建 JavaBean 对象
obj = clazz.newInstance();
// 给 JavaBean 对象的属性赋值
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (int i = 0; i < propertyDescriptors.length; i++) {
PropertyDescriptor descriptor = propertyDescriptors[i];
String propertyName = descriptor.getName();
if (map.containsKey(propertyName)) {
Object value = map.get(propertyName);
if ("".equals(value)) {
value = null;
}
Object[] args = new Object[1];
args[0] = value;
descriptor.getWriteMethod().invoke(obj, args);
}
}
} catch (IllegalAccessException e) {
System.out.println(StrUtil.format("convertMapToBean 实例化JavaBean失败 Error{}",e));
} catch (IntrospectionException e) {
System.out.println(StrUtil.format("convertMapToBean 分析类属性失败 Error{}" ,e));
} catch (IllegalArgumentException e) {
System.out.println(StrUtil.format("convertMapToBean 映射错误 Error{}" ,e));
} catch (InstantiationException e) {
System.out.println(StrUtil.format("convertMapToBean 实例化 JavaBean 失败 Error{}" ,e));
}catch (InvocationTargetException e){
System.out.println(StrUtil.format("convertMapToBean字段映射失败 Error{}" ,e));
}catch (Exception e){
System.out.println(StrUtil.format("convertMapToBean Error{}" ,e));
}
return (T) obj;
}

[12] 合并两个Map

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 合并两个Map
* @param map1
* @param map2
* @return
*/
public static Map<String, Object> mergeHashMap(Map<String, Object> map1, Map<String, Object> map2) {
HashMap<String, Object> combineResultMap = new HashMap<>();
combineResultMap.putAll(map1);
combineResultMap.putAll(map2);
return combineResultMap;
}

[13] 合并两个List<Map<String, Object>>

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
/**
* 合并两个List<Map<String, Object>>
* @param list1
* @param list2
* @return
*/
public static List<Map<String, Object>> mergeMapList(List<Map<String, Object>> list1, List<Map<String, Object>> list2){
list1.addAll(list2);
Set<String> set = new HashSet<>();
return list1.stream()
.collect(Collectors.groupingBy(o->{
//暂存所有key
set.addAll(o.keySet());
//按a_id分组
return o.get("id");
})).entrySet().stream().map(o->{
//合并
Map<String, Object> map = o.getValue().stream().flatMap(m->{
return m.entrySet().stream();
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b)->b));
//为没有的key赋值0
set.stream().forEach(k->{
if(!map.containsKey(k)) {
map.put(k, 0);
}
});
return map;
}).collect(Collectors.toList());
}

[14] 取某分隔符之前的字符串

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
/**
* 取某分隔符(第一处)之前的字符串
* @param str
* @param delimiter
* @return
*/
public static String substringBefore(String str, String delimiter) {
String result = "";
try{
result = StringUtils.substringBefore(str, delimiter);
}catch(Exception e){
e.printStackTrace();
}
return result;
}

/**
* 取某分隔符(最后一处)之前的字符串
* @param str
* @param delimiter
* @return
*/
public static String substringBeforeLast(String str, String delimiter) {
String result = "";
try{
result = StringUtils.substringBeforeLast(str, delimiter);
}catch(Exception e){
e.printStackTrace();
}
return result;
}

4.2.3 文件及目录操作

可使用 java.io.File 包进行文件和目录操作,常用方法如下:

  • public String getName() 返回由此抽象路径名表示的文件或目录的名称。
  • public String getParent() 返回此抽象路径名的父路径名的路径名字符串,如果此路径名没有指定父目录,则返回 null。
  • public File getParentFile() 返回此抽象路径名的父路径名的抽象路径名,如果此路径名没有指定父目录,则返回 null。
  • public String getPath() 将此抽象路径名转换为一个相对路径名字符串。
  • public boolean isAbsolute() 测试此抽象路径名是否为绝对路径名。
  • public String getAbsolutePath() 返回抽象路径名的绝对路径名字符串。
  • public boolean canRead() 测试应用程序是否可以读取此抽象路径名表示的文件。
  • public boolean canWrite() 测试应用程序是否可以修改此抽象路径名表示的文件。
  • public boolean exists() 测试此抽象路径名表示的文件或目录是否存在。
  • public boolean isDirectory() 测试此抽象路径名表示的文件是否是一个目录。
  • public boolean isFile() 测试此抽象路径名表示的文件是否是一个文件。
  • public long lastModified() 返回此抽象路径名表示的文件最后一次被修改的时间。
  • public long length() 返回由此抽象路径名表示的文件的长度。
  • public boolean createNewFile() throws IOException 当且仅当不存在具有此抽象路径名指定的名称的文件时,原子地创建一个新的空文件。
  • public boolean delete() 删除此抽象路径名表示的文件或目录。
  • public void deleteOnExit() 在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。
  • public String[] list() 返回由此抽象路径名所表示的目录中的文件和目录的名称所组成字符串数组。
  • public String[] list(FilenameFilter filter) 返回由包含在目录中的文件和目录的名称所组成的字符串数组,这一目录是通过满足指定过滤器的抽象路径名来表示的
  • public File[] listFiles() 返回一个抽象路径名数组,这些路径名表示此抽象路径名所表示目录中的文件。
  • public File[] listFiles(FileFilter filter) 返回表示此抽象路径名所表示目录中的文件和目录的抽象路径名数组,这些路径名满足特定过滤器。
  • public boolean mkdir() 创建指定目录,只能创建一个父目录(如:c:\java)。
  • public boolean mkdirs() 创建目录,同时创建多个目录(如:c:\java\nn)。
  • public boolean renameTo(File dest) 重新命名此抽象路径名表示的文件。
  • public boolean setLastModified(long time) 设置由此抽象路径名所指定的文件或目录的最后一次修改时间。
  • public boolean setReadOnly() 标记此抽象路径名指定的文件或目录,以便只可对其进行读操作。
  • public static File createTempFile(String prefix, String suffix, File directory) throws IOException 在指定目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称。
  • public static File createTempFile(String prefix, String suffix) throws IOException 在默认临时文件目录中创建一个空文件,使用给定前缀和后缀生成其名称。
  • public int compareTo(File pathname) 按字母顺序比较两个抽象路径名。
  • public int compareTo(Object o) 按字母顺序比较抽象路径名与给定对象。
  • public boolean equals(Object obj) 测试此抽象路径名与给定对象是否相等。
  • public String toString() 返回此抽象路径名的路径名字符串。

用于测试的代码示例:

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;

/**
* 文件及目录操作示例
*/
public class FileOperate {

public static void main(String[] args) throws IOException {

// ================ 【创建目录或文件夹】
String nDir ="D:\\Java_File_Test";
File n = new File(nDir);
if(!n.exists()){
n.mkdir(); // mkdir只能创建一个文件夹
}
String mDir ="D:\\Java_File_Test\\demo";
File m = new File(mDir);
if(!m.exists()){
m.mkdirs(); // mkdirs一次性创建多级目录
}


// ================ 【创建一个txt文件】
String mFile ="D:\\Java_File_Test\\demo\\nn.txt";
File f=new File(mFile);
if(!f.exists()){
f.createNewFile();
}


// ================ 【获取文件的信息】
System.out.println("===== 获取文件信息 =====");
String pFile ="D:\\Java_File_Test\\demo\\nn.txt";
File p=new File(pFile);
// 判断文件和文件夹是否存在
if(p.exists()) {
// 判断是否是一个文件
if(p.isFile()){
System.out.println("文件名:"+p.getName());
System.out.println("文件绝对路径:"+p.getAbsolutePath());
System.out.println("文件相对路径:"+p.getPath());
System.out.println("文件大小(字节数):"+p.length());
}
// 是否是文件夹
if(p.isDirectory()) {
System.out.println("这个文件夹存在!");
}
}


// ================ 【文件重命名】
String rFile ="D:\\Java_File_Test\\demo\\nn.txt";
File r = new File(rFile);
if(r.exists()){
if(r.isFile()) {
r.renameTo(new File("D:\\Java_File_Test\\demo\\demo.txt")); // 重命名指定文件(注意加路径)
System.out.println(rFile + "文件重命名成功!");
}
}


// ================ 【删除指定文件】
String dFile ="D:\\Java_File_Test\\demo\\demo.txt"; // 指定目录
File d = new File(dFile); // 创建一个文件操作对象
if(d.exists()){
if(d.isFile()) {
//d.delete(); // 删除指定文件(为了后续测试先注释掉)
System.out.println(dFile + "文件删除成功!");
}
}


// ================ 【向文本文件写入内容】
try {
String w = "D:\\Java_File_Test\\demo\\demo.txt";
FileOutputStream wFile = new FileOutputStream(w); // 写入内容 - 【覆盖内容】
//FileOutputStream wFile = new FileOutputStream(w,true); // 写入内容 - 【追加写入内容】
String mWrite="Hello world";
byte[] mess = mWrite.getBytes();
wFile.write(mess,0,mess.length);
}catch(FileNotFoundException e) {
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}


// ================ 【读取文件内容】
try {
String uu="D:\\Java_File_Test\\demo\\demo.txt";
FileInputStream readFile = new FileInputStream(uu);
// 读取文件的字节数
int len = readFile.available();
System.out.println("文件【"+uu+"】的字节数是"+len);
// ---------- 读取文本文件内容
System.out.println("----- 读取文本文件内容 -----");
FileReader fReader = new FileReader(uu);
char[] chars = new char[len];
fReader.read(chars,0,len); // 读取全部文本文件内容
for(char aaa:chars){
System.out.print(aaa);
}
}catch(FileNotFoundException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}

}

}

4.3 完整代码示例

4.3.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.3.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.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