# 后端手册
# 项目结构
包名 | 含义 |
---|---|
com.eva.api | 控制层/接口层 |
com.eva.biz | 复合业务层 |
com.eva.service | 颗粒业务层 |
com.eva.dao | 持久层 |
com.eva.config | 配置 |
com.eva.core | 项目核心 |
如果我们划分得更细致些,那么可以得到下表
包名 | 含义 |
---|---|
com.eva.api | 控制层/接口层 |
com.eva.biz | 复合业务层 |
com.eva.service | 颗粒业务层 |
com.eva.service.aware | 数据意识 |
com.eva.service.common | 通用颗粒业务 |
com.eva.service.proxy | 业务代理 |
com.eva.dao | 持久层 |
com.eva.config | 配置 |
com.eva.core | 项目核心 |
com.eva.core.annotation | 项目注解 |
com.eva.core.aware | 通用意识接口、意识默认实现及意识辅助注解 |
com.eva.core.constants | 常量定义 |
com.eva.core.exception | 自定义异常 |
com.eva.core.model | 通用模型 |
com.eva.core.utils | 通用工具类 |
设计思想
项目为四层结构,api > biz > service > dao
,相信很多新手会疑惑,但随着你技术、经验和思想的提升,你很快也会这么去做!现在让我们加快你成长的步伐,尝试着理解每一层的作用,为了更方便的理解,需要从后往前进行讲解!
- 持久层
持久层用于将 处理好的数据保存至数据库。这一层的代码可直接生成,不需要手动编写。
- 颗粒业务层
颗粒业务层用于 编写颗粒业务,例如怎样删除(逻辑删除还是物理删除),怎样批量删除(循环调用还是SQL实现)等。在这一层中不涉及多表操作(除查询外)。这一层的代码可直接生成,不需要手动编写。
- 复合业务层
复合业务层用于 编写复合业务,这一层可能涉及到多表的增删改操作,而单表的增删改操作的业务实现在颗粒业务层。所以复合业务层应该使用颗粒业务层对象,而不应该直接使用持久层对象。当然,如果你的业务足够简单,例如删除一条单表记录,那么这一层可以不用实现。如果你的业务是一个复合业务,例如新建时需要验证编码字段是否重复,那么此时需要在该层添加创建方法,并在该方法中进行验证。
- 控制层/接口层
控制层/接口层用于 定义接口并调用相关业务方法,而业务方法可以来自复合业务层和颗粒业务层。
# 服务端口配置
端口配置在application.yml的server.port属性中。调整后需重启服务。
# 权限控制
Eva整合Shiro实现权限控制,通过Shiro提供的@RequiresRoles
和@RequiresPermissions
注解可完成角色或权限的控制。
# @RequiresRoles
@RequiresRoles
注解用于配置接口要求用户拥有某(些)角色才可访问,它拥有两个参数:
- value: 角色列表
- logical: 角色之间的判断关系,默认为Logical.AND
示例1: 以下代码表示必须拥有admin角色才可访问
@RequiresRoles("admin")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例2: 以下代码表示必须拥有admin 和 manager角色才可访问
@RequiresRoles({"admin", "manager"})
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例3: 以下代码表示必须拥有admin 或 manager角色才可访问
@RequiresRoles(value = {"admin", "manager"}, logical = Logical.OR)
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# @RequiresPermissions
@RequiresPermissions
注解用于配置接口要求用户拥有某(些)权限才可访问,它拥有两个参数:
- value: 权限列表
- logical: 权限之间的判断关系,默认为Logical.AND
示例1: 以下代码表示必须拥有order:create权限才可访问
@RequiresPermissions("order:create")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例2: 以下代码表示必须拥有order:create 和 order:update权限才可访问
@RequiresPermissions({"order:create", "order:update"})
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例3: 以下代码表示必须拥有order:create 或 order:update角色才可访问
@RequiresPermissions(value = {"order:create", "order:update"}, logical = Logical.OR)
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# 参数验证
Eva采用spring自带的@Validated
注解进行参数验证。为了在使用实体类的情况下更好的区分出新建、编辑和其他操作验证的不同,Eva提供了OperaType
操作类型标识类。使用方式如下
MyController.java
// 新建
public ApiResponse create(@Validated(OperaType.Create.class) @RequestBody MyModel myModel) {
return ApiResponse.success(...);
}
// 编辑
public ApiResponse updateById(@Validated(OperaType.Update.class) @RequestBody MyModel myModel) {
return ApiResponse.success(...);
}
2
3
4
5
6
7
8
9
MyModel.java
public class MyModel {
// 仅在编辑时验证
@NotNull(message = "主键不能为空", groups = {OperaType.Update.class})
private Integer id;
// 在新建和编辑时验证
@NotBlank(message = "名称不能为空", groups = {OperaType.Create.class, OperaType.Update.class})
private String name;
}
2
3
4
5
6
7
8
9
10
11
扩展
OperaType类只是用于标识出不同的操作类型,所以如果你有更多的操作类型,你可以在OperaType类中继续添加新的标识。
# 无认证接口实现
有些时候我们允许不登录直接访问接口,此时需要在拦截器中配置接口放行。如下
com.myproject.WebConfigurer.java
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/resource/result/**",
"/resource/image/**",
"/resource/result",
"/resource/record",
"/sms/**",
"/doc.html",
"/swagger-resources/**",
"/webjars/**",
"/user/regis",
"/user/login",
"/user/setPwd",
"/user/findPwd",
"/new/interface"
);
}
}
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
注意
系统中提供的需要认证才能访问的接口,请勿在此放行。这样接口获取不到用户信息导致出现错误。
# 事务处理
在需要进行事务处理的方法上添加@Transactional
注解即可。无论是service层还是biz层这种方式都是有效的。
误区
有些同学可能因为"懒"的缘故,直接在Controller中实现biz层做的事情,无论Controller方法上添加@Transactional
是否能使事务生效,这种做法都是不规范的。
# 分页实现
虽然分页代码可以直接生成,但我们仍然需要为您说明分页的实现逻辑。
# 单表的分页实现
单表的分页使用MyBatis Plus自带的分页方法来实现是再好不过的选择了,这样可方便的实现多条件查询。
public PageData<MyModel> findPage(PageWrap<MyModel> pageWrap) {
IPage<MyModel> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
QueryWrapper<MyModel> queryWrapper = new QueryWrapper<>(WrapperUtil.blankToNull(pageWrap.getModel()));
return PageData.from(myMapper.selectPage(page, queryWrapper));
}
2
3
4
5
# 多表的分页实现
多表的分页需要编写SQL语句,使用MyBatis的分页插件来实现会是一个明智的选择。
@Override
public PageData<MyListVO> findPage(PageWrap<MyQueryDTO> pageWrap) {
PageHelper.startPage(pageWrap.getPage(), pageWrap.getCapacity());
List<MyListVO> myList = myMapper.selectManageList(pageWrap.getModel());
return PageData.from(new PageInfo<>(myList));
}
2
3
4
5
6
PageData和PageWrap
细心的同学可能已经发现,无论是单表分页还是多表分页,方法参数均为PageWrap
,方法返回均为PageData
,为什么不使用MyBatis Plus自带的IPage?有经验的同学可能不难理解这是一种包装,这样可以方便的扩展我们想要的字段,并且无论分页方式如何变化,方法的请求参数和响应结构都能始终保持一致。
# 分页字段排序的实现
有时候我们需要实现列表中存在按自定义列排序的需求。Eva做了些简单的封装支持了字段排序的功能。
# 单表的分页字段排序
单表的分页字段排序可以使用MyBatis Plus默认的实现来处理。
public PageData<MyModel> findPage(PageWrap<MyModel> pageWrap) {
IPage<MyModel> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
QueryWrapper<MyModel> queryWrapper = new QueryWrapper<>(MyBatisPlus.blankToNull(pageWrap.getModel()));
// 字段排序
for(PageWrap.SortData sortData: pageWrap.getSorts()) {
if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
queryWrapper.orderByDesc(sortData.getProperty());
} else {
queryWrapper.orderByAsc(sortData.getProperty());
}
}
return PageData.from(myMapper.selectPage(page, queryWrapper));
}
2
3
4
5
6
7
8
9
10
11
12
13
# 多表的分页字段排序
多表的排序需要结合新的方法参数和SQL处理来完成,不过实现也非常简单。
MyMapper.java
增加orderByClause,并通过@Param
注解标记SQL参数名称。
public interface MyMapper extends BaseMapper<MyModel> {
List<MyModelListVO> selectManageList(@Param("dto") QueryMyModelDTO dto, @Param("orderByClause") String orderByClause);
}
2
3
4
MyMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="...">
<select id="selectManageList" ...>
SELECT ...
FROM ...
LEFT JOIN ...
LEFT JOIN ...
<where>
<if dto.param != null>
AND ...
</if>
</where>
${orderByClause}
</select>
</mapper>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
MyServiceImpl.java
@Override
public PageData<MyListVO> findPage(PageWrap<MyQueryDTO> pageWrap) {
PageHelper.startPage(pageWrap.getPage(), pageWrap.getCapacity());
List<MyListVO> myList = myMapper.selectManageList(pageWrap.getModel(), pageWrap.getOrderByClause());
return PageData.from(new PageInfo<>(myList));
}
2
3
4
5
6
注意
MyMapper.java中使用@Param注解标识了SQL参数的名称,条件参数对象被标识为dto
,因此在使用条件字段时应使用dto.prop
(prop为字段名称),而不应直接使用prop
。
# 导出Excel
在Eva中实现一个导出功能是非常简单的。如下
# 1. 定义导出接口
public void exportExcel (HttpServletResponse response) {
// 使用ExcelExporter导出文件
ExcelExporter.build(MyModel.class).export(myModelService.findList(), "My Excel", response);
// 你还可以指定Sheet的名称
// ExcelExporter.build(MyModel.class).export(myModelService.findList(), "My Excel", "My Sheet", response);
}
2
3
4
5
6
# 2. 配置Excel列信息
文件:MyModel.java
public class MyModel {
@ExcelColumn(name="列名1")
private String name;
@ExcelColumn(name="列名2")
private String name2;
}
2
3
4
5
6
7
8
这样我们就完成了导出Excel接口的实现。
# ExcelColumn注解参数说明
参数 | 说明 |
---|---|
name | 列名 |
width | 列宽 |
index | 列排序,默认为-1(按字段定义顺序) |
align | 对齐方式 |
backgroundColor | 列头背景色 |
dataBackgroundColor | 数据单元格的背景色 |
color | 字体颜色 |
fontSize | 字体大小 |
bold | 是否加粗 |
italic | 是否倾斜 |
valueMapping | 值映射,如0=女;1=男 |
prefix | 数据前缀 |
suffix | 数据后缀 |
dateFormat | 日期格式,只有数据为java.util.Date时才生效 |
handler | Class, 自定义数据处理器 |
args | String[], 自定义数据处理器的额外参数 |
# 自定义数据处理器
有时候我们希望数据展现为一个特殊的格式,或者需要对数据进行其它处理。Eva提供了自定义数据处理器以满足各种业务场景。而实现一个数据处理器也是非常简单的。如下:
- 1. 指定自定义的数据处理器
public class MyModel {
@ExcelColumn(name="列名1", handler=MyDataHandler.class, args = {"arg1", "arg2"})
private String name;
}
2
3
4
5
- 2. 编写数据处理器MyDataHandler
public class MyDataHandler implements ExcelDataHandlerAdapter {
@Override
Object format (Object... args) {
// 其中args[0]为单元格的数据,arg[1]及其之后为@ExcelColumn的args参数(在这里args[1]的结果为"arg1")
}
}
2
3
4
5
6
7
# 自定义数据权限
默认情况下,Eva实现了部门数据权限和独立的岗位数据权限控制。如果您需要添加新的数据权限,如审批数据权限,物品数据权限等,那么您也可以清晰且方便的实现。实现一套数据权限的步骤如下:
# 1. 定义业务模块和权限类型
文件:core/DataPermissionConstants.java
/**
* 数据权限模块
*/
@Getter
@AllArgsConstructor
enum Module {
MY_MODULE("MY_MODULE", "我的模块"),
;
}
/**
* 数据权限类型
*/
@Getter
@AllArgsConstructor
enum Type {
ALL((short)0,"全部", new Module[]{}),
MY_MODULE_CUSTOM((short)1001, "自定义数据", new Module[]{Module.MY_MODULE}),
MY_MODULE_CHILD((short)1002, "用户所属及其子数据", new Module[]{Module.MY_MODULE}),
;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2. 编写数据意识类继承DefaultDataPermissionAware
@Component
public class MyModelDataPermissionAware extends DefaultDataPermissionAware<MyModel> {
/**
* 覆盖父类方法:返回当前数据权限的模块
*/
@Override
public DataPermissionConstants.Module module() {
return DataPermissionConstants.Module.MY_MODULE;
}
/**
* 覆盖父类方法:当角色未配置数据权限时的默认数据权限。
*/
@Override
public List<SystemDepartmentListVO> defaultData(Integer userId) {
return child(userId);
}
/**
* 所有权限
*/
@DataPermissionMapping(value = DataPermissionConstants.Type.ALL, priority = 1)
public List<SystemDepartmentListVO> all() {
// 数据权限为"全部"时的数据获取逻辑。
}
/**
* 自定义数据
*/
@DataPermissionMapping(value = DataPermissionConstants.Type.MY_MODULE_CUSTOM, priority = 2, injectCustomData = true)
public List<MyModel> custom (String customData) {
// 数据权限为"自定义数据"时的数据获取逻辑。
}
/**
* 用户所属及其子数据
*/
@DataPermissionMapping(value = DataPermissionConstants.Type.MY_MODULE_CUSTOM, priority = 3, injectUser = true)
public List<MyModel> child (Integer userId) {
// 数据权限为"用户所属及其子数据"时的数据获取逻辑。
}
}
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
# 3. 调用数据权限意识
DefaultDataPermissionAware
类提供了execute方法,该方法将自动根据用户角色配置的数据权限执行权限方法。
@Autowired
private MyModelDataPermissionAware myModelDataPermissionAware;
public List<MyModel> findList () {
return myModelDataPermissionAware.excute();
}
2
3
4
5
6
7
# @DataPermissionMapping说明
@DataPermissionMapping
注解用于标记和配置数据权限处理方法。他有如下参数:
参数 | 说明 |
---|---|
value | 权限类型 |
priority | 优先级,不允许重复 |
injectUser | 是否注入用户ID参数,为true时方法可接收Integer userId参数 |
injectCustomData | 是否注入自定义数据参数,为true时方法可接收String customData参数 |
priority/优先级的作用
一个用户可能拥有多个角色,多个角色意味着可能拥有不同的数据权限。priority
用于在一个用户拥有多种数据权限时该如何选择使用哪种数据权限,值越小,优先级越高。
# 防重复提交
在接口方法上添加@PreventRepeat
注解即可,他有如下参数:
# @PreventRepeat
参数 | 说明 |
---|---|
value | 防重复规则设定类,默认为PreventRepeatDefaultHandler.class |
interval | 间隔时间,小于此时间视为重复提交,默认为800毫秒 |
message | 错误消息 |
limit | 1分钟内限制的请求次数(<=0时表示不限制),默认为0 |
limitMessage | 被限制时的错误消息 |
lockTime | 超出请求限制次数时锁定的时长(ms),默认10分钟 |
示例1:采用默认参数
@PreventRepeat
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例2:指定防重复时间和错误消息
@PreventRepeat(interval = 1000, message = "请求过于频繁")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例3:指定接口时限调用次数和消息
@PreventRepeat(limit = 10, limitMessage = "请求过于频繁,请休息10分钟以后再试")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
示例4:指定自定义防重复规则实现类
@PreventRepeat(value = MyPreventRepeatHandler.class ,interval = 1000, message = "请求过于频繁")
public ApiResponse create(...) {
return ApiResponse.success(...);
}
2
3
4
# 自定义防重复提交规则
默认情况下防重复的规则由PreventRepeatDefaultHandler.class
设定,它将请求路径、请求体参数、Cookie信息合并在一起并签名作为请求的Key。如果你并不希望这样,那么你可以自定义防重复的规则。实现方式如下
@Component
public class MyPreventRepeatHandler extends PreventRepeatAdapter {
@Override
public String sign(HttpServletRequest request) {
// 根据您要验证的参数进行签名并返回即可
}
}
2
3
4
5
6
7
8
# 操作日志
Eva的操作日志分为两种模式——智能模式和手动模式。您可以在构建项目时选择你喜欢的模式,当然我们推荐智能模式,因为它的确非常方便。无论是手动模式还是智能模式,Eva都通过@Trace
注解来描述日志内容。
# @Trace
@Trace
注解可以添加在接口方法上,也可以添加到Controller类上。以下是注解参数说明。
参数 | 含义 | 默认值 | 说明 |
---|---|---|---|
module | 模块名称 | 不存在值时读取类上@Trace 的module参数,如果类上无@Trace 则读取Swagger @Api 的tags参数 | |
type | 操作类型 | TraceType.AUTO | 默认自动推测 |
remark | 操作备注 | 不存在值时读取Swagger @ApiOperation ,如果不存在@ApiOperation ,则读取操作类型的备注 | |
exclude | 是否排除 | false | 为true时表示不为接口添加日志记录 |
withRequestParameters | 是否写入请求参数 | true | 为false时表示日志中不记录请求参数 |
withRequestResult | 是否写入请求结果 | true | 为false时表示日志中不记录请求结果 |
# 智能模式
智能模式下,默认会为所有的非查询接口添加操作日志。并且会根据接口路径来自动识别操作类型。在一个管理系统中通常所有的数据流转操作都需要记录日志,所以我们推荐您使用智能模式来实现操作日志的功能。为了更好的管理不记录操作日志的情况,Eva提供了注解排除方式和路径排除方式。注解排除方式即@Trace(exclude = true)
,路径排除方式则通过配置文件处理,如下:
application.yml
trace:
# 开启智能跟踪模式
smart: true
# 排除跟踪的URL正则
exclude-patterns: .*/list$, .*/tree$, .*/page$
2
3
4
5
约定
智能模式自动识别的原理是通过正则匹配接口路径,为了能够准确的识别,你应该按如下约定进行接口路径的定义:
- 新增: 满足正则表达式
.+/create.*
- 修改: 满足正则表达式
.+/update.**
- 删除: 满足正则表达式
.+/delete.*
- 批量删除: 满足正则表达式
.+/delete/batch$
- 导入: 满足正则表达式
.+/import.*
- 导出: 满足正则表达式
.+/export.*
- 重置: 满足正则表达式
.+/reset.**
# 手动模式
手动模式下,接口必须添加@Trace
注解才会使接口记录日志。
不要随意切换
由于智能模式和手动模式在使用时刚好是反向作用,所以建议您在项目初期就定好使用哪种模式。在项目后期不要随意切换,防止操作日志的丢失。
# 实用工具类
所有工具类都通过Utils调用,这样你可以不用去查找工具类就可以方便的获取工具类对象。
# Utils.Http
Utils.Http为Http请求工具类,我们在设计时采用了链式调用的方案,这样可以增强代码可读性,并且使用起来也更加方便。在Eva中,发起一次请求的基本逻辑如下:
- 构造Http.HttpWrap对象
- 请求设置(如设置请求头,请求超时时间等)
- 发起请求
- 请求结果转换
构造Http.HttpWrap对象
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
build | String url | Http.HttpWrap | 根据请求地址构造 |
build | String url, String charset | Http.HttpWrap | 根据请求地址和编码构造 |
请求设置
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
setRequestProperty | String key, String value | Http.HttpWrap | 设置请求头 |
setConnectTimeout | int timeout | Http.HttpWrap | 设置连接超时时间 |
setReadTimeout | int timeout | Http.HttpWrap | 设置读取超时时间 |
gzip | Http.HttpWrap | 开启gzip压缩 |
发起请求
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
get | Http.HttpResult | 发起GET请求 | |
post | Http.HttpResult | 发起POST请求 | |
post | String params | Http.HttpResult | 发起POST请求 |
postJSON | Map<String, Object> paramsMap | Http.HttpResult | 发起POST请求 |
postJSON | JSONObject paramJSONObject | Http.HttpResult | 发起POST请求 |
请求结果转换
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
toStringResult | String | 转为字符串 | |
toJSONObject | JSONObject | 转为fastjson JSONObject对象 | |
toClass | Class | 转为指定class后的对象 | 转为指定类对象 |
示例1: 发起GET请求
Utils.Http.build(url)
.get()
.toStringResult();
2
3
示例2: 发起POST请求
Utils.Http.build(url)
.post(data)
.toStringResult();
2
3
示例3: 开启gzip压缩
Utils.Http.build(url)
.gzip()
.post(data)
.toStringResult();
2
3
4
示例4: 设置请求编码
Utils.Http.build(url, Charset.forName("GBK").toString())
.get()
.toStringResult();
2
3
# Utils.OSS
Utils.OSS为OSS工具类,用于处理文件的上传与下载,跟Utils.Http一样,Utils.OSS也采用了链式调用设计。它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
setMaxSize | int | Utils.OSS | 设置文件大小限制,单位为M |
setFileTypes | String | Utils.OSS | 设置文件类型限制(多个类型使用","隔开,如".jpg,jpeg,.png") |
uploadImage | MultipartFile | OSS.UploadResult | 上传图片 |
uploadImage | MultipartFile, String | OSS.UploadResult | 上传图片,第二个参数为业务路径,如使用"avatar"表示用户头像路径,"goods/cover"表示商品封面图片等 |
upload | MultipartFile | OSS.UploadResult | 上传文件 |
upload | MultipartFile, String | OSS.UploadResult | 上传文件,如使用"contract"表示合同文件,"contract/attach"表示合同附件 |
download | String | InputStream | 下载文件 |
可以看到,所有上传的方法均返回OSS.UploadResult
对象,这样可以方便我们获取文件相关的重要内容。
OSS.UploadResult属性说明
属性名称 | 类型 | 说明 |
---|---|---|
originalFilename | String | 源文件名称 |
fileKey | String | 文件的key |
accessUri | String | 访问路径/下载路径,默认情况下图片访问路径为/resource/image ,文件下载路径为/resource/attach 。 |
示例1:文件大小和格式的限制
// 上传文件"avatar.jpg"
Utils.OSS
// 限定文件大小不超过5M
.setMaxSize(5)
// 限定只允许.png和.jpg格式文件的上传
.setFileTypes('.png,.jpg')
.uploadImage(file);
// 方法返回: { originalFilename: "avatar.jpg", fileKey: "avatar.jpg", accessUri: "/resource/image?f=avatar.jpg" }
2
3
4
5
6
7
8
示例2:指定业务路径
// 上传文件"avatar.jpg"
Utils.OSS.uploadImage(file, "avatar");
// 方法返回: { originalFilename: "avatar.jpg", fileKey: "avatar/avatar.jpg", accessUri: "/resource/avatar?f=avatar/avatar.jpg" }
2
3
下载/预览文件的特殊处理 & 业务路径
您可能会遇见在下载/预览文件时需要对文件特殊处理的场景,如为图片或PDF添加水印,下载时添加下载时间等。此时我们应该通过业务路径将其在业务上进行区分(这也是业务路径的主要用途),如上传文件时设置业务路径为contract
(标识为合同文件),那么合同文件的下载路径将变为/resource/contract
,我们为此实现接口即可。具体实现可参考FileAccessController.java
类。
# Utils.Date
Utils.Date为日期工具类,它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
getStart | java.util.Date date | java.util.Date date | 获取日期的开始时间 |
getEnd | java.util.Date date | java.util.Date date | 获取日期的结束时间 |
示例
// 返回时分秒毫秒均为0的当前日期对象
Utils.Date.getStart(new Date());
// 返回时分秒毫秒均为0的下一天日期对象
Utils.Date.getEnd(new Date());
2
3
4
5
# Utils.Location
Utils.Location为地区工具类,它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
getLocation | String ip | Location.Info | 根据IP获取地区信息 |
getLocationString | String ip | Location.Info | 根据IP获取地区简要 |
# Utils.UserClient
Utils.UserClient为用户客户端信息工具类,它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
getOS | HttpServletRequest request | String | 获取客户端操作系统信息 |
getBrowser | HttpServletRequest request | String | 获取客户端浏览器信息 |
getIP | HttpServletRequest request | String | 获取客户端IP |
getPlatform | HttpServletRequest request | String | 获取用户操作的平台 |
# Utils.Server
Utils.Server为服务器信息工具类,它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
getIP | String | 获取当前服务器IP(局域网) | |
getMAC | String | 获取当前服务器MAC地址 |
# Utils.Monitor
Utils.Monitor为服务监听工具类,它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
current | Monitor | 获取当前时刻的监听信息 |
# Utils.MyBatisPlus
Utils.MyBatisPlus为MyBatis Plus工具类,它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
blankToNull | T object | T | 将对象中所有的空字符串转为null |
提示
相信很多人会疑惑,blankToNull方法应该跟MyBatis Plus没关系才对。的确,但Eva期望MyBatis plus中的blankToNull是独立的,因为它可能有别于常见的blankToNull方法的处理逻辑。或许,在Eva 2.0版本将调整它的名称。
# Utils.Secure
Utils.Secure为安全处理工具类,它有如下方法:
方法名称 | 参数 | 方法返回 | 说明 |
---|---|---|---|
encryptPassword | String password, String salt | String | 根据密码和密码盐加密密码 |
# 响应状态定义及规范
系统的响应状态定义在ResponseStatus
枚举中,如下
@Getter
@AllArgsConstructor
public enum ResponseStatus {
// 400开头表示参数错误
BAD_REQUEST(4000, "参数错误"),
DATA_EMPTY(4001, "找不到目标数据"),
DATA_EXISTS(4002, "记录已存在"),
PWD_INCORRECT(4003, "密码不正确"),
VERIFICATION_CODE_INCORRECT(4004, "验证码不正确或已过期"),
ACCOUNT_INCORRECT(4005, "账号或密码不正确"),
// 510开头表示可能导致数据错误的异常
DUPLICATE_SUBMIT(5100, "请勿重复提交"),
NOT_ALLOWED(5101, "不允许的操作"),
;
private int code;
private String message;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
响应码如何定义对程序不会产生直接影响,但我们希望您在使用Eva时可以培养一个好的习惯——定义可识别的响应码。除了目前定义的响应码外,下面再给出一些例子供参考
响应码前缀 | 说明 |
---|---|
10 | 以10开头表示用户模块错误,如1000表示登录过于频繁 |
11 | 以11开头表示会员模块错误,如1100表示会员已到期 |
12 | 以12开头表示订单模块错误,如1200表示货源不足 |
400 | 以400开头表示参数错误所导致的各类情况的响应码 |
500 | 以500开头表示程序因异常终止,不同类型的异常对应不同的响应码 |
510 | 以510开头表示请求可能导致数据错误 |
您可以按照您团队和业务的考量将响应码以其它方式规范化。这样的一个显而易见的好处是,我们仅凭响应码就能识别错误的严重性。
# 自定义全局异常处理
全局异常处理在类GlobalExceptionHandler
中实现,通过spring的@RestControllerAdvice
注解实现。如果您需要全局捕获某异常,可以参考以下代码来增加处理。
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 业务异常处理
*/
@ExceptionHandler(BusinessException.class)
public <T> ApiResponse<T> handleBusinessException (BusinessException e) {
log.error(e.getMessage(), e);
return ApiResponse.failed(e.getCode(), e.getMessage());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
无法捕获异常?
如果您的异常无法捕获,您可以从以下几个方面着手检查
- 异常是否已被处理,即抛出异常后被catch,打印了日志或抛出了其它异常
- 异常是否非Controller抛出,即在拦截器或过滤器中出现的异常
# 业务异常处理
Eva为业务异常封装了异常对象BusinessException
,通过四个构造方法可方便的构造业务异常对象。定义如下
@Data
public class BusinessException extends RuntimeException {
private Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
public BusinessException(Integer code, String message, Throwable e) {
super(message, e);
this.code = code;
}
public BusinessException(ResponseStatus status) {
super(status.getMessage());
this.code = status.getCode();
}
public BusinessException(ResponseStatus status, Throwable e) {
super(status.getMessage(), e);
this.code = status.getCode();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 登录令牌规则 & 安全验证
登录令牌对于一个系统来说十分重要,因为它标识了是谁在访问系统。所以,您可能需要更为复杂的登录令牌生成规则,并希望可以对令牌进行安全验证。Eva在ShiroDefaultTokenManager
类中实现了令牌的生成和验证。实现非常简单,如下
@Component
public class ShiroDefaultTokenManager {
public String build() {
return UUID.randomUUID().toString();
}
public void check(String token) throws UnSafeSessionException {
if (token == null || token.length() != 36) {
throw new UnSafeSessionException();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
您可以直接修改此类来完成令牌的生成和校验。
# 登录保留时长/会话过期时长
可通过application.yml
中的cache.session.expire
控制,单位为秒。
# 验证码过期时长
可通过application.yml
中的cache.captcha.expire
控制,单位为秒。
# Swagger的启用和禁用
可通过application.yml
中的swagger.enable
控制。为true
时表示启用,为false
时表示禁用。
记得关闭
为了系统安全,通常生产环境不建议开启swagger。
# Redis高可用配置
在Eva中,Redis的高可用采用主从 + 哨兵模式
方案。至于是否需要读写分离,可以根据您的项目需要来更改配置。下面详细的说明如何进行配置。
Redis版本:2.8.17
我将采用一台机器启动三台Redis服务的方式来模拟实际情况下的多台机器。因为无论是一台机器还是多台机器,他们的配置是一样的。所以我将Redis原解压文件拷贝了三份,得到了如下目录:
- master:主服务redis
- 7001:从服务redis
- 7002:从服务redis
每个目录下的内容目前都是一致的(都是原redis解压后的文件)。现在让我们进行主从服务的配置。
# 配置主服务master
文件:master/redis.conf
port 7000 # 主服务端口
requirepass <master-password> # 主服务密码(可为空,安全起见,在生产环境上必须设置)
2
# 配置从服务7001
文件:7001/redis.conf
port 7001 # 端口号
slaveof localhost 7000 # 设置本服务为主服务localhost 7000的从服务
slave-read-only yes # 设置从服务只读(读写分离时需设置此项为yes)
masterauth <master-password> # 主服务密码(如果主服务存在密码的话需要设置此项)
2
3
4
# 配置从服务7002
文件:7002/redis.conf
port 7002 # 端口号
slaveof localhost 7000 # 设置本服务为主服务localhost 7000的从服务
slave-read-only yes # 设置从服务只读(读写分离时需设置此项为yes)
masterauth <master-password> # 主服务密码(如果主服务存在密码的话需要设置此项)
2
3
4
这样,主从服务就配置完成了。下面我们还需要配置哨兵,以实现在主服务宕机时自动选举一个从服务为主服务,避免在读写分离时因主服务宕机而无法继续写入缓存。通常情况下,我们应该为每个服务节点配置一个哨兵。
# 配置主服务master哨兵
文件:master/sentinel.conf
port 27000
sentinel monitor mymaster 127.0.0.1 7000 2
2
# 配置从服务7001哨兵
文件:7001/sentinel.conf
port 27001
sentinel monitor mymaster 127.0.0.1 7000 2
2
# 配置主服务7002哨兵
文件:7002/sentinel.conf
port 27002
sentinel monitor mymaster 127.0.0.1 7000 2
2
其中127.0.0.1 7000
表示主服务,最后的数字2
为超过多少个哨兵认为主服务宕机时可以决定主服务宕机的常数。在我们的配置中,表示超过2个哨兵认为主服务宕机了就可以确定主服务确实宕机了。通常情况下这个数字不宜太高,当所需的服务不多时,此配置应该要大于服务总数的1/2
,但如果所需的服务足够多,则至少需要大于服务总数的1/3
。
到这里,服务和哨兵都配置完成了。我们需要相继启动服务和哨兵,如下
启动所有redis服务
cd master
./src/redis-server redis.conf
cd 7001
./src/redis-server redis.conf
cd 7002
./src/redis-server redis.conf
2
3
4
5
6
7
8
启动所有哨兵
cd master
./src/redis-sentinel sentinel.conf
cd 7001
./src/redis-sentinel sentinel.conf
cd 7002
./src/redis-sentinel sentinel.conf
2
3
4
5
6
7
8
现在,你可以对主从同步、自动选举进行测试了,注意留意redis服务和哨兵的日志,相信你很快就会明白。