Spring Boot aop

基于SpringBoot的自定义注解

Posted by leone on 2018-07-25

Spring Boot入门系列之:十一、Spring Boot Aop

spring-boot-aop

什么是aop

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,如“对所有方法名以‘set*’开头的方法添加后台日志”。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

面向切面的程序设计将代码逻辑切分为不同的模块(即关注点(Concern),一段特定的逻辑功能)。几乎所有的编程思想都涉及代码功能的分类,将各个关注点封装成独立的抽象模块(如函数、过程、模块、类以及方法等),后者又可供进一步实现、封装和重写。部分关注点“横切”程序代码中的数个模块,即在多个模块中都有出现,它们即被称作“横切关注点(Cross-cutting concerns, Horizontal concerns)”。

日志功能即是横切关注点的一个典型案例,因为日志功能往往横跨系统中的每个业务模块,即“横切”所有有日志需求的类及方法体。而对于一个信用卡应用程序来说,存款、取款、帐单管理是它的核心关注点,日志和持久化将成为横切整个对象结构的横切关注点。

切面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与切面相关的编程概念还包括元对象协议、主题(Subject)、混入(Mixin)和委托(Delegate)。

AOP中的相关概念

看过了上面解释,想必大家对aop已经有个大致的雏形了,但是又对上面提到的切面之类的术语有一些模糊的地方,接下来就来讲解一下AOP中的相关概念,了解了AOP中的概念,才能真正的掌握AOP的精髓。

  • Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。

  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。

  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

  • Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。

  • Target(目标对象):织入 Advice 的目标对象.。
    Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程

spring aop

Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器,它在运行期通过代理方式向目标类织入增强代码。在Spring中可以无缝地将Spring AOP、IoC和AspectJ整合在一起。Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。在Java中动态代理有两种方式:JDK动态代理和CGLib动态代理

  • jdk proxy

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

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

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
* <p>
*
* @author leone
* @since 2018-11-09
**/
public class JdkProxy {

interface IUserService {
Integer delete(Integer userId);
}

static class UserServiceImpl implements IUserService {
@Override
public Integer delete(Integer userId) {
// 业务
System.out.println("delete user");
return userId;
}
}

// 自定义InvocationHandler
static class UserServiceProxy implements InvocationHandler {
// 目标对象
private Object target;

public UserServiceProxy(Object target) {
this.target = target;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("------方法调用前---------");
//执行相应的目标方法
Object result = method.invoke(target, args);
System.out.println("------方法调用后---------");
return result;
}
}

public static void main(String[] args) {
IUserService userService = new UserServiceImpl();
// 创建调用处理类
UserServiceProxy handler = new UserServiceProxy(userService);
// 得到代理类实例
IUserService proxy = (IUserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(),
new Class[]{IUserService.class}, handler);
// 调用代理类的方法
Integer userId = proxy.delete(3);
System.out.println(userId);
}

}
  • cglib proxy

而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

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
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
* <p>
*
* @author leone
* @since 2018-11-09
**/
public class CglibProxy {

static class UserService implements MethodInterceptor {

private Object target;

/**
* 业务方法
*
* @param userId
* @return
*/
public Integer delete(Integer userId) {
System.out.println("delete user");
return userId;
}

/**
* 利用Enhancer类生成代理类
*
* @param target
* @return
*/
public Object getInstance(Object target) {
this.target = target;
// 创建加强器,用来创建动态代理类
Enhancer enhancer = new Enhancer();
// 为加强器指定要代理的业务类(即:为下面生成的代理类指定父类)
enhancer.setSuperclass(target.getClass());
// 设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦
enhancer.setCallback(this);
// 创建动态代理类对象并返回
return enhancer.create();
}


/**
* @param o
* @param method
* @param objects
* @param methodProxy
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
throws Throwable {
System.out.println("------方法调用前---------");
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("------方法调用后---------");
return object;
}

}

public static void main(String[] args) {
UserService userService = new UserService();
UserService proxy = (UserService) userService.getInstance(userService);
Integer userId = proxy.delete(2);
System.out.println(userId);
}

}

1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP,可以强制使用CGLIB实现AOP
2、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换

spirng boot aop

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Advice的主要类型

  • @Before:该注解标注的方法在业务模块代码执行之前执行,其不能阻止业务模块的执行,除非抛出异常;

  • @AfterReturning:该注解标注的方法在业务模块代码执行之后执行;

  • @AfterThrowing:该注解标注的方法在业务模块抛出指定异常后执行;

  • @After:该注解标注的方法在所有的Advice执行完成后执行,无论业务模块是否抛出异常,类似于finally的作用;

  • @Around:该注解功能最为强大,其所标注的方法用于编写包裹业务模块执行的代码,其可以传入一个ProceedingJoinPoint用于调用业务模块的代码,无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用;

  • @DeclareParents:其是一种Introduction类型的模型,在属性声明上使用,主要用于为指定的业务模块添加新的接口和相应的实现。

切点表达式

1.通配符

  • [*] 匹配任意字符,但只能匹配一个元素

  • […] 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用

  • [+] 必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类

2.逻辑运算符

表达式可由多个切点函数通过逻辑运算组成

  • && 与操作,求交集,也可以写成and

例如 execution(* chop(…)) && target(Horseman)  表示Horseman及其子类的chop方法

  • || 或操作,任一表达式成立即为true,也可以写成 or

例如 execution(* chop(…)) || args(String)  表示名称为chop的方法或者有一个String型参数的方法

  • ! 非操作,表达式为false则结果为true,也可以写成 not

例如 execution(* chop(…)) and !args(String)  表示名称为chop的方法但是不能是只有一个String型参数的方法

  • execution() 方法匹配模式串

表示满足某一匹配模式的所有目标类方法连接点。如execution(* save(…))表示所有目标类中的 save()方法。

由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且在Spring中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。如下是execution表达式的语法

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

execution(<修饰符> <返回类型> <类路径> <方法名>(<参数列表>) <异常模式> )

  • modifiers-pattern:方法的可见性,如public,protected;

  • ret-type-pattern:方法的返回值类型,如int,void等;

  • declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;

  • name-pattern:方法名类型,如buisinessService();

  • param-pattern:方法的参数类型,如java.lang.String;

  • throws-pattern:方法抛出的异常类型,如java.lang.Exception;

切点函数

  • @annotation(annotation-type) 方法注解类名

    如下示例表示匹配使用com.leone.aop.AopTest注解标注的方法:

    @annotation(com.leone.aop.AopTest)

  • args(param-pattern) 方法入参切点函数

    如下示例表示匹配所有只有一个参数,并且参数类型是java.lang.String类型的方法:

    args(java.lang.String)

  • @args(annotation-type) 方法入参类注解切点函数

    如下示例表示匹配使用了com.leone.aop.AopTest注解标注的类作为参数的方法:

    @args(com.leone.aop.AopTest)

  • within(declaring-type-pattern) 类名匹配切点函数

    within表达式只能指定到类级别,如下示例表示匹配com.leone.aop.UserService中的所有方法:

    within(com.leone.aop.UserService)

  • @within(annotation-type) 类注解匹配切点函数

    如下示例表示匹配使用org.springframework.web.bind.annotation.RestController注解标注的类:

    @within(org.springframework.web.bind.annotation.RestController)

  • target(declaring-type-pattern) 类名切点函数

    如下示例表示匹配com.leone.aop.UserService中的所有方法:

    target(com.leone.aop.UserService)

  • this

spring-boot-aop 实战

  • 配置切面类,实现代理

1.在类上使用 @Component 注解把切面类加入到IOC容器中
2.在类上使用 @Aspect 注解使之成为切面类

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

/**
* 描述一个切面类
*
* @author leone
* @since 2018-06-21
**/
@Slf4j
@Aspect
@Component
public class AopConfig {

/**
* 1.通配符
* [*] 匹配任意字符,但只能匹配一个元素
* <p>
* [..] 匹配任意字符,可以匹配任意多个元素,表示类时,必须和*联合使用
* <p>
* [+] 必须跟在类名后面,如Horseman+,表示类本身和继承或扩展指定类的所有类
* <p>
* 切点表达式分为 修饰符 返回类型 包路径 方法名 参数
* <p>
* 2.切点表达式
* <p>
* 3.逻辑运算符
* 表达式可由多个切点函数通过逻辑运算组成
* ** && 与操作,求交集,也可以写成and
* <p>
* 例如 execution(* chop(..)) && target(Horseman)  表示Horseman及其子类的chop方法
* <p>
* ** || 或操作,任一表达式成立即为true,也可以写成 or
* <p>
* 例如 execution(* chop(..)) || args(String)  表示名称为chop的方法或者有一个String型参数的方法
* <p>
* ** ! 非操作,表达式为false则结果为true,也可以写成 not
* <p>
* 例如 execution(* chop(..)) and !args(String)  表示名称为chop的方法但是不能是只有一个String型参数的方法
*/
@Pointcut("execution(* com.leone.boot.aop.service.*.*(..))")
public void pointCut() {
}

/**
* 环绕通知在 target 开始和结束执行
*
* @param point
* @return
*/
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint point) {
long start = System.currentTimeMillis();
String methodName = point.getSignature().getName();
log.info("around method name: {} params: {}", methodName, Arrays.asList(point.getArgs()));
try {
log.info("around end time: {}", (System.currentTimeMillis() - start) + " ms!");
return point.proceed();
} catch (Throwable e) {
log.error("message: {}", e.getMessage());
}
return null;
}

/**
* 前置通知在 target 前执行
*
* @param joinPoint
*/
// @Before("@annotation(com.leone.boot.aop.anno.AopBefore)")
// @Before("within(com.leone.boot.aop.controller.*)")
// @Before("@within(org.springframework.web.bind.annotation.RestController)")
// @Before("target(com.leone.boot.aop.controller.UserController)")
@Before("@target(com.leone.boot.aop.anno.ClassAop) && @annotation(com.leone.boot.aop.anno.AopBefore)")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
log.info("before inform method name: {} param: {}", methodName, args);
}

/**
* 后置通知在target后执行
*
* @param joinPoint
*/
@After("@args(org.springframework.stereotype.Component) &&
execution(* com.leone.boot.aop.controller.*.*(..))")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
log.info("after inform method name : {} param: {}", methodName, args);
}

/**
* 后置返回在target返回后执行
*
* @param joinPoint
* @param result
*/
@AfterReturning(value = "within(com.leone.boot.aop.controller.*)", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
log.info("afterReturning inform method name: {} return value: {}", methodName, result);
}

/**
* 后置异常通知在target异常后执行
*
* @param joinPoint
* @param ex
*/
@AfterThrowing(value = "args(com.leone.boot.common.entity.User) &&
execution(* com.leone.boot.aop.controller.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
log.info("afterThrowing inform method name: {} exceptions: {}" + methodName, ex);
}
}
  • 测试类
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
import com.leone.boot.aop.anno.AopBefore;
import com.leone.boot.aop.anno.ClassAop;
import com.leone.boot.aop.interf.UserService;
import com.leone.boot.common.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

/**
* @author leone
* @since 2018-06-21
**/
@Slf4j
@ClassAop
@RestController
@RequestMapping("/api")
public class UserController {

private UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}


@AopBefore
@RequestMapping(value = "/user/{userId}", method = RequestMethod.GET)
public User findOne(@PathVariable Long userId) {
return userService.findOne(userId);
}

@AopBefore
@RequestMapping("/user")
public User save(User user) {
return user;
}
}

github