# 插件集成
为了让开发者更加方便和快速的满足需求,提供了各种插件集成实现方案。
# 集成redis实现集群会话
目前的会话信息通过ehcache
存储在本地,不方便集群会话管理,由于不少小伙伴需要,所以抽时间整合了一下。如果有需要可以参考我的步骤去集成。改动比较多,请根据实际情况调整。
1、由于切换成redis
,可以删除一些处理类(不在同步到数据库表)和ehcache
相关内容。
// 删除的java类
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\service\SysShiroService.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\session\OnlineSessionDAO.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\filter\online\OnlineSessionFilter.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\filter\sync\SyncOnlineSessionFilter.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\session\OnlineWebSessionManager.java
ruoyi-framework\src\main\java\com\ruoyi\framework\shiro\web\session\SpringSessionValidationScheduler.java
ruoyi-system\src\main\java\com\ruoyi\system\mapper\SysUserOnlineMapper.java
ruoyi-system\src\main\java\com\ruoyi\system\service\ISysUserOnlineService.java
ruoyi-system\src\main\java\com\ruoyi\system\service\impl\SysUserOnlineServiceImpl.java
// 删除mybatis的数据库操作
ruoyi-system\src\main\resources\mapper\system\SysUserOnlineMapper.xml
// 删除ehcache配置
ruoyi-admin\src\main\resources\ehcache\ehcache-shiro.xml
// 删除ruoyi-common\pom.xml中的shiro-ehcache依赖
<!-- Shiro使用EhCache缓存框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2、ruoyi-common\pom.xml
模块添加整合依赖
<!-- shiro整合redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.3.1</version>
<exclusions>
<exclusion>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- springboot整合redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3、ruoyi-admin
文件application-druid.yml
,添加redis
配置
# 数据源配置
spring:
# redis配置
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
timeout: 6000ms # 连接超时时长(毫秒)
lettuce:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
2
3
4
5
6
7
8
9
10
11
12
13
14
15
4、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成redis实现集群会话管理.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
5、测试验证会话集群,在线用户,缓存监控等功能是否正常。
# 集成jwt实现登录授权访问
jwt
适用于前后端分离,但是不分离版本对外提供接口有时候也需要。不少小伙伴有提过要求,最近抽空整合了一下方案,参考步骤如下。
1、ruoyi-framework\pom.xml
添加jwt
依赖
<!-- jwt jar-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
2
3
4
5
6
2、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成jwt实现权限登录授权.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
3、添加测试接口类
ruoyi-admin\ApiController.java
package com.ruoyi.web.controller.system;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.domain.AjaxResult;
@RestController
@RequestMapping("/api")
public class ApiController
{
/**
* 无权限访问
*
* @return
*/
@GetMapping("/list")
public AjaxResult list()
{
return AjaxResult.success("list success");
}
/**
* 菜单权限 system:user:list
*/
@GetMapping("/user/list")
@RequiresPermissions("system:user:list")
public AjaxResult userlist()
{
return AjaxResult.success("user list success");
}
/**
* 角色权限 admin
*/
@GetMapping("/role/list")
@RequiresRoles("admin")
public AjaxResult rolelist()
{
return AjaxResult.success("role list success");
}
}
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
4、测试权限登录访问请求
登录访问(返回token)
POST
/ http://localhost:80/jwt/login?username=ry&password=admin123
测试任意权限(header携带token)
GET
/ http://localhost:80/api/list
测试菜单权限(header携带token)
GET
/ http://localhost:80/api/user/list
测试角色权限(header携带token)
GET
/ http://localhost:80/api/role/list
# 集成cas实现单点登录认证
单点登录(Single Sign On),简称为SSO
,是比较流行的企业业务整合的解决方案之一。SSO
的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
1、下载cas-overlay-template搭建cas服务器
下载项目https://github.com/apereo/cas-overlay-template.git
# 构建项目(需要安装gradle环境)
gradlew.bat clean build
# 解压
gradlew.bat explodeWar
2
3
4
5
此时将会在bulid
目录下生成一个cas-resources
文件夹,我们把里面的文件全部拷贝到cas-overlay-template/src/main/resources
,将/etc/cas/thekeystore
也拷贝到该目录下
修改配置application.properties
server.ssl.key-store=classpath:thekeystore
为了方便测试直接屏蔽了ssl
,端口改成了8080
server.ssl.enabled=false
server.port=8080
2
在内嵌的Tomcat中运行cas
gradlew.bat run
启动完成后浏览器中打开(http://localhost:8080/cas/login (opens new window))就可以访问了。
在登录也面输入用户名和密码:casuser/Mellon
,出现界面表明cas
已经部署成功。
2、cas服务端整合Mysql数据库,添加service-registry依赖
修改build.gradle
文件,加入mysql驱动配置
dependencies {
// Add modules in format compatible with overlay casModules property
if (project.hasProperty("casModules")) {
def dependencies = project.getProperty("casModules").split(",")
dependencies.each {
def projectsToAdd = rootProject.subprojects.findAll {project ->
project.name == "cas-server-core-${it}" || project.name == "cas-server-support-${it}"
}
projectsToAdd.each {implementation it}
}
}
// CAS dependencies/modules may be listed here statically...
implementation "org.apereo.cas:cas-server-webapp-init:${casServerVersion}"
implementation "org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}"
implementation "org.apereo.cas:cas-server-support-jdbc:${casServerVersion}"
implementation "org.apereo.cas:cas-server-support-jdbc-drivers:${casServerVersion}"
implementation "mysql:mysql-connector-java:8.0.22"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
修改resources/application.properties
文件,加入数据库连接配置
# 取消静态配置
# cas.authn.accept.users=casuser::Mellon
# cas.authn.accept.name=Static Credentials
# 本地的数据库配置信息
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/ry?serverTimezone=UTC&allowMultiQueries=true
cas.authn.jdbc.query[0].user=root
cas.authn.jdbc.query[0].password=password
cas.authn.jdbc.query[0].sql=select password from sys_user where login_name= ?
cas.authn.jdbc.query[0].fieldPassword=password
cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5
2
3
4
5
6
7
8
9
10
11
12
13
14
3、设置允许http访问
修改resources/application.properties
开启识别json
# 开启识别json文件配置
cas.tgc.secure=false
cas.service-registry.init-from-json=true
cas.service-registry.json.location=classpath:/services
2
3
4
修改services/HTTPSandIMAPS-10000001.json
,加入http
{
"@class": "org.apereo.cas.services.RegexRegisteredService",
"serviceId": "^(https|http|imaps)://.*",
"name": "HTTPS and IMAPS",
"id": 10000001,
"description": "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
"evaluationOrder": 10000
}
2
3
4
5
6
7
8
4、ruoyi-framework\pom.xml
添加pac4j
依赖
<!-- pac4j安全引擎 -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-cas</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>io.buji</groupId>
<artifactId>buji-pac4j</artifactId>
<version>4.0.0</version>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
5、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成cas实现单点登录认证.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
6、测试单点登录访问请求,是否正常登陆以及退出,同时能访问多个不同系统。
# 集成docker实现一键部署
Docker
是一个虚拟环境容器,可以将你的开发环境、代码、配置文件等一并打包到这个容器中,最终只需要一个命令即可打包发布应用到任意平台中。
1、安装docker
yum install https://download.docker.com/linux/fedora/30/x86_64/stable/Packages/containerd.io-1.2.6-3.3.fc30.x86_64.rpm
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce
curl -L "https://github.com/docker/compose/releases/download/1.25.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
2
3
4
5
2、检查docker
和docker-compose
是否安装成功
docker version
docker-compose --version
2
3、文件授权
chmod +x /usr/local/bin/docker-compose
4、下载若依docker插件,上传到自己的服务器目录
插件相关脚本实现ruoyi/集成docker实现一键部署.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
- 其中
db目录
存放ruoyi数据库脚本
- 其中
jar目录
存放打包好的jar应用文件
- 数据库
mysql
地址需要修改成ruoyi-mysql
- 数据库脚本头部需要添加
SET NAMES 'utf8';
(防止乱码)
5、启动docker
systemctl start docker
6、构建docker服务
docker-compose build
7、启动docker容器
docker-compose up -d
8、访问应用地址
打开浏览器,输入:(http://localhost:80 (opens new window)),若能正确展示页面,则表明环境搭建成功。
启动服务的容器docker-compose up ruoyi-mysql ruoyi-server
停止服务的容器docker-compose stop ruoyi-mysql ruoyi-server
时区设置
如果服务器的时区不正确,可以在dockerfile
文件中添加ENV TZ=Asia/Shanghai
# 升级springboot到最新版本3.x
Spring Boot 3.x
要求使用Java 17
或更高版本,所以需要确保项目使用的Java
版本符合要求。
1、修改pom.xml
文件,version
版本根据实际情况配置最新。
<!-- java.version版本8更换为17 -->
<java.version>17</java.version>
<!-- 新增mybatis节点,版本为3.0.2 -->
<mybatis-spring-boot.version>3.0.3</mybatis-spring-boot.version>
<!-- spring-boot版本2.5.15更换为3.1.5 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- shiro-core & shiro-spring 修改为`jakarta`依赖,排除 shiro-web -->
<!-- Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
</dependency>
<!-- Shiro使用Spring框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
<version>${shiro.version}</version>
<!-- 排除仍使用了javax.servlet的依赖 -->
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 新增三个配置依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
</dependency>
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
2、修改ruoyi-admin/pom.xml
文件mysql
依赖。
<!-- Mysql驱动包 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
2
3
4
5
3、修改ruoyi-framework/pom.xml
文件shiro
依赖,新增shiro-core & shiro-web
依赖。
<!-- 验证码 -->
<!-- Shiro使用Spring框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<classifier>jakarta</classifier>
</dependency>
<!-- Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<classifier>jakarta</classifier>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
4、修改ruoyi-common/pom.xml
文件servlet
依赖为jakarta
。
<!--Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<classifier>jakarta</classifier>
</dependency>
<!-- servlet包 -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
5、Java EE
转Jakarta EE
Spring Boot 3.0
将所有底层依赖项从Java EE
迁移到了Jakarta EE
,会对一些使用了Java EE
的方法造成影响,需要进行相应的修改和调整。
将javax.xxxx
替换成jakarta.xxxx
,例如
javax.annotation 替换成 jakarta.validation
javax.servlet 替换成 jakarta.servlet
javax.validation 替换成 jakarta.validation
javax.xxxxxxxxxx 替换成 jakarta.xxxxxxxxxx
2
3
4
但是有些原生方法是不需要去进行修改的,例如项目中的这几个方法,包不需要替换成jakarta.xxxx
import javax.imageio.ImageIO;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.sql.DataSource
2
3
4
5
6
7
8
PS:如果嫌麻烦可以使用idea
自带的转换功能
6、到此就对springboot3
做了全部的兼容,提供springboot3.x分支下载地址。
下载地址
链接: https://gitee.com/y_project/RuoYi/blob/springboot3 (opens new window) 不定时同步更新。
# 集成websocket实现实时通信
WebSocket
是一种通信协议,可在单个TCP
连接上进行全双工通信。WebSocket
使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API
中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
1、ruoyi-framework/pom.xml
文件添加websocket
依赖。
<!-- SpringBoot Websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2
3
4
5
2、配置匿名访问(可选)
// 如果需要不登录也可以访问,需要在`ShiroConfig.java`中设置匿名访问
filterChainDefinitionMap.put("/websocket/**", "anon");
2
3、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成websocket实现实时通信.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
4、测试验证
如果要测试验证可以把websocket.html
内容复制到login.html
,点击连接发送消息测试返回结果。
# 集成atomikos实现分布式事务
在一些复杂的应用开发中,一个应用可能会涉及到连接多个数据源,所谓多数据源这里就定义为至少连接两个及以上的数据库了。
对于这种多数据的应用中,数据源就是一种典型的分布式场景,因此系统在多个数据源间的数据操作必须做好事务控制。在SpringBoot
的官网推荐我们使用Atomikos (opens new window)。
当然分布式事务的作用并不仅仅应用于多数据源。例如:在做数据插入的时候往一个kafka
消息队列写消息,如果信息很重要同样需要保证分布式数据的一致性。
若依框架已经通过Druid
实现了多数据源切换,但是Spring
开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。所以我们需要Atomikos
解决多数据源事务的一致性问题
1、ruoyi-framework/pom.xml
文件添加atomikos
依赖。
<!-- atomikos分布式事务 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
2
3
4
5
2、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成atomikos实现分布式事务.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
3、测试验证
加入多数据源,如果不会使用可以参考多数据源实现。
对应需要操作多数据源方法加入@Transactional
测试一致性,例如。
@Transactional
public void insert()
{
SpringUtils.getAopProxy(this).insertA();
SpringUtils.getAopProxy(this).insertB();
}
@DataSource(DataSourceType.MASTER)
public void insertA()
{
return xxxxMapper.insertXxxx();
}
@DataSource(DataSourceType.SLAVE)
public void insertB()
{
return xxxxMapper.insertXxxx();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
到此我们项目多个数据源的事务控制生效了
# 集成minio实现分布式文件存储
框架默认存储使用的本地磁盘,对于一些文件较大较多且有数据备份、数据安全、分布式等等就满足不了我们的要求,对于这种情况我们可以集成OSS
对象存储服务。
minio
是目前github
上star
最多的数据存储框架。minio
可以用来搭建分布式存储服务,可以很好的和机器学习相结合。
1、ruoyi-common/pom.xml
文件添加minio
依赖。
<!-- Minio 文件存储 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.2.1</version>
</dependency>
2
3
4
5
6
2、ruoyi-admin
文件application.yml
,添加minio
配置
# Minio配置
minio:
url: http://localhost:9000
accessKey: minioadmin
secretKey: minioadmin
bucketName: ruoyi
2
3
4
5
6
3、CommonController.java
自定义Minio
服务器上传请求
/**
* 自定义 Minio 服务器上传请求
*/
@PostMapping("/uploadMinio")
@ResponseBody
public AjaxResult uploadFileMinio(MultipartFile file) throws Exception
{
try
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.uploadMinio(file);
AjaxResult ajax = AjaxResult.success();
ajax.put("url", fileName);
ajax.put("fileName", fileName);
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
4、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成minio实现分布式文件存储.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
5、测试验证文件存储的功能
启动Minio
创建存储桶名称ruoyi
,访问策略设置公开。
代码测试可以将自己的FileUploadUtils.upload
修改为FileUploadUtils.uploadMinio
,返回值为文件的url
路径。
页面测试可以在通知公告新增和修改页面将文件上传的路径common/upload
修改为common/uploadMinio
,然后上传图片测试验证结果。
# 集成easy-es实现分布式全文检索
Easy-Es
是一款简化ElasticSearch
搜索引擎操作的开源框架,与Mybatis-plus
一致的API,屏蔽语言差异,开发者只需要会MySQL
语法即可完成对Es
的相关操作,学习成本低.底层采用RestHighLevelClient
,兼具低码,易用,易拓展等特性,支持es
独有的高亮,权重,分词,Geo,嵌套,父子类型等功能。
提示
本示例演示Easy-Es
与RuoYi
项目无缝集成,以系统管理/通知公告
作为案例实现全文检索,需要启动elasticsearch
服务,提供下载地址(含ik
分词器)。
目录:其他/elasticsearch-7.14.0.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
1、ruoyi-common\pom.xml
模块修改web
容器依赖,使用easy-es
来替代elasticsearch
容器
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.14.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.dromara.easy-es</groupId>
<artifactId>easy-es-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
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
2、修改application.yml
,加入easy-es配置
# easy-es
easy-es:
enable: true
banner: false
address: 127.0.0.1:9200
global-config:
process-index-mode: manual
db-config:
refresh-policy: immediate
2
3
4
5
6
7
8
9
3、启动类RuoYiApplication
新增扫描注解
package com.ruoyi;
import org.dromara.easyes.starter.register.EsMapperScan;
....
/**
* 启动程序
*
* @author ruoyi
*/
@EsMapperScan("com.ruoyi.web.controller.search.mapper")
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class RuoYiApplication
{
....
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
4、修改SysNoticeMapper.xml的
insertNotice节点
,加入useGeneratedKeys
和keyProperty
,便于新增后获取编号。
<insert id="insertNotice" parameterType="SysNotice" useGeneratedKeys="true" keyProperty="noticeId">
....
</insert>
2
3
5、下载插件相关包和代码实现覆盖到工程中
提示
插件相关包和代码实现ruoyi/集成easy-es实现分布式全文检索.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
注意:如果是分离版本在ruoyi-vue/集成easy-es实现分布式全文检索.zip
目录。
# 使用undertow来替代tomcat容器
SpringBoot
中我们既可以使用Tomcat
作为Http
服务,也可以用Undertow
来代替。Undertow
在高并发业务场景中,性能优于Tomcat
。所以,如果我们的系统是高并发请求,不妨使用一下Undertow
,你会发现你的系统性能会得到很大的提升。
1、ruoyi-framework\pom.xml
模块修改web容器依赖,使用undertow来替代tomcat容器
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- web 容器使用 undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2、修改application.yml
,使用undertow来替代tomcat容器
# 开发环境配置
server:
# 服务器的HTTP端口,默认为80
port: 80
servlet:
# 应用的访问路径
context-path: /
# undertow 配置
undertow:
# HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
max-http-post-size: -1
# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分
buffer-size: 512
# 是否分配的直接内存
direct-buffers: true
threads:
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
io: 8
# 阻塞任务线程池, 当执行类似servlet请求阻塞操作, undertow会从这个线程池中取得线程,它的值设置取决于系统的负载
worker: 256
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
3、修改文件上传工具类FileUploadUtils.java
private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException
{
File desc = new File(uploadDir + File.separator + fileName);
if (!desc.getParentFile().exists())
{
desc.getParentFile().mkdirs();
}
// undertow文件上传,因底层实现不同,无需创建新文件
// if (!desc.exists())
// {
// desc.createNewFile();
// }
return desc;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 集成actuator实现优雅关闭应用
优雅停机主要应用在版本更新的时候,为了等待正在工作的线程全部执行完毕,然后再停止。我们可以使用SpringBoot
提供的Actuator
1、pom.xml
中引入actuator
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2
3
4
2、配置文件中endpoint
开启shutdown
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: "shutdown"
base-path: /monitor
2
3
4
5
6
7
8
9
3、在ShiroConfig
中设置filterChainDefinitionMap
配置url=anon
filterChainDefinitionMap.put("/monitor/shutdown", "anon");
4、Post
请求测试验证优雅停机
curl -X POST http://localhost:80/monitor/shutdown
# 集成aj-captcha实现滑块验证码
集成以AJ-Captcha
滑块验证码为例,不需要键盘手动输入,极大优化了传统验证码用户体验不佳的问题。目前对外提供两种类型的验证码,其中包含滑动拼图、文字点选。
1、ruoyi-framework\pom.xml
添加依赖
<!-- 滑块验证码 -->
<dependency>
<groupId>com.github.anji-plus</groupId>
<artifactId>captcha-spring-boot-starter</artifactId>
<version>1.2.7</version>
</dependency>
2
3
4
5
6
2、修改application.yml
,加入aj-captcha
配置
# 滑块验证码
aj:
captcha:
# blockPuzzle滑块 clickWord文字点选 default默认两者都实例化
type: blockPuzzle
# 右下角显示字
water-mark: ruoyi.vip
# 校验滑动拼图允许误差偏移量(默认5像素)
slip-offset: 5
# aes加密坐标开启或者禁用(true|false)
aes-status: true
# 滑动干扰项(0/1/2)
interference-options: 2
2
3
4
5
6
7
8
9
10
11
12
13
3、下载插件相关包和代码实现覆盖到工程中
提示
下载前端插件相关包和代码实现ruoyi/集成滑动验证码.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
4、测试验证登录和注册页面滑块验证使用是否正常。
# 集成sharding-jdbc实现分库分表
sharding-jdbc
是由当当捐入给apache
的一款分布式数据库中间件,支持垂直分库、垂直分表、水平分库、水平分表、读写分离、分布式事务和高可用等相关功能。
1、ruoyi-framework\pom.xml
模块添加sharding-jdbc整合依赖
<!-- sharding-jdbc分库分表 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>4.1.1</version>
</dependency>
2
3
4
5
6
2、创建两个测试数据库
create database `ry-order1`;
create database `ry-order2`;
2
3、创建两个测试订单表
-- ----------------------------
-- 订单信息表sys_order_0
-- ----------------------------
drop table if exists sys_order_0;
create table sys_order_0
(
order_id bigint(20) not null comment '订单ID',
user_id bigint(64) not null comment '用户编号',
status char(1) not null comment '状态(0交易成功 1交易失败)',
order_no varchar(64) default null comment '订单流水',
primary key (order_id)
) engine=innodb comment = '订单信息表';
-- ----------------------------
-- 订单信息表sys_order_1
-- ----------------------------
drop table if exists sys_order_1;
create table sys_order_1
(
order_id bigint(20) not null comment '订单ID',
user_id bigint(64) not null comment '用户编号',
status char(1) not null comment '状态(0交易成功 1交易失败)',
order_no varchar(64) default null comment '订单流水',
primary key (order_id)
) engine=innodb comment = '订单信息表';
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
4、配置文件application-druid.yml
添加测试数据源
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
# 订单库1
order1:
enabled: true
url: jdbc:mysql://localhost:3306/ry-order1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
# 订单库2
order2:
enabled: true
url: jdbc:mysql://localhost:3306/ry-order2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
...................
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
5、下载插件相关包和代码实现覆盖到工程中
提示
下载插件相关包和代码实现ruoyi/集成sharding-jdbc实现分库分表.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
6、测试验证
访问http://localhost/order/add/1
入库到ry-order2
访问http://localhost/order/add/2
入库到ry-order1
同时根据订单号order_id % 2
入库到sys_order_0
或者sys_order_1
# 集成just-auth实现第三方授权登录
对于一些想使用第三方平台授权登录可以使用JustAuth
,支持Github、Gitee、微博、钉钉、百度、Coding、腾讯云开发者平台、OSChina、支付宝、QQ、微信、淘宝、Google、Facebook、抖音、领英、小米、微软、今日头条、Teambition、StackOverflow、Pinterest、人人、华为、企业微信、酷家乐、Gitlab、美团、饿了么和推特等第三方平台的授权登录。
1、ruoyi-common\pom.xml
模块添加整合依赖
<!-- 第三方授权登录 -->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>1.15.6</version>
</dependency>
<!-- HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
2、新建第三方登录授权表
-- ----------------------------
-- 第三方授权表
-- ----------------------------
drop table if exists sys_auth_user;
create table sys_auth_user (
auth_id bigint(20) not null auto_increment comment '授权ID',
uuid varchar(500) not null comment '第三方平台用户唯一ID',
user_id bigint(20) not null comment '系统用户ID',
login_name varchar(30) not null comment '登录账号',
user_name varchar(30) default '' comment '用户昵称',
avatar varchar(500) default '' comment '头像地址',
email varchar(255) default '' comment '用户邮箱',
source varchar(255) default '' comment '用户来源',
create_time datetime comment '创建时间',
primary key (auth_id)
) engine=innodb auto_increment=100 comment = '第三方授权表';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3、下载插件相关包和代码实现覆盖到工程中
提示
下载前端插件相关包和代码实现ruoyi/集成JustAuth实现第三方授权登录.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
4、测试登录页面第三方授权登录,个人中心授权及取消功能是否正常使用。
# 集成mybatis-plus实现mybatis增强
Mybatis-Plus
是在Mybatis
的基础上进行扩展,只做增强不做改变,可以兼容Mybatis
原生的特性。同时支持通用CRUD操作、多种主键策略、分页、性能分析、全局拦截等。极大帮助我们简化开发工作。
1、ruoyi-common\pom.xml
模块添加整合依赖
<!-- mybatis-plus 增强CRUD -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
2
3
4
5
6
2、ruoyi-admin
文件application.yml
,修改mybatis配置为mybatis-plus
# MyBatis Plus配置
mybatis-plus:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.**.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
2
3
4
5
6
7
8
3、添加Mybatis Plus
配置MybatisPlusConfig.java
。
PS:原来的MyBatisConfig.java
需要删除掉
package com.ruoyi.framework.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* Mybatis Plus 配置
*
* @author ruoyi
*/
@EnableTransactionManagement(proxyTargetClass = true)
@Configuration
public class MybatisPlusConfig
{
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor()
{
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
// 乐观锁插件
interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor());
// 阻断插件
interceptor.addInnerInterceptor(blockAttackInnerInterceptor());
return interceptor;
}
/**
* 分页插件,自动识别数据库类型 https://baomidou.com/guide/interceptor-pagination.html
*/
public PaginationInnerInterceptor paginationInnerInterceptor()
{
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
// 设置数据库类型为mysql
paginationInnerInterceptor.setDbType(DbType.MYSQL);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInnerInterceptor.setMaxLimit(-1L);
return paginationInnerInterceptor;
}
/**
* 乐观锁插件 https://baomidou.com/guide/interceptor-optimistic-locker.html
*/
public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor()
{
return new OptimisticLockerInnerInterceptor();
}
/**
* 如果是对全表的删除或更新操作,就会终止该操作 https://baomidou.com/guide/interceptor-block-attack.html
*/
public BlockAttackInnerInterceptor blockAttackInnerInterceptor()
{
return new BlockAttackInnerInterceptor();
}
}
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
4、添加测试表和菜单信息
drop table if exists sys_student;
create table sys_student (
student_id int(11) auto_increment comment '编号',
student_name varchar(30) default '' comment '学生名称',
student_age int(3) default null comment '年龄',
student_hobby varchar(30) default '' comment '爱好(0代码 1音乐 2电影)',
student_sex char(1) default '0' comment '性别(0男 1女 2未知)',
student_status char(1) default '0' comment '状态(0正常 1停用)',
student_birthday datetime comment '生日',
primary key (student_id)
) engine=innodb auto_increment=1 comment = '学生信息表';
-- 菜单 sql
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息', '3', '1', '/system/student', 'c', '0', 'system:student:view', '#', 'admin', sysdate(), '', null, '学生信息菜单');
-- 按钮父菜单id
select @parentid := last_insert_id();
-- 按钮 sql
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息查询', @parentid, '1', '#', 'f', '0', 'system:student:list', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息新增', @parentid, '2', '#', 'f', '0', 'system:student:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息修改', @parentid, '3', '#', 'f', '0', 'system:student:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息删除', @parentid, '4', '#', 'f', '0', 'system:student:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu (menu_name, parent_id, order_num, url, menu_type, visible, perms, icon, create_by, create_time, update_by, update_time, remark)
values('学生信息导出', @parentid, '5', '#', 'f', '0', 'system:student:export', '#', 'admin', sysdate(), '', null, '');
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
5、新增测试代码验证 新增 ruoyi-system\com\ruoyi\system\controller\SysStudentController.java
package com.ruoyi.system.controller;
import java.util.Arrays;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.SysStudent;
import com.ruoyi.system.service.ISysStudentService;
/**
* 学生信息Controller
*
* @author ruoyi
*/
@Controller
@RequestMapping("/system/student")
public class SysStudentController extends BaseController
{
private String prefix = "system/student";
@Autowired
private ISysStudentService sysStudentService;
@RequiresPermissions("system:student:view")
@GetMapping()
public String student()
{
return prefix + "/student";
}
/**
* 查询学生信息列表
*/
@RequiresPermissions("system:student:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(SysStudent sysStudent)
{
startPage();
List<SysStudent> list = sysStudentService.queryList(sysStudent);
return getDataTable(list);
}
/**
* 导出学生信息列表
*/
@RequiresPermissions("system:student:export")
@Log(title = "学生信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(SysStudent sysStudent)
{
List<SysStudent> list = sysStudentService.queryList(sysStudent);
ExcelUtil<SysStudent> util = new ExcelUtil<SysStudent>(SysStudent.class);
return util.exportExcel(list, "student");
}
/**
* 新增学生信息
*/
@GetMapping("/add")
public String add()
{
return prefix + "/add";
}
/**
* 新增保存学生信息
*/
@RequiresPermissions("system:student:add")
@Log(title = "学生信息", businessType = BusinessType.INSERT)
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(SysStudent sysStudent)
{
return toAjax(sysStudentService.save(sysStudent));
}
/**
* 修改学生信息
*/
@GetMapping("/edit/{studentId}")
public String edit(@PathVariable("studentId") Long studentId, ModelMap mmap)
{
SysStudent sysStudent = sysStudentService.getById(studentId);
mmap.put("sysStudent", sysStudent);
return prefix + "/edit";
}
/**
* 修改保存学生信息
*/
@RequiresPermissions("system:student:edit")
@Log(title = "学生信息", businessType = BusinessType.UPDATE)
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(SysStudent sysStudent)
{
return toAjax(sysStudentService.updateById(sysStudent));
}
/**
* 删除学生信息
*/
@RequiresPermissions("system:student:remove")
@Log(title = "学生信息", businessType = BusinessType.DELETE)
@PostMapping("/remove")
@ResponseBody
public AjaxResult remove(String ids)
{
return toAjax(sysStudentService.removeByIds(Arrays.asList(ids)));
}
}
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
新增 ruoyi-system\com\ruoyi\system\domain\SysStudent.java
package com.ruoyi.system.domain;
import java.io.Serializable;
import java.util.Date;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
/**
* 学生信息对象 sys_student
*
* @author ruoyi
*/
@TableName(value = "sys_student")
public class SysStudent implements Serializable
{
@TableField(exist = false)
private static final long serialVersionUID = 1L;
/** 编号 */
@TableId(type = IdType.AUTO)
private Long studentId;
/** 学生名称 */
@Excel(name = "学生名称")
private String studentName;
/** 年龄 */
@Excel(name = "年龄")
private Integer studentAge;
/** 爱好(0代码 1音乐 2电影) */
@Excel(name = "爱好", readConverterExp = "0=代码,1=音乐,2=电影")
private String studentHobby;
/** 性别(0男 1女 2未知) */
@Excel(name = "性别", readConverterExp = "0=男,1=女,2=未知")
private String studentSex;
/** 状态(0正常 1停用) */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String studentStatus;
/** 生日 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "生日", width = 30, dateFormat = "yyyy-MM-dd")
private Date studentBirthday;
public void setStudentId(Long studentId)
{
this.studentId = studentId;
}
public Long getStudentId()
{
return studentId;
}
public void setStudentName(String studentName)
{
this.studentName = studentName;
}
public String getStudentName()
{
return studentName;
}
public void setStudentAge(Integer studentAge)
{
this.studentAge = studentAge;
}
public Integer getStudentAge()
{
return studentAge;
}
public void setStudentHobby(String studentHobby)
{
this.studentHobby = studentHobby;
}
public String getStudentHobby()
{
return studentHobby;
}
public void setStudentSex(String studentSex)
{
this.studentSex = studentSex;
}
public String getStudentSex()
{
return studentSex;
}
public void setStudentStatus(String studentStatus)
{
this.studentStatus = studentStatus;
}
public String getStudentStatus()
{
return studentStatus;
}
public void setStudentBirthday(Date studentBirthday)
{
this.studentBirthday = studentBirthday;
}
public Date getStudentBirthday()
{
return studentBirthday;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("studentId", getStudentId())
.append("studentName", getStudentName())
.append("studentAge", getStudentAge())
.append("studentHobby", getStudentHobby())
.append("studentSex", getStudentSex())
.append("studentStatus", getStudentStatus())
.append("studentBirthday", getStudentBirthday())
.toString();
}
}
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
新增 ruoyi-system\com\ruoyi\system\mapper\SysStudentMapper.java
package com.ruoyi.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.domain.SysStudent;
/**
* 学生信息Mapper接口
*
* @author ruoyi
*/
public interface SysStudentMapper extends BaseMapper<SysStudent>
{
}
2
3
4
5
6
7
8
9
10
11
12
13
14
新增 ruoyi-system\com\ruoyi\system\service\ISysStudentService.java
package com.ruoyi.system.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.system.domain.SysStudent;
/**
* 学生信息Service接口
*
* @author ruoyi
*/
public interface ISysStudentService extends IService<SysStudent>
{
/**
* 查询学生信息列表
*
* @param sysStudent 学生信息
* @return 学生信息集合
*/
public List<SysStudent> queryList(SysStudent sysStudent);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
新增 ruoyi-system\com\ruoyi\system\service\impl\SysStudentServiceImpl.java
package com.ruoyi.system.service.impl;
import java.util.List;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.SysStudent;
import com.ruoyi.system.mapper.SysStudentMapper;
import com.ruoyi.system.service.ISysStudentService;
/**
* 学生信息Service业务层处理
*
* @author ruoyi
*/
@Service
public class SysStudentServiceImpl extends ServiceImpl<SysStudentMapper, SysStudent> implements ISysStudentService
{
@Override
public List<SysStudent> queryList(SysStudent sysStudent)
{
// 注意:mybatis-plus lambda 模式不支持 eclipse 的编译器
// LambdaQueryWrapper<SysStudent> queryWrapper = Wrappers.lambdaQuery();
// queryWrapper.eq(SysStudent::getStudentName, sysStudent.getStudentName());
QueryWrapper<SysStudent> queryWrapper = Wrappers.query();
if (StringUtils.isNotEmpty(sysStudent.getStudentName()))
{
queryWrapper.eq("student_name", sysStudent.getStudentName());
}
if (StringUtils.isNotNull(sysStudent.getStudentAge()))
{
queryWrapper.eq("student_age", sysStudent.getStudentAge());
}
if (StringUtils.isNotEmpty(sysStudent.getStudentHobby()))
{
queryWrapper.eq("student_hobby", sysStudent.getStudentHobby());
}
return this.list(queryWrapper);
}
}
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
新增 ruoyi-system\templates\system\student\add.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
<th:block th:include="include :: header('新增学生信息')" />
<th:block th:include="include :: datetimepicker-css" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-student-add">
<div class="form-group">
<label class="col-sm-3 control-label">学生名称:</label>
<div class="col-sm-8">
<input name="studentName" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">年龄:</label>
<div class="col-sm-8">
<input name="studentAge" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">爱好:</label>
<div class="col-sm-8">
<input name="studentHobby" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">性别:</label>
<div class="col-sm-8">
<select name="studentSex" class="form-control m-b">
<option value="">所有</option>
</select>
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">状态:</label>
<div class="col-sm-8">
<div class="radio-box">
<input type="radio" name="studentStatus" value="">
<label th:for="studentStatus" th:text="未知"></label>
</div>
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">生日:</label>
<div class="col-sm-8">
<div class="input-group date">
<input name="studentBirthday" class="form-control" placeholder="yyyy-MM-dd" type="text">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: datetimepicker-js" />
<script th:inline="javascript">
var prefix = ctx + "system/student"
$("#form-student-add").validate({
focusCleanup: true
});
function submitHandler() {
if ($.validate.form()) {
$.operate.save(prefix + "/add", $('#form-student-add').serialize());
}
}
$("input[name='studentBirthday']").datetimepicker({
format: "yyyy-mm-dd",
minView: "month",
autoclose: true
});
</script>
</body>
</html>
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
新增 ruoyi-system\templates\system\student\edit.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
<th:block th:include="include :: header('修改学生信息')" />
<th:block th:include="include :: datetimepicker-css" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-student-edit" th:object="${sysStudent}">
<input name="studentId" th:field="*{studentId}" type="hidden">
<div class="form-group">
<label class="col-sm-3 control-label">学生名称:</label>
<div class="col-sm-8">
<input name="studentName" th:field="*{studentName}" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">年龄:</label>
<div class="col-sm-8">
<input name="studentAge" th:field="*{studentAge}" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">爱好:</label>
<div class="col-sm-8">
<input name="studentHobby" th:field="*{studentHobby}" class="form-control" type="text">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">性别:</label>
<div class="col-sm-8">
<select name="studentSex" class="form-control m-b">
<option value="">所有</option>
</select>
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">状态:</label>
<div class="col-sm-8">
<div class="radio-box">
<input type="radio" name="studentStatus" value="">
<label th:for="studentStatus" th:text="未知"></label>
</div>
<span class="help-block m-b-none"><i class="fa fa-info-circle"></i> 代码生成请选择字典属性</span>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">生日:</label>
<div class="col-sm-8">
<div class="input-group date">
<input name="studentBirthday" th:value="${#dates.format(sysStudent.studentBirthday, 'yyyy-MM-dd')}" class="form-control" placeholder="yyyy-MM-dd" type="text">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: datetimepicker-js" />
<script th:inline="javascript">
var prefix = ctx + "system/student";
$("#form-student-edit").validate({
focusCleanup: true
});
function submitHandler() {
if ($.validate.form()) {
$.operate.save(prefix + "/edit", $('#form-student-edit').serialize());
}
}
$("input[name='studentBirthday']").datetimepicker({
format: "yyyy-mm-dd",
minView: "month",
autoclose: true
});
</script>
</body>
</html>
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
新增 ruoyi-system\templates\system\student\student.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('学生信息列表')" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li>
<label>学生名称:</label>
<input type="text" name="studentName"/>
</li>
<li>
<label>年龄:</label>
<input type="text" name="studentAge"/>
</li>
<li>
<label>爱好:</label>
<input type="text" name="studentHobby"/>
</li>
<li>
<label>性别:</label>
<select name="studentSex">
<option value="">所有</option>
<option value="-1">代码生成请选择字典属性</option>
</select>
</li>
<li>
<label>状态:</label>
<select name="studentStatus">
<option value="">所有</option>
<option value="-1">代码生成请选择字典属性</option>
</select>
</li>
<li>
<label>生日:</label>
<input type="text" class="time-input" placeholder="请选择生日" name="studentBirthday"/>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()"><i class="fa fa-search"></i> 搜索</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()"><i class="fa fa-refresh"></i> 重置</a>
</li>
</ul>
</div>
</form>
</div>
<div class="btn-group-sm" id="toolbar" role="group">
<a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="system:student:add">
<i class="fa fa-plus"></i> 添加
</a>
<a class="btn btn-primary single disabled" onclick="$.operate.edit()" shiro:hasPermission="system:student:edit">
<i class="fa fa-edit"></i> 修改
</a>
<a class="btn btn-danger multiple disabled" onclick="$.operate.removeAll()" shiro:hasPermission="system:student:remove">
<i class="fa fa-remove"></i> 删除
</a>
<a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="system:student:export">
<i class="fa fa-download"></i> 导出
</a>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var editFlag = [[${@permission.hasPermi('system:student:edit')}]];
var removeFlag = [[${@permission.hasPermi('system:student:remove')}]];
var prefix = ctx + "system/student";
$(function() {
var options = {
url: prefix + "/list",
createUrl: prefix + "/add",
updateUrl: prefix + "/edit/{id}",
removeUrl: prefix + "/remove",
exportUrl: prefix + "/export",
modalName: "学生信息",
columns: [{
checkbox: true
},
{
field: 'studentId',
title: '编号',
visible: false
},
{
field: 'studentName',
title: '学生名称'
},
{
field: 'studentAge',
title: '年龄'
},
{
field: 'studentHobby',
title: '爱好'
},
{
field: 'studentSex',
title: '性别'
},
{
field: 'studentStatus',
title: '状态'
},
{
field: 'studentBirthday',
title: '生日'
},
{
title: '操作',
align: 'center',
formatter: function(value, row, index) {
var actions = [];
actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.studentId + '\')"><i class="fa fa-edit"></i>编辑</a> ');
actions.push('<a class="btn btn-danger btn-xs ' + removeFlag + '" href="javascript:void(0)" onclick="$.operate.remove(\'' + row.studentId + '\')"><i class="fa fa-remove"></i>删除</a>');
return actions.join('');
}
}]
};
$.table.init(options);
});
</script>
</body>
</html>
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
6、登录系统测试学生菜单增删改查功能。
提示
下载相关代码实现示例 ruoyi/集成mybatisplus实现mybatis增强.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
# 集成easyexcel实现excel表格增强
如果默认的excel
注解已经满足不了你的需求,可以使用excel
的增强解决方案easyexcel
,它是阿里巴巴开源的一个excel
处理框架,使用简单、功能特性多、以节省内存著称。
1、ruoyi-common\pom.xml
模块添加整合依赖
<!-- easyexcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
2
3
4
5
6
2、ExcelUtil.java
新增easyexcel
导出导入方法
import com.alibaba.excel.EasyExcel;
/**
* 对excel表单默认第一个索引名转换成list(EasyExcel)
*
* @param is 输入流
* @return 转换后集合
*/
public List<T> importEasyExcel(InputStream is) throws Exception
{
return EasyExcel.read(is).head(clazz).sheet().doReadSync();
}
/**
* 对list数据源将其里面的数据导入到excel表单(EasyExcel)
*
* @param list 导出数据集合
* @param sheetName 工作表的名称
* @return 结果
*/
public AjaxResult exportEasyExcel(List<T> list, String sheetName)
{
String filename = encodingFilename(sheetName);
EasyExcel.write(getAbsoluteFile(filename), clazz).sheet(sheetName).doWrite(list);
return AjaxResult.success(filename);
}
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
3、模拟测试,以操作日志为例,修改相关类。
SysOperlogController.java改为exportEasyExcel
@Log(title = "操作日志", businessType = BusinessType.EXPORT)
@RequiresPermissions("monitor:operlog:export")
@PostMapping("/export")
@ResponseBody
public AjaxResult export(SysOperLog operLog)
{
List<SysOperLog> list = operLogService.selectOperLogList(operLog);
ExcelUtil<SysOperLog> util = new ExcelUtil<SysOperLog>(SysOperLog.class);
return util.exportEasyExcel(list, "操作日志");
}
2
3
4
5
6
7
8
9
10
SysOperLog.java修改为@ExcelProperty
注解
package com.ruoyi.system.domain;
import java.util.Date;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.HeadFontStyle;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.system.domain.read.BusiTypeStringNumberConverter;
import com.ruoyi.system.domain.read.OperTypeConverter;
import com.ruoyi.system.domain.read.StatusConverter;
/**
* 操作日志记录表 oper_log
*
* @author ruoyi
*/
@ExcelIgnoreUnannotated
@ColumnWidth(16)
@HeadRowHeight(14)
@HeadFontStyle(fontHeightInPoints = 11)
public class SysOperLog extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 日志主键 */
@ExcelProperty(value = "操作序号")
private Long operId;
/** 操作模块 */
@ExcelProperty(value = "操作模块")
private String title;
/** 业务类型(0其它 1新增 2修改 3删除) */
@ExcelProperty(value = "业务类型", converter = BusiTypeStringNumberConverter.class)
private Integer businessType;
/** 业务类型数组 */
private Integer[] businessTypes;
/** 请求方法 */
@ExcelProperty(value = "请求方法")
private String method;
/** 请求方式 */
@ExcelProperty(value = "请求方式")
private String requestMethod;
/** 操作类别(0其它 1后台用户 2手机端用户) */
@ExcelProperty(value = "操作类别", converter = OperTypeConverter.class)
private Integer operatorType;
/** 操作人员 */
@ExcelProperty(value = "操作人员")
private String operName;
/** 部门名称 */
@ExcelProperty(value = "部门名称")
private String deptName;
/** 请求url */
@ExcelProperty(value = "请求地址")
private String operUrl;
/** 操作地址 */
@ExcelProperty(value = "操作地址")
private String operIp;
/** 操作地点 */
@ExcelProperty(value = "操作地点")
private String operLocation;
/** 请求参数 */
@ExcelProperty(value = "请求参数")
private String operParam;
/** 返回参数 */
@ExcelProperty(value = "返回参数")
private String jsonResult;
/** 操作状态(0正常 1异常) */
@ExcelProperty(value = "状态", converter = StatusConverter.class)
private Integer status;
/** 错误消息 */
@ExcelProperty(value = "错误消息")
private String errorMsg;
/** 操作时间 */
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
@ExcelProperty(value = "操作时间")
private Date operTime;
/** 消耗时间 */
@ExcelProperty(value = "消耗时间")
private Long costTime;
......
}
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
添加字符串翻译内容
ruoyi-system\com\ruoyi\system\domain\read\BusiTypeStringNumberConverter.java
package com.ruoyi.system.domain.read;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* 业务类型字符串处理
*
* @author ruoyi
*/
@SuppressWarnings("rawtypes")
public class BusiTypeStringNumberConverter implements Converter<Integer>
{
@Override
public Class supportJavaTypeKey()
{
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey()
{
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration)
{
Integer value = 0;
String str = cellData.getStringValue();
if ("新增".equals(str))
{
value = 1;
}
else if ("修改".equals(str))
{
value = 2;
}
else if ("删除".equals(str))
{
value = 3;
}
else if ("授权".equals(str))
{
value = 4;
}
else if ("导出".equals(str))
{
value = 5;
}
else if ("导入".equals(str))
{
value = 6;
}
else if ("强退".equals(str))
{
value = 7;
}
else if ("生成代码".equals(str))
{
value = 8;
}
else if ("清空数据".equals(str))
{
value = 9;
}
return value;
}
@Override
public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration)
{
String str = "其他";
if (1 == value)
{
str = "新增";
}
else if (2 == value)
{
str = "修改";
}
else if (3 == value)
{
str = "删除";
}
else if (4 == value)
{
str = "授权";
}
else if (5 == value)
{
str = "导出";
}
else if (6 == value)
{
str = "导入";
}
else if (7 == value)
{
str = "强退";
}
else if (8 == value)
{
str = "生成代码";
}
else if (9 == value)
{
str = "清空数据";
}
return new CellData(str);
}
}
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
ruoyi-system\com\ruoyi\system\domain\read\OperTypeConverter.java
package com.ruoyi.system.domain.read;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* 操作类别字符串处理
*
* @author ruoyi
*/
@SuppressWarnings("rawtypes")
public class OperTypeConverter implements Converter<Integer>
{
@Override
public Class supportJavaTypeKey()
{
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey()
{
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration)
{
Integer value = 0;
String str = cellData.getStringValue();
if ("后台用户".equals(str))
{
value = 1;
}
else if ("手机端用户".equals(str))
{
value = 2;
}
return value;
}
@Override
public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration)
{
String str = "其他";
if (1 == value)
{
str = "后台用户";
}
else if (2 == value)
{
str = "手机端用户";
}
return new CellData(str);
}
}
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
ruoyi-system\com\ruoyi\system\domain\read\StatusConverter.java
package com.ruoyi.system.domain.read;
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
/**
* 状态字符串处理
*
* @author ruoyi
*/
@SuppressWarnings("rawtypes")
public class StatusConverter implements Converter<Integer>
{
@Override
public Class supportJavaTypeKey()
{
return Integer.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey()
{
return CellDataTypeEnum.STRING;
}
@Override
public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration)
{
return "正常".equals(cellData.getStringValue()) ? 0 : 1;
}
@Override
public CellData convertToExcelData(Integer value, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration)
{
return new CellData(0 == value ? "正常" : "异常");
}
}
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
4、登录系统,进入系统管理-日志管理-操作日志-执行导出功能
相关包和代码实现ruoyi/集成easyexcel实现excel表格增强.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
# 集成knife4j实现swagger文档增强
如果不习惯使用swagger
可以使用前端UI
的增强解决方案knife4j
,对比swagger
相比有以下优势,友好界面,离线文档,接口排序,安全控制,在线调试,文档清晰,注解增强,容易上手。
1、ruoyi-admin\pom.xml
模块添加整合依赖
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
2
3
4
5
6
2、SwaggerController.java
修改跳转访问地址
// 默认swagger-ui.html前端ui访问地址
public String index()
{
return redirect("/swagger-ui.html");
}
// 修改成knife4j前端ui访问地址doc.html
public String index()
{
return redirect("/doc.html");
}
2
3
4
5
6
7
8
9
10
3、登录系统,访问菜单系统工具/系统接口,出现如下图表示成功。
提示
引用knife4j-spring-boot-starter
依赖,项目中的swagger
依赖可以删除。
# 集成ueditor实现富文本编辑器增强
UEditor
是由百度前端研发部开发所见即所得富文本web编辑器,具有轻量、可定制、注重用户体验等特点。可以很好的满足国内用户的需求。
1、下载UEditor前端插件
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
ruoyi/集成ueditor实现富文本编辑器增强.zip
ruoyi-admin\src\main\resources\static\ajax\libs\ueditor
复制插件文件到自己的项目
2、ruoyi-admin\include.html
添加ueditor
<!-- ueditor富文本编辑器插件 -->
<div th:fragment="ueditor-js">
<script th:src="@{/ajax/libs/ueditor/ueditor.config.js}"></script>
<script th:src="@{/ajax/libs/ueditor/ueditor.all.min.js}"></script>
<script th:src="@{/ajax/libs/ueditor/lang/zh-cn/zh-cn.js}"></script>
</div>
2
3
4
5
6
3、修改通知公告相关页面
修改 templates\system\notice\add.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
<th:block th:include="include :: header('新增通知公告')" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-notice-add">
<div class="form-group">
<label class="col-sm-2 control-label is-required">公告标题:</label>
<div class="col-sm-10">
<input id="noticeTitle" name="noticeTitle" class="form-control" type="text" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">公告类型:</label>
<div class="col-sm-10">
<select name="noticeType" class="form-control m-b" th:with="type=${@dict.getType('sys_notice_type')}">
<option th:each="dict : ${type}" th:text="${dict.dictLabel}" th:value="${dict.dictValue}"></option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">公告内容:</label>
<div class="col-sm-10">
<script id="editor" name="noticeContent" type="text/plain" style="height: 300px;"></script>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">公告状态:</label>
<div class="col-sm-10">
<div class="radio-box" th:each="dict : ${@dict.getType('sys_notice_status')}">
<input type="radio" th:id="${dict.dictCode}" name="status" th:value="${dict.dictValue}" th:checked="${dict.default}">
<label th:for="${dict.dictCode}" th:text="${dict.dictLabel}"></label>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: ueditor-js" />
<script type="text/javascript">
var prefix = ctx + "system/notice";
var ue = UE.getEditor('editor');
function getContentTxt() {
return UE.getEditor('editor').getContentTxt();
}
$("#form-notice-add").validate({
focusCleanup: true
});
function submitHandler() {
if ($.validate.form()) {
var text = getContentTxt();
if (text == '' || text.length == 0) {
$.modal.alertWarning("请输入公告内容!");
return;
}
$.operate.save(prefix + "/add", $('#form-notice-add').serialize());
}
}
</script>
</body>
</html>
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
修改 templates\system\notice\edit.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" >
<head>
<th:block th:include="include :: header('修改通知公告')" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-notice-edit" th:object="${notice}">
<input id="noticeId" name="noticeId" th:field="*{noticeId}" type="hidden">
<div class="form-group">
<label class="col-sm-2 control-label is-required">公告标题:</label>
<div class="col-sm-10">
<input id="noticeTitle" name="noticeTitle" th:field="*{noticeTitle}" class="form-control" type="text" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">公告类型:</label>
<div class="col-sm-10">
<select name="noticeType" class="form-control m-b" th:with="type=${@dict.getType('sys_notice_type')}">
<option th:each="dict : ${type}" th:text="${dict.dictLabel}" th:value="${dict.dictValue}" th:field="*{noticeType}"></option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">公告内容:</label>
<div class="col-sm-10">
<script id="editor" name="noticeContent" type="text/plain" style="height: 300px;"></script>
<textarea id="noticeContent" style="display: none;">[[*{noticeContent}]]</textarea>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">公告状态:</label>
<div class="col-sm-10">
<div class="radio-box" th:each="dict : ${@dict.getType('sys_notice_status')}">
<input type="radio" th:id="${dict.dictCode}" name="status" th:value="${dict.dictValue}" th:field="*{status}">
<label th:for="${dict.dictCode}" th:text="${dict.dictLabel}"></label>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: ueditor-js" />
<script type="text/javascript">
var prefix = ctx + "system/notice";
$(function () {
var text = $("#noticeContent").text();
var ue = UE.getEditor('editor');
ue.ready(function () {
ue.setContent(text);
});
})
function getContentTxt() {
return UE.getEditor('editor').getContentTxt();
}
$("#form-notice-edit").validate({
focusCleanup: true
});
function submitHandler() {
if ($.validate.form()) {
var text = getContentTxt();
if (text == '' || text.length == 0) {
$.modal.alertWarning("请输入通知内容!");
return;
}
$.operate.save(prefix + "/edit", $('#form-notice-edit').serialize());
}
}
</script>
</body>
</html>
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
4、添加配置文件到ruoyi-admin\src\main\resources
新增 ueditor-config.json
/* 前后端通信相关的配置,注释只允许使用多行方式 */
{
/* 上传图片配置项 */
"imageActionName": "uploadimage", /* 执行上传图片的action名称 */
"imageFieldName": "upfile", /* 提交的图片表单名称 */
"imageMaxSize": 2048000, /* 上传大小限制,单位B */
"imageAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 上传图片格式显示 */
"imageCompressEnable": true, /* 是否压缩图片,默认是true */
"imageCompressBorder": 1600, /* 图片压缩最长边限制 */
"imageInsertAlign": "none", /* 插入的图片浮动方式 */
"imageUrlPrefix": "", /* 图片访问路径前缀 */
"imagePathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
/* {filename} 会替换成原文件名,配置这项需要注意中文乱码问题 */
/* {rand:6} 会替换成随机数,后面的数字是随机数的位数 */
/* {time} 会替换成时间戳 */
/* {yyyy} 会替换成四位年份 */
/* {yy} 会替换成两位年份 */
/* {mm} 会替换成两位月份 */
/* {dd} 会替换成两位日期 */
/* {hh} 会替换成两位小时 */
/* {ii} 会替换成两位分钟 */
/* {ss} 会替换成两位秒 */
/* 非法字符 \ : * ? " < > | */
/* 具请体看线上文档: fex.baidu.com/ueditor/#use-format_upload_filename */
/* 涂鸦图片上传配置项 */
"scrawlActionName": "uploadscrawl", /* 执行上传涂鸦的action名称 */
"scrawlFieldName": "upfile", /* 提交的图片表单名称 */
"scrawlPathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
"scrawlMaxSize": 2048000, /* 上传大小限制,单位B */
"scrawlUrlPrefix": "", /* 图片访问路径前缀 */
"scrawlInsertAlign": "none",
/* 截图工具上传 */
"snapscreenActionName": "uploadimage", /* 执行上传截图的action名称 */
"snapscreenPathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
"snapscreenUrlPrefix": "", /* 图片访问路径前缀 */
"snapscreenInsertAlign": "none", /* 插入的图片浮动方式 */
/* 抓取远程图片配置 */
"catcherLocalDomain": ["127.0.0.1", "localhost", "img.baidu.com"],
"catcherActionName": "catchimage", /* 执行抓取远程图片的action名称 */
"catcherFieldName": "source", /* 提交的图片列表表单名称 */
"catcherPathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
"catcherUrlPrefix": "", /* 图片访问路径前缀 */
"catcherMaxSize": 2048000, /* 上传大小限制,单位B */
"catcherAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 抓取图片格式显示 */
/* 上传视频配置 */
"videoActionName": "uploadvideo", /* 执行上传视频的action名称 */
"videoFieldName": "upfile", /* 提交的视频表单名称 */
"videoPathFormat": "/ueditor/jsp/upload/video/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
"videoUrlPrefix": "", /* 视频访问路径前缀 */
"videoMaxSize": 102400000, /* 上传大小限制,单位B,默认100MB */
"videoAllowFiles": [
".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid"], /* 上传视频格式显示 */
/* 上传文件配置 */
"fileActionName": "uploadfile", /* controller里,执行上传视频的action名称 */
"fileFieldName": "upfile", /* 提交的文件表单名称 */
"filePathFormat": "/ueditor/jsp/upload/file/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */
"fileUrlPrefix": "", /* 文件访问路径前缀 */
"fileMaxSize": 51200000, /* 上传大小限制,单位B,默认50MB */
"fileAllowFiles": [
".png", ".jpg", ".jpeg", ".gif", ".bmp",
".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
], /* 上传文件格式显示 */
/* 列出指定目录下的图片 */
"imageManagerActionName": "listimage", /* 执行图片管理的action名称 */
"imageManagerListPath": "/ueditor/jsp/upload/image/", /* 指定要列出图片的目录 */
"imageManagerListSize": 20, /* 每次列出文件数量 */
"imageManagerUrlPrefix": "", /* 图片访问路径前缀 */
"imageManagerInsertAlign": "none", /* 插入的图片浮动方式 */
"imageManagerAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 列出的文件类型 */
/* 列出指定目录下的文件 */
"fileManagerActionName": "listfile", /* 执行文件管理的action名称 */
"fileManagerListPath": "/ueditor/jsp/upload/file/", /* 指定要列出文件的目录 */
"fileManagerUrlPrefix": "", /* 文件访问路径前缀 */
"fileManagerListSize": 20, /* 每次列出文件数量 */
"fileManagerAllowFiles": [
".png", ".jpg", ".jpeg", ".gif", ".bmp",
".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
] /* 列出的文件类型 */
}
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
5、新增Ueditor
请求处理控制器
新增 ruoyi-admin\controller\common\UeditorController.java
package com.ruoyi.web.controller.common;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.config.ServerConfig;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.utils.file.FileUploadUtils;
/**
* Ueditor 请求处理
*
* @author ruoyi
*/
@SuppressWarnings("serial")
@Controller
@RequestMapping("/ajax/libs/ueditor")
public class UeditorController extends BaseController
{
private final String METHOD_HEAD = "ueditor";
private final String IMGE_PATH = "/ueditor/images/";
private final String VIDEO_PATH = "/ueditor/videos/";
private final String FILE_PATH = "/ueditor/files/";
@Autowired
private ServerConfig serverConfig;
/**
* ueditor
*/
@ResponseBody
@RequestMapping(value = "/ueditor/controller")
public Object ueditor(HttpServletRequest request, @RequestParam(value = "action", required = true) String action,
MultipartFile upfile) throws Exception
{
List<Object> param = new ArrayList<Object>()
{
{
add(action);
add(upfile);
}
};
Method method = this.getClass().getMethod(METHOD_HEAD + action, List.class, String.class);
return method.invoke(this.getClass().newInstance(), param, serverConfig.getUrl());
}
/**
* 读取配置文件
*/
public JSONObject ueditorconfig(List<Object> param, String fileSuffixUrl) throws Exception
{
ClassPathResource classPathResource = new ClassPathResource("ueditor-config.json");
String jsonString = new BufferedReader(new InputStreamReader(classPathResource.getInputStream())).lines().parallel().collect(Collectors.joining(System.lineSeparator()));
JSONObject json = JSON.parseObject(jsonString, JSONObject.class);
return json;
}
/**
* 上传图片
*/
public JSONObject ueditoruploadimage(List<Object> param, String fileSuffixUrl) throws Exception
{
JSONObject json = new JSONObject();
json.put("state", "SUCCESS");
json.put("url", ueditorcore(param, IMGE_PATH, false, fileSuffixUrl));
return json;
}
/**
* 上传视频
*/
public JSONObject ueditoruploadvideo(List<Object> param, String fileSuffixUrl) throws Exception
{
JSONObject json = new JSONObject();
json.put("state", "SUCCESS");
json.put("url", ueditorcore(param, VIDEO_PATH, false, fileSuffixUrl));
return json;
}
/**
* 上传附件
*/
public JSONObject ueditoruploadfile(List<Object> param, String fileSuffixUrl) throws Exception
{
JSONObject json = new JSONObject();
json.put("state", "SUCCESS");
json.put("url", ueditorcore(param, FILE_PATH, true, fileSuffixUrl));
return json;
}
public String ueditorcore(List<Object> param, String path, boolean isFileName, String fileSuffixUrl)
throws Exception
{
MultipartFile upfile = (MultipartFile) param.get(1);
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
String fileName = FileUploadUtils.upload(filePath, upfile);
String url = fileSuffixUrl + fileName;
return url;
}
}
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
6、登录系统,进入通知公告菜单测试富文本操作。
# 集成ip2region实现离线IP地址定位
离线IP地址定位库主要用于内网或想减少对外访问http
带来的资源消耗。(代码已兼容支持jar包部署)
1、引入依赖
<!-- 离线IP地址定位库 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>1.7.2</version>
</dependency>
2
3
4
5
6
2、添加工具类RegionUtil.java
package com.ruoyi.common.utils;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Method;
import org.apache.commons.io.FileUtils;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.lionsoul.ip2region.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
/**
* 根据ip离线查询地址
*
* @author ruoyi
*/
public class RegionUtil
{
private static final Logger log = LoggerFactory.getLogger(RegionUtil.class);
private static final String JAVA_TEMP_DIR = "java.io.tmpdir";
static DbConfig config = null;
static DbSearcher searcher = null;
/**
* 初始化IP库
*/
static
{
try
{
// 因为jar无法读取文件,复制创建临时文件
String dbPath = RegionUtil.class.getResource("/ip2region/ip2region.db").getPath();
File file = new File(dbPath);
if (!file.exists())
{
String tmpDir = System.getProperties().getProperty(JAVA_TEMP_DIR);
dbPath = tmpDir + "ip2region.db";
file = new File(dbPath);
ClassPathResource cpr = new ClassPathResource("ip2region" + File.separator + "ip2region.db");
InputStream resourceAsStream = cpr.getInputStream();
if (resourceAsStream != null)
{
FileUtils.copyInputStreamToFile(resourceAsStream, file);
}
}
config = new DbConfig();
searcher = new DbSearcher(config, dbPath);
log.info("bean [{}]", config);
log.info("bean [{}]", searcher);
}
catch (Exception e)
{
log.error("init ip region error:{}", e);
}
}
/**
* 解析IP
*
* @param ip
* @return
*/
public static String getRegion(String ip)
{
try
{
// db
if (searcher == null || StringUtils.isEmpty(ip))
{
log.error("DbSearcher is null");
return StringUtils.EMPTY;
}
long startTime = System.currentTimeMillis();
// 查询算法
int algorithm = DbSearcher.MEMORY_ALGORITYM;
Method method = null;
switch (algorithm)
{
case DbSearcher.BTREE_ALGORITHM:
method = searcher.getClass().getMethod("btreeSearch", String.class);
break;
case DbSearcher.BINARY_ALGORITHM:
method = searcher.getClass().getMethod("binarySearch", String.class);
break;
case DbSearcher.MEMORY_ALGORITYM:
method = searcher.getClass().getMethod("memorySearch", String.class);
break;
}
DataBlock dataBlock = null;
if (Util.isIpAddress(ip) == false)
{
log.warn("warning: Invalid ip address");
}
dataBlock = (DataBlock) method.invoke(searcher, ip);
String result = dataBlock.getRegion();
long endTime = System.currentTimeMillis();
log.debug("region use time[{}] result[{}]", endTime - startTime, result);
return result;
}
catch (Exception e)
{
log.error("error:{}", e);
}
return StringUtils.EMPTY;
}
}
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
3、修改AddressUtils.java
package com.ruoyi.common.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.config.RuoYiConfig;
/**
* 获取地址类
*
* @author ruoyi
*/
public class AddressUtils
{
private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);
// 未知地址
public static final String UNKNOWN = "XX XX";
public static String getRealAddressByIP(String ip)
{
String address = UNKNOWN;
// 内网不查询
if (IpUtils.internalIp(ip))
{
return "内网IP";
}
if (RuoYiConfig.isAddressEnabled())
{
try
{
String rspStr = RegionUtil.getRegion(ip);
if (StringUtils.isEmpty(rspStr))
{
log.error("获取地理位置异常 {}", ip);
return UNKNOWN;
}
String[] obj = rspStr.split("\\|");
String region = obj[2];
String city = obj[3];
return String.format("%s %s", region, city);
}
catch (Exception e)
{
log.error("获取地理位置异常 {}", e);
}
}
return address;
}
}
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
4、添加离线IP地址库插件
下载前端插件相关包和代码实现ruoyi/集成ip2region离线地址定位.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
5、添加离线IP地址库
在src/main/resources
下新建ip2region
复制文件ip2region.db
到目录下。
# 集成jsencrypt实现密码加密传输方式
目前登录接口密码是明文传输,如果安全性有要求,可以调整成加密方式传输。参考如下
1、修改前端login.js对密码进行rsa加密。
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
// 加密
function encrypt(txt) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
$(function() {
validateKickout();
validateRule();
$('.imgcode').click(function() {
var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random();
$(".imgcode").attr("src", url);
});
});
function login() {
var username = $.common.trim($("input[name='username']").val());
var password = $.common.trim($("input[name='password']").val());
var validateCode = $("input[name='validateCode']").val();
var rememberMe = $("input[name='rememberme']").is(':checked');
if($.common.isEmpty(validateCode) && captchaEnabled) {
$.modal.msg("请输入验证码");
return false;
}
$.ajax({
type: "post",
url: ctx + "login",
data: {
"username": username,
"password": encrypt(password),
"validateCode": validateCode,
"rememberMe": rememberMe
},
beforeSend: function () {
$.modal.loading($("#btnSubmit").data("loading"));
},
success: function(r) {
if (r.code == web_status.SUCCESS) {
location.href = ctx + 'index';
} else {
$('.imgcode').click();
$(".code").val("");
$.modal.msg(r.msg);
}
$.modal.closeLoading();
}
});
}
function validateRule() {
var icon = "<i class='fa fa-times-circle'></i> ";
$("#signupForm").validate({
rules: {
username: {
required: true
},
password: {
required: true
}
},
messages: {
username: {
required: icon + "请输入您的用户名",
},
password: {
required: icon + "请输入您的密码",
}
},
submitHandler: function(form) {
login();
}
})
}
function validateKickout() {
if (getParam("kickout") == 1) {
layer.alert("<font color='red'>您已在别处登录,请您修改密码或重新登录</font>", {
icon: 0,
title: "系统提示"
},
function(index) {
//关闭弹窗
layer.close(index);
if (top != self) {
top.location = self.location;
} else {
var url = location.search;
if (url) {
var oldUrl = window.location.href;
var newUrl = oldUrl.substring(0, oldUrl.indexOf('?'));
self.location = newUrl;
}
}
});
}
}
function getParam(paramName) {
var reg = new RegExp("(^|&)" + paramName + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return decodeURI(r[2]);
return null;
}
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
2、修改login.html文件,引入jsencrypt插件
<script src="../static/js/jsencrypt.min.js" th:src="@{/js/jsencrypt.min.js}"></script>
3、工具类security包下添加RsaUtils.java,用于RSA加密解密。
package com.ruoyi.common.utils.security;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* RSA加密解密
*
* @author ruoyi
**/
public class RsaUtils
{
// Rsa 私钥
public static String privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY"
+ "7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN"
+ "PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA"
+ "kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow"
+ "cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv"
+ "DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh"
+ "YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3" + "UP8iWi1Qw0Y=";
/**
* 私钥解密
*
* @param privateKeyString 私钥
* @param text 待解密的文本
* @return 解密后的文本
*/
public static String decryptByPrivateKey(String text) throws Exception
{
return decryptByPrivateKey(privateKey, text);
}
/**
* 公钥解密
*
* @param publicKeyString 公钥
* @param text 待解密的信息
* @return 解密后的文本
*/
public static String decryptByPublicKey(String publicKeyString, String text) throws Exception
{
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
/**
* 私钥加密
*
* @param privateKeyString 私钥
* @param text 待加密的信息
* @return 加密后的文本
*/
public static String encryptByPrivateKey(String privateKeyString, String text) throws Exception
{
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 私钥解密
*
* @param privateKeyString 私钥
* @param text 待解密的文本
* @return 解密后的文本
*/
public static String decryptByPrivateKey(String privateKeyString, String text) throws Exception
{
PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(Base64.decodeBase64(text));
return new String(result);
}
/**
* 公钥加密
*
* @param publicKeyString 公钥
* @param text 待加密的文本
* @return 加密后的文本
*/
public static String encryptByPublicKey(String publicKeyString, String text) throws Exception
{
X509EncodedKeySpec x509EncodedKeySpec2 = new X509EncodedKeySpec(Base64.decodeBase64(publicKeyString));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec2);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(text.getBytes());
return Base64.encodeBase64String(result);
}
/**
* 构建RSA密钥对
*
* @return 生成后的公私钥信息
*/
public static RsaKeyPair generateKeyPair() throws NoSuchAlgorithmException
{
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
String publicKeyString = Base64.encodeBase64String(rsaPublicKey.getEncoded());
String privateKeyString = Base64.encodeBase64String(rsaPrivateKey.getEncoded());
return new RsaKeyPair(publicKeyString, privateKeyString);
}
/**
* RSA密钥对对象
*/
public static class RsaKeyPair
{
private final String publicKey;
private final String privateKey;
public RsaKeyPair(String publicKey, String privateKey)
{
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public String getPublicKey()
{
return publicKey;
}
public String getPrivateKey()
{
return privateKey;
}
}
}
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
4、登录方法SysLoginController.java,对密码进行rsa解密。
@Controller
public class SysLoginController extends BaseController
{
@PostMapping("/login")
@ResponseBody
public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe)
{
try
{
UsernamePasswordToken token = new UsernamePasswordToken(username, RsaUtils.decryptByPrivateKey(password), rememberMe);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
return success();
}
catch (Exception e)
{
String msg = "用户或密码错误";
if (StringUtils.isNotEmpty(e.getMessage()))
{
msg = e.getMessage();
}
return error(msg);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
4、测试访问验证
访问 http://localhost/login 登录页面。提交时检查密码是否为加密传输,且后台也能正常解密。
下载前端插件相关包和代码实现ruoyi/集成jsencrypt实现密码加密传输方式.zip
链接: https://pan.baidu.com/s/1y1g8NkelRT_pS0fIbmyP8g 提取码: mjs7
# 集成httpclient实现http接口增强
HTTP
协议是互联网上使用得最多、最重要的协议之一,越来越多的Java
应用程序需要直接通过HTTP
协议来访问网络资源。虽然在JDK
的java net
包中已经提供了访问HTTP
协议的基本功能,但是对于大部分应用程序来说,JDK
库本身提供的功能还不够丰富和灵活。HttpClient
是Apache Jakarta Common
下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP
协议的客户端编程工具包,并且它支持HTTP
协议最新的版本和建议。HttpClient
已经应用在很多的项目中。
1、ruoyi-common\pom.xml
模块添加整合依赖
<!-- httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
2
3
4
5
2、新增http
配置信息类
package com.ruoyi.common.utils.http;
/**
* http 配置信息
*
* @author ruoyi
*/
public class HttpConf
{
// 获取连接的最大等待时间
public static int WAIT_TIMEOUT = 10000;
// 连接超时时间
public static int CONNECT_TIMEOUT = 10000;
// 读取超时时间
public static int SO_TIMEOUT = 60000;
// 最大连接数
public static int MAX_TOTAL_CONN = 200;
// 每个路由最大连接数
public static int MAX_ROUTE_CONN = 150;
// 重试次数
public static int RETRY_COUNT = 3;
// EPTWebServes地址
public static String EPTWEBSERVES_URL;
// tomcat默认keepAliveTimeout为20s
public static int KEEP_ALIVE_TIMEOUT;
}
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
3、新增连接池清理类
package com.ruoyi.common.utils.http;
import java.util.concurrent.TimeUnit;
import org.apache.http.conn.HttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 连接池清理
*
* @author ruoyi
*/
public class IdleConnectionMonitorThread extends Thread
{
private static final Logger log = LoggerFactory.getLogger(IdleConnectionMonitorThread.class);
private final HttpClientConnectionManager connMgr;
private volatile boolean shutdown;
public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr)
{
super();
this.shutdown = false;
this.connMgr = connMgr;
}
@Override
public void run()
{
while (!shutdown)
{
try
{
synchronized (this)
{
// 每5秒检查一次关闭连接
wait(HttpConf.KEEP_ALIVE_TIMEOUT / 4);
// 关闭失效的连接
connMgr.closeExpiredConnections();
// 可选的, 关闭20秒内不活动的连接
connMgr.closeIdleConnections(HttpConf.KEEP_ALIVE_TIMEOUT, TimeUnit.MILLISECONDS);
// log.debug("关闭失效的连接");
}
}
catch (Exception e)
{
log.error("关闭失效连接异常", e);
}
}
}
public void shutdown()
{
shutdown = true;
if (connMgr != null)
{
try
{
connMgr.shutdown();
}
catch (Exception e)
{
log.error("连接池异常", e);
}
}
synchronized (this)
{
notifyAll();
}
}
}
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
4、修改HttpUtils.java
请求类
package com.ruoyi.common.utils.http;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.StringUtils;
/**
* 通用http发送方法
*
* @author ruoyi
*/
public class HttpUtils
{
private static final Logger log = LoggerFactory.getLogger(HttpUtils.class);
public static RequestConfig requestConfig;
private static CloseableHttpClient httpClient;
private static PoolingHttpClientConnectionManager connMgr;
private static IdleConnectionMonitorThread idleThread;
static
{
HttpUtils.initClient();
}
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url)
{
return sendGet(url, StringUtils.EMPTY);
}
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param)
{
return sendGet(url, param, Constants.UTF8);
}
/**
* 向指定 URL 发送GET方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @param contentType 编码类型
* @return 所代表远程资源的响应结果
*/
public static String sendGet(String url, String param, String contentType)
{
StringBuilder result = new StringBuilder();
BufferedReader in = null;
try
{
String urlNameString = StringUtils.isNotBlank(param) ? url + "?" + param : url;
log.info("sendGet - {}", urlNameString);
URL realUrl = new URL(urlNameString);
URLConnection connection = realUrl.openConnection();
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
connection.connect();
in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
log.info("recv - {}", result);
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e);
}
finally
{
try
{
if (in != null)
{
in.close();
}
}
catch (Exception ex)
{
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, String param)
{
PrintWriter out = null;
BufferedReader in = null;
StringBuilder result = new StringBuilder();
try
{
log.info("sendPost - {}", url);
URL realUrl = new URL(url);
URLConnection conn = realUrl.openConnection();
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setDoOutput(true);
conn.setDoInput(true);
out = new PrintWriter(conn.getOutputStream());
out.print(param);
out.flush();
in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
log.info("recv - {}", result);
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e);
}
finally
{
try
{
if (out != null)
{
out.close();
}
if (in != null)
{
in.close();
}
}
catch (IOException ex)
{
log.error("调用in.close Exception, url=" + url + ",param=" + param, ex);
}
}
return result.toString();
}
public static String sendSSLPost(String url, String param)
{
StringBuilder result = new StringBuilder();
String urlNameString = url + "?" + param;
try
{
log.info("sendSSLPost - {}", urlNameString);
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());
URL console = new URL(urlNameString);
HttpsURLConnection conn = (HttpsURLConnection) console.openConnection();
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setSSLSocketFactory(sc.getSocketFactory());
conn.setHostnameVerifier(new TrustAnyHostnameVerifier());
conn.connect();
InputStream is = conn.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String ret = "";
while ((ret = br.readLine()) != null)
{
if (ret != null && !ret.trim().equals(""))
{
result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8));
}
}
log.info("recv - {}", result);
conn.disconnect();
br.close();
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e);
}
catch (Exception e)
{
log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e);
}
return result.toString();
}
private static class TrustAnyTrustManager implements X509TrustManager
{
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
{
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
{
}
@Override
public X509Certificate[] getAcceptedIssuers()
{
return new X509Certificate[] {};
}
}
private static class TrustAnyHostnameVerifier implements HostnameVerifier
{
@Override
public boolean verify(String hostname, SSLSession session)
{
return true;
}
}
/**
* 获取httpClient
*
* @return
*/
public static CloseableHttpClient getHttpClient()
{
if (httpClient != null)
{
return httpClient;
}
else
{
return HttpClients.createDefault();
}
}
/**
* 创建连接池管理器
*
* @return
*/
private static PoolingHttpClientConnectionManager createConnectionManager()
{
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager();
// 将最大连接数增加到
connMgr.setMaxTotal(HttpConf.MAX_TOTAL_CONN);
// 将每个路由基础的连接增加到
connMgr.setDefaultMaxPerRoute(HttpConf.MAX_ROUTE_CONN);
return connMgr;
}
/**
* 根据当前配置创建HTTP请求配置参数。
*
* @return 返回HTTP请求配置。
*/
private static RequestConfig createRequestConfig()
{
Builder builder = RequestConfig.custom();
builder.setConnectionRequestTimeout(StringUtils.nvl(HttpConf.WAIT_TIMEOUT, 10000));
builder.setConnectTimeout(StringUtils.nvl(HttpConf.CONNECT_TIMEOUT, 10000));
builder.setSocketTimeout(StringUtils.nvl(HttpConf.SO_TIMEOUT, 60000));
return builder.build();
}
/**
* 创建默认的HTTPS客户端,信任所有的证书。
*
* @return 返回HTTPS客户端,如果创建失败,返回HTTP客户端。
*/
private static CloseableHttpClient createHttpClient(HttpClientConnectionManager connMgr)
{
try
{
final SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy()
{
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException
{
// 信任所有
return true;
}
}).build();
final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
// 重试机制
HttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(HttpConf.RETRY_COUNT, true);
ConnectionKeepAliveStrategy connectionKeepAliveStrategy = new ConnectionKeepAliveStrategy()
{
@Override
public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext)
{
return HttpConf.KEEP_ALIVE_TIMEOUT; // tomcat默认keepAliveTimeout为20s
}
};
httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).setConnectionManager(connMgr)
.setDefaultRequestConfig(requestConfig).setRetryHandler(retryHandler)
.setKeepAliveStrategy(connectionKeepAliveStrategy).build();
}
catch (Exception e)
{
log.error("Create http client failed", e);
httpClient = HttpClients.createDefault();
}
return httpClient;
}
/**
* 初始化 只需调用一次
*/
public synchronized static CloseableHttpClient initClient()
{
if (httpClient == null)
{
connMgr = createConnectionManager();
requestConfig = createRequestConfig();
// 初始化httpClient连接池
httpClient = createHttpClient(connMgr);
// 清理连接池
idleThread = new IdleConnectionMonitorThread(connMgr);
idleThread.start();
}
return httpClient;
}
/**
* 关闭HTTP客户端。
*
* @param httpClient HTTP客户端。
*/
public synchronized static void shutdown()
{
try
{
if (idleThread != null)
{
idleThread.shutdown();
idleThread = null;
}
}
catch (Exception e)
{
log.error("httpclient connection manager close", e);
}
try
{
if (httpClient != null)
{
httpClient.close();
httpClient = null;
}
}
catch (IOException e)
{
log.error("httpclient close", e);
}
}
/**
* 请求上游 GET提交
*
* @param uri
* @throws IOException
*/
public static String getCall(final String uri) throws Exception
{
return getCall(uri, null, Constants.UTF8);
}
/**
* 请求上游 GET提交
*
* @param uri
* @param contentType
* @throws IOException
*/
public static String getCall(final String uri, String contentType) throws Exception
{
return getCall(uri, contentType, Constants.UTF8);
}
/**
* 请求上游 GET提交
*
* @param uri
* @param contentType
* @param charsetName
* @throws IOException
*/
public static String getCall(final String uri, String contentType, String charsetName) throws Exception
{
final String url = uri;
final HttpGet httpGet = new HttpGet(url);
httpGet.setConfig(requestConfig);
if (!StringUtils.isEmpty(contentType))
{
httpGet.addHeader("Content-Type", contentType);
}
final CloseableHttpResponse httpRsp = getHttpClient().execute(httpGet);
try
{
if (httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_OK
|| httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_FORBIDDEN)
{
final HttpEntity entity = httpRsp.getEntity();
final String rspText = EntityUtils.toString(entity, charsetName);
EntityUtils.consume(entity);
return rspText;
}
else
{
throw new IOException("HTTP StatusCode=" + httpRsp.getStatusLine().getStatusCode());
}
}
finally
{
try
{
httpRsp.close();
}
catch (Exception e)
{
log.error("关闭httpRsp异常", e);
}
}
}
/**
* 请求上游 POST提交
*
* @param uri
* @param paramsMap
* @throws IOException
*/
public static String postCall(final String uri, Map<String, Object> paramsMap) throws Exception
{
return postCall(uri, null, paramsMap, Constants.UTF8);
}
/**
* 请求上游 POST提交
*
* @param uri
* @param contentType
* @param paramsMap
* @throws IOException
*/
public static String postCall(final String uri, String contentType, Map<String, Object> paramsMap) throws Exception
{
return postCall(uri, contentType, paramsMap, Constants.UTF8);
}
/**
* 请求上游 POST提交
*
* @param uri
* @param contentType
* @param paramsMap
* @param charsetName
* @throws IOException
*/
public static String postCall(final String uri, String contentType, Map<String, Object> paramsMap,
String charsetName) throws Exception
{
final String url = uri;
final HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
if (!StringUtils.isEmpty(contentType))
{
httpPost.addHeader("Content-Type", contentType);
}
// 添加参数
List<NameValuePair> list = new ArrayList<NameValuePair>();
if (paramsMap != null)
{
for (Map.Entry<String, Object> entry : paramsMap.entrySet())
{
list.add(new BasicNameValuePair(entry.getKey(), (String) entry.getValue()));
}
}
httpPost.setEntity(new UrlEncodedFormEntity(list, charsetName));
final CloseableHttpResponse httpRsp = getHttpClient().execute(httpPost);
try
{
if (httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
final HttpEntity entity = httpRsp.getEntity();
final String rspText = EntityUtils.toString(entity, charsetName);
EntityUtils.consume(entity);
return rspText;
}
else
{
throw new IOException("HTTP StatusCode=" + httpRsp.getStatusLine().getStatusCode());
}
}
finally
{
try
{
httpRsp.close();
}
catch (Exception e)
{
log.error("关闭httpRsp异常", e);
}
}
}
/**
* 请求上游 POST提交
*
* @param uri
* @param param
* @throws IOException
*/
public static String postCall(final String uri, String param) throws Exception
{
return postCall(uri, null, param, Constants.UTF8);
}
/**
* 请求上游 POST提交
*
* @param uri
* @param contentType
* @param param
* @throws IOException
*/
public static String postCall(final String uri, String contentType, String param) throws Exception
{
return postCall(uri, contentType, param, Constants.UTF8);
}
/**
* 请求上游 POST提交
*
* @param uri
* @param contentType
* @param param
* @param charsetName
* @throws IOException
*/
public static String postCall(final String uri, String contentType, String param, String charsetName)
throws Exception
{
final String url = uri;
final HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
if (!StringUtils.isEmpty(contentType))
{
httpPost.addHeader("Content-Type", contentType);
}
else
{
httpPost.addHeader("Content-Type", "application/json");
}
// 添加参数
StringEntity paramEntity = new StringEntity(param, charsetName);
httpPost.setEntity(paramEntity);
final CloseableHttpResponse httpRsp = getHttpClient().execute(httpPost);
try
{
if (httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
final HttpEntity entity = httpRsp.getEntity();
final String rspText = EntityUtils.toString(entity, charsetName);
EntityUtils.consume(entity);
return rspText;
}
else
{
throw new IOException("HTTP StatusCode=" + httpRsp.getStatusLine().getStatusCode());
}
}
finally
{
try
{
httpRsp.close();
}
catch (Exception e)
{
log.error("关闭httpRsp异常", e);
}
}
}
/**
* 判断HTTP异常是否为读取超时。
*
* @param e 异常对象。
* @return 如果是读取引起的异常(而非连接),则返回true;否则返回false。
*/
public static boolean isReadTimeout(final Throwable e)
{
return (!isCausedBy(e, ConnectTimeoutException.class) && isCausedBy(e, SocketTimeoutException.class));
}
/**
* 检测异常e被触发的原因是不是因为异常cause。检测被封装的异常。
*
* @param e 捕获的异常。
* @param cause 异常触发原因。
* @return 如果异常e是由cause类异常触发,则返回true;否则返回false。
*/
public static boolean isCausedBy(final Throwable e, final Class<? extends Throwable> cause)
{
if (cause.isAssignableFrom(e.getClass()))
{
return true;
}
else
{
Throwable t = e.getCause();
while (t != null && t != e)
{
if (cause.isAssignableFrom(t.getClass()))
{
return true;
}
t = t.getCause();
}
return false;
}
}
}
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
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
5、在ShutdownManager.java
关闭http连接线程池
@PreDestroy
public void destroy()
{
....
HttpUtils.shutdown();
}
2
3
4
5
6
6、测试验证
Map<String, Object> paramsMap = new HashMap<String, Object>();
paramsMap.put("id", "1");
paramsMap.put("name", "ruoyi");
String json = "{\"id\": 1, \"name\": \"ry\"}";
HttpUtils.getCall(uri);
HttpUtils.postCall(uri, paramsMap);
HttpUtils.postCall(uri, json);
2
3
4
5
6
7
8
9
# 集成druid实现数据库密码加密功能
数据库密码直接写在配置中,对运维安全来说,是一个很大的挑战。可以使用Druid
为此提供一种数据库密码加密的手段ConfigFilter
。项目已经集成druid
所以只需按要求配置即可。
1、执行命令加密数据库密码
java -cp druid-1.2.4.jar com.alibaba.druid.filter.config.ConfigTools password
password
输入你的数据库密码,输出的是加密后的结果,版本号视情况而定。
privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuLMVAFmcew+mPfVnzI6utEvhHWO2s6e4R1bVW3a9IpH+pEypeNV6KtZ/w9PuysPfdPxW5fN3BmnKFZUAIMvWhQIDAQABAkA6rnsfr1juKFyzFsMx1KthETKmucWUctczoz0KYEFbN+joNsd/ApQqsS/2MVG1QWbDJLUsSLWkchvRbtiqOlVJAiEA6KmgVeLR2qUU9gv6DJfuWk4Ol1M9GJnTamgyDttsSGcCIQDLOdjcht29s954vApG1fiPTP/kMvZ5aLrccw1lEuEGMwIhAKoe3c3u++MTsi/2se9jaDU/vguIIbRLRfsYFQIoDxUhAiAnCm/cvZPvk5RTgVxAC276qIIoJpou7K2pF/kkx6Gu/QIgKUVFiM8GVZkOWZC+nUm3UIfpGjrKXjvGrlHNvt89uBA=
publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==
password:gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA==
2
3
2、配置数据源,提示Druid
数据源需要对数据库密码进行解密。
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA==
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
connectProperties: config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
filter:
config:
# 是否配置加密
enabled: true
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
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
3、DruidProperties
配置connectProperties
属性
package com.ruoyi.framework.config.properties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import com.alibaba.druid.pool.DruidDataSource;
/**
* druid 配置属性
*
* @author ruoyi
*/
@Configuration
public class DruidProperties
{
@Value("${spring.datasource.druid.initialSize}")
private int initialSize;
@Value("${spring.datasource.druid.minIdle}")
private int minIdle;
@Value("${spring.datasource.druid.maxActive}")
private int maxActive;
@Value("${spring.datasource.druid.maxWait}")
private int maxWait;
@Value("${spring.datasource.druid.connectTimeout}")
private int connectTimeout;
@Value("${spring.datasource.druid.socketTimeout}")
private int socketTimeout;
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
private int maxEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.druid.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.druid.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.testOnReturn}")
private boolean testOnReturn;
@Value("${spring.datasource.druid.connectProperties}")
private String connectProperties;
public DruidDataSource dataSource(DruidDataSource datasource)
{
/** 配置初始化大小、最小、最大 */
datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);
/** 配置获取连接等待超时的时间 */
datasource.setMaxWait(maxWait);
/** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */
datasource.setConnectTimeout(connectTimeout);
/** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */
datasource.setSocketTimeout(socketTimeout);
/** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
/** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
/**
* 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
*/
datasource.setValidationQuery(validationQuery);
/** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
datasource.setTestWhileIdle(testWhileIdle);
/** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnBorrow(testOnBorrow);
/** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnReturn(testOnReturn);
/** 为数据库密码提供加密功能 */
datasource.setConnectionProperties(connectProperties);
return datasource;
}
}
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
4、启动应用程序测试验证加密结果
提示
如若忘记密码可以使用工具类解密(传入生成的公钥+密码)
public static void main(String[] args) throws Exception
{
String password = ConfigTools.decrypt(
"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALizFQBZnHsPpj31Z8yOrrRL4R1jtrOnuEdW1Vt2vSKR/qRMqXjVeirWf8PT7srD33T8VuXzdwZpyhWVACDL1oUCAwEAAQ==",
"gkYlljNHKe0/4z7bbJxD7v/txWJIFbiGWwsIPo176Q7fG0UjcSizNxuRUI2ll27ZPQf2ekiHFptus2/Rc4cmvA==");
System.out.println("解密密码:" + password);
}
2
3
4
5
6
7
# 集成yuicompressor实现(CSS/JS压缩)
在Maven
打包的时候可以使用YUI Compressor
(压缩CSS/JS)文件,使用yuicompressor-maven-plugin
插件进行压缩后会减小体积,提高请求速度。
在pom.xml
文件中增加该插件的定义,示例如下:
<build>
<plugins>
<!-- YUI Compressor (CSS/JS压缩) -->
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>yuicompressor-maven-plugin</artifactId>
<version>1.5.1</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>compress</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 读取js,css文件采用UTF-8编码 -->
<encoding>UTF-8</encoding>
<!-- 是否忽略警告 -->
<jswarn>false</jswarn>
<!-- 是否添加.min后缀 -->
<nosuffix>true</nosuffix>
<!-- 压缩多少字节换行 -->
<linebreakpos>50000</linebreakpos>
<!-- 源目录,即需压缩的根目录 -->
<sourceDirectory>src/main/resources/static</sourceDirectory>
<!-- 若存在已压缩的文件,会先对比源文件是否有改动。有改动便压缩,无改动就不压缩 -->
<force>true</force>
<includes>
<include>**/*.js</include>
<include>**/*.css</include>
</includes>
<excludes>
<exclude>**/*.min.js</exclude>
<exclude>**/*.min.css</exclude>
<exclude>**/fileinput.js</exclude>
<exclude>**/bootstrap-treetable.js</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
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
# 集成watermark实现页面添加水印
在网站浏览中,常常需要网页水印,以便防止用户截图或录屏暴露敏感信息后,方便追踪用户来源。
1、在ry-ui.js
文件通用方法中增加watermark
方法,示例如下:
// 为网页添加文字水印
watermark: function(settings) {
// 默认设置
var defaultSettings = {
watermark_txt: "text",
watermark_x: 20,
// 水印起始位置x轴坐标
watermark_y: 20,
// 水印起始位置Y轴坐标
watermark_rows: 100,
// 水印行数
watermark_cols: 20,
// 水印列数
watermark_x_space: 10,
// 水印x轴间隔
watermark_y_space: 10,
// 水印y轴间隔
watermark_color: '#aaa',
// 水印字体颜色
watermark_alpha: 0.3,
// 水印透明度
watermark_fontsize: '15px',
// 水印字体大小
watermark_font: '微软雅黑',
// 水印字体
watermark_width: 150,
// 水印宽度
watermark_height: 80,
// 水印长度
watermark_angle: 15 // 水印倾斜度数
};
// 采用配置项替换默认值,作用类似jquery.extend
if (arguments.length === 1 && typeof arguments[0] === "object") {
var src = arguments[0] || {};
for (key in src) {
if (src[key] && defaultSettings[key] && src[key] === defaultSettings[key]) continue;
else if (src[key]) defaultSettings[key] = src[key];
}
}
var oTemp = document.createDocumentFragment();
// 获取页面最大宽度
var page_width = Math.max(document.body.scrollWidth, document.body.clientWidth);
var cutWidth = page_width * 0.0150;
page_width = page_width - cutWidth;
// 获取页面最大高度
var page_height = Math.max(document.body.scrollHeight - 80, document.body.clientHeight - 40);
// var page_height = document.body.scrollHeight+document.body.scrollTop;
// 如果将水印列数设置为0,或水印列数设置过大,超过页面最大宽度,则重新计算水印列数和水印x轴间隔
if (defaultSettings.watermark_cols == 0 || (parseInt(defaultSettings.watermark_x + defaultSettings.watermark_width * defaultSettings.watermark_cols + defaultSettings.watermark_x_space * (defaultSettings.watermark_cols - 1)) > page_width)) {
defaultSettings.watermark_cols = parseInt((page_width - defaultSettings.watermark_x + defaultSettings.watermark_x_space) / (defaultSettings.watermark_width + defaultSettings.watermark_x_space));
defaultSettings.watermark_x_space = parseInt((page_width - defaultSettings.watermark_x - defaultSettings.watermark_width * defaultSettings.watermark_cols) / (defaultSettings.watermark_cols - 1));
}
// 如果将水印行数设置为0,或水印行数设置过大,超过页面最大长度,则重新计算水印行数和水印y轴间隔
if (defaultSettings.watermark_rows == 0 || (parseInt(defaultSettings.watermark_y + defaultSettings.watermark_height * defaultSettings.watermark_rows + defaultSettings.watermark_y_space * (defaultSettings.watermark_rows - 1)) > page_height)) {
defaultSettings.watermark_rows = parseInt((defaultSettings.watermark_y_space + page_height - defaultSettings.watermark_y) / (defaultSettings.watermark_height + defaultSettings.watermark_y_space));
defaultSettings.watermark_y_space = parseInt(((page_height - defaultSettings.watermark_y) - defaultSettings.watermark_height * defaultSettings.watermark_rows) / (defaultSettings.watermark_rows - 1));
}
var x;
var y;
for (var i = 0; i < defaultSettings.watermark_rows; i++) {
y = defaultSettings.watermark_y + (defaultSettings.watermark_y_space + defaultSettings.watermark_height) * i;
for (var j = 0; j < defaultSettings.watermark_cols; j++) {
x = defaultSettings.watermark_x + (defaultSettings.watermark_width + defaultSettings.watermark_x_space) * j;
var mask_div = document.createElement('div');
mask_div.id = 'mask_div' + i + j;
mask_div.className = 'mask_div';
mask_div.appendChild(document.createTextNode(defaultSettings.watermark_txt));
// 设置水印div倾斜显示
mask_div.style.webkitTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
mask_div.style.MozTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
mask_div.style.msTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
mask_div.style.OTransform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
mask_div.style.transform = "rotate(-" + defaultSettings.watermark_angle + "deg)";
mask_div.style.visibility = "";
mask_div.style.position = "fixed";
mask_div.style.left = x + 'px';
mask_div.style.top = y + 'px';
mask_div.style.overflow = "hidden";
mask_div.style.zIndex = "19920219";
mask_div.style.pointerEvents = 'none'; // pointer-events:none 让水印不遮挡页面的点击事件
// mask_div.style.border="solid #eee 1px";
mask_div.style.opacity = defaultSettings.watermark_alpha;
mask_div.style.fontSize = defaultSettings.watermark_fontsize;
mask_div.style.fontFamily = defaultSettings.watermark_font;
mask_div.style.color = defaultSettings.watermark_color;
mask_div.style.textAlign = "center";
mask_div.style.width = defaultSettings.watermark_width + 'px';
mask_div.style.height = defaultSettings.watermark_height + 'px';
mask_div.style.display = "block";
// 交叉网格显示
if ((i % 2 == 0) && (j % 2 == 0)) {
oTemp.appendChild(mask_div);
}
if ((i % 2 == 1) && (j % 2 == 1)) {
oTemp.appendChild(mask_div);
}
};
};
document.body.appendChild(oTemp);
},
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
2、在index.html
、index-topnav.html
文件调用watermark
方法,示例如下:
$(function() {
var loginName = [[${@permission.getPrincipalProperty('loginName')}]];
$.common.watermark({ "watermark_txt": loginName + "水印" })
});
2
3
4
3、访问页面,检查页面水印是否显示。
注意
如需Excel导出时添加水印参考 - 参考如何Excel导出时添加水印
# 集成browscap读取浏览器用户代理
由于项目使用的UserAgentUtils
早在18年就停止维护了,对于目前市面上的新版本浏览器及系统没有进行区分,所以可以选择更换为browscap-java
,但是browscap-java
有一个缺点就是首次加载会很慢
,大概10秒
左右,根据机器的性能决定。
因为这个原因所以项目目前没有采纳,等待后续在看browscap-java
有没有对这个进行算法优化,如果觉得不是什么大问题可以参考如下流程进行升级,如果发现有更好的其他插件也可以反馈给我。
1、修改pom.xml
,将bitwalker
替换成browscap-java
<browscap.version>1.3.12</browscap.version>
<!-- 解析客户端操作系统、浏览器等 -->
<dependency>
<groupId>com.blueconic</groupId>
<artifactId>browscap-java</artifactId>
<version>${browscap.version}</version>
</dependency>
2
3
4
5
6
7
8
2、ruoyi-framework/pom.xml
删除bitwalker
3、ruoyi-common/pom.xml
新增browscap-java
<dependency>
<groupId>com.blueconic</groupId>
<artifactId>browscap-java</artifactId>
</dependency>
2
3
4
4、新增用户代理解析类UserAgent.java
package com.ruoyi.common.utils.http;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.blueconic.browscap.Capabilities;
import com.blueconic.browscap.UserAgentParser;
import com.blueconic.browscap.UserAgentService;
import com.ruoyi.common.utils.AddressUtils;
/**
* 浏览器用户代理解析
*
* @author ruoyi
*/
public class UserAgent
{
private static final Logger log = LoggerFactory.getLogger(AddressUtils.class);
/** 浏览器 */
public String browser = "";
/** 操作系统 */
public String operatingSystem = "";
/** 解析器 */
private static UserAgentParser parser = null;
static
{
try
{
parser = new UserAgentService().loadParser();
}
catch (Exception e)
{
log.error("获取用户代理异常 {}", e);
}
}
public UserAgent(String userAgentString)
{
if (parser != null)
{
String userAgentLowercaseString = userAgentString == null ? null : userAgentString.toLowerCase();
Capabilities capabilities = parser.parse(userAgentLowercaseString);
this.browser = String.format("%s %s", capabilities.getBrowser(), capabilities.getBrowserMajorVersion());
this.operatingSystem = capabilities.getPlatform();
}
}
public static UserAgent parseUserAgentString(String userAgentString)
{
return new UserAgent(userAgentString);
}
public String getBrowser()
{
return browser;
}
public String getOperatingSystem()
{
return operatingSystem;
}
}
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
5、AsyncFactory
、OnlineSessionFactory
修改UserAgent
包路径并修改获取方法。
// bitwalker 获取浏览器/操作系统方法
String os = userAgent.getOperatingSystem().getName();
String browser = userAgent.getBrowser().getName();
// ======== 修改为 ========
// browscap-java 获取浏览器/操作系统方法
String os = userAgent.getOperatingSystem();
String browser = userAgent.getBrowser();
2
3
4
5
6
7
8
9
# 集成dynamic-datasource实现多数据源增强
dynamic-datasource
是一个基于springboot
的快速集成多数据源的启动器。同时支持数据源分组、数据库敏感配置信息加密、自定义注解、动态增加移除数据源、读写分离、本地多数据源事务方案、基于Seata
的分布式事务方案等等。
- 提供并简化对
Druid
,HikariCp
,BeeCp
,Dbcp2
的快速集成。 - 提供对
Mybatis-Plus
,Quartz
,ShardingJdbc
,P6spy
,Jndi
等组件的集成方案。
集成多数据源dynamic-datasource,可以删除原先的默认多数据源处理类
ruoyi-framework\src\main\java\com\ruoyi\framework\config\DruidConfig.java
ruoyi-framework\src\main\java\com\ruoyi\framework\config\properties\DruidProperties.java
ruoyi-framework\src\main\java\com\ruoyi\framework\datasource\DynamicDataSource.java
ruoyi-framework\src\main\java\com\ruoyi\framework\datasource\DynamicDataSourceContextHolder.java
ruoyi-framework\src\main\java\com\ruoyi\framework\aspectj\DataSourceAspect.java
1、ruoyi-common\pom.xml
模块添加整合依赖
<!-- 动态数据源 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
2
3
4
5
6
2、ruoyi-admin
文件application-druid.yml
,修改spirng.datasource
配置
# spring配置
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: ruoyi
loginPassword: 123456
dynamic:
druid:
initial-size: 5
min-idle: 5
maxActive: 20
maxWait: 60000
connectTimeout: 30000
socketTimeout: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,slf4j
connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
datasource:
# 主库数据源
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
# 测试数据源
test:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
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
3、测试验证,修改参数管理《增删改查》切换到test
数据源。
package com.ruoyi.system.mapper;
import java.util.List;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.ruoyi.system.domain.SysConfig;
/**
* 参数配置 数据层
*
* @author ruoyi
*/
public interface SysConfigMapper
{
/**
* 查询参数配置信息
*
* @param config 参数配置信息
* @return 参数配置信息
*/
@DS("test")
public SysConfig selectConfig(SysConfig config);
/**
* 查询参数配置列表
*
* @param config 参数配置信息
* @return 参数配置集合
*/
@DS("test")
public List<SysConfig> selectConfigList(SysConfig config);
/**
* 根据键名查询参数配置信息
*
* @param configKey 参数键名
* @return 参数配置信息
*/
@DS("test")
public SysConfig checkConfigKeyUnique(String configKey);
/**
* 新增参数配置
*
* @param config 参数配置信息
* @return 结果
*/
@DS("test")
public int insertConfig(SysConfig config);
/**
* 修改参数配置
*
* @param config 参数配置信息
* @return 结果
*/
@DS("test")
public int updateConfig(SysConfig config);
/**
* 删除参数配置
*
* @param configId 参数主键
* @return 结果
*/
@DS("test")
public int deleteConfigById(Long configId);
/**
* 批量删除参数配置
*
* @param configIds 需要删除的数据ID
* @return 结果
*/
@DS("test")
public int deleteConfigByIds(String[] configIds);
}
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