Spring自定义注解实现AOP

Spring AOP Demo

AOP和OOP

使用Java开发,大家应该都听过这两个概念,其实两者很好分辨:

  • OOP(Object-Oriended Programming,面向对象编程):自顶向下
  • AOP(Aspect-Oriented Programming,面向方面编程):切面编程

OOP编程下,代码自顶向下查看就能了解它的业务含义;AOP编程,需要去查看它的切入点(PointCut)和通知(Advice)

关于AOP的详细概念可以参考资料一,感觉讲得很不错!


为啥要用AOP

有些OOP编程会造成重复的胶水代码,这些可以通过AOP切面编程消灭它们,例如下面我遇到过的业务场景。

在业务上,需要对方法增加前置校验和后置操作,然后有多个类和多个方法需要增加这些操作,这时有三种方案可以选择:

  1. 使用硬编码,在每个方法前后各加一段代码。

  2. 抽离相同方法,在同一个类中,将相同的代码抽成同一个方法,然后进行调用。比第一种略微减少胶水代码,但还是存在多个类中会重复。

  3. 使用切面进行统一管理。使用自定义注解或者@Aspect进行管理,减少了胶水代码,同时便于管理。

通过上图对比,切面可以使代码更加整洁和管理,唯一的缺点可能就类膨胀,多了很多个类=-=,可是为了编程水平的提高,我选择了切面管理嘿嘿~


Demo思路

  1. 自定义一个注解,设定注解的属性
  2. 定义注解的Handler,在xml文件中配置该Handler为自定义注解的切入点
  3. 根据注解中的属性,通过工厂模式取出真正的执行者
  4. 在连接点(JoinPoint)前执行前置方法
  5. 执行连接点的方法
  6. 在连接点后执行后置方法
  7. End

Demo代码

项目结构:

先看主要的注解和XML文件如何配置:


自定义注解TypeCheck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//定义了该Annotation被保留的时间长短,RUNTIME表示在运行时有效
@Retention(RUNTIME)
//说明了Annotation所修饰的对象范围,可以定义为package、parameter
//在这里我用的是METHOD,表示修饰范围是方法
@Target(ElementType.METHOD)
public @interface TypeCheck {
/**
* 加一些自定义的属性吧
**/
Type type();
int paramIndex();
}

定义切入点和XML配置

定义切入点有两种方式,一种是XML,还有一种是使用@Aspect注解和@PointCut(我这次使用的是XML,与@Around环绕切入类似)

TypeCheckHandler.java

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
public class TypeCheckHandler {
@Autowired
private TypeFactory typeFactory;
//可以看参考资料三,直接使用@Around注解实现
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取连接点的参数列表
Object[] args = joinPoint.getArgs();
// 获取连接点方法的签名
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 方法签名中获取注解
TypeCheck ann = method.getAnnotation(TypeCheck.class);
String content = "";
if (ann.paramIndex() < args.length) {
content = (String) args[ann.paramIndex()];
}
// 根据注解中的属性,从工厂类中取出真正的执行者
TypeProcessor processor = typeFactory.getProcessor(ann.type());
// 前置方法
processor.before();
// 执行连接点的方法
joinPoint.proceed();
// 后置方法
processor.after(Lists.newArrayList(content));
return null;
}
}

XML配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 切面处理器 -->
<bean id="checkHandler" class="com.example.demo.test.ann.TypeCheckHandler"/>
<!-- 拦截切面 -->
<aop:config proxy-target-class="true">
<aop:aspect ref="checkHandler">
<!-- 该expression表示拦截使用TypeCheck注解的地方 -->
<aop:pointcut id="typeCut" expression=" @annotation(com.example.demo.test.ann.TypeCheck)"/>
<!-- 设定Advice通知时机,我这边使用的是Around环绕通知 -->
<aop:around pointcut-ref="typeCut" method="doAround"/>
</aop:aspect>
</aop:config>
</beans>


接口和枚举

接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface TypeProcessor {
/**
* Before
*/
void before();
/**
* After
* @param content 自定义内容
*/
void after(List<String> content);
}

枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum Type {
/**
* 客户
*/
CLIENT("c", "客户"),
/**
* 供应商
*/
SUPPLIER("s", "供应商");
private String code;
private String text;
Type(String code, String text) {
this.code = code;
this.text = text;
}
}

实际执行者ClientProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class ClientProcessor implements TypeProcessor {
@Override
public void before() {
System.out.println("Before I'm a client");
}
@Override
public void after(List<String> content) {
System.out.println("I'm a client processor");
StringBuilder stringBuilder = new StringBuilder();
for (String temp : content) {
stringBuilder.append(temp).append(",");
}
System.out.println(stringBuilder.toString());
}
}

执行者工厂类Factory

根据注解的属性判断实际取哪个执行者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class TypeFactory {
@Autowired
private ClientProcessor clientProcessor;
@Autowired
private SupplierProcessor supplierProcessor;
public TypeProcessor getProcessor(Type type) {
TypeProcessor processor = null;
if (Type.CLIENT.equals(type)) {
processor = clientProcessor;
} else if (Type.SUPPLIER.equals(type)) {
processor = supplierProcessor;
}
return processor;
}
}

测试代码

1
2
3
4
5
@TypeCheck(type = Type.CLIENT, paramIndex = 0)
public void test(String content) {
// Do something
System.out.println("Hei, I'm the middle process");
}

例如我传入的content是“TEST TEST TEST”,实际输出接口为:

Before I’m a client
Hei, I’m the middle process
I’m a client processor
TEST TEST TEST,

可以看到该方法成功被切面注入了~~


参考资料

  1. Spring思维导图,让Spring不再难懂(AOP 篇)
  2. spring 自定义注解(annotation)与 aop获取注解
  3. Spring AOP基于注解的Around通知