AOP面向切面编程
用于解决系统层面上的问题,比如:日志、事务、权限等待。它是一种编程范式,不是编程语言。
1、AOP的优点
- 降低模块之间的耦合度
- 使系统更容易扩展
- 更好的代码复用
- 非业务代码更加集中,不分散,便于统一管理
- 业务代码更加简洁纯粹,不掺杂其他的代码的影响
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
通常是一个类,里面可以定义切入点和通知。
连接点是在应用执行中能够插入切面的一个点。即程序执行过程中能够应用通知的所有点。
程序执行过程中明确的点,一般是方法的调用。
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
AOP在特定的切入点上执行的增强处理
就是带有通知的连接点,在程序中主要体现为书写切入点表达式
将切面应用到目标对象,并创建新的代理对象的过程。
切面在指定的连接点被织入到目标对象中。
在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。Aspect的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader) ,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时, AOP容器会为目标对象动态地创建一个代理对象(动态代理)。Spring AOP就是以这种方式织入切面的。
AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类
5种Advice
前置通知(Before):在目标方法被调用之前调用通知功能。
后置通知(After-returning):在目标方法成功执行之后调用通知。目标方法异常不执行。
异常通知(After-throwing)):在目标方法抛出异常后调用通知。
环绕通知(Around) :通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
最终通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。
AOP实践——基于xml的Spring
添加依赖
1 2 3 4 5 6
| <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> <scope>compile</scope> </dependency>
|
配置命名空间
关于aop的三行
1 2 3 4 5 6 7 8 9 10 11 12
| <?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:context="http://www.springframework.org/schema/context" context:xsi="schemaLocation=http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd" 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">
|
AOP的两种配置方式
第一种,使用<aspect>
配置。
这种方式更加灵活。Advice方法的配置用标签实现。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <aop:config> <aop:pointcut id="myPointCut" expression="execution(void com.service.impl.UserServiceImpl.show())" /> <aop:aspect ref="myAdvice"> <aop:before method="beforeAdvice" pointcut-ref="myPointCut"/> <aop:after-returning method="afterReturnAdvice" pointcut-ref="myPointCut"/> <aop:around method="aroundAdvice" pointcut-ref="myPointCut"/> <aop:after-throwing method="afterThrowingAdvice" pointcut-ref="myPointCut"/> <aop:after method="afterAdvice" pointcut-ref="myPointCut"/> </aop:aspect> </aop:config>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.advice;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAdvice { public void beforeAdvice() { System.out.println("执行了前置增强..."); } public void afterReturnAdvice() { System.out.println("执行了后置增强..."); } public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("执行了环绕(前)增强..."); Object proceed = proceedingJoinPoint.proceed(); System.out.println("执行了环绕(后)增强..."); return proceed; } public void afterThrowingAdvice() { System.out.println("执行了异常后增强..."); } public void afterAdvice() { System.out.println("执行了最终增强..."); } }
|
第二种,使用<advisor>
配置。(开发中基本不用)
需要增强类实现相应的Advice接口或其子接口
。
相比于前者,其Advice方法在advice类中实现,标签只需要引用advice类和切入点即可。
1 2 3 4
| <aop:config> <aop:pointcut id="myPointCut" expression="execution(void com.service.impl.UserServiceImpl.show())" /> <aop:advisor pointcut-ref="myPointCut" advice-ref="myAdvice2"></aop:advisor> </aop:config>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.advice;
import java.lang.reflect.Method;
import org.springframework.aop.AfterReturningAdvice; import org.springframework.aop.MethodBeforeAdvice; import org.springframework.lang.Nullable;
public class MyAdvice2 implements MethodBeforeAdvice, AfterReturningAdvice {
@Override public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { System.out.println("执行了前置增强..."); }
@Override public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable { System.out.println("执行了后置增强..."); }
}
|
语法形式不同:
- advisor是通过实现接口来确认通知的类型
- aspect是通过配置确认通知的类型,更加灵活
可配置的切面数量不同:
- 一个advisor只能配置一个固定通知和一个切点表达式
- 一个aspect可以配置多个通知和多个切点表达式任意组合
使用场景不同:
- 允许随意搭配情况下可以使用aspect进行配置
- 如果通知类型单一、切面单一的情况下可以使用advisor进行配置
- 在通知类型已经固定,不用人为指定通知类型时,可以使用advisor进行配置,例如后面要学习的 Spring事务控制的配置
AOP原理刨析
Bilibili精准空降,网友文字版
AOP实践——基于注解的Spring
启用AOP的注解
如果你选择使用配置文件来使用注解
1
| <aop:aspectj-autoproxy />
|
使用配置类
切面的配置
1 2 3 4 5 6
| @Aspect @Before() @AfterReturning() @Around() @AfterThrowing(value或pointcut="",throwing="",...) @After()
|
切点表达式抽取
- 写一个
空方法
,添加注解@Pointcut("execution(* com.package.*.*(..))")
1 2 3 4 5
| public class MyAdvice{ @Pointcut("execution(* com.package.*.*(..))") public void myPointcut(); ... }
|
- 使用
类名.方法名()
的语法引用切点表达式
“ ”
注意这种格式只是一种语法规范,而不是Java代码!
1
| @Before("MyAdvice.myPointcut()")
|
SpringBoot AOP实践
添加依赖
1 2 3 4
| <dependency> <groupld>org.springframework.boot</groupld> <artifactld>spring-boot-starter-aop</artifactld> </dependency>
|
拦截方法
方法一 声明自定义注解进行拦截
新建Annotation
包,新建adminOnly
接口
- 在
interface
前添加@
表明定义的是注解
- 添加
@annotation
注解
1 2 3 4 5 6 7 8 9 10 11 12
| package com.mysqlapi.Annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface adminOnly {
}
|
方法二 使用execution表达式拦截
语法格式
1
| execution(修饰符pattern 返回值pattern 描述包名方法名(参数) 方法抛出异常pattern)
|
示例
1 2 3 4 5 6 7 8 9
| @Pointcut("execution(public * com.example.controller.*Controller.*(..))") public void match(){ }
@Before("match()") public void before(){ }
|
切面的管理
新建Aspect
包对切面进行统一管理,并新建CheckUserAscpect.java
类
@Aspect
注解声明这个类是一个切面
@Component
注解,将这个类标记为Spring容器中的一个Bean,这样就可以使用其它注解了(@Autowired
)
- 使用
CheckUserService.java
这个类,实现逻辑处理
- 编写切入点,
@Pointcut("@annotation(com.mysqlapi.Annotation.adminOnly)")
,其中参数用来指定在何处插入,此处表示在使用了(自定义)注解@adminOnly处切入
,也可以使用execution表达式进行拦截
- 编写通知,决定了何时执行,以
@Before
通知为例,即在执行操作前,检查用户是否为管理员。@Before("checkAdmin()")
其中的参数为切入点的方法
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
| package com.mysqlapi.Aspect;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import com.mysqlapi.service.CheckUserService;
@Aspect @Component public class CheckUserAscpect { @Autowired private CheckUserService checkUserService; @Pointcut("@annotation(com.mysqlapi.Annotation.adminOnly)") public void checkAdmin() { } @Before("checkAdmin()") public void check() throws Exception { checkUserService.check(); } }
|
修改之前的权限验证
UserService.java
- 在
addUser()
方法前,添加注解@adminOnly
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
| package com.mysqlapi.Aspect;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component;
import com.mysqlapi.service.CheckUserService;
@Aspect @Component public class CheckUserAscpect { @Autowired private CheckUserService checkUserService;
@Pointcut("@annotation(com.mysqlapi.Annotation.adminOnly)")
public void checkAdmin() {
}
@Before("checkAdmin()") public void check() throws Exception { checkUserService.check(); }
}
|
进行单元测试
UserServiceTest.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 30
| package com.mysqlapi.service;
import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner;
import com.mysqlapi.holder.CurrentUserHolder;
@RunWith(SpringRunner.class) @SpringBootTest class UserServiceTest2 {
@Autowired private UserService userService;
@Test void testFindAll() throws Exception { CurrentUserHolder.set("sds"); userService.findAll(); }
@Test void testAddUser() throws Exception { CurrentUserHolder.set("sds"); userService.addUser(); }
}
|
同样可以实现,权限控制。这种添加自定义注解的方式,比在每有一个方法都要添加check()
和throws Exception
要更加简洁,但还是需要对每个需要权限的方法添加注解。

用execution表达式的单元测试
AspectTestUserService1.java <– 这个是service包中的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package com.mysqlapi.service;
import org.springframework.stereotype.Service;
@Service public class AspectTestUserService1 { public void findAll() { System.out.println("AspectTestUserService1 查找成功!"); } public void addUser() { System.out.println("AspectTestUserService1 插入成功!"); } }
|
AspectTestUserService1Test.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
| package com.mysqlapi.service;
import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest class AspectTestUserService1Test {
@Autowired private AspectTestUserService1 aspectTestUserService1; @Test void testFindAll() { aspectTestUserService1.findAll(); }
@Test void testAddUser() { aspectTestUserService1.addUser(); }
}
|
excution表达式
语法格式
1
| execution (* com.sample.service.impl..*.*(..))
|
整个表达式可以分为五个部分:
1、execution(): 表达式主体。
2、第一个号:表示返回类型,号表示所有的类型。
3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。
4、第二个号:表示类名,号表示所有的类。
5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。