本文已参与「新人创作礼」活动,一起开启掘金创作之路。
目录
一.概念
AOP(Aspect Orient Programming)。
切面:公共的,通用的,重复的功能称为切面,面向切面编程就是将切面提取出来,单独开发,在需要调用的方法中通过动态代理方式进行织入。
编辑
二.手写AOP框架
编辑
版本1:
//图书购买业务和事务切面耦合在一起
public class BookServiceImpl {
public void buy(){
try{
System.out.println("事务开启");
System.out.println("图书购买业务功能实现");
System.out.println("事务提交");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
编辑
版本2:
public class BookServiceImpl {
//todo 在父类中只有业务
public void buy(){
System.out.println("图书购买业务功能实现");
}
}
//todo 子类是代理类,将父类的图书购买功能添加事务的切面
public class SubBookServiceImpl extends BookServiceImpl{
@Override
public void buy() {
try{
//todo 事务的切面
System.out.println("事务开启");
//todo 主业务实现
super.buy();
//todo 事务切面
System.out.println("事务提交");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
编辑
将业务和切面分开,实现了相同的功能
版本3:
静态代理,实现业务灵活切换,切面的功能在代理中体现。
编辑
编辑
public interface Service {
//todo 业务功能
void buy();
}
//todo 业务功能的具体实现
public class BookServiceImpl implements Service{
@Override
public void buy() {
System.out.println("图书购买业务实现");
}
}
public class FoodServiceImpl implements Service{
@Override
public void buy() {
System.out.println("购买食品业务");
}
/*todo 静态代理已经实现了目标对象的灵活切换,如图书购买业务,食品购买业务*/
public class Agent implements Service{
//todo 设计成员变量为接口,为了灵活切换目标对象
public Service target;
//todo 使用构造方法传入目标对象
public Agent(Service target){
this.target = target;
}
@Override
public void buy() {
try{
//切面功能
System.out.println("事务开启");
//业务功能
target.buy();
//切面功能
System.out.println("事务开启");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
public class Test {
@org.junit.Test
public void test(){
Agent bookAgent = new Agent(new BookServiceImpl());
bookAgent.buy();
Agent foodAgent = new Agent(new FoodServiceImpl());
foodAgent.buy();
}
}
版本4:
在静态代理中,会发现代理实现的切面功能是死的只能实现事务功能,不能灵活调用其他的切面功能比如日志权限验证功能。
有了接口使业务的功能实现更加灵活。
编辑
代理要实现切面接口而不是将切面接口的对象传进来。
要是传对象的话,业务接口和切面接口耦合在代理对象中,业务有变化或切面有变化,代理都得改变。
编辑
public interface AOP {
//default 实现类没必要实现该接口中所有的方法
default void before(){};
default void after(){};
default void exception(){};
}
public class LogAOP implements AOP{
@Override
public void before() {
System.out.println("日志输出");
}
}
public class TransactionAOP implements AOP{
@Override
public void before() {
System.out.println("事务开启");
}
@Override
public void after() {
System.out.println("事务提交");
}
@Override
public void exception() {
System.out.println("事务回滚");
}
}
/*todo 静态代理已经实现了目标对象的灵活切换,如图书购买业务,食品购买业务*/
public class Agent implements Service,AOP{
//todo 设计成员变量为接口,为了灵活切换目标对象
public Service target;
public AOP aop;
//todo 使用构造方法传入目标对象
public Agent(Service target,AOP aop){
this.target = target;
this.aop = aop;
}
@Override
public void buy() {
try{
//切面功能
aop.before();
//业务功能
target.buy();
//切面功能
aop.after();
}catch(Exception e){
aop.exception();
}
}
}
public class Test {
@org.junit.Test
public void test(){
pro4.Agent bookAgent = new pro4.Agent(new BookServiceImpl(),new TransactionAOP());
bookAgent.buy();
Agent foodAgent = new pro4.Agent(new FoodServiceImpl(),new LogAOP());
foodAgent.buy();
}
}
编辑
一个业务增加多个切面功能
public class Test {
@org.junit.Test
public void test(){
pro4.Agent bookAgent = new pro4.Agent(new BookServiceImpl(),new TransactionAOP());
//代理也是实现业务的一部分
Agent bookAgent1 = new Agent(bookAgent,new LogAOP());
bookAgent1.buy();
}
}
编辑
版本5:
动态代理
拆掉代理对象和业务的耦合即不再让代理对象实现业务的接口。
动态代理实现了业务功能的灵活改变,在静态代理时,要完成buy业务代理也需要实现buy方法,使用了动态代理后,代理不需要实现业务的任何方法。
public class ProxyFactory {
public static Object getAgent(Service target,AOP aop){
//返回生成的动态代理对象
return Proxy.newProxyInstance(
//类加载器
target.getClass().getClassLoader(),
//目标对象实现的所有的接口
target.getClass().getInterfaces(),
//代理功能的实现
new InvocationHandler() {
@Override
public Object invoke(
//生成的代理对象
Object proxy,
//正在被调用的目标方法如buy()
Method method,
//目标方法的参数
Object[] args
) throws Throwable {
Object obj = null;
try{
//切面
aop.before();
//业务
obj = method.invoke(target,args);
//切面
aop.after();
}catch(Exception e){
aop.exception();
}
return obj;
}
});
}
}
业务随意添加,都不会影响代理的代码
public interface Service {
//todo 业务功能
void buy();
default String show(int num){return null;}
}
public class Test {
@org.junit.Test
public void test(){
Service agent = (Service) ProxyFactory.getAgent(new BookServiceImpl(),new TransactionAOP());
//agent.buy();
String show = agent.show(5);
System.out.println(show);
}
}
三.Spring支持的AOP的实现
1.Spring的AOP的类型
编辑
Advice:通知
Interceptor:拦截
2.AOP的常用术语
编辑
3.AspectJ框架
AspectJ是一个优秀的切面框架,它扩展了Java语言,提供了强大的切面实现。它基于Java语言开发的,可以无缝扩展原有的功能。
对于AOP编程思想,很多框架都进行了实现,Spring就是其中之一,可以完成面向切面编程,然而AspectJ也实现了AOP的功能,且实现方式更为简捷,使用更为方便,而且还支持注解开发,所以Spring将AspectJ的对于AOP的实现引入到了自己的框架中,在Spring使用AOP开发时,一般使用AspectJ的实现方式。
(1) Aspect的通知类型
1.前置通知@Before
2.后置通知@AfterReturning
3.环绕通知@Around
事务就是环绕通知
4.最终通知@After
5.定义切入点@Pointcut(了解)
(2)AspectJ的切入点表达式
AspectJ定义了专门的表达式用于指定切入点。
表达式原型
编辑
编辑
规范的公式
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
表达式简化
execution (方法返回值 方法声明(参数))
编辑
例子
1.execution(public * *(..))
访问权限类型:public
返回值类型:*
方法声明:*
参数:..
切入点:任意公共方法
2.execution(* set*(..))
返回值类型:*
方法声明:set*
参数:..
切入点:以set开头的任意方法
3.execution( com.xyz.service.impl..*(..))**
返回值类型:*
包名类名: com.xyz.service.impl.* 这里的*是类
方法名:*
参数:..
切入点:service包下的impl包下的任意类的任意方法
4.execution(* com.xyz.service...(..))
返回值类型:*
包名类名:com.xyz.service..*
方法名:*
参数:..
service包下的任意路径(包括本路径和子路径)的任意方法
编辑
5.execution(* com.xyz.service.*(..))
这个没有类名,本意是想找service包下的所有类的所有方法,但会报错
6.execution(* com.xyz.service..*(..))
service路径包括子路径的所有类的所有方法,不会报错。注意本例中是.. 而上例是.
切入点表达式的用法
(3)使用AspectJ的环境
依赖
//spring依赖 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.19</version> </dependency> //aspectJ依赖 <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.19</version> </dependency>
将配置文件编译时加载到target文件中,不然会显示找不到配置文件
<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> <include>**/*.properties</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.xml</include> <include>**/*.properties</include> </includes> </resource> </resources> </build>
Maven当中resources标签的用法_怪咖软妹@的博客-CSDN博客_maven resources标签
4.前置通知
(1)前置通知流程分析
前置通知在目标方法前执行,所以在前置方法中无法获取目标方法执行后的结果,但能获取目标方法的签名,也就是public后面的一堆,因为只有知道目标方法的签名,才能作为他的前置通知。
编辑
(2)前置通知切面方法开发
前置通知切面方法开发
编辑
Step1:创建业务接口
public interface SomeService { String doSome(String name,int age); }
Step2:创建业务实现
@Component public class SomeServiceImpl implements SomeService{ @Override public String doSome(String name, int age) { System.out.println("doSome功能实现"); return "doSome"; } }
Step3:创建切面类,实现切面方法
//切面类 @Component @Aspect //交给AspectJ框架去识别的切面类 public class MyAspect { /* 前置通知的规范 1.访问权限是public 2.方法返回值是void 3.方法名称自定义 4.方法没有参数,如果有也只能是JoinPoint类型 5.必须使用@Before注解来声明切入的时机是前切功能和切入点 参数:value 指定切入点表达式 public String doSome(String name, int age) */ @Before(value = "execution(public String Before.SomeServiceImpl.doSome(String,int))") public void myBefore(){ System.out.println("前置通知功能实现"); } }
Step4:在applicationContext.xml文件中进行切面绑定
<!--创建业务对象--> <bean id="someService" class="Before.SomeServiceImpl"></bean> <!--创建切面对象--> <bean id="myAspect" class="Before.MyAspect"></bean> <!--绑定,之前是通过代理来绑定的,等同于ProxyFactory中的代码--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
或用包来扫描类
<context:component-scan base-package="Before"></context:component-scan> <!--绑定,之前是通过代理来绑定的,等同于ProxyFactory中的代码--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
Step:测试
public class Test { @org.junit.Test public void test(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); //取出代理对象 SomeService someService = (SomeService) ac.getBean("someServiceImpl"); System.out.println(someService.getClass());//class jdk.proxy2.$Proxy10 //表示这个someService是加入了切面功能的对象,而不是普通的someService对象 someService.doSome("1",1); } }
编辑
JDK动态代理和CGLib动态代理
JDK动态代理只能用接口接收代理对象,而CGLib可以用接口实现类接收代理对象也就是用子类来接收代理对象,可以使用标签来灵活改变代理方式
编辑
CGLib代理
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
SomeServiceImpl someService = (SomeServiceImpl) ac.getBean("someService");
JDK代理
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
SomeService someService = (SomeService) ac.getBean("someService");
方法参数JoinPoint解析
JoinPoint来获取目标参数的信息
@Component @Aspect //交给AspectJ框架去识别的切面类 public class MyAspect { @Before(value = "execution(public String Before.SomeServiceImpl.doSome(String,int))") public void myBefore(JoinPoint joinPoint){ System.out.println("前置通知功能实现"); System.out.println("目标方法的签名"+joinPoint.getSignature()); System.out.println("目标方法的参数"+ Arrays.toString(joinPoint.getArgs())); } }
5.后置通知
(1)后置通知流程分析
编辑
后置通知可以修改目标方法的返回值,但也是要分两种情况。
如果目标方法的返回值类型是8种基本类型或String类型,则不可改变,如果目标方法的返回值是引用类型则可以改变。可变与不可变是在测试代码中体现的。
(2)后置通知切面方法开发
/* 后置通知的规范 1.访问权限是public 2.方法返回值是void 3.方法名称自定义 4.如果目标方法有返回值,需要写参数,没有返回值则不需要写参数。写参数也可以处理没有返回值的情况,所以一般要写参数 5.必须使用@AfterReturning注解来声明切入的时机是后切功能和切入点 参数:value 指定切入点表达式 returning:指定目标方法的返回值的名称,此名称必须与切面方法的名称一致。 public String doSome(String name, int age) */ @AfterReturning(value = "execution(* After.*.*(..))", returning = "obj") public void myAfterReturning(Object obj){ System.out.println("后置通知功能实现"); if(obj != null){ if(obj instanceof String){//判断是否是String类型 obj = obj.toString().toUpperCase(); } } System.out.println(obj); }
判断目标方法的返回值是否是String类型,如果是则转为大写。在后置切面方法中输出的obj为大写,但是在测试类中,测试目标方法的返回值还是小写。
6.环绕通知
拦截目标方法,在目标方法前后增强功能的通知。是功能最强大的通知,一般事务使用此通知。
(1)环绕通知执行流程分析
编辑
(2)环绕通知切面方法开发
/* 环绕通知的规范 1.访问权限是public 2.方法返回值是是目标方法的返回值 3.方法名称自定义 4.方法有参数,参数就是目标方法 5.回避异常 6.使用@Around注解声明是环绕通知 参数:value:指定切入点表达式 public String doSome(String name, int age) */ @Around(value = "execution(* Around.*.*(..))") public Object myAround(ProceedingJoinPoint pjp) throws Throwable{ //前切功能实现 System.out.println("环绕通知前切功能实现"); //目标方法调用 Object obj = pjp.proceed(pjp.getArgs()); //后切功能实现 System.out.println("环绕通知后切功能实现"); return obj.toString().toUpperCase(); }
环绕通知可以随意修改目标方法的返回值。
7.最终通知
无论目标方法是否正常执行,最终通知的代码都会被执行。相当于try-catch-finally中的finally。
/* 最终通知的规范 1.访问权限是public 2.方法没有返回值 3.方法名称自定义 4.方法不需要参数,若需要写则只能写JoinPoint 5.使用@After注解声明是最终通知 参数:value:指定切入点表达式 public String doSome(String name, int age) */ @After(value = "execution(* fin.*.*(..))") public void myAfter(){ System.out.println("最终通知功能实现"); }
8.为一个方法添加多种通知
可以为一个方法绑定若干通知,而已统一通知类型也可以绑定多个
//切面类 @Component @Aspect //交给AspectJ框架去识别的切面类 public class MyAspect { @Before(value = "execution(* *(..))") public void myBefore(JoinPoint joinPoint){ System.out.println("前置通知功能实现"); System.out.println("目标方法的签名"+joinPoint.getSignature()); System.out.println("目标方法的参数"+ Arrays.toString(joinPoint.getArgs())); } @AfterReturning(value = "execution(* fin.*.*(..))", returning = "obj") public void myAfterReturning(Object obj){ System.out.println("后置通知功能实现"); if(obj != null){ if(obj instanceof String){//判断是否是String类型 obj = obj.toString().toUpperCase(); } } } @Around(value = "execution(* fin.*.*(..))") public Object myAround(ProceedingJoinPoint pjp) throws Throwable{ //前切功能实现 System.out.println("环绕通知前切功能实现"); //目标方法调用 Object obj = pjp.proceed(pjp.getArgs()); //后切功能实现 System.out.println("环绕通知后切功能实现"); return obj.toString().toUpperCase(); } @After(value = "execution(* fin.*.*(..))") public void myAfter(){ System.out.println("最终通知功能实现"); } }
编辑
可以参见出各个通知的执行顺序
环绕通知前切
前置通知
后置通知
最终通知
环绕通知后切
如果多个切面切入同一个切入点,可给切入点起别名简化开发
使用@Pointcut注解,创建空方法,此方法的名称就是别名。
@After(value = "myCut()") public void myAfter(){ System.out.println("最终通知功能实现"); } @Pointcut(value = "execution(* fin.*.*(..))") public void myCut(){ }
\