这是我参与8月更文挑战的第 24 天,活动详情查看:8月更文挑战
阅读本章之前,建议有一定的 Spring AOP 基础,有需要的小伙伴可以去看看我的写的 AOP 基础的文章
项目结构
mapper
|- Mapper
|- xxxMapper
|- ...
aspect
|- MapperAspect
mapper 层
定义一个标准的 mapper
层,里面全是 xxMapper
,正常的情况下,我们会使用 MyBaits
访问数据库,MyBaits
会提供一个具备大部分常规 sql 操作的父类 Mapper
由于项目这边的规划是,有另一个模块封装所有对数据库的操作,我这里通过 http协议
去那个模块读写数据,需要自己封装一个公共的 Mapper
父类 Mapper
public class Mapper<D extends RcsDto> {
protected Class<D> dtoClz;
public Mapper() {
ParameterizedType pt = (ParameterizedType) this.getClass().getGenericSuperclass();
dtoClz = (Class<D>) pt.getActualTypeArguments()[0];
}
public List<D> findAll() {
return HttpResultUtil.rGetList(dtoClz);
}
public Result delete(String id) {
return HttpResultUtil.rDeleteById(id, dtoClz);
}
}
Mapper
类的声明上使用了泛型,表示操作的实体,全是 RcsDto
的子类
Mapper
中定义了一个受保护的 clz 变量,表示的是继承 Mapper
的时候泛型对应的具体类的 class
注意看 Mapper
的构造器,通过两行代码,获取到声明为 Mapper
的子类的类定义上的泛型的具体类型(建议背诵),大概是这么个效果
子类 Mapper
一个常规的子类,不需要额外定义方法,直接继承至父类,就能拥有其父类的全部方法,而且这个子类,操作的具体实体,在继承时声明,如下代码,其声明操作的实体就是 AreaInfoDto
@Component
public class AreaMapper extends Mapper<AreaInfoDto> {}
// 此时,Mapper 中的 dtoClz = AreaInfoDto.class
如果,我们需要追加实现其他的方法,如下,代码编写非常简单轻松
@Component
public class TaskMapper extends Mapper<TaskInfoDto> {
public void deleteAll(String rid) {
HttpResultUtil.rDeleteByRcsId(rid, TaskInfoDto.class);
}
}
aspect 切面
先上 Mapper
的切面代码
@Aspect
@Component
public class HttpResultAspect {
//更新,更新缓存,http请求传递数据
@Around("execution (* com.rlzz.r9.mapper.*Mapper.update(..)) "
+ "|| execution (* com.rlzz.r9.mapper.*Mapper.save(..))")
public <D extends RcsDto> D afterSaveOrUpdate(ProceedingJoinPoint joinPoint) {
try {
D dto = (D) joinPoint.getArgs()[0];
Object obj = joinPoint.proceed();
if (!ObjectUtils.isEmpty(obj)) {
dto = (D) obj;
DtoCache.getInstance().add(dto);
}
return dto;
} catch (Throwable e) {
LogUtil.exception(e);
}
return null;
}
// 删除,清空对应的缓存记录
@Before("execution (* com.rlzz.r9.mapper.*Mapper.delete(..))")
public void beforeDelete(JoinPoint joinPoint) {
}
//查询单个,判断缓存中是否存在
@Around("execution (* com.rlzz.r9.mapper.*Mapper.getById(..))")
public <D extends RcsDto> D aroundGetById(ProceedingJoinPoint joinPoint) {
}
//查询全部,获取返回的结果,直接覆盖缓存中的数据
@Around("execution (* com.rlzz.r9.mapper.*Mapper.findAll(..))")
public <D extends RcsDto> List<D> aroundFindAll(ProceedingJoinPoint joinPoint) {
Class dtoClz = AspectMothod.getDtoClass(joinPoint);
List<D> dtos = DtoCache.getInstance().getList(dtoClz);
// LogUtil.info(dtos);
return dtos;
}
}
像第一个 @Around
,我们拦截了 xxMapper
里所有的 save(), update()
,我们通过 Object obj = joinPoint.proceed();
的方式,执行了目标方法,并拿到了返回值,然后对返回值做检查并添加到 Cache
中(当然,又是自己造的轮子)
比如最后一个 @Around
,我们虽然拦截了 xxMapper.findAll()
,但实际上并没去去执行,而且选择去缓存中直接拉去对应类型的全部数据,这里涉及到一个方法 AspectMothod.getDtoClass(joinPoint)
代码如下
public class AspectMothod {
// 通过 JoinPoint 获取mapper中自定义的字段,dtoClz 的值
static <D extends RcsDto> Class<D> getDtoClass(JoinPoint joinPoint) {
// JsonUtil.error(joinPoint.getSignature().getDeclaringType(), joinPoint.getSignature().getName(), joinPoint.getArgs());
Object obj = joinPoint.getTarget();
if (map.get(obj.getClass()) != null) { // 如果之前获取过,直接返回
return map.get(obj.getClass());
}
Class<D> clz = null;
Field field = null;
try {
field = obj.getClass().getSuperclass().getDeclaredField("dtoClz"); // (1)
field.setAccessible(true);
clz = (Class<D>) field.get(obj); // (2)
} catch (Exception e) {
LogUtil.exception(e);
}
map.put(obj.getClass(), clz); // (3)
return clz;
}
private static Map<Class, Class> map = new HashMap<>();
}
这里用到了一个非常有意思的细节点,反射,不同的小伙伴来看看这个就离精通不远了 Java 高级特性の反射
再回想一下,我们在声明 Mapper
的时候,是在里面定义了一个泛型的class 变量, Class<D> dtoClz
,虽然是定义在父类里,但是,我们在创建子类实例的时候,又会将他赋值为运行时传入的泛型类型
现在,我们在 AOP 切面中,需要知道我们切的是哪一个具体的 xxxMapper
,这个 mapper
对应的实例又是什么。因为我们在切面中,用 joinPoint.getTarget()
就能得知我们具体切了哪一个实例
然后,通过反射的方式,拿到他的父类,在去拿父类里的某一个字段 (1)
调用反射方法,获取这个字段的值 (2)
将得到的结果缓存到 map 中,供下次快速访问 (3)