后台手册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="如果登录账户已经存在,更新这条数据。"> 是否更新已经存在的用户数据 &nbsp; <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) 登录系统(系统管理-操作日志)可以查询操作日志列表和详细信息。