# 常见问题

# 如何不登录直接访问

ShiroConfig中设置filterChainDefinitionMap配置url=anon

	/admins/**=anon               # 表示该 uri 可以匿名访问
	/admins/**=auth               # 表示该 uri 需要认证才能访问
	/admins/**=authcBasic         # 表示该 uri 需要 httpBasic 认证
	/admins/**=perms[user:add:*]  # 表示该 uri 需要认证用户拥有 user:add:* 权限才能访问
	/admins/**=port[8080]         # 表示该 uri 需要使用 8080 端口
	/admins/**=roles[admin]       # 表示该 uri 需要认证用户拥有 admin 角色才能访问
	/admins/**=ssl                # 表示该 uri 需要使用 https 协议
	/admins/**=user               # 表示该 uri 需要认证或通过记住我认证才能访问
	/logout=logout                # 表示注销,可以当作固定配置
	
	注意:
	anon,authcBasic,authc,user 是认证过滤器。
	perms,roles,ssl,rest,port 是授权过滤器。
1
2
3
4
5
6
7
8
9
10
11
12
13

# 如何更换项目包路径

懒人可以使用若依框架包名修改器 (opens new window)一键替换。

1、更换目录名称

├── xxxxx
│       └── xxxxx-admin
│       └── xxxxx-common
│       └── xxxxx-framework
│       └── xxxxx-generator
│       └── xxxxx-quartz
│       └── xxxxx-system
│       └── pom.xml
1
2
3
4
5
6
7
8

2、更换顶级目录中的pom.xml

<modules>
	<module>xxxxx-admin</module>
	<module>xxxxx-framework</module>
	<module>xxxxx-system</module>
	<module>xxxxx-quartz</module>
	<module>xxxxx-generator</module>
	<module>xxxxx-common</module>
</modules>
1
2
3
4
5
6
7
8

3、更换项目所有包名称com.ruoyi.xxx换成com.xxxxx.xxx

提示

DataScopeAspect,DataSourceAspect,LogAspect 这三个类@Pointcut注解上面的包路径也需要替换com.xxxxx

CaptchaConfig 这个类验证码文本生成器参数KAPTCHA_TEXTPRODUCER_IMPL的包路径也需要替换com.xxxxx

ApplicationConfig 这个类@MapperScan注解上面的包路径也需要替换com.xxxxx

4、更换application.yml指定要扫描的Mapper类的包的路径typeAliasesPackage包路径名称替换com.xxxxx




 

# MyBatis
mybatis:
    # 搜索指定包别名
    typeAliasesPackage: com.你的包名.**.domain
1
2
3
4

5、更换mapper文件的namespace包路径

ruoyi-system/resources/mapper/system/* 
ruoyi-quartz/resources/mapper/quartz/* 
ruoyi-generator/resources/mapper/generator/*
1
2
3

xml包路径名称替换com.xxxxx

6、更换pom文件内容

提示

以下pom.xml文件中包含ruoyi的关键字替换成xxxxx

├── xxxxx
│       └── xxxxx-admin      pom.xml
│       └── xxxxx-common     pom.xml
│       └── xxxxx-framework  pom.xml
│       └── xxxxx-generator  pom.xml
│       └── xxxxx-quartz     pom.xml
│       └── xxxxx-system     pom.xml
│       └── pom.xml
1
2
3
4
5
6
7
8

7、更换日志路径

  • 更换application.yml文件logging属性为com.xxxxx: debug
  • 更换logback.xml文件为com.xxxxx

8、启动项目验证

提示

到此步骤如能正常启动,表示更换完成。剩余的小细节可以自行调整。

# 业务模块访问出现404

1、单应用检查

  • 确认此用户是否已经配置菜单
  • 确认此角色是否已经配置菜单权限
  • 确认此菜单url是否和后台地址一致

如参数管理,后台地址配置@RequestMapping("/system/config")对应参数管理url/system/config

2、多模块检查(多了几个步骤)

  • pom.xml 引入了业务子系统
  • ruoyi-admin 添加业务子模块的依赖
  • ruoyi-xxxxx 新增业务模块pom检查配置是否正确

PS:IDEA可能存在缓存,需要清理下缓存在编译。

提示

如果业务模块和项目的包名不一致,需要在启动类上指定扫描包路径,如 @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }, scanBasePackages = { "com.ruoyi.*", "com.test.*" }) 或者加上@ComponentScan({ "com.ruoyi.*", "com.test.*" })

# IDEA更改页面不重启

经常有小伙伴问到这个问题,为什么我的用IDEA修改html页面之后不实时生效呢?

1、修改IDEA设置 File -> Settings -> Build Execution Deployment -> Build Project automatically 勾选

2、勾选Running Ctrl + Shift + Alt + / 然后选择 Registry,勾上 Compiler.autoMake.allow.when.app.running

PS:Eclipse开发工具无需任何配置。

# 如何使用多数据源

  1. resources 目录下修改application-druid.yml
# 从库数据源
slave:
    # 开启从库
    enabled: true
    url: 数据源
    username: 用户名
    password: 密码
1
2
3
4
5
6
7
  1. Service实现中添加DataSource注解
 





@DataSource(value = DataSourceType.SLAVE)
public List<User> selectUserList()
{
    return mapper.selectUserList();
}
1
2
3
4
5

# 如何更换主题皮肤

1、项目主页-个人信息中选择切换主题

2、修改主框架页-默认皮肤,在菜单参数设置修改参数键名sys.index.skinName支持如下几种皮肤

  • 蓝色 skin-blue
  • 绿色 skin-green
  • 紫色 skin-purple
  • 红色 skin-red
  • 黄色 skin-yellow

3、修改主框架页-侧边栏主题,在菜单参数设置修改参数键名sys.index.sideTheme支持如下几种主题

  • 深色主题theme-dark
  • 浅色主题theme-light

注:如需新增修改皮肤主题可以在skins.css中调整

# 如何获取用户登录信息

  1. 第一种方法
// 获取当前的用户信息
User currentUser = ShiroUtils.getSysUser();
// 获取当前的用户名称
String userName = currentUser.getUserName();
1
2
3
4
  1. 第二种方法(子模块可使用)
// 获取当前的用户名称
String userName = (String) PermissionUtils.getPrincipalProperty("userName");
1
2

3、界面获取当前用户信息(支持任意th标签)

<input th:value="${@permission.getPrincipalProperty('userName')}">
1

4、js中获取当前用户信息

var userName = [[${@permission.getPrincipalProperty('userName')}]];
1

# 如何防止请求重复提交

  1. 前端通过js控制
// 禁用按钮
$.modal.disable();
// 启用按钮
$.modal.enable();
1
2
3
4
  1. 后端通过@RepeatSubmit注解控制
/**
 * 在对应方法添加注解 @RepeatSubmit
 */
@RepeatSubmit
public AjaxResult editSave()
1
2
3
4
5

# 如何配置允许跨域访问

现在开发的项目一般都是前后端分离的项目,所以跨域访问会经常使用。

1、单个控制器方法CORS注解

@RestController
@RequestMapping("/system/test")
public class TestController {

    @CrossOrigin
    @GetMapping("/{id}")
    public AjaxResult getUser(@PathVariable Integer userId) {
        // ...
    }
	
	@DeleteMapping("/{userId}")
    public AjaxResult delete(@PathVariable Integer userId) {
        // ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

2、整个控制器启用CORS注解

@CrossOrigin(origins = "http://ruoyi.vip", maxAge = 3600)
@RestController
@RequestMapping("/system/test")
public class TestController {

    @GetMapping("/{id}")
    public AjaxResult getUser(@PathVariable Integer userId) {
        // ...
    }
	
	@DeleteMapping("/{userId}")
    public AjaxResult delete(@PathVariable Integer userId) {
        // ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3、全局CORS配置(在ResourcesConfig重写addCorsMappings方法)

/**
 * web跨域访问配置
 */
@Override
public void addCorsMappings(CorsRegistry registry)
{
	// 设置允许跨域的路径
	registry.addMapping("/**")
			// 设置允许跨域请求的域名
			.allowedOrigins("*")
			// 是否允许证书
			.allowCredentials(true)
			// 设置允许的方法
			.allowedMethods("GET", "POST", "DELETE", "PUT")
			// 设置允许的header属性
			.allowedHeaders("*")
			// 跨域允许时间
			.maxAge(3600);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 日期插件精确到时分秒

1、界面设置时间格式data-format,选择类型data-type属性。

<!-- data-type="date"(年)| data-type="month(月)| data-type="date"(日)| data-type="time"(时、分、秒)| data-type="datetime"(年、月、日、时、分、秒) -->
<li class="select-time">
<label>创建时间: </label>
<input type="text" class="time-input" placeholder="开始时间" name="params[beginTime]" data-type="datetime" data-format="yyyy-MM-dd HH:mm:ss"/>
<span>-</span>
<input type="text" class="time-input" placeholder="结束时间" name="params[endTime]" data-type="month" data-format="yyyy-MM"/>
</li>
1
2
3
4
5
6
7

2、通过js函数设置 datetimepicker日期控件可以设置format

$('.input-group.date').datetimepicker({
    format: 'yyyy-mm-dd hh:ii:ss',
    autoclose: true,
    minView: 0,
    minuteStep:1
});
1
2
3
4
5
6

laydate日期控件可以设置common.js 配置type=datetime

layui.use('laydate', function() {
	var laydate = layui.laydate;
	var startDate = laydate.render({
		elem: '#startTime',
		max: $('#endTime').val(),
		theme: 'molv',
		trigger: 'click',
		type : 'datetime',
		done: function(value, date) {
			// 结束时间大于开始时间
			if (value !== '') {
				endDate.config.min.year = date.year;
				endDate.config.min.month = date.month - 1;
				endDate.config.min.date = date.date;
			} else {
				endDate.config.min.year = '';
				endDate.config.min.month = '';
				endDate.config.min.date = '';
			}
		}
	});
	var endDate = laydate.render({
		elem: '#endTime',
		min: $('#startTime').val(),
		theme: 'molv',
		trigger: 'click',
		type : 'datetime',
		done: function(value, date) {
			// 开始时间小于结束时间
			if (value !== '') {
				startDate.config.max.year = date.year;
				startDate.config.max.month = date.month - 1;
				startDate.config.max.date = date.date;
			} else {
				startDate.config.max.year = '';
				startDate.config.max.month = '';
				startDate.config.max.date = '';
			}
		}
	});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 代码生成不显示新建表

默认条件需要表注释,特殊情况可在GenMapper.xml去除table_comment条件

<select id="selectTableByName" parameterType="String" resultMap="TableInfoResult">
	<include refid="selectGenVo"/>
	where table_comment <> '' and table_schema = (select database())
</select>
1
2
3
4

提示

如果版本>=4.0不需要表注解,在代码生成页面导入即可。

# 提示您没有数据的权限

这种情况都属于权限标识配置不对在菜单管理配置好权限标识(菜单&按钮)

  1. 确认此用户是否已经配置角色
  2. 确认此角色是否已经配置菜单权限
  3. 确认此菜单权限标识是否和后台代码一致

如参数管理
后台配置@RequiresPermissions("system:config:view")对应参数管理权限标识为system:config:view

注:如需要角色权限,配置角色权限字符 使用@RequiresRoles("admin")

# 富文本编辑器文件上传

富文本控件采用的summernote,图片上传处理需要设置callbacks函数

$('.summernote').summernote({
	height : '220px',
	lang : 'zh-CN',
	callbacks: {
		onImageUpload: function(files, editor, $editable) {
			var formData = new FormData();
			formData.append("file", files[0]);
			$.ajax({
	            type: "POST",
	            url: ctx + "common/upload",
	            data: data,
	            cache: false,
	            contentType: false,
	            processData: false,
	            dataType: 'json',
	            success: function(result) {
	                if (result.code == web_status.SUCCESS) {
	                	$(obj).summernote('editor.insertImage', result.url, result.fileName);
					} else {
						$.modal.alertError(result.msg);
					}
	            },
	            error: function(error) {
	                $.modal.alertWarning("图片上传失败。");
	            }
	        });
		}
	}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 富文本编辑器底部回弹

富文本控件采用的summernote,如果不需要底部回弹设置followingToolbar: false

$('.summernote').summernote({
	placeholder: '请输入公告内容',
	height : 192,
	lang : 'zh-CN',
	followingToolbar: false,
	callbacks: {
		onImageUpload: function (files) {
			sendFile(files[0], this);
		}
	}
});
1
2
3
4
5
6
7
8
9
10
11

# 如何创建新的菜单页签

建新新的页签有以下两种方式(js&html)

// 方式1 打开新的选项卡
function dept() {
	var url = ctx + "system/dept";
	$.modal.openTab("部门管理", url);
	// 如果需要打开并刷新 $.modal.openTab("部门管理", url, true);
}

// 方式2 选卡页同一页签打开
function dept() {
	var url = ctx + "system/dept";
	$.modal.parentTab("部门管理", url);
}

// 方式3 html创建
<a class="menuItem" href="/system/dept">部门管理</a>
// 如果需要打开并刷新 
<a class="menuItem" data-refresh="true" href="/system/dept">部门管理</a>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 表格数据进行汇总统计

对于某些数据需要对金额,数量等进行汇总,可以配置showFooter: true表示尾部统计

// options 选项中添加尾部统计
showFooter: true, 
// columns 中添加   
{
	field : 'balance',
	title : '余额',
	sortable: true,
	footerFormatter:function (value) {
		var sumBalance = 0;
		for (var i in value) {
			sumBalance += parseFloat(value[i].balance);
		}
		return "总金额:" + sumBalance;
	}
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 表格设置行列单元格样式

1、options参数中配置属性

rowStyle: rowStyle,
1

2、对应js添加响应方法(根据rowindex定义规则)即可

function rowStyle(row, index) {
	var style = { css: { 'color': '#ed5565' } };
	return style;
}
1
2
3
4

# 如何去除数据监控广告

服务监控中使用的Driud,默认底部有阿里的广告。如果是一个商业项目这个是很不雅也是不允许的

  1. 找到本地maven库中的对应的druid-1.1.xx.jar文件,用压缩包软件打开
  2. 找到support/http/resource/js/common.js, 打开找到 buildFooter 方法
this.buildFooter();
buildFooter : function() {
	var html ='此处省略一些相关JS代码';
	$(document.body).append(html);
},
1
2
3
4
5
  1. 删除此函数和及初始方法后覆盖文件
  2. 重启项目后,广告就会消失了

# 如何支持多类型数据库

对于某些特殊需要支持不同数据库,参考以下支持oracle mysql配置

<!--oracle驱动-->
<dependency>
	<groupId>com.oracle</groupId>
	<artifactId>ojdbc6</artifactId>
	<version>11.2.0.3</version>
</dependency>
1
2
3
4
5
6
# 数据源配置
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://127.0.0.1:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: password
            # 从库数据源
            slave:
                # 从数据源开关/默认关闭
                enabled: true
                url: jdbc:oracle:thin:@127.0.0.1:1521:oracle
                username: root
                password: password
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

对于不同数据源造成的驱动问题,可以删除driverClassName。会自动识别驱动
如需要对不同数据源分页需要操作application.yml中的pagehelper配置 删除helperDialect: mysql 会自动识别数据源 新增autoRuntimeDialect=true 表示运行时获取数据源

# 如何实现翻页保留选择

  1. 配置checkbox选项field属性为state
{
	field: 'state',
	checkbox: true
},
1
2
3
4
  1. 表格选项options添加rememberSelected
rememberSelected: true,
1

# 如何实现跳转至指定页

  1. 表格选项options添加showPageGo
showPageGo: true,
1

# 如何自定义查询条件参数

1、在options中添加queryParams参数

var options = {
	url: prefix + "/list",
	queryParams: queryParams,
	columns: [{
		field: 'id',
		title: '主键'
	},
	{
		field: 'name',
		title: '名称'
	}]
};
$.table.init(options);
1
2
3
4
5
6
7
8
9
10
11
12
13

2、在当前页添加queryParams方法设置自定义查询条件如userName

function queryParams(params) {
	var search = $.table.queryParams(params);
	search.userName = $("#userName").val();
	return search;
}
1
2
3
4
5

请求后台参数为:pageSize、pageNum、searchValue、orderByColumn、isAsc、userName

3、如果是表格树,添加参数ajaxParams参数

var options = {
	code: "deptId",
	parentCode: "parentId",
	uniqueId: "deptId",
	url: prefix + "/list",
	ajaxParams: {
		"userId": "1",
		"userName": "ruoyi"
	},
	columns: [{
		field: 'id',
		title: '主键'
	},
	{
		field: 'name',
		title: '名称'
	}]
};
$.treeTable.init(options);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 如何让表格某些列隐藏掉

1、在optionscolumns设置visible

visible: false,  // 隐藏某列(列选项可见)
ignore: true,    // 列选项不可见
1
2

对于需要列选项不可见状态可以设置ignore

# 如何给默认的表格加边框

table-striped 换成 table-bordered

<div class="col-sm-12 select-table table-bordered">
    <table id="bootstrap-table"></table>
</div>
1
2
3

# 普通用户创建文件无权限

常见的有几种错误,对应去调整即可。

1、修改logback.xml,value路径

<!-- 日志存放路径 -->
<property name="log.path" value="/home/ruoyi/logs" />
1
2

2、修改ehcache-shiro.xml,path路径

<diskStore path="java.io.tmpdir"/>
1

3、修改tomcat临时的日志目录

server:
  tomcat:
    basedir: /home/ruoyi/temp
1
2
3

# 如何降低mysql驱动版本

1、在pom.xmlproperties新增节点如:

<mysql.version>6.0.6</mysql.version>
1

2、单应用可以不添加,多模块需要在dependencyManagement声明依赖

<!-- Mysql驱动包 -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>${mysql.version}</version>
</dependency>
1
2
3
4
5
6

注意:如果是6以下的版本需要修改application-druid.ymldriverClassName
com.mysql.jdbc.Driver 是 mysql-connector-java 5中的
com.mysql.cj.jdbc.Driver 是 mysql-connector-java 6中的

# 如何配置tomcat访问日志

1、修改application.yml中的server开发环境配置

# 开发环境配置
server:
  # 服务器的HTTP端口,默认为80
  port: 80
  servlet:
    # 应用的访问路径
    context-path: /
  tomcat:
    # 存放Tomcat的日志目录
    basedir: D:/tomcat
    accesslog: 
        # 开启日志记录
        enabled: true
        # 访问日志存放目录
        directory: logs
    # tomcat的URI编码
    uri-encoding: UTF-8
    # tomcat最大线程数,默认为200
    max-threads: 800
    # Tomcat启动初始化的线程数,默认值25
    min-spare-threads: 30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

2、重启项目后,在D:/tomcat/logs目录就可以看到服务器访问日志了

# 如何汉化系统接口Swagger

想必很多小伙伴都曾经使用过Swagger,但是打开UI界面是纯英文的界面并不太友好,作为国人还是习惯中文界面。

  1. 找到m2/repository/io/springfox/springfox-swagger-ui/x.x.x/springfox-swagger-ui-x.x.x.jar
  2. 修改对应springfox-swagger-ui-x.x.x.jar包内resources目录下swagger-ui.html,添加如下JS代码
<!-- 选择中文版 -->
<script src='webjars/springfox-swagger-ui/lang/translator.js' type='text/javascript'></script>
<script src='webjars/springfox-swagger-ui/lang/zh-cn.js' type='text/javascript'></script>
1
2
3
  1. 本地修改结束后,在覆盖压缩包文件重启就实现汉化了

# 如何在html页面格式化日期

Thymeleaf主要使用org.thymeleaf.expression.Dates这个类来处理日期,在模板中使用"#dates"来表示这个对象。

1、格式化日期
[[${#dates.format(date)}]]th:text="${#dates.format(date)}
[[${#dates.formatISO(date)}]]th:text="${#dates.formatISO(date)}
[[${#dates.format(date, 'yyyy-MM-dd HH:mm:ss')}]]th:text="${#dates.format(date, 'yyyy-MM-dd HH:mm:ss')}

2、获取日期字段
获取当前的年份:[[${#dates.year(date)}]]
获取当前的月份:[[${#dates.month(date)}]]
获取当月的天数:[[${#dates.day(date)}]]
获取当前的小时:[[${#dates.hour(date)}]]
获取当前的分钟:[[${#dates.minute(date)}]]
获取当前的秒数:[[${#dates.second(date)}]]
获取当前的毫秒:[[${#dates.millisecond(date)}]]
获取当前的月份名称:[[${#dates.monthName(date)}]]
获取当前是星期几:[[${#dates.dayOfWeek(date)-1}]]

# 如何在表格中实现图片预览

对于某些图片需要在表格中显示,可以使用imageView方法

// 在columns中格式化对应相关的列属性
{
	field: 'avatar',
	title: '用户头像',
	formatter: function(value, row, index) {
		return $.table.imageView(value, '/profile/avatar');
	}
},
1
2
3
4
5
6
7
8

# 如何去掉页脚及左侧菜单栏

1、去除页脚修改style.css,同时删除index.html元素

#content-main {
    height: calc(100% - 91px);
    overflow: hidden;
}
1
2
3
4
<div class="footer">
    <div class="pull-right">© [[${copyrightYear}]] RuoYi Copyright </div>
</div>
1
2
3

2、去左侧菜单栏(收起时隐藏左侧菜单)修改style.css

body.fixed-sidebar.mini-navbar #page-wrapper {
    margin: 0 0 0 0px;
}

body.body-small.fixed-sidebar.mini-navbar #page-wrapper {
    margin: 0 0 0 0px;
}
1
2
3
4
5
6
7

3、去左侧菜单栏(收起时隐藏左侧菜单)修改index.js

function() {
    if ($(this).width() < 769) {
        $('body').addClass('mini-navbar');
        $('.navbar-static-side').fadeIn(); // 换成 $('.navbar-static-side').hide();
        $(".sidebar-collapse .logo").addClass("hide");
    }
});

function SmoothlyMenu() {
    if (!$('body').hasClass('mini-navbar')) {
    	$(".navbar-static-side").show();  // 添加显示这一行
        $('#side-menu').hide();
        $(".sidebar-collapse .logo").removeClass("hide");
        setTimeout(function() {
            $('#side-menu').fadeIn(500);
        },
        100);
    } else if ($('body').hasClass('fixed-sidebar')) {
    	$(".navbar-static-side").hide();  // 添加隐藏这一行
        $('#side-menu').hide();
        $(".sidebar-collapse .logo").addClass("hide");
        setTimeout(function() {
            $('#side-menu').fadeIn(500);
        },
        300);
    } else {
        $('#side-menu').removeAttr('style');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

4、隐藏左侧菜单,需要添加.canvas-menu到body元素

<body class = "canvas-menu"> 
1

# 登录页如何开启注册用户功能

在菜单参数设置修改参数键名sys.account.registerUser设置true即可。默认为false关闭。

# 如何限制账户只能一个人登录

application.yml设置maxSession1即可。

# Shiro
shiro:
  session:
    # 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
    maxSession: 1
    # 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
    kickoutAfter: false
1
2
3
4
5
6
7

# 登录页面如何不显示验证码

application.yml设置captchaEnabledfalse即可

# Shiro
shiro:
  user:
    # 验证码开关
    captchaEnabled: false
1
2
3
4
5

# 如何Excel导出子对象多个字段

// 单个字段导出
@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT)
private Dept dept;

// 多个字段导出
@Excels({
    @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
    @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
})
private Dept dept;
1
2
3
4
5
6
7
8
9
10

# 更多操作字符串参数读取问题

事件中需要传递字符串参数,可以参考resetPwd传递方式。

onclick=resetPwd(" + row.userId + ',' + "'" + row.userName + "'" + ")
1

完整代码

formatter: function(value, row, index) {
	var actions = [];
	actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.editTab(\'' + row.userId + '\')"><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.userId + '\')"><i class="fa fa-remove"></i>删除</a> ');
	var more = [];
	more.push("<a class='btn btn-default btn-xs " + resetPwdFlag + "' href='javascript:void(0)' onclick=resetPwd(" + row.userId + ',' + "'" + row.userName + "'" + ")><i class='fa fa-key'></i>重置密码</a> ");
	more.push("<a class='btn btn-default btn-xs " + editFlag + "' href='javascript:void(0)' onclick='authRole(" + row.userId + ")'><i class='fa fa-check-square-o'></i>分配角色</a>");
	actions.push('<a tabindex="0" class="btn btn-info btn-xs" role="button" data-container="body" data-placement="left" data-toggle="popover" data-html="true" data-trigger="hover" data-content="' + more.join('') + '"><i class="fa fa-chevron-circle-right"></i>更多操作</a>');
	return actions.join('');
}
1
2
3
4
5
6
7
8
9
10

# 单元格内容过长显示处理方法

1、使用系统自带的方法格式化处理

{
	field: 'remark',
	title: '备注',
	align: 'center',
	formatter: function(value, row, index) {
		return $.table.tooltip(value);
	}
},
1
2
3
4
5
6
7
8

2、添加css控制

.select-table table {
    table-layout:fixed;
}

.select-table .table td {
	/* 超出部分隐藏 */
	overflow:hidden;
	/* 超出部分显示省略号 */
    text-overflow:ellipsis;
    /*规定段落中的文本不进行换行 */
    white-space:nowrap;
    /* 配合宽度来使用 */
	height:40px;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 表格禁用某列复选框选择方法

条件成立禁用checkbox返回(disabled : true)即可。

{
	checkbox: true,
	formatter: function (value, row, index) {
		if($.common.equals("ry", row.loginName)){
			return { disabled : true}
		} else {
			return { disabled : false}
		}
	}
},
1
2
3
4
5
6
7
8
9
10

# 表格默认勾选某列复选框方法

条件成立禁用checkbox返回(disabled : true)即可。

{
	checkbox: true,
	formatter: function (value, row, index) {
		if($.common.equals("ry", row.loginName)){
			return { checked : true}
		} else {
			return { checked : false}
		}
	}
},
1
2
3
4
5
6
7
8
9
10

提示

如果默认勾选,并且配置了 rememberSelected: true, 需要特殊处理下。参考demo

<!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="btn-group-sm" id="toolbar" role="group">
	        <a class="btn btn-success" onclick="checkItem()">
	            <i class="fa fa-check"></i> 选中项
	        </a>
        </div>
		<div class="row">
			<div class="col-sm-12 select-table table-striped">
				<table id="bootstrap-table"></table>
			</div>
		</div>
	</div>
    <div th:include="include :: footer"></div>
    <script th:inline="javascript">
        var prefix = ctx + "demo/table";
        var datas = [[${@dict.getType('sys_normal_disable')}]];

        $(function() {
            var options = {
                uniqueId: "userCode",
                url: prefix + "/list",
		        rememberSelected: true,
                columns: [{
                	field: 'state',
		            checkbox: true,
		            formatter: function(value, row, index) {
		            	if($.inArray(row.userCode, table.rememberSelectedIds[table.options.id]) !== -1 || row.userCode == 1000001 || row.userCode == 1000002){
		            		if($.inArray(row.userCode, uncheckUserCode) !== -1)
		            		{
		            			return { checked : false };
		            		}
		            		var selectedRows = table.rememberSelecteds[table.options.id];
		            		func = $.inArray('check', ['check', 'check-all']) > -1 ? 'union' : 'difference';
		            		if($.common.isNotEmpty(selectedRows)) {
		            			table.rememberSelecteds[table.options.id] = _[func](selectedRows, row);
	            			} else {
	            				table.rememberSelecteds[table.options.id] = _[func]([], row);
	            			}
		            		return { checked : true };
		            	}
		            	return { checked : false };
		        	}
		        },
				{
					field : 'userId', 
					title : '用户ID'
				},
				{
					field : 'userCode', 
					title : '用户编号'
				},
				{
					field : 'userName', 
					title : '用户姓名'
				},
				{
					field : 'userPhone', 
					title : '用户手机'
				},
				{
					field : 'userEmail', 
					title : '用户邮箱'
				},
				{
				    field : 'userBalance',
				    title : '用户余额'
				},
				{
                    field: 'status',
                    title: '用户状态',
                    align: 'center',
                    formatter: function(value, row, index) {
                    	return $.table.selectDictLabel(datas, value);
                    }
                },
		        {
		            title: '操作',
		            align: 'center',
		            formatter: function(value, row, index) {
		            	var actions = [];
		            	actions.push('<a class="btn btn-success btn-xs" href="#"><i class="fa fa-edit"></i>编辑</a> ');
                        actions.push('<a class="btn btn-danger btn-xs" href="#"><i class="fa fa-remove"></i>删除</a>');
						return actions.join('');
		            }
		        }]
            };
            $.table.init(options);
        });
        
        
        var uncheckUserCode = [];
    	$("#bootstrap-table").on("uncheck.bs.table uncheck-all.bs.table", function (e, rows) {
    		if(rows.length > 0) {
    			for (var index in rows) {
    				uncheckUserCode.unshift(rows[index].userCode);
   		        }
    		} else {
    			uncheckUserCode.unshift(rows.userCode);
    		}
    	});
        
    	$("#bootstrap-table").on("check.bs.table check-all.bs.table", function (e, rows) {
    		if(rows.length > 0) {
    			for (var index in rows) {
    				deleteItem(rows[index].userCode);
   		        }
    		} else {
    			deleteItem(rows.userCode);
    		}
    	});
    	
    	function deleteItem(item) {
    	    for (var key in uncheckUserCode) {
    	        if (uncheckUserCode[key] === item) {
    	        	uncheckUserCode.splice(key, 1)
    	        }
    	    }
    	}
        
        // 选中数据
        function checkItem(){
        	// var arrays = $.table.selectColumns("userId");
        	var arrays = $.table.selectColumns("userCode");
        	alert(arrays);
        }
    </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

# 页面如何一次初始化多个表格

options中添加id参数,如果有按钮组也需要添加toolbar

// 表格1
var options = {
	id: "bootstrap-table1",
    toolbar: "toolbar1",
	// 省略 ....
};
$.table.init(options);

// 表格2
var options = {
	id: "bootstrap-table2",
    toolbar: "toolbar2",
	// 省略 ....
};
$.table.init(options);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 表格底部合计列拖动显示问题

options中添加onLoadSuccess参数。

onLoadSuccess: onLoadSuccess,

1
2
// 监听表体fixed-table-body滚动事件,赋值给表尾fixed-table-footer
function onLoadSuccess() {
	$(".fixed-table-body").on("scroll",function(){
		var sl=this.scrollLeft;
		$(this).next()[0].scrollLeft = sl;
	})
}
1
2
3
4
5
6
7

# 日期控件初始化时间并格式化

使用thymeleaf在页面直接获取当前时间并格式化输出

<input type="text" th:value="${#dates.format(new java.util.Date(), 'yyyy-MM-dd')}" />
<a th:text="${#dates.format(new java.util.Date().getTime(), 'yyyy-MM-dd HH:mm:ss')}">time</a>
1
2

# 如何调整首页左侧菜单栏宽度

调整style.css对应样式宽度,例如宽度200修改成250

body.fixed-sidebar .navbar-static-side, body.canvas-menu .navbar-static-side {
    width: 250px;
}
1
2
3
nav .logo {
	width: 250px;
}
1
2
3
#page-wrapper {
    margin: 0 0 0 250px;
}
1
2
3

# 如何默认显示表格卡片视图

options中添加 mobileResponsive cardView 参数

mobileResponsive: false,
cardView: true,
1
2

# 编辑和删除操作按钮不可用

这种情况一般是因为第一列不是唯一键或formatter序号造成的。解决方案如下,指定唯一列属性 配合删除/修改使用 未指定则使用表格行首列

options中添加 uniqueId参数,userId修改成你表的唯一列字段。

uniqueId: 'userId',
1

# Tomcat部署多个War包项目异常

default-domain的值不一样就可以了 在application.yml里面配置上

spring:
  jmx:
    default-domain: applicationname
1
2
3

# Tomcat临时目录tmp抛错误异常

首先,我们应该知道,对于http POST请求来说,它需要使用这个临时目录来存储post数据。
其次,因为该目录是挂在到/temp目录下的临时文件,那么对于一些OS系统,像centOS将经常删除这个临时目录,所有导致该目录不存在了

解决方案

1.在application.yml文件中设置multipart location ,并重启项目

spring:
  http:
    multipart:
      location: /data/upload_tmp
1
2
3
4

2.在application.yml文件中设置

server
  tomcat:
     basedir: /tmp/tomcat
1
2
3

3.在配置文件添加bean

@Bean
public MultipartConfigElement multipartConfigElement() {
  MultipartConfigFactory factory = new MultipartConfigFactory();
  factory.setLocation("/tmp/tomcat");
  return factory.createMultipartConfig();
}
1
2
3
4
5
6

4.添加启动参数-java.tmp.dir=/path/to/application/temp/,并重启。

# 如何部署配置支持https访问

Nginx 配置为例,完整流程如下

申请下载ssl证书 证书有很多种,申请成功后会得到一个压缩包,里面有2个证书

1、安装OpenSSL yum -y install openssl openssl-devel

2、运行添加ssl模块
./configure --prefix=/usr/local/nginx --with-http_ssl_module

3、配置完成后,运行命令 make

4、然后将刚刚编译好的nginx覆盖掉原有的nginx(这个时候nginx要停止状态)
cp objs/nginx /usr/local/nginx/sbin/

5、复制crt证书文件和key私钥文件到Nginx服务器/usr/local/nginx/conf目录(此处为 Nginx 默认安装目录,请根据实际情况操作)下。

6、编辑 Nginx 根目录下的 conf/nginx.conf 文件。添加内容如下:

# https 服务配置
server {
	# 侦听80端口
	listen 443 default ssl;
	ssl on;
	#证书文件名称
	ssl_certificate 1_ruoyi.vip_bundle.crt; 
	#私钥文件名称
	ssl_certificate_key 2_ruoyi.vip.key; 
	# 定义访问域名
	server_name ruoyi.vip;
	location / {
		# 存放了静态页面的根目录
		root   /home/ruoyi/projects/static-web;
		# 默认主页
		index index.html;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

7、重启Nginx通过https访问 https://ruoyi.vip

8、如需把http的域名请求转成https,添加rewrite

rewrite ^(.*) https://$server_name$1 permanent;
1

9、解决重定向后https变成了http 的问题

proxy_redirect http:// https://; 
1

# 特殊字符串被过滤的解决办法

默认所有的都会过滤,可以配置 XSS excludes 属性 排除URL

# 防止XSS攻击
xss: 
  # 过滤开关
  enabled: true
  # 排除链接(多个用逗号分隔)
  excludes: /system/notice/*
  # 匹配链接
  urlPatterns: /system/*,/monitor/*,/tool/*
1
2
3
4
5
6
7
8

# 进入首页如何自动展开某菜单

例如,进入自动打开用户管理,调用applyPath,填入你请求菜单对应的url地址。

applyPath("/system/role")
1

# 进入首页如何默认记忆控制台

例如用户退出后,下次登陆系统,能默认打开之前工作路径。

可以在index.htmlindex-topnav.html,去掉window.performance.navigation.type == 1

if($.common.equals("history", mode) && window.performance.navigation.type == 1)
1

换成

if($.common.equals("history", mode))
1

# 打包如何分离jar包和资源文件

特殊情况需要分离libresouce可以修改ruoyi-admin 参考如下

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>ruoyi</artifactId>
        <groupId>com.ruoyi</groupId>
        <version>4.4.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
	<packaging>jar</packaging>
    <artifactId>ruoyi-admin</artifactId>
	
	<description>
	    web服务入口
	</description>

    <dependencies>
    
        <!-- SpringBoot集成thymeleaf模板 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- spring-boot-devtools -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<optional>true</optional> <!-- 表示依赖不会传递 -->
		</dependency>

		<!-- swagger2-->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
		</dependency>
		
		<!--防止进入swagger页面报类型转换错误,排除2.9.2中的引用,手动增加1.5.21版本-->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.21</version>
        </dependency>
        
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>1.5.21</version>
        </dependency>
		
		<!-- swagger2-UI-->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
		</dependency>
		 
    	 <!-- Mysql驱动包 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

		<!-- 核心模块-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-framework</artifactId>
        </dependency>
        
        <!-- 定时任务-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-quartz</artifactId>
        </dependency>
        
        <!-- 代码生成-->
        <dependency>
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-generator</artifactId>
        </dependency>
        
    </dependencies>

     <build>
        <!-- jar包名 -->
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- 分离lib -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <!-- 依赖包输出目录,将来不打进jar包里 -->
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                            <excludeTransitive>false</excludeTransitive>
                            <stripVersion>false</stripVersion>
                            <includeScope>runtime</includeScope>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- copy资源文件 -->
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <resources>
                                <resource>
                                    <directory>src/main/resources</directory>
                                </resource>
                            </resources>
                            <outputDirectory>${project.build.directory}/resources</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!-- 打jar包时忽略配置文件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>**/*.yml</exclude>
                        <exclude>**/*.xml</exclude>
                    </excludes>
                </configuration>
            </plugin>
            <!-- spring boot repackage -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <layout>ZIP</layout>
                    <includes>
                        <include>
                            <groupId>non-exists</groupId>
                            <artifactId>non-exists</artifactId>
                        </include>
                    </includes>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
	
</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

提示

启动命令java -jar -Dloader.path=resources,lib ruoyi-admin.jar

# Linux系统验证码乱码解决方法

在云服务器(少许),或者干净的服务器上,服务器没有安装字体。

1、上传本地的 Arial.ttf (opens new window) 字体

2、此时执行以下三个命令:(建立字体索引信息,更新字体缓存)
mkfontscale mkfontdir fc-cache -fv

3、重新刷新你的页面

# 如何实现用户免密登录配置方法

免密使用的场景,例如短信验证码,第三方应用登录等。下面列出一个简单的实现方法,当然还有更多实现方式可以自己尝试。

1、新增一个登录类型枚举类LoginType

package com.ruoyi.framework.shiro.token;

/**
 * 登录类型枚举类
 * 
 * @author ruoyi
 */
public enum LoginType
{
    /**
     * 密码登录
     */
    PASSWORD("password"),
    /**
     * 免密码登录
     */
    NOPASSWD("nopasswd");

    private String desc;

    LoginType(String desc)
    {
        this.desc = desc;
    }

    public String getDesc()
    {
        return desc;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

2、自定义登录Token

package com.ruoyi.framework.shiro.token;

import org.apache.shiro.authc.UsernamePasswordToken;

/**
 * 自定义登录Token
 * 
 * @author ruoyi
 */
public class UserToken extends UsernamePasswordToken
{
    private static final long serialVersionUID = 1L;

    private LoginType type;

    public UserToken()
    {
    }

    public UserToken(String username, String password, LoginType type, boolean rememberMe)
    {
        super(username, password, rememberMe);
        this.type = type;
    }

    public UserToken(String username, LoginType type)
    {
        super(username, "", false, null);
        this.type = type;
    }

    public UserToken(String username, String password, LoginType type)
    {
        super(username, password, false, null);
        this.type = type;
    }

    public LoginType getType()
    {
        return type;
    }

    public void setType(LoginType type)
    {
        this.type = type;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

3、对应Realm中添加登录类型判断,例如UserRealm(这里演示公用一个realm,如单独有免密realm不需要)

/**
 * 登录认证
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
{
	UserToken upToken = (UserToken) token;
	LoginType type = upToken.getType();
	String username = upToken.getUsername();
	String password = "";
	if (upToken.getPassword() != null)
	{
		password = new String(upToken.getPassword());
	}

	User user = null;
	try
	{
		if (LoginType.PASSWORD.equals(type))
		{
			user = loginService.login(username, password);
		}
		else if (LoginType.NOPASSWD.equals(type))
		{
			user = loginService.login(username);
		}
	}
	catch (CaptchaException e)
	{
		throw new AuthenticationException(e.getMessage(), e);
	}
	catch (UserNotExistsException e)
	{
		throw new UnknownAccountException(e.getMessage(), e);
	}
	catch (UserPasswordNotMatchException e)
	{
		throw new IncorrectCredentialsException(e.getMessage(), e);
	}
	catch (UserPasswordRetryLimitExceedException e)
	{
		throw new ExcessiveAttemptsException(e.getMessage(), e);
	}
	catch (UserBlockedException e)
	{
		throw new LockedAccountException(e.getMessage(), e);
	}
	catch (RoleBlockedException e)
	{
		throw new LockedAccountException(e.getMessage(), e);
	}
	catch (Exception e)
	{
		log.info("对用户[" + username + "]进行登录验证..验证未通过{}", e.getMessage());
		throw new AuthenticationException(e.getMessage(), e);
	}
	SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
	return info;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

4、LoginService添加login方法,去掉密码验证。

/**
 * 登录
 */
public User login(String username)
{
	// 验证码校验
	if (!StringUtils.isEmpty(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA)))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
		throw new CaptchaException();
	}
	// 用户名或密码为空 错误
	if (StringUtils.isEmpty(username))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
		throw new UserNotExistsException();
	}

	// 用户名不在指定范围内 错误
	if (username.length() < UserConstants.USERNAME_MIN_LENGTH
			|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
		throw new UserPasswordNotMatchException();
	}

	// 查询用户信息
	User user = userService.selectUserByLoginName(username);

	if (user == null)
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists")));
		throw new UserNotExistsException();
	}
	
	if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.delete")));
		throw new UserDeleteException();
	}
	
	if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
	{
		AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.blocked", user.getRemark())));
		throw new UserBlockedException();
	}

	AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
	recordLoginInfo(user);
	return user;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

5、在对应的登录方法中传入LoginType.NOPASSWD调用

UserToken token = new UserToken(username, LoginType.NOPASSWD);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
1
2
3

# 如何处理Long类型精度丢失问题

当字段实体类为Long类型且值超过前端js显示的长度范围时会导致前端回显错误,解决方案如下

1、使用JsonSerialize注解序列化的时候把Long自动转为String(针对单个属性)

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

@JsonSerialize(using = ToStringSerializer.class)
private Long xxx;
1
2
3
4
5

2、添加JacksonConfig配置全局序列化(针对所有属性)

package com.ruoyi.framework.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

/**
 * Jackson配置
 * 
 * @author ruoyi
 *
 */
@Configuration
public class JacksonConfig
{
    @Bean
    public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter()
    {
        final Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
        builder.serializationInclusion(JsonInclude.Include.NON_NULL);
        final ObjectMapper objectMapper = builder.build();
        SimpleModule simpleModule = new SimpleModule();
        // Long 转为 String 防止 js 丢失精度
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        objectMapper.registerModule(simpleModule);
        // 忽略 transient 关键词属性
        objectMapper.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true);
        return new MappingJackson2HttpMessageConverter(objectMapper);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 如何修改超级管理员登录密码

1、如果是自己知道超级管理员的密码且需要修改的情况。
默认口令 admin/admin123,可以登录后在首页个人中心修改密码。

2、如果自己忘记了超级管理员的密码可以重新生成秘钥替换数据库密码。

public static void main(String[] args)
{
    // 第一个参数为账户名 第二个参数为密码 第三个参数为盐对应用户表salt(如果没有可以不用填)
	System.out.println(new PasswordService().encryptPassword("admin", "admin123", "111111"));
}
1
2
3
4
5

# 如何修改成自定义的Cookie名称

ShiroConfigsessionManager方法添加,其中ruoyi为设置的Cookie名称

// 自定义 Cookie
manager.setSessionIdCookie(new SimpleCookie("ruoyi"));
1
2

# 如何格式化前端日期时间戳内容

对应一些时间格式需要在前端进行格式化操作情况,解决方案如下

1、后端使用JsonFormat注解格式化日期,时间戳yyyy-MM-dd HH:mm:ss

/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date time;
1
2
3

2、前端使用dateFormat方法格式化日期,时间戳yyyy-MM-dd HH:mm:ss

{
	field: 'createTime',
	title: '创建时间',
	formatter: function(value, row, index) {
		return $.common.dateFormat(value, "yyyy-MM-dd HH-mm-ss");
	}
},
1
2
3
4
5
6
7

# 使用Velocity模板引擎兼容$符号

当我们服务器端使用velocity模板来渲染前端页面的时候,而前端使用jquery,vue,angular等等也使用$运算符渲染变量,那么就会产生冲突, 对于这种特殊情况需要加入新的指令#[[您前端不需要让velocity处理的内容]],可以完美解决这个问题。

示例:

// 无法解析
let list = (params) => vm.$u.get("/${moduleName}/${businessName}/list", params);

// 正常解析
let list = (params) => vm.#[[$u]]#.get("/${moduleName}/${businessName}/list", params);
1
2
3
4
5

# 如何解决多数据源事务的一致性

在一些复杂的应用开发中,一个应用可能会涉及到连接多个数据源,所谓多数据源这里就定义为至少连接两个及以上的数据库了。 对于这种多数据的应用中,数据源就是一种典型的分布式场景,因此系统在多个数据源间的数据操作必须做好事务控制。在SpringBoot的官网推荐我们使用Atomikos (opens new window)。 当然分布式事务的作用并不仅仅应用于多数据源。例如:在做数据插入的时候往一个kafka消息队列写消息,如果信息很重要同样需要保证分布式数据的一致性。

若依框架已经通过Druid实现了多数据源切换,但是Spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。所以我们需要Atomikos解决多数据源事务的一致性问题

1、pom文件添加atomikos依赖。

<!-- atomikos分布式事务 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
1
2
3
4
5

2、新增文件AtomikosConfig.java,用于jta整合atomikos配置

package com.ruoyi.framework.config;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.jta.JtaTransactionManager;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;

/**
 * JTA 事务配置
 *
 * @author ruoyi
 */
@Configuration
public class AtomikosConfig
{
    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable
    {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
    public TransactionManager atomikosTransactionManager() throws Throwable
    {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);
        return userTransactionManager;
    }

    @Bean(name = "transactionManager")
    @DependsOn({ "userTransaction", "atomikosTransactionManager" })
    public PlatformTransactionManager transactionManager() throws Throwable
    {
        UserTransaction userTransaction = userTransaction();
        TransactionManager atomikosTransactionManager = atomikosTransactionManager();
        return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

3、新增文件DynamicSqlSessionTemplate.java,用于SqlSession通过注解动态切换数据

package com.ruoyi.framework.datasource;

import static java.lang.reflect.Proxy.newProxyInstance;
import static org.apache.ibatis.reflection.ExceptionUtil.unwrapThrowable;
import static org.mybatis.spring.SqlSessionUtils.closeSqlSession;
import static org.mybatis.spring.SqlSessionUtils.getSqlSession;
import static org.mybatis.spring.SqlSessionUtils.isSqlSessionTransactional;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.MyBatisExceptionTranslator;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.dao.support.PersistenceExceptionTranslator;

/**
 * 自定义SqlSessionTemplate,动态切换数据源
 * 
 * @author ruoyi
 */
public class DynamicSqlSessionTemplate extends SqlSessionTemplate
{
    private final SqlSessionFactory sqlSessionFactory;
    private final ExecutorType executorType;
    private final SqlSession sqlSessionProxy;
    private final PersistenceExceptionTranslator exceptionTranslator;
    private Map<Object, SqlSessionFactory> targetSqlSessionFactorys;
    private SqlSessionFactory defaultTargetSqlSessionFactory;

    public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory)
    {
        this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());
    }

    public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType)
    {
        this(sqlSessionFactory, executorType, new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));
    }

    public DynamicSqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator)
    {
        super(sqlSessionFactory, executorType, exceptionTranslator);
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor());
        this.defaultTargetSqlSessionFactory = sqlSessionFactory;
    }

    public void setTargetSqlSessionFactorys(Map<Object, SqlSessionFactory> targetSqlSessionFactorys)
    {
        this.targetSqlSessionFactorys = targetSqlSessionFactorys;
    }

    public void setDefaultTargetSqlSessionFactory(SqlSessionFactory defaultTargetSqlSessionFactory)
    {
        this.defaultTargetSqlSessionFactory = defaultTargetSqlSessionFactory;
    }

    @Override
    public SqlSessionFactory getSqlSessionFactory()
    {
        SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(DynamicDataSourceContextHolder.getDataSourceType());
        if (targetSqlSessionFactory != null)
        {
            return targetSqlSessionFactory;
        }
        else if (defaultTargetSqlSessionFactory != null)
        {
            return defaultTargetSqlSessionFactory;
        }
        return this.sqlSessionFactory;
    }

    @Override
    public Configuration getConfiguration()
    {
        return this.getSqlSessionFactory().getConfiguration();
    }

    public ExecutorType getExecutorType()
    {
        return this.executorType;
    }

    public PersistenceExceptionTranslator getPersistenceExceptionTranslator()
    {
        return this.exceptionTranslator;
    }

    /**
     * {@inheritDoc}
     */
    public <T> T selectOne(String statement)
    {
        return this.sqlSessionProxy.<T> selectOne(statement);
    }

    /**
     * {@inheritDoc}
     */
    public <T> T selectOne(String statement, Object parameter)
    {
        return this.sqlSessionProxy.<T> selectOne(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, String mapKey)
    {
        return this.sqlSessionProxy.<K, V> selectMap(statement, mapKey);
    }

    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey)
    {
        return this.sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey);
    }

    /**
     * {@inheritDoc}
     */
    public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds)
    {
        return this.sqlSessionProxy.<K, V> selectMap(statement, parameter, mapKey, rowBounds);
    }

    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement)
    {
        return this.sqlSessionProxy.<E> selectList(statement);
    }

    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement, Object parameter)
    {
        return this.sqlSessionProxy.<E> selectList(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds)
    {
        return this.sqlSessionProxy.<E> selectList(statement, parameter, rowBounds);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("rawtypes")
    public void select(String statement, ResultHandler handler)
    {
        this.sqlSessionProxy.select(statement, handler);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("rawtypes")
    public void select(String statement, Object parameter, ResultHandler handler)
    {
        this.sqlSessionProxy.select(statement, parameter, handler);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("rawtypes")
    public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler)
    {
        this.sqlSessionProxy.select(statement, parameter, rowBounds, handler);
    }

    /**
     * {@inheritDoc}
     */
    public int insert(String statement)
    {
        return this.sqlSessionProxy.insert(statement);
    }

    /**
     * {@inheritDoc}
     */
    public int insert(String statement, Object parameter)
    {
        return this.sqlSessionProxy.insert(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public int update(String statement)
    {
        return this.sqlSessionProxy.update(statement);
    }

    /**
     * {@inheritDoc}
     */
    public int update(String statement, Object parameter)
    {
        return this.sqlSessionProxy.update(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public int delete(String statement)
    {
        return this.sqlSessionProxy.delete(statement);
    }

    /**
     * {@inheritDoc}
     */
    public int delete(String statement, Object parameter)
    {
        return this.sqlSessionProxy.delete(statement, parameter);
    }

    /**
     * {@inheritDoc}
     */
    public <T> T getMapper(Class<T> type)
    {
        return getConfiguration().getMapper(type, this);
    }

    /**
     * {@inheritDoc}
     */
    public void commit()
    {
        throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void commit(boolean force)
    {
        throw new UnsupportedOperationException("Manual commit is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void rollback()
    {
        throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void rollback(boolean force)
    {
        throw new UnsupportedOperationException("Manual rollback is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void close()
    {
        throw new UnsupportedOperationException("Manual close is not allowed over a Spring managed SqlSession");
    }

    /**
     * {@inheritDoc}
     */
    public void clearCache()
    {
        this.sqlSessionProxy.clearCache();
    }

    /**
     * {@inheritDoc}
     */
    public Connection getConnection()
    {
        return this.sqlSessionProxy.getConnection();
    }

    /**
     * {@inheritDoc}
     *
     * @since 1.0.2
     */
    public List<BatchResult> flushStatements()
    {
        return this.sqlSessionProxy.flushStatements();
    }

    /**
     * Proxy needed to route MyBatis method calls to the proper SqlSession got from Spring's Transaction Manager It also
     * unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to pass a {@code PersistenceException} to
     * the {@code PersistenceExceptionTranslator}.
     */
    private class SqlSessionInterceptor implements InvocationHandler
    {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
        {
            final SqlSession sqlSession = getSqlSession(DynamicSqlSessionTemplate.this.getSqlSessionFactory(), DynamicSqlSessionTemplate.this.executorType, DynamicSqlSessionTemplate.this.exceptionTranslator);
            try
            {
                Object result = method.invoke(sqlSession, args);
                if (!isSqlSessionTransactional(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory()))
                {
                    sqlSession.commit(true);
                }
                return result;
            }
            catch (Throwable t)
            {
                Throwable unwrapped = unwrapThrowable(t);
                if (DynamicSqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException)
                {
                    Throwable translated = DynamicSqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null)
                    {
                        unwrapped = translated;
                    }
                }
                throw unwrapped;
            }
            finally
            {
                closeSqlSession(sqlSession, DynamicSqlSessionTemplate.this.getSqlSessionFactory());
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351

4、ApplicationConfig.java文件MapperScan注解添加sqlSessionTemplateRef属性

// basePackages 根据自己的包名变化
@MapperScan(basePackages = "com.ruoyi.**.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
1
2

5、DruidConfig.java文件添加atomikos支持

package com.ruoyi.framework.config;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.ruoyi.common.enums.DataSourceType;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.DruidProperties;
import com.ruoyi.framework.datasource.DynamicDataSource;

/**
 * druid 配置多数据源
 * 
 * @author ruoyi
 */
@Configuration
public class DruidConfig
{
    public static final String MASTER = DataSourceType.MASTER.name();

    public static final String SLAVE = DataSourceType.SLAVE.name();
    
    @Autowired
    private DruidProperties druidProperties;

    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(Environment env)
    {
        String prefix = "spring.datasource.druid.master.";
        return getDataSource(env, prefix, MASTER);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(Environment env)
    {
        String prefix = "spring.datasource.druid.slave.";
        return getDataSource(env, prefix, SLAVE);
    }

    protected DataSource getDataSource(Environment env, String prefix, String dataSourceName)
    {
        Properties prop = build(env, prefix);
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        ds.setUniqueResourceName(dataSourceName);
        ds.setXaProperties(prop);
        return ds;
    }

    protected Properties build(Environment env, String prefix)
    {
        Properties prop = new Properties();
        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("initialSize", druidProperties.getInitialSize());
        prop.put("minIdle", druidProperties.getMinIdle());
        prop.put("maxActive", druidProperties.getMaxActive());
        prop.put("maxWait", druidProperties.getMaxWait());
        prop.put("timeBetweenEvictionRunsMillis", druidProperties.getTimeBetweenEvictionRunsMillis());
        prop.put("minEvictableIdleTimeMillis", druidProperties.getMinEvictableIdleTimeMillis());
        prop.put("maxEvictableIdleTimeMillis", druidProperties.getMaxEvictableIdleTimeMillis());
        prop.put("validationQuery", druidProperties.getValidationQuery());
        prop.put("testWhileIdle", druidProperties.isTestWhileIdle());
        prop.put("testOnBorrow", druidProperties.isTestOnBorrow());
        prop.put("testOnReturn", druidProperties.isTestOnReturn());
        return prop;
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource)
    {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(MASTER, masterDataSource);
        setDataSource(targetDataSources, SLAVE, "slaveDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

    /**
     * 设置数据源
     * 
     * @param targetDataSources 备选数据源集合
     * @param sourceName 数据源名称
     * @param beanName bean名称
     */
    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
    {
        try
        {
            DataSource dataSource = SpringUtils.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        }
        catch (Exception e)
        {
        }
    }

    /**
     * 去除监控页面底部的广告
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Bean
    @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
    {
        // 获取web监控页面的参数
        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
        // 提取common.js的配置路径
        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
        final String filePath = "support/http/resources/js/common.js";
        // 创建filter进行过滤
        Filter filter = new Filter()
        {
            @Override
            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
            {
            }

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

            @Override
            public void destroy()
            {
            }
        };
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns(commonJsPattern);
        return registrationBean;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

6、修改DruidProperties,加入getset方法,方便读取。

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

    public DruidDataSource dataSource(DruidDataSource datasource)
    {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        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);
        return datasource;
    }

    public int getInitialSize()
    {
        return initialSize;
    }

    public void setInitialSize(int initialSize)
    {
        this.initialSize = initialSize;
    }

    public int getMinIdle()
    {
        return minIdle;
    }

    public void setMinIdle(int minIdle)
    {
        this.minIdle = minIdle;
    }

    public int getMaxActive()
    {
        return maxActive;
    }

    public void setMaxActive(int maxActive)
    {
        this.maxActive = maxActive;
    }

    public int getMaxWait()
    {
        return maxWait;
    }

    public void setMaxWait(int maxWait)
    {
        this.maxWait = maxWait;
    }

    public int getTimeBetweenEvictionRunsMillis()
    {
        return timeBetweenEvictionRunsMillis;
    }

    public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis)
    {
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }

    public int getMinEvictableIdleTimeMillis()
    {
        return minEvictableIdleTimeMillis;
    }

    public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis)
    {
        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }

    public int getMaxEvictableIdleTimeMillis()
    {
        return maxEvictableIdleTimeMillis;
    }

    public void setMaxEvictableIdleTimeMillis(int maxEvictableIdleTimeMillis)
    {
        this.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
    }

    public String getValidationQuery()
    {
        return validationQuery;
    }

    public void setValidationQuery(String validationQuery)
    {
        this.validationQuery = validationQuery;
    }

    public boolean isTestWhileIdle()
    {
        return testWhileIdle;
    }

    public void setTestWhileIdle(boolean testWhileIdle)
    {
        this.testWhileIdle = testWhileIdle;
    }

    public boolean isTestOnBorrow()
    {
        return testOnBorrow;
    }

    public void setTestOnBorrow(boolean testOnBorrow)
    {
        this.testOnBorrow = testOnBorrow;
    }

    public boolean isTestOnReturn()
    {
        return testOnReturn;
    }

    public void setTestOnReturn(boolean testOnReturn)
    {
        this.testOnReturn = testOnReturn;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187

7、MyBatisConfig.java文件添加sqlSessionTemplate加入多数据源

package com.ruoyi.framework.config;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.datasource.DynamicSqlSessionTemplate;

/**
 * Mybatis支持*匹配扫描包
 * 
 * @author ruoyi
 */
@Configuration
public class MyBatisConfig
{
    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    public static String setTypeAliasesPackage(String typeAliasesPackage)
    {
        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
        List<String> allResult = new ArrayList<String>();
        try
        {
            for (String aliasesPackage : typeAliasesPackage.split(","))
            {
                List<String> result = new ArrayList<String>();
                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
                Resource[] resources = resolver.getResources(aliasesPackage);
                if (resources != null && resources.length > 0)
                {
                    MetadataReader metadataReader = null;
                    for (Resource resource : resources)
                    {
                        if (resource.isReadable())
                        {
                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
                            try
                            {
                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
                            }
                            catch (ClassNotFoundException e)
                            {
                                e.printStackTrace();
                            }
                        }
                    }
                }
                if (result.size() > 0)
                {
                    HashSet<String> hashResult = new HashSet<String>(result);
                    allResult.addAll(hashResult);
                }
            }
            if (allResult.size() > 0)
            {
                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
            }
            else
            {
                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return typeAliasesPackage;
    }

    public Resource[] resolveMapperLocations(String[] mapperLocations)
    {
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        List<Resource> resources = new ArrayList<Resource>();
        if (mapperLocations != null)
        {
            for (String mapperLocation : mapperLocations)
            {
                try
                {
                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
                    resources.addAll(Arrays.asList(mappers));
                }
                catch (IOException e)
                {
                    // ignore
                }
            }
        }
        return resources.toArray(new Resource[resources.size()]);
    }

    public SqlSessionFactory createSqlSessionFactory(Environment env, DataSource dataSource) throws Exception
    {
        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        String configLocation = env.getProperty("mybatis.configLocation");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);

        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
        return sessionFactory.getObject();
    }

    @Bean(name = "sqlSessionFactoryMaster")
    public SqlSessionFactory sqlSessionFactoryMaster(Environment env, @Qualifier("masterDataSource") DataSource dataSource) throws Exception
    {
        return createSqlSessionFactory(env, dataSource);
    }

    @Bean(name = "sqlSessionFactorySlave")
    public SqlSessionFactory sqlSessionFactorySlave(Environment env, @Qualifier("slaveDataSource") DataSource dataSource) throws Exception
    {
        return createSqlSessionFactory(env, dataSource);
    }

    @Bean(name = "sqlSessionTemplate")
    public DynamicSqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactoryMaster") SqlSessionFactory factoryMaster,
            @Qualifier("sqlSessionFactorySlave") SqlSessionFactory factorySlave) throws Exception
    {
        Map<Object, SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
        sqlSessionFactoryMap.put(DruidConfig.MASTER, factoryMaster);
        sqlSessionFactoryMap.put(DruidConfig.SLAVE, factorySlave);

        DynamicSqlSessionTemplate customSqlSessionTemplate = new DynamicSqlSessionTemplate(factoryMaster);
        customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap);
        return customSqlSessionTemplate;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156

到此我们就完成了Atomikos分布式事务配置

8、测试验证

加入多数据源,如果不会使用可以参考多数据源实现。 对应需要操作多数据源方法加入@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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 如何优雅的关闭后台系统服务

优雅停机主要应用在版本更新的时候,为了等待正在工作的线程全部执行完毕,然后再停止。我们可以使用SpringBoot提供的Actuator

1、pom.xml中引入actuator依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
3
4

2、配置文件中endpoint开启shutdown

management:
  endpoint:
    shutdown:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "shutdown"
      base-path: /monitor
1
2
3
4
5
6
7
8
9

3、在ShiroConfig中设置filterChainDefinitionMap配置url=anon

filterChainDefinitionMap.put("/monitor/shutdown", "anon");
1

4、Post请求测试验证优雅停机 curl -X POST http://localhost:80/monitor/shutdown