后台手册1
# 后台手册
## 分页实现
前端采用基于bootstrap的轻量级表格插件bootstrap-table(opens new window)
后端采用基于mybatis的轻量级分页插件pageHelper(opens new window)
> 提示
> 前后端分页实现流程
### 前端调用实现
```js
var options = {
url: prefix + "/list",
columns: [{
field: 'id',
title: '主键'
},
{
field: 'name',
title: '名称'
}]
};
$.table.init(options);
```
> **自定义查询条件参数(特殊情况提前设置查询条件下使用)**
```js
var options = {
url: prefix + "/list",
queryParams: queryParams,
columns: [{
field: 'id',
title: '主键'
},
{
field: 'name',
title: '名称'
}]
};
$.table.init(options);
function queryParams(params) {
var search = $.table.queryParams(params);
search.userName = $("#userName").val();
return search;
}
```
###
后台逻辑实现
```java
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(User user)
{
startPage(); // 此方法配合前端完成自动分页
List<User> list = userService.selectUserList(user);
return getDataTable(list);
}
```
**常见坑点1**:selectPostById莫名其妙的分页。例如下面这段代码
```java
startPage();
List<User> list;
if(user != null){
list = userService.selectUserList(user);
} else {
list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
```
**原因分析**:这种情况下由于user存在null的情况,就会导致pageHelper生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。 当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。
上面这个代码,应该写成下面这个样子才能保证安全。
```java
List<User> list;
if(user != null){
startPage();
list = userService.selectUserList(user);
} else {
list = new ArrayList<User>();
}
Post post = postService.selectPostById(1L);
return getDataTable(list);
```
**常见坑点2**:添加了startPage方法。也没有正常分页。例如下面这段代码
```java
startPage();
Post post = postService.selectPostById(1L);
List<User> list = userService.selectUserList(user);
return getDataTable(list);
```
**原因分析**:只对该语句以后的第一个查询(Select)语句得到的数据进行分页。
上面这个代码,应该写成下面这个样子才能正常分页。
```java
Post post = postService.selectPostById(1L);
startPage();
List<User> list = userService.selectUserList(user);
return getDataTable(list);
```
> 注意
> 如果改为其他数据库需修改配置application.yml文件中的属性helperDialect=你的数据库
## 导入导出
在实际开发中经常需要使用导入导出功能来加快数据的操作。在项目中可以使用注解来完成此项功能。 在需要被导入导出的实体类属性添加`@Excel`注解,目前支持参数如下:
### 注解参数说明
|参数|类型|默认值|描述|
|-|-|-|-|
|sort |int |Integer.MAX_VALUE |导出时在excel中排序,值越小越靠前|
|name |String |空|导出到Excel中的名字|
|dateFormat |String |空|日期格式, 如: yyyy-MM-dd|
|dictType |String |空 |如果是字典类型,请设置字典的type值 (如: sys_user_sex)|
|readConverterExp |String |空 |读取内容转表达式 (如: 0=男,1=女,2=未知)|
|separator |String |,| 分隔符,读取字符串组内容|
|scale |int |-1| BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)|
|roundingMode|int|BigDecimal.ROUND_HALF_EVEN|BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN|
|columnType|Enum|Type.STRING|导出类型(0数字 1字符串 2图片)|
|height|String|14|导出时在excel中每个列的高度 单位为字符|
|width|String|16|导出时在excel中每个列的宽 单位为字符|
|suffix|String|空|文字后缀,如% 90 变成90%|
|defaultValue|String|空|当值为空时,字段的默认值|
|prompt|String|空|提示信息|
|combo|String|Null|设置只能选择不能输入的列内容|
|targetAttr|String|空|另一个类中的属性名称,支持多级获取,以小数点隔开|
|isStatistics|boolean|false|是否自动统计数据,在最后追加一行统计数据总和|
|type|Enum|Type.ALL|字段类型(0:导出导入;1:仅导出;2:仅导入)|
|align|Enum|Type.AUTO|导出字段对齐方式(0:默认;1:靠左;2:居中;3:靠右)|
|handler|Class|ExcelHandlerAdapter.class|自定义数据处理器|
|args|String[]|{}|自定义数据处理器参数|
### 导出实现流程
1、前端调用封装好的方法$.table.init,传入后台exportUrl
```js
var options = {
exportUrl: prefix + "/export",
columns: [{
field: 'id',
title: '主键'
},
{
field: 'name',
title: '名称'
}]
};
$.table.init(options);
```
2、添加导出按钮事件
```js
<a class="btn btn-warning" onclick="$.table.exportExcel()">
<i class="fa fa-download"></i> 导出
</a>
```
3、在实体变量上添加@Excel注解
```java
@Excel(name = "用户序号", prompt = "用户编号")
private Long userId;
@Excel(name = "用户名称")
private String userName;
@Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
private String sex;
@Excel(name = "用户头像", cellType = ColumnType.IMAGE)
private String avatar;
@Excel(name = "帐号状态", dictType = "sys_normal_disable")
private String status;
@Excel(name = "最后登陆时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date loginDate;
```
4、在Controller添加导出方法
```java
@PostMapping("/export")
@ResponseBody
public AjaxResult export(User user)
{
List<User> list = userService.selectUserList(user);
ExcelUtil<User> util = new ExcelUtil<User>(User.class);
return util.exportExcel(list, "用户数据");
}
```
> 提示
> 导出默认流程是先创建一个临时文件,等待前端请求下载结束后马上删除这个临时文件。如遇到迅雷这种二次请求下载应用可能会导致文件已经被删除,我们也可以改成流的形式返回给前端。 参考实现 - 如何解决导出使用下载插件出现异常(opens new window)
### 导入实现流程
1、前端调用封装好的方法$.table.init,传入后台importUrl。
```js
var options = {
importUrl: prefix + "/importData",
columns: [{
field: 'id',
title: '主键'
},
{
field: 'name',
title: '名称'
}]
};
$.table.init(options);
```
2、添加导入按钮事件
```js
<a class="btn btn-info" onclick="$.table.importExcel()">
<i class="fa fa-upload"></i> 导入
</a>
```
3、添加导入前端代码,form默认id为importForm,也可指定importExcel(id)
```js
<!-- 导入区域 -->
<script id="importTpl" type="text/template">
<form enctype="multipart/form-data" class="mt20 mb10">
<div class="col-xs-offset-1">
<input type="file" id="file" name="file"/>
<div class="mt10 pt5">
<input type="checkbox" id="updateSupport" name="updateSupport" title="如果登录账户已经存在,更新这条数据。"> 是否更新已经存在的用户数据
<a onclick="$.table.importTemplate()" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> 下载模板</a>
</div>
<font color="red" class="pull-left mt10">
提示:仅允许导入“xls”或“xlsx”格式文件!
</font>
</div>
</form>
</script>
```
4、在实体变量上添加@Excel注解,默认为导出导入,也可以单独设置仅导入Type.IMPORT
```java
@Excel(name = "用户序号")
private Long id;
@Excel(name = "部门编号", type = Type.IMPORT)
private Long deptId;
@Excel(name = "用户名称")
private String userName;
/** 导出部门多个对象 */
@Excels({
@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
@Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
})
private SysDept dept;
/** 导出部门单个对象 */
@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT)
private SysDept dept;
```
5、在Controller添加导入方法,updateSupport属性为是否存在则覆盖(可选)
```java
@PostMapping("/importData")
@ResponseBody
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
List<SysUser> userList = util.importExcel(file.getInputStream());
String operName = ShiroUtils.getSysUser().getLoginName();
String message = userService.importUser(userList, updateSupport, operName);
return AjaxResult.success(message);
}
```
> 提示
> 也可以直接到main运行此方法测试。
```java
InputStream is = new FileInputStream(new File("D:\\test.xlsx"));
ExcelUtil<Entity> util = new ExcelUtil<Entity>(Entity.class);
List<Entity> userList = util.importExcel(is);
```
### 自定义标题信息
有时候我们希望导出表格包含标题信息,我们可以这样做。
**导出用户管理表格新增标题(用户列表)**
```java
public AjaxResult export(SysUser user)
{
List<SysUser> list = userService.selectUserList(user);
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
return util.exportExcel(list, "用户数据", "用户列表");
}
```
**导入表格包含标题处理方式,其中1表示标题占用行数,根据实际情况填写。**
```java
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
{
ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
List<SysUser> userList = util.importExcel(file.getInputStream(), 1);
String operName = SecurityUtils.getUsername();
String message = userService.importUser(userList, updateSupport, operName);
return AjaxResult.success(message);
}
```
### 自定义数据处理器
有时候我们希望数据展现为一个特殊的格式,或者需要对数据进行其它处理。Excel注解提供了自定义数据处理器以满足各种业务场景。而实现一个数据处理器也是非常简单的。如下:
1、在实体类用Excel注解handler属性指定自定义的数据处理器
```java
public class User extends BaseEntity
{
@Excel(name = "用户名称", handler = MyDataHandler.class, args = { "aaa", "bbb" })
private String userName;
}
```
2、编写数据处理器MyDataHandler继承ExcelHandlerAdapter,返回值为处理后的值。
```java
public class MyDataHandler implements ExcelHandlerAdapter
{
@Override
public Object format(Object value, String[] args)
{
// value 为单元格数据值
// args 为excel注解args参数组
return value;
}
}
```
## 上传下载
首先创建一张上传文件的表,例如:
```sql
drop table if exists sys_file_info;
create table sys_file_info (
file_id int(11) not null auto_increment comment '文件id',
file_name varchar(50) default '' comment '文件名称',
file_path varchar(255) default '' comment '文件路径',
primary key (file_id)
) engine=innodb auto_increment=1 default charset=utf8 comment = '文件信息表';
```
### 上传实现流程
1、代码生成sys_file_info表相关代码并复制到对应目录。
2、参考示例修改代码。
```js
<input id="filePath" name="filePath" class="form-control" type="file">
function submitHandler() {
if ($.validate.form()) {
uploadFile();
}
}
function uploadFile() {
var formData = new FormData();
if ($('#filePath')[0].files[0] == null) {
$.modal.alertWarning("请先选择文件路径");
return false;
}
formData.append('fileName', $("#fileName").val());
formData.append('file', $('#filePath')[0].files[0]);
$.ajax({
url: prefix + "/add",
type: 'post',
cache: false,
data: formData,
processData: false,
contentType: false,
dataType: "json",
success: function(result) {
$.operate.successCallback(result);
}
});
}
```
3、在FileInfoController添加对应上传方法
```java
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(@RequestParam("file") MultipartFile file, FileInfo fileInfo) throws IOException
{
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
fileInfo.setFilePath(fileName);
return toAjax(fileInfoService.insertFileInfo(fileInfo));
}
```
4、上传成功后需要预览可以对该属性格式化处理
```yaml
{
field : 'filePath',
title: '文件预览',
formatter: function(value, row, index) {
return $.table.imageView(value);
}
},
```
如需对文件格式控制,设置application.yml中的multipart属性
### 文件上传
```yaml
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
```
> 注意:如果只是单纯的上传一张图片没有其他参数可以使用通用方法 /common/upload
> 请求处理方法 com.ruoyi.web.controller.common.CommonController
### 下载实现流程
1、参考示例代码。
```js
function downloadFile(value){
window.location.href = ctx + "common/download/resource?resource=" + value;
}
```
2、参考Controller下载方法
```java
/**
* 本地资源通用下载
*/
@GetMapping("/common/download/resource")
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
throws Exception
{
// 本地资源路径
String localPath = Global.getProfile();
// 数据库资源地址
String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
// 下载名称
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
response.setCharacterEncoding("utf-8");
response.setContentType("multipart/form-data");
response.setHeader("Content-Disposition",
"attachment;fileName=" + FileUtils.setFileDownloadHeader(request, downloadName));
FileUtils.writeBytes(downloadPath, response.getOutputStream());
}
```
### 权限注解
Shiro注解权限控制
- @RequiresAuthentication使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。
- @RequiresGuest使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是gust身份,不需要经过认证或者在原先的session中存在记录。
- @RequiresPermissions当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
- @RequiresRoles当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
- @RequiresUser当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
#### @RequiresRoles
@RequiresRoles注解用于配置接口要求用户拥有某(些)角色才可访问,它拥有两个参数
|参数 |类型 |描述|
|-|-|-|
|value |String[] |角色列表|
|logical |Logical [] |角色之间的判断关系,默认为Logical.AND|
**示例1: **以下代码表示必须拥有admin角色才可访问
```java
@RequiresRoles("admin")
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
```
**示例2:** 以下代码表示必须拥有admin和common角色才可访问
```java
@RequiresRoles({"admin", "common"})
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
```
**示例3: **以下代码表示需要拥有admin或common角色才可访问
```java
@RequiresRoles(value = {"admin", "common"}, logical = Logical.OR)
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
```
#### @RequiresPermissions
@RequiresPermissions注解用于配置接口要求用户拥有某(些)权限才可访问,它拥有两个参数
|参数 |类型 |描述|
|-|-|-|
|value |String[] |权限列表|
|logical |Logical [] |权限之间的判断关系,默认为Logical.AND|
**示例1:** 以下代码表示必须拥有system:user:add权限才可访问
```java
@RequiresPermissions("system:user:add")
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
```
**示例2: **以下代码表示必须拥有system:user:add和system:user:update权限才可访问
```java
@RequiresPermissions({"system:user:add", "system:user:update"})
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
```
示例3: 以下代码表示需要拥有system:user:add或system:user:update角色才可访问
```java
@RequiresPermissions(value = {"system:user:add", "system:user:update"}, logical = Logical.OR)
public AjaxResult save(...)
{
return AjaxResult.success(...);
}
```
> 提示
> Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关) RequiresRoles、RequiresPermissions、RequiresAuthentication、RequiresUser、RequiresGuest。
> 例如:你同时声明了RequiresRoles和RequiresPermissions,那就要求拥有此角色的同时还得拥有相应的权限。
## 事务管理
新建的Spring Boot项目中,一般都会引用spring-boot-starter或者spring-boot-starter-web,而这两个起步依赖中都已经包含了对于spring-boot-starter-jdbc或spring-boot-starter-data-jpa的依赖。 当我们使用了这两个依赖的时候,框架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager。 所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用。
> 提示
> @Transactional注解只能应用到public可见度的方法上,可以被应用于接口定义和接口方法,方法会覆盖类上面声明的事务。
例如用户新增需要插入用户表、用户与岗位关联表、用户与角色关联表,如果插入成功,那么一起成功,如果中间有一条出现异常,那么回滚之前的所有操作, 这样可以防止出现脏数据,就可以使用事务让它实现回退。
做法非常简单,我们只需要在方法或类添加@Transactional注解即可。
```java
@Transactional
public int insertUser(User user)
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
return rows;
}
```
**常见坑点1:**遇到检查异常时,事务开启,也无法回滚。 例如下面这段代码,用户依旧增加成功,并没有因为后面遇到检查异常而回滚!!
```java
@Transactional
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
throw new SQLException("发生异常了..");
}
return rows;
}
```
**原因分析:**因为Spring的默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。如果想针对检查异常进行事务回滚,可以在@Transactional注解里使用 rollbackFor属性明确指定异常。
例如下面这样,就可以正常回滚:
```java
@Transactional(rollbackFor = Exception.class)
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
throw new SQLException("发生异常了..");
}
return rows;
}
```
**常见坑点2**: 在业务层捕捉异常后,发现事务不生效。 这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。
例如:下面这段代码直接导致用户新增的事务回滚没有生效。
```java
@Transactional
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
try
{
// 谨慎:尽量不要在业务层捕捉异常并处理
throw new SQLException("发生异常了..");
}
catch (Exception e)
{
e.printStackTrace();
}
}
return rows;
}
```
**推荐做法:** 在业务层统一抛出异常,然后在控制层统一处理。
```java
@Transactional
public int insertUser(User user) throws Exception
{
// 新增用户信息
int rows = userMapper.insertUser(user);
// 新增用户岗位关联
insertUserPost(user);
// 新增用户与角色管理
insertUserRole(user);
// 模拟抛出SQLException异常
boolean flag = true;
if (flag)
{
throw new RuntimeException("发生异常了..");
}
return rows;
}
```
**Transactional注解的常用属性表:**
|属性 |说明|
|-|-|
|propagation|事务的传播行为,默认值为 REQUIRED。|
|isolation|事务的隔离度,默认值采用 DEFAULT|
|timeout|事务的超时时间,默认值为-1,不超时。如果设置了超时时间(单位秒),那么如果超过该时间限制了但事务还没有完成,则自动回滚事务。|
|read-only|指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。|
|rollbackFor|用于指定能够触发事务回滚的异常类型,如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。{xxx1.class, xxx2.class,……}|
|noRollbackFor|抛出 no-rollback-for 指定的异常类型,不回滚事务。{xxx1.class, xxx2.class,……}|
|....| |
> 提示
> 事务的传播机制是指如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。 即:在执行一个@Transactinal注解标注的方法时,开启了事务;当该方法还在执行中时,另一个人也触发了该方法;那么此时怎么算事务呢,这时就可以通过事务的传播机制来指定处理方式。
**TransactionDefinition传播行为的常量:**
|常量 |含义|
|-|-|
|TransactionDefinition.PROPAGATION_REQUIRED|如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。|
|TransactionDefinition.PROPAGATION_REQUIRES_NEW|创建一个新的事务,如果当前存在事务,则把当前事务挂起。|
|TransactionDefinition.PROPAGATION_SUPPORTS|如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。|
|TransactionDefinition.PROPAGATION_NOT_SUPPORTED|以非事务方式运行,如果当前存在事务,则把当前事务挂起。|
|TransactionDefinition.PROPAGATION_NEVER|以非事务方式运行,如果当前存在事务,则抛出异常。|
|TransactionDefinition.PROPAGATION_MANDATORY|如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。|
|TransactionDefinition.PROPAGATION_NESTED|如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。|
## 异常处理
通常一个web框架中,有大量需要处理的异常。比如业务异常,权限不足等等。前端通过弹出提示信息的方式告诉用户出了什么错误。 通常情况下我们用try.....catch....对异常进行捕捉处理,但是在实际项目中对业务模块进行异常捕捉,会造成代码重复和繁杂, 我们希望代码中只有业务相关的操作,所有的异常我们单独设立一个类来处理它。全局异常就是对框架所有异常进行统一管理。 我们在可能发生异常的方法里throw抛给控制器。然后由全局异常处理器对异常进行统一处理。 如此,我们的Controller中的方法就可以很简洁了。
**所谓全局异常处理器就是使用@ControllerAdvice注解。示例如下:**
1、统一返回实体定义
```java
package com.ruoyi.common.core.domain;
import java.util.HashMap;
/**
* 操作消息提醒
*
* @author ruoyi
*/
public class AjaxResult extends HashMap<String, Object>
{
private static final long serialVersionUID = 1L;
/**
* 返回错误消息
*
* @param code 错误码
* @param msg 内容
* @return 错误消息
*/
public static AjaxResult error(String msg)
{
AjaxResult json = new AjaxResult();
json.put("msg", msg);
json.put("code", 500);
return json;
}
/**
* 返回成功消息
*
* @param msg 内容
* @return 成功消息
*/
public static AjaxResult success(String msg)
{
AjaxResult json = new AjaxResult();
json.put("msg", msg);
json.put("code", 0);
return json;
}
}
```
2、定义登录异常定义
```java
package com.ruoyi.common.exception;
/**
* 登录异常
*
* @author ruoyi
*/
public class LoginException extends RuntimeException
{
private static final long serialVersionUID = 1L;
protected final String message;
public LoginException(String message)
{
this.message = message;
}
@Override
public String getMessage()
{
return message;
}
}
```
3、基于@ControllerAdvice注解的Controller层的全局异常统一处理
```java
package com.ruoyi.framework.web.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.LoginException;
/**
* 全局异常处理器
*
* @author ruoyi
*/
@RestControllerAdvice
public class GlobalExceptionHandler
{
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 登录异常
*/
@ExceptionHandler(LoginException.class)
public AjaxResult loginException(LoginException e)
{
log.error(e.getMessage(), e);
return AjaxResult.error(e.getMessage());
}
}
```
4、测试访问请求
```java
@Controller
public class SysIndexController
{
/**
* 首页方法
*/
@GetMapping("/index")
public String index(ModelMap mmap)
{
/**
* 模拟用户未登录,抛出业务逻辑异常
*/
SysUser user = ShiroUtils.getSysUser();
if (StringUtils.isNull(user))
{
throw new LoginException("用户未登录,无法访问请求。");
}
mmap.put("user", user);
return "index";
}
}
```
根据上面代码含义,当我们未登录访问/index时就会发生LoginException业务逻辑异常,按照我们之前的全局异常配置以及统一返回实体实例化,访问后会出现AjaxResult格式JSON数据, 下面我们运行项目访问查看效果。
界面输出内容如下所示:
```js
{
"msg": "用户未登录,无法访问请求。",
"code": 500
}
```
对于一些特殊情况,如接口需要返回json,页面请求返回html可以使用如下方法:
```java
@ExceptionHandler(LoginException.class)
public Object loginException(HttpServletRequest request, LoginException e)
{
log.error(e.getMessage(), e);
if (ServletUtils.isAjaxRequest(request))
{
return AjaxResult.error(e.getMessage());
}
else
{
return new ModelAndView("/error/500");
}
}
```
> 若依系统的全局异常处理器GlobalExceptionHandler
> 注意:如果全部异常处理返回json,那么可以使用@RestControllerAdvice代替@ControllerAdvice,这样在方法上就可以不需要添加@ResponseBody。
> **无法捕获异常?**
> 如果您的异常无法捕获,您可以从以下几个方面着手检查
> 异常是否已被处理,即抛出异常后被catch,打印了日志或抛出了其它异常 异常是否非Controller抛出,即在拦截器或过滤器中出现的异常
## 参数验证
spring boot中可以用@Validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。
#注解参数说明
|注解名称 |功能|
|-|-|
|@Null|检查该字段为空|
|@NotNull|不能为null|
|@NotBlank|不能为空,常用于检查空字符串|
|@NotEmpty|不能为空,多用于检测list是否size是0|
|@Max|该字段的值只能小于或等于该值|
|@Min|该字段的值只能大于或等于该值|
|@Past|检查该字段的日期是在过去|
|@Future|检查该字段的日期是否是属于将来的日期|
|@Email|检查是否是一个有效的email地址|
|@Pattern(regex=,flag=)|被注释的元素必须符合指定的正则表达式|
|@Range(min=,max=,message=)|被注释的元素必须在合适的范围内|
|@Size(min=, max=)|检查该字段的size是否在min和max之间,可以是字符串、数组、集合、Map等|
|@Length(min=,max=)|检查所属的字段的长度是否在min和max之间,只能用于字符串|
|@AssertTrue|用于boolean字段,该字段只能为true|
|@AssertFalse|该字段的值只能为false|
## 数据校验使用
1、基础使用 因为spring boot已经引入了基础包,所以直接使用就可以了。首先在controller上声明@Validated需要对数据进行校验。
```java
public AjaxResult add(@Validated @RequestBody SysUser user)
{
.....
}
```
2、然后在对应字段Get方法加上参数校验注解,如果不符合验证要求,则会以message的信息为准,返回给前端。
```java
@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
public String getNickName()
{
return nickName;
}
@NotBlank(message = "用户账号不能为空")
@Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")
public String getUserName()
{
return userName;
}
@Email(message = "邮箱格式不正确")
@Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
public String getEmail()
{
return email;
}
@Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符")
public String getPhonenumber()
{
return phonenumber;
}
```
也可以直接放在字段上面声明。
```java
@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
private String nickName;
```
## 系统日志
在实际开发中,对于某些关键业务,我们通常需要记录该操作的内容,一个操作调一次记录方法,每次还得去收集参数等等,会造成大量代码重复。 我们希望代码中只有业务相关的操作,在项目中使用注解来完成此项功能。
在需要被记录日志的controller方法上添加@Log注解,使用方法如下:
```java
@Log(title = "用户管理", businessType = BusinessType.INSERT)
public AjaxResult addSave(...)
{
return success(...);
}
```
### 注解参数说明
|参数|类型|默认值|描述|
|-|-|-|-|
|title|String|空|操作模块|
|businessType|BusinessType|OTHER|操作功能(OTHER其他、INSERT新增、UPDATE修改、DELETE删除、GRANT授权、EXPORT导出、IMPORT导入、FORCE强退、GENCODE生成代码、CLEAN清空数据)|
|operatorType|OperatorType|MANAGE|操作人类别(OTHER其他、MANAGE后台用户、MOBILE手机端用户)|
|isSaveRequestData|boolean|true|是否保存请求的参数|
|isSaveResponseData|boolean|true|是否保存响应的参数|
### 自定义操作功能
1、在BusinessType中新增业务操作类型如:
```java
/**
* 测试
*/
TEST,
```
2、在sys_dict_data字典数据表中初始化操作业务类型
```sql
insert into sys_dict_data values(25, 10, '测试', '10', 'sys_oper_type', '', 'primary', 'N', '0', 'admin', '2018-03-16 11-33-00', 'ry', '2018-03-16 11-33-00', '测试操作');
```
3、在Controller中使用注解
```java
@Log(title = "测试标题", businessType = BusinessType.TEST)
public AjaxResult test(...)
{
return success(...);
}
```
操作日志记录逻辑实现代码LogAspect.java(opens new window)
登录系统(系统管理-操作日志)可以查询操作日志列表和详细信息。