资源与业务分离Aop的实现方式
本章内容比较偏向系统设计方面,简单的封装就可以应用到系统中使用,从而提高我们的编码效率以及代码的可读性。统一资源在系统内是不可避免的模块,资源分类也有很多种,比较常见如:图片资源、文本资源、视频资源等,那么资源统一处理的好处是什么呢?大家有可能会有疑问,我把资源存放到业务表内岂不更好吗?这样查询起来也方便,并不需要关联资源信息表!当然设计不分好坏,只有更适合、更简单!接下来带着疑问进入本章的内容。
本章目标
基于SpringBoot
平台结合AOP
完成统一资源的自动查询映射。
构建项目
本章使用到的依赖相对来说比较多,大致:Web
、MapStruct
、SpringDataJpa
、LomBok
等,数据库方面采用MySQL
来作为数据支持。
数据初始化
本章用到的数据表结构以及初始化的数据之前都是放在项目的resources
目录下,为了大家使用方面我在这里直接贴出来,如下所示:
1 | -- |
用到的数据库为
resources
,可以自行创建或者更换其他数据库使用。
搭建项目
本章我们把统一资源单独拿出来作为一个项目子模块来构建,而用户服务作为另外一个单独模块构建,下面先来贴出父项目的pom.xml
配置文件内容,如下所示:
1 | ....// |
接下来我们开始创建common-resource
子模块,将资源处理完全独立出来,在创建子模块时要注意package
命名要保证可以被SpringBoot
运行时扫描到!!!
common-resource
我们需要先创建一个BaseEntity
作为所有实体的父类存在,如下所示:
1 | /** |
该类仅仅实现了Serializable
接口,在创建业务实体时需要继承该类,这也是基本的设计规则,方便后期添加全局统一的字段或者配置。
资源实体
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/**
* 资源实体
* @author yuqiyu
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:上午11:21
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public class CommonResourceEntity
extends BaseEntity
{
/**
* 资源编号
*/
private Integer resourceId;
/**
* 资源所属目标编号
*/
private String targetId;
/**
* 类型编号
*/
private String typeId;
/**
* 资源路径
*/
private String resourceUrl;
/**
* 创建时间
*/
private Timestamp createTime;
/**
* 排序
*/
private int order;
}资源类型实体
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/**
* 资源类型实体
* @author yuqiyu
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:上午11:22
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public class CommonResourceTypeEntity
extends BaseEntity
{
/**
* 类型编号
*/
private String id;
/**
* 类型名称
*/
private String name;
/**
* 类型标识
*/
private String flag;
/**
* 类型添加时间
*/
private Timestamp createTime;
}下面我们来创建对应实体的数据接口,我们采用
SpringDataJPA
的方法名查询规则来查询对应的数据。资源数据接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* 资源数据接口
* @author yuqiyu
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:上午11:31
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public interface CommonResourceRepository
extends JpaRepository<CommonResourceEntity,Integer>
{
/**
* 根据类型编号 & 目标编号查询出资源实体
* @param typeId 类型编号
* @param targetId 目标编号
* @return
*/
List<CommonResourceEntity> findByTypeIdAndTargetId(String typeId, String targetId);
}资源类型数据接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 资源类型数据接口
* @author yuqiyu
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:上午11:32
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public interface CommonResourceTypeRepository
extends JpaRepository<CommonResourceTypeEntity,String>
{
/**
* 根据类别标识查询
* @param flag 资源类型标识
* @return
*/
CommonResourceTypeEntity findTopByFlag(String flag);
}接下来我们开始编写根据资源类型获取指定目标编号的资源列表业务逻辑方法,创建名为
CommonResourceService
统一资源业务逻辑实现类,如下所示: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/**
* 公共资源业务逻辑实现类
* @author yuqiyu
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:下午4:18
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public class CommonResourceService {
/**
* 资源类型数据接口
*/
private CommonResourceTypeRepository resourceTypeRepository;
/**
* 资源数据接口
*/
private CommonResourceRepository resourceRepository;
/**
* 根据资源标识 & 所属目标编号查询资源路径路边
*
* @param resourceFlag 资源标识
* @param targetId 目标编号
* @return
*/
public List<String> selectUrlsByFlag(CommonResourceFlag resourceFlag, String targetId) throws Exception {
/**
* 获取资源类型
*/
CommonResourceTypeEntity resourceType = selectResourceTypeByFlag(resourceFlag);
/**
* 查询该目标编号 & 类型的资源列表
*/
List<CommonResourceEntity> resources = resourceRepository.findByTypeIdAndTargetId(resourceType.getId(), targetId);
return convertUrl(resources);
}
/**
* 转换路径
* 通过实体集合转换成路径集合
*
* @param resources 资源实体列表
* @return
*/
List<String> convertUrl(List<CommonResourceEntity> resources) {
List<String> urls = null;
if (!ObjectUtils.isEmpty(resources)) {
urls = new ArrayList();
for (CommonResourceEntity resource : resources) {
urls.add(resource.getResourceUrl());
}
}
return urls;
}
/**
* 根据资源类型标识查询资源类型基本信息
*
* @param resourceFlag 资源类型标识
* @return
* @throws Exception
*/
CommonResourceTypeEntity selectResourceTypeByFlag(CommonResourceFlag resourceFlag) throws Exception {
/**
* 查询资源类型
*/
CommonResourceTypeEntity resourceType = resourceTypeRepository.findTopByFlag(resourceFlag.getName());
if (ObjectUtils.isEmpty(resourceFlag)) {
throw new Exception("未查询到资源");
}
return resourceType;
}
}在
CommonResourceService
提供了对外的方法selectUrlsByFlag
可以查询指定目标编号 & 指定类型的多个资源地址。
统一资源映射
在common-resource
子模块项目内添加统一资源的相关映射内容,我们预计的目标效果是根据我们自定义的注解结合AOP
来实现指定方法的结果处理映射,我们需要创建两个自定义的注解来完成我们的预想效果,注解分别为:ResourceField
、ResourceMethod
,下面我们来看看ResourceField
注解的属性定义,如下所示:
1 |
|
ResourceField
注解用于配置在查询结果的字段上,如:我们查询用户头像时定义的字段为userHeadImage
,我们这时仅仅需要在userHeadImage
字段上添加ResourceField
即可。
另外一个注解ResourceMethod
的作用仅仅是为了AOP
根据该注解切面方法,也是只有被该注解切面的方法才会去执行AOP
切面方法的返回值进行处理,代码如下所示:
1 | /** |
我们的自定义注解已经编写完成,转过头来我们先看看@Around
切面方法所需要的逻辑实现方法,创建ResourcePushService
接口添加如下两个方法:
1 |
|
分别提供了设置单个、多个资源的方法,由于实现类内容比较多这里就不贴出具体的实现代码了,详细请下载源码进行查看,源码地址:spring-boot-chapter内的Chapter44
项目。
资源切面类
我们一直都在说资源统一切面映射,那么我们的资源的切面该如何去配置切面切入点呢?在之前我们创建了ResourceMethod
注解,我们就用它作为方法切入点完成切面的环绕
实现, ResourceAspect
代码如下所示:
1 | /** |
切面环绕方法resourcePutAround
大致流程为:
- 执行需要切面的方法,获取方法结果
- 根据方法返回的结果判断是单个、多个对象进行调用不同的方法
- 统一资源方法自动根据
@ResourceField
注解配置信息以及对象类型配置@Id
字段的值作为目标对象编号设置资源到返回对象内。 - 返回处理后的对象实例
为了方便配置我们在@ResourceField
注解内添加了CommonResourceFlag
枚举类型的flag
属性,该属性就是配置了资源类型的标识,切面会根据该标识去查询资源的类型编号,再拿着资源类型的编号 & 目标编号去查询资源列表,CommonResourceFlag
枚举代码如下所示:
1 | /** |
以上我们简单介绍了common-resource
子模块的核心内容以及基本的运行流程原理,下面我们来创建一个user-provider
子模块来使用同一资源查询用户的头像、用户背景图片列表。
user-provider
user-provider
子模块目内我们预计添加一个查询用户详情的方法,在方法上配置@ResourceMethod
注解,这样可以让切面切到该方法,然后在查询用户详情方法返回的对象类型内字段上添加@ResourceField
注解并添加对应的资源类型标识配置,这样我们就可以实现资源的自动映射。
由于该模块需要数据库的支持,在application.yml
配置文件内添加对应的数据库链接配置信息,如下所示:
1 | #数据源配置 |
配置文件内使用的druid
是alibaba
针对SpringBoot
封装的jar
,提供了yml
配置文件相关支持以及提示。
用户实体构建
针对数据库内的用户基本信息表我们需要创建对应的Entity
实体,代码如下所示:
1 | /** |
由于我们的用户头像以及用户背景图片并没有在用户基本信息表
内所以我们需要单独创建一个用户详情实体并继承用户基本信息实体,如下所示:
1 | /** |
在上面实体内我们仅仅是配置了字段所需的资源类型枚举。
我们一般在开发过程中,用户表内对应的实体是不允许根据业务逻辑修改的,如果你需要变动需要继承实体后添加对应的字段即可。
用户数据接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 用户基本信息数据接口
* @author yuqiyu
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:上午11:30
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public interface UserInfoRepository
extends JpaRepository<UserInfoEntity,String>
{
/**
* 根据用户名称查询
* @param userName
* @return
*/
UserInfoEntity findUserInfoEntityByUserName(String userName);
}用户业务逻辑实现
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/**
* 用户基本信息业务逻辑实现
*
* @author yuqiyu
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:上午11:53
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public class UserInfoService {
/**
* 用户数据接口
*/
private UserInfoRepository userInfoRepository;
/**
* 更新用户名称查询用户详情
* @param userName 用户名
* @return
*/
public UserDetailDTO selectByUserName(String userName) {
/**
* 获取用户基本信息
*/
UserInfoEntity userInfoEntity = userInfoRepository.findUserInfoEntityByUserName(userName);
/**
* 通过mapStruct转换detailDto
*/
UserDetailDTO detailDTO = UserMapStruct.INSTANCE.fromUserEntity(userInfoEntity);
return detailDTO;
}
}我们在方法
selectByUserName
上配置了@ResourceMethod
,让统一资源可以切面到该方法上,在selectByUserName
方法内我们只需要去处理根据用户名查询的业务逻辑,通过MapStruct
进行UserInfoEntity
与UserDetailDTO
转换。在方法返回对象时就会被资源自动处理分别将查询到的资源设置到UserDetailDTO
内的headImage
、backImage
。用户控制器
我们在控制器内添加一个根据用户名查询用户详情的方法,如下所示: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/**
* ========================
* Created with IntelliJ IDEA.
* User:恒宇少年
* Date:2017/12/31
* Time:下午3:09
* 码云:http://git.oschina.net/jnyqy
* ========================
*/
public class UserInfoController
{
/**
* 用户基本信息业务逻辑实现
*/
private UserInfoService userInfoService;
/**
* 根据用户名查询详情
* @param userName 用户名
* @return
*/
public UserDetailDTO detail( String userName)
{
return userInfoService.selectByUserName(userName);
}
}下面我们来编写一个测试用例,查看是否能够达到我们预计的效果。
测试
我们在src/test
下创建一个名为CommonResourceTester
测试类,代码如下所示:
1 | /** |
接下来我们执行selectDetail
测试方法,看下控制台输出对应的 JSON
内容,格式化后如下所示:
1 | { |
根据结果我们可以看到,我们已经自动的读取了配置的资源列表,也通过反射自动设置到字段内。
总结
本章的代码比较多,还是建议大家根据源码比对学习,这种方式也是我们在平时开发中总结出来的,我们仅仅需要配置下@ResourceField
以及@ResourceMethod
就可以了完成资源的自动映射,资源与业务逻辑的耦合度得到的很好的降低。
资源与业务分离Aop的实现方式