Spring AOP

1.AOP概述

1.1 什么是AOP?

AOP(Aspect Oriented Programming 面向切面编程),通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
常用于日志记录,性能统计,安全控制,事务处理,异常处理等等。

1.2 AOP术语

切面(Aspect):由横切关注点构成的特殊对象。
连接点(Join Point):连接点是指在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候;
通知(Advice):指在切面的某个特定的连接点上执行的动作
Spring切面可以应用5种通知:

  • 前置通知(Before):在目标方法或者说连接点被调用前执行的通知;
  • 后置通知(After):指在某个连接点完成后执行的通知;
  • 返回通知(After-returning):指在某个连接点成功执行之后执行的通知;
  • 异常通知(After-throwing):指在方法抛出异常后执行的通知;
  • 环绕通知(Around):指包围一个连接点通知,在被通知的方法调用之前和之后执行自定义的方法
    切点(Pointcut):指匹配连接点的断言。通知与一个切入点表达式关联,并在满足这个切入的连接点上运行,例如:当执行某个特定的名称的方法。
    引入(Introduction):引入也被称为内部类型声明,声明额外的方法或者某个类型的字段。
    目标对象(Target Object):目标对象是被一个或者多个切面所通知的对象
    AOP代理(AOP Proxy):向目标对象应用通知之后创建的对象。
    织入(Wearving):增强添加到目标类具体连接点上的过程。AOP有三种织入的方式:编译期织入、类装载期织入、动态代理织入(spring采用动态代理织入)。

1.3 通知执行顺序

  • 正常情况
    @Around ->@Before->主方法体->@Around中pjp.proceed()->@After->@AfterReturning
  • 存在异常
    1. 异常在Around中pjp.proceed()之前
      @Around -> @After -> @AfterThrowing
    2. 异常在Around中pjp.proceed()之后
      @Around ->@Before->主方法体->@Around中pjp.proceed()->@After->@AfterThrowing

1.4 实现原理

  1. JDK动态代理(JDK提供,只能代理接口)
    使用动态代理可以为一个或多个接口在运行期动态生成实现对象,生成的对象中实现接口的方法时可以添加增强代码,从而实现AOP。缺点是只能针对接口进行代理,另外由于动态代理是通过反射实现的,有时可能要考虑反射调用的开销
  2. CGLib动态代理: (适用CGLib工具)
    采用动态的字节码生成技术,运行时动态生成指定类的一个子类对象,并覆盖其中特定方法,覆盖方法时可以添加增强代码,从而实现AOP 。

1.5 Pointcut切入点的语法

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
/**
* 1、使用within表达式匹配
* 下面示例表示匹配com.example.controller包下所有的类的方法
*/
@Pointcut("within(com.example.controller..*)")
public void pointcutWithin(){
}
/**
* 2、this匹配目标指定的方法,此处就是HelloController的方法
*/
@Pointcut("this(com.example.controller.HelloController)")
public void pointcutThis(){
}
/**
* 3、target匹配实现UserInfoService接口的目标对象
*/
@Pointcut("target(com.leo.service.UserInfoService)")
public void pointcutTarge(){
}
/**
* 4、bean匹配所有以Service结尾的bean里面的方法,
* 注意:使用自动注入的时候默认实现类首字母小写为bean的id
*/
@Pointcut("bean(*ServiceImpl)")
public void pointcutBean(){
}
/**
* 5、args匹配第一个入参是String类型的方法
*/
@Pointcut("args(String, ..)")
public void pointcutArgs(){
}

/**
* 6、@annotation匹配是@Controller类型的方法
*/
@Pointcut("@annotation(org.springframework.stereotype.Controller)")
public void pointcutAnnocation(){
}
/**
* 7、@within匹配@Controller注解下的方法,要求注解的@Controller级别为@Retention(RetentionPolicy.CLASS)
*/
@Pointcut("@within(org.springframework.stereotype.Controller)")
public void pointcutWithinAnno(){

}
/**
* 8、@target匹配的是@Controller的类下面的方法,要求注解的@Controller级别为@Retention(RetentionPolicy.RUNTIME)
*/
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void pointcutTargetAnno(){
}
/**
* 9、@args匹配参数中标注为@Sevice的注解的方法
*/
@Pointcut("@args(org.springframework.stereotype.Service)")
public void pointcutArgsAnno(){
}

/**
* 10、使用excution表达式
* execution(
* modifier-pattern? //用于匹配public、private等访问修饰符
* ret-type-pattern //用于匹配返回值类型,不可省略
* declaring-type-pattern? //用于匹配包类型
* name-pattern(param-pattern) //用于匹配类中的方法,不可省略
* throws-pattern? //用于匹配抛出异常的方法
* )
*
* 下面的表达式解释为:匹配com.example.controller.HelloController类中以hello开头的修饰符为public返回类型任意的方法
*/
@Pointcut(value = "execution(public * com.example.controller.HelloController.hello*(..))")
public void pointCut() {
}

2.AOP实践

2.1 HTTP接口鉴权

需求:

  1. 可以定制地为某些指定的 HTTP RESTful api 提供权限验证功能。
  2. 当调用方的权限不符时, 返回错误。
    相关依赖
    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
2.1.1 自定义注解
1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthChecker {
}

AuthChecker 注解是一个方法注解,它用于注解 RequestMapping 方法。

2.1.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
@Component
@Aspect
public class HttpAopAdviseDefine {
//定义一个 Pointcut, 使用切点表达式函数来描述对哪些Join point使用advice.
@Pointcut("@annotation(com.example.annotation.AuthChecker)")
public void pointcut(){

}
//定义advice
@Around("pointcut()")
public Object checkAuth(ProceedingJoinPoint proceedingJoinPoint){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//检查用户所传递的 token 是否合法
String token = getToken(request);
if (!token.equalsIgnoreCase("111")){
return "token不合法!";
}
try {
return proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
return null;
}
}
private String getToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return "";
}
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("token")) {
return cookie.getValue();
}
}
return "";
}
}
2.1.3 Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/aop/http")
public class AopController {
@GetMapping("alive")
public String alive(){
return "服务一切正常";
}
@AuthChecker
@GetMapping("login")
public String login(){
return "登录成功!";
}
}
2.1.4 测试


token缺失/不正确:

token正确:

2.2 方法调用日志

需求:

  1. 某个服务下的方法的调用需要有log记录调用的参数以及返回结果。
  2. 当方法调用出异常时,有特殊处理,例如打印异常 log,报警等。
2.2.1 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
@Component
@Aspect
public class LogAopAdviseDefine {
private Logger logger = LoggerFactory.getLogger(getClass());

// 定义一个Pointcut, 使用切点表达式函数来描述对哪些 Join point 使用 advise.
@Pointcut("within(com.example.service..*)")
public void poincut() {

}

// 定义advise
@Before("poincut()")
public void logMethodInvokeParam(JoinPoint joinPoint) {
logger.info("---Before method {} invoke, param: {}---",
joinPoint.getSignature().toShortString(),
joinPoint.getArgs());
}

@AfterReturning(pointcut = "poincut()",returning = "message")
public void logMethodInvokeResult(JoinPoint joinPoint,Object message){
logger.info("---After method {} invoke, result: {}---",
joinPoint.getSignature().toShortString(),
joinPoint.getArgs());
}

@AfterThrowing(pointcut = "poincut()",throwing = "exception")
public void logMethodInvokeException(JoinPoint joinPoint,Exception exception){
logger.info("---method {} invoke exception: {}---",
joinPoint.getSignature().toShortString(),
exception.getMessage());
}
}
2.2.2 Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class LogServiceImpl implements LogService {
private Logger logger = LoggerFactory.getLogger(getClass());
private Random random = new Random(System.currentTimeMillis());

@Override
public int LogMethod(String param) {
logger.info("---LogService: logMethod invoked, param: {}---", param);
return random.nextInt();
}

@Override
public void exceptionMethod() throws Exception {
logger.info("---LogService: exceptionMethod invoked---");
throw new Exception("Something bad happened!");
}
}
1
2
3
4
5
6
7
8
9
@Service
public class NormalServiceImpl implements NormalService {
private Logger logger = LoggerFactory.getLogger(getClass());

@Override
public void normalMethod() {
logger.info("---NormalService: someMethod invoked---");
}
}
2.2.3 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(SpringRunner.class)
@SpringBootTest
public class LogAdviseTest {

@Autowired
private LogService logService;
@Autowired
private NormalService normalService;

@Test
@PostConstruct
public void testLogAdvise(){
logService.LogMethod("LogMethod Test!");
try {
logService.exceptionMethod();
}catch (Exception e){

}
normalService.normalMethod();
}
}

2.3 方法耗时统计

需求:

  1. 为服务中的每个方法调用进行调用耗时记录.
  2. 将方法调用的时间戳, 方法名, 调用耗时上报到监控平台
2.3.1 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
@Component
@Aspect
public class ExpiredAopAdviseDefine {
private Logger logger = LoggerFactory.getLogger(getClass());

@Pointcut("within(com.example.service.impl.ExpiredServiceImpl)")
public void pointcut() {

}

@Around("pointcut()")
public Object methodInvokeExpiredTime(
ProceedingJoinPoint proceedingJoinPoint) {
try {
// StopWatch 任务执行时间监视器
StopWatch stopWatch = new StopWatch();
// 开始
stopWatch.start();
Object proceed = proceedingJoinPoint.proceed();
// 结束
stopWatch.stop();
// 上报到监控平台
reportToMonitorSystem(
proceedingJoinPoint.getSignature().toShortString(),
stopWatch.getTotalTimeMillis());
return proceed;

}
catch (Throwable throwable) {
throwable.printStackTrace();
return null;
}
}

public void reportToMonitorSystem(String methodName, long expiredTime) {
logger.info("---method {} invoked, expired time: {} ms---",
methodName,
expiredTime);
}
}
2.3.2 Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class ExpiredServiceImpl implements ExpiredService {
private Logger logger = LoggerFactory.getLogger(getClass());

private Random random = new Random(System.currentTimeMillis());

@Override
public void expiredTimeMethod() {
logger.info("---SomeService: someMethod invoked---");
try {
//模拟耗时任务
Thread.sleep(random.nextInt(500));
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.3.3 测试
1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringRunner.class)
@SpringBootTest
public class ExpiredAdviseTest {
@Autowired
private ExpiredService expiredService;

@Test
@PostConstruct
public void testExpiredTime() {
expiredService.expiredTimeMethod();
}
}

请作者喝瓶肥宅快乐水