Spring AOP

AOP开发

AOP 是 Aspect Oriented Programming 的缩写,意为面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,是OOP的延续。

AOP 能够对程序进行增强,在不修改源码的情况下,可以进行权限校验,日志记录,性能监控,事务控制等。

也就是说功能分为两大类,一类是核心业务功能,一类是辅助增强功能。两类功能彼此独立进行开发。比如登录功能是核心业务功能,日志功能是辅助增强功能,如果有需要,将日志和登录编制在一起。辅助功能就称为切面,这种能选择性的、低耦合的把切面和核心业务功能结合的编程思想称为切面编程。

AOP的初衷

DRY:Dont Repeat Yourself SoC: Sepration of Concerns

  • 水平分离:展示层->服务层->持久层
  • 垂直分离:模块划分
  • 切面分离:分离功能性和非功能性需求

为何使用AOP

  • 集中处理某一关注点/横切逻辑
  • 方便添加/删除关注点
  • 侵入性少,增强代码可读性和可维护性

主要应用于权限控制,缓存控制,事务控制,审计日志,性能监控,分布式追踪,异常处理等等。

底层实现

JDK 动态代理只能对实现了接口的类产生代理。Cglib 动态代理可以对没有实现接口的类产生代理对象,生成的是子类对象。

JDK 动态代理

JDK 动态代理基于接口实现,底层是基于反射。

img

public interface Subject {
    public void request();
}

实现类:

public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("real subject request");
    }
}

JDK 代理:

public class JdkProxySubject implements InvocationHandler {

    private RealSubject realSubject;

    public JdkProxySubject(RealSubject realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before");
        Object result = null;
        try {
            result = method.invoke(realSubject, args);
        } catch (Exception e) {
            throw e;
        } finally {
            System.out.println("after");
        }
        return result;
    }
}

测试类

public class Client {
    public static void main(String[] args) {
        RealSubject rs=new RealSubject();
        InvocationHandler handler=new JdkProxySubject(rs);
        Subject subject=(Subject) Proxy.newProxyInstance(rs.getClass().getClassLoader(),rs.getClass().getInterfaces(),handler);
        subject.request();
    }
}

运行结果:

before
real subject request
after

在 Client 中,通过调用 Proxy.newProxyInstance() 生成代理对象,其中参数分别是 classLoader, 要代理的接口, 以及代理对象的 InvocationHandler。当调用subject.request()时,实际调用的是JdkProxySubject里的invoke方法。

Cglib

Cglib 是第三方开源代码生成类库,可以动态添加类的属性和方法。

JDK 代理基于接口实现,而 Cglib 基于继承实现:

public class Hello {
    public void hello(String str){
        System.out.println("hello "+str);
    }
}

该类并没有实现任何接口。通过CGLIB代理实现如下: 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。 然后在需要使用该类的时候,通过CGLIB动态代理获取代理对象。

class MyMethodInterceptor implements MethodInterceptor{
  ...
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        logger.info("You said: " + Arrays.toString(args));
        return proxy.invokeSuper(obj, args);
    }
}

public class Test {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
				enhancer.setSuperclass(HelloConcrete.class);
				enhancer.setCallback(new MyMethodInterceptor());

				HelloConcrete hello = (HelloConcrete)enhancer.create();
				System.out.println(hello.sayHello("I love you!"));
    }
}

通过CGLIB的Enhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个对象所有非final方法的调用都会转发给MethodInterceptor.intercept()方法。

两者比较

何时使用JDK还是CGLiB?

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

Spring如何选择用JDK还是CGLiB?

  • 当Bean实现接口时,Spring就会用JDK的动态代理。
  • 当Bean没有实现接口时,Spring使用CGlib是实现。
  • 可以强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。

Spring 的 AOP 开发(AspectJ 的 XML 方式)

AspectJ 是一个 AOP 的框架,Spring 引入 AspectJ,基于 AspectJ 进行 AOP 的开发。

相关术语

  • Joinpoint: 连接点,可以被拦截到的点。指程序运行时允许插入切面的一个点,可以是一个函数、一个包路径、一个类、或者抛出的异常
  • Pointcut: 切入点,用于指定连接点的范围,真正被拦截到的点,也就是真正被增强的方法
  • Advice: 通知,定义了切面在切点附近的操作,也就是在什么时候做什么事。如 before, after 等。
  • Aspect: 切面,切入点和通知的聚合,一般是一个横跨多各类的通用逻辑抽象而成的类,切面类会定义在什么时间、什么地点、做什么事。
  • Introduction: 引介,类层面的增强。
  • Target: 目标,被增强的对象(类),也就是被切面织入的对象。
  • Weaving: 织入是把切面应用到切点对应的连接点的过程。切面在指定连接点被织入到目标对象中。
  • Proxy: 代理对象,被增强的对象。

使用方法

  1. 引入相关包
  2. 引入配置文件
<?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 definitions here -->

</beans>

  1. 编写目标类并配置:
public class ProductDaoImpl implements ProductDao {
	@Override
	public void save() {
		System.out.println("save");
	}
	@Override
	public void update() {
		System.out.println("update");
	}
	@Override
	public void find() {
		System.out.println("find");
	}
	@Override
	public void delete() {
		System.out.println("delete");
	}
}

<bean id="productDao" class="demo1.ProductDaoImpl"></bean>

  1. 编写切面类,假设用于权限验证并配置
public class MyAspectXML {
	public void checkPri(){
		System.out.println("check auth");
	}
}

<bean id="myAspect" class="demo1.MyAspectXML"></bean>

  1. 通过AOP配置完成对目标类的增强
	<aop:config>
		<aop:pointcut expression="execution(* demo1.ProductDaoImpl.save(..))" id="pointcut1"/>
		
		<aop:aspect ref="myAspect">
			<aop:before method="chechPri" pointcut-ref="pointcut1"/>
		</aop:aspect> 
	</aop:config>

通知类型

  1. 前置通知:在目标方法执行前操作,可以获得切入点信息
<aop:before method="chechPri" pointcut-ref="pointcut1"/>

public void checkPri(JoinPoint joinPoint){
	System.out.println("check auth "+joinPoint);
}

  1. 后置通知:在目标方法执行后操作,可以获得方法返回值
<aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>

public void writeLog(Object result){
    System.out.println("writeLog "+result);
}

  1. 环绕通知:在目标方法执行前和后操作,可以阻止目标方法执行
<aop:around method="around" pointcut-ref="pointcut3"/>

public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
	System.out.println("before");
	Object result=joinPoint.proceed();
	System.out.println("after");
	return result;
}

  1. 异常抛出通知:程序出现异常时操作
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="ex"/>

public void afterThrowing(Throwable ex){
		System.out.println("exception "+ex.getMessage());
	}

  1. 最终通知:相当于finally块,无论代码是否有异常,都会执行
<aop:after method="finallyFunc" pointcut-ref="pointcut4"/>

public void finallyFunc(){
	System.out.println("finally");
}

  1. 引介通知:不常用

Spring 切入点表达式

基于 execution 函数完成

语法:[访问修饰符] 方法返回值 包名.类名.方法名(参数)

其中任意字段可以使用*代替表示任意值

Spring 的 AOP 基于 AspectJ 注解开发

开发步骤

  1. 引入jar包
  2. 设置配置文件:
<?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"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context.xsd
	http://www.springframework.org/schema/aop
	http://www.springframework.org/schema/aop/spring-aop.xsd
	http://www.springframework.org/schema/tx 
	http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>

  1. 编写配置目标类
<bean id="orderDao" class="demo1.OrderDao"></bean>

public class OrderDao {
	public void save(){
		System.out.println("save order");
	}
	
	public void update(){
		System.out.println("update order");
	}
	public void delete(){
		System.out.println("delete order");
	}
	public void find(){
		System.out.println("find order");
	}
}

  1. 开启aop注解自动代理
<aop:aspectj-autoproxy/>
  1. 编写切面类并配置
@Aspect
public class MyAspectAnno {
	
	@Before(value="execution(* demo1.OrderDao.save(..))")
	public void before(){
		System.out.println("before");
	}
}

<bean id="myAspect" class="demo1.MyAspectAnno">

注解类型

注解主要分为三类:

  1. @Aspect:表示这是一个切面类
  2. @Pointcut:表示切入点,也就是需要增强的类或方法

这个注解需要借助切面表达式组成。切面表达式由三个部分构成:指示器(designators)、通配符(wildcards)以及操作符(operators)。

指示器分为:

  • 匹配方法:execution()
execution(
    [modifier-pattern]// 修饰符
    ret-type-pattern//返回类型
    [declaring-type-pattern]//包名
    name-pattern(param-pattern)//方法名
    [throws-pattern]//抛出异常声明
)

  • 匹配注解:@target(),@args(),@within(),@annotation()
  • 匹配包/类:within()
@Pointcut("within(demo.service.*)")
public void pointcut(){}

  • 匹配对象:this(),bean(),target()
  • 匹配参数:args()
//匹配任何只有一个Long参数的方法
@Pointcut("args(Long)")
//匹配第一个参数为Long的方法
@Pointcut("args(Long,..)")

通配符有:

    • 匹配任意数量的字符
    • 匹配指定类及其子列
  • .. 用于匹配任意数的子包或参数

运算符主要有&&,||,!也就是与或非三个。

  1. Advice: 在什么时候执行增强的方法
  • @Before: 前置通知
  • @AfterReturning: 后置通知
@AfterReturning(value="execution(* demo1.OrderDao.save(..))",returning="result")
public void after(Object result){
	System.out.println("after "+result);
}
  • @Around:环绕通知
@Around(value="execution(* demo1.OrderDao.save(..))")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
		System.out.println("before");
		Object obj=joinPoint.proceed();
		System.out.println("after");
		return obj;
	}

  • @AfterThrowing: 抛出异常
@AfterThrowing(value="execution(* demo1.OrderDao.save(..))",throwing="e")
public void afterThrowing(Throwable e){
	System.out.println("exception:"+e.getMessage();
}
  • @After: 最终通知
@After(value="execution(* demo1.OrderDao.save(..))")
	public void after(){
		System.out.println("finally");
	}
  • @PointCut:切入点注解
@PointCut(value="execution(* demo1.OrderDao.save(..))")
	private void pointcut1(){}

注解的好处是,只需要维护切入点即可,不用在修改时修改每个注解。
本文主要来源于:https://juejin.im/post/6844903752860696583

posted @ 2020-11-19 19:08  xuewenのblog  阅读(66)  评论(0编辑  收藏  举报