概要
在开发苍穹外卖员工管理模块,我们常可见新增和更新用户数据时需要指定一些字段
setUpdateUser()
setUpdateTime()
setCreateUser()
setCreateTime()
这些字段会在项目代码中被反复使用,我们若要使项目代码简洁明了,就需要想办法进行公共字段的自动填充(AOP)。
技术名词解释
- AOP
实现
如何实现公共字段的自动填充?我们要在基本不改变原有代码的基础上,对代码功能进行增强,那么,这就需要使用到AOP的编程思想。
什么是AOP
我们熟知的OOP,也就是面向对象编程思想,秉持着万物皆为对象。那么AOP思想,也就是我们所说的面向切面编程,与OOP类似的,AOP也是一种编程思想。
工作流程
Spring启动
|
↓
扫描@Component、@Aspect
|
↓
找到切面类和目标类
|
↓
判断哪些方法匹配切入点
|
↓
创建代理对象 Proxy
|
↓
------------------------------------------------
|
用户调用方法
|
↓
调用代理对象
|
↓
执行AOP增强逻辑(Advice)
|
↓
调用真实业务方法
|
↓
返回结果
|
↓
执行后置增强逻辑
------------------------------------------------
AOP通知类型
- 前置通知 –
@Before - 后置通知 –
@After - 环绕通知 –
@Around - 返回后通知 –
@AfterReturning - 抛出异常后通知 –
@AfterThrowing
一个简单的流程
了解了AOP的基本流程以及AOP的通知类型后,我们就来瞧瞧我们如何使用简单AOP
- 引入AOP依赖项
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 创建切面类(Aspect)
@Aspect
@Component
public class MyAspect {
}
对于@Aspect标识这是一个切面类
对于@Component标识这个类交给Spring管理
- 在切面类中定义切入点
@Pointcut("execution(* com.example.service.*.*(..))")
public void pointCut(){}
- 编写通知逻辑
- 前置通知
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.blog.dao.BookDao.update())")
private void pt() {
}
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
}
- 后置通知
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.blog.dao.BookDao.update())")
private void pt() {
}
@After("pt()")
public void after(){
System.out.println("after advice ...");
}
}
- 环绕通知
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.blog.dao.BookDao.update())")
private void pt() {
}
@Around("pt()")
public void around(){
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
}
对于环绕通知,像上述使用后,我们得到的结果为
around before advice …
around after advice …
只得到了增强方法的结果,原始方法并没有被执行
所以,我们在实际使用中,应在方法中添加ProceedingJoinPoint,同时在需要的位置使用proceed()调用原始方法。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.blog.dao.BookDao.update())")
private void pt() {
}
@Around("pt()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
//表示对原始操作的调用
pjp.proceed();
System.out.println("around after advice ...");
}
}
特别注意的是,函数的返回类型很重要。上述案例中的类型是void,返回的是NULL,但是当有返回类型时,我们应该按照返回类型去设计返回值。
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.blog.dao.BookDao.select())")
private void pt() {
}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
Object res = pjp.proceed();
System.out.println("around after advice ...");
return res;
}
}
- 返回后通知
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.blog.dao.BookDao.select())")
private void pt() {
}
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
- 抛出异常后通知
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.blog.dao.BookDao.select())")
private void pt() {
}
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
对于AOP,我们就先了解到这,更多的,我们将来再进行讨论。
苍穹外卖中的实现
对于苍穹外卖中,想要通过AOP对公共字段赋值,首先就是要先识别出操作类型(Insert / Update),那么我们采用的方式就是通过注解去标识。
首先定义枚举类:
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
然后创建自定义注解:
/**
* 自定义注解,标识某个方法需要进行公共字段的填充
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
OperationType value();
}
随后,编写切面类:
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 定义切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前缀通知
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
log.info("开始进行公共字段的填充...");
// 获取当前拦截的方法的参数
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
// 获取到当前被拦截的实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0) {
return;
}
Object entity = args[0];
// 准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
// 根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT) {
// 为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 通过反射为属性赋值
setCreateUser.invoke(entity, currentId);
setUpdateUser.invoke(entity, currentId);
setCreateTime.invoke(entity, now);
setUpdateTime.invoke(entity, now);
} catch (Exception e) {
e.printStackTrace();
}
} else {
// 为两个字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 通过反射为属性赋值
setUpdateUser.invoke(entity, currentId);
setUpdateTime.invoke(entity, now);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
我们来详细看一下切面类,依照上述的简单流程,我们首先需要编写切入点(指定需要增强代码的地方) ,由于指定整个mapper的所有方法过于宽泛,所有的方法都会被拦截,所以我们使用
@annotation(com.sky.annotation.AutoFill)
接着,编写通知类,因为我们需要实现公共字段的填充,在mapper语句执行前进行填充,所以我们采取前置通知。
编写通知类,我们需要实现两种不同的操作,因此,我们需要先获取操作类型,通过反射来获取:
// 获取当前拦截的方法的参数
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
由于我们使用AOP去做代码增强,所以会对 employeeMapper 中的 insert / update 方法将会进行拦截,比如employeeMapper.insert(employee)中的 employee 被拦截了,我们要将其获取出来(我们约定在第一个位置放这个实体对象,所以取args[0]),然后通过反射去将其值赋给公共字段。
// 获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0) {
return;
}
Object entity = args[0];
随后就是利用反射进行赋值啦~~
为什么我们不直接写entity.setCreateUser(currentId);,反而要采用反射呢?
对于Java编译器来说,直接使用实体类去赋值,编译器只知道entity的类型是Object,不关心取到的到底是什么实体类,对于Object来说,里面没有setCurrentId()这个方法,会直接报错。
而反射,是在运行时动态查找和调用方法,所以没有这种问题。
那我们直接对类型强转不就好了?eg:((Employee) entity).setCreateUser(currentId);
主要的问题就是,我们的AOP设计是对于整个mapper层的,对于需要增强的代码所涉及的实体类不确定(Employee,Category等),因此还是使用反射进行赋值。
Method setCreateUser = entity.getClass().getDeclaredMethod("setCreateUser", Long.class);
setCreateUser.invoke(entity, currentId);
小结
在苍穹外卖的实现中,核心思路是通过自定义注解 @AutoFill 标识目标方法,利用切面类统一拦截 Mapper 层的 Insert / Update 操作,再通过反射机制动态为实体对象填充公共字段,从而将重复代码从业务逻辑中彻底抽离,实现代码的简洁与易维护。


被折叠的 条评论
为什么被折叠?



