动态代理与 SpringAOP

108 阅读7分钟

从代理模式开始,一文让你彻底搞懂SpringAOP

要了解什么是SpringAOP首先要搞懂设计模式中的-代理模式

静态代理

假设有一个房东,有一个租房子的接口

public interface Owner {
    void rentHouse(String info);
}

现在写一个租房子的类来实现这个

public class RentHouse implements Owner{
    @Override
    public void rentHouse(String info) {
        System.out.println(info);
    }
}

然后我们需要在打印信息上面下面写一些无关乎租房子但又需要的代码,可能会写成这样

public class RentHouse implements Owner{
    @Override
    public void rentHouse(String info) {
        before();
        System.out.println(info);
        after();
    }

    private void before() {
        System.out.println("before");
    }

    private void after() {
        System.out.println("after");
    }
}

这样虽然可以成功,但是一般的项目开发完成,改动业务是大忌,要是出了点错,全盘崩溃,所以这样是万万不可的。我们可以使用静态代理来帮我们做这件事情。就像例子中的,房东可以去找中介(代理)去替自己进行租房子的事情,房东只管把任务交给代理,让代理替自己去做这件事情。

创建一个代理类,实现Owner接口

public class RentHouseProxy implements Owner{

    private RentHouse rentHouse;

    public RentHouseProxy(RentHouse rentHouse) {
        this.rentHouse = rentHouse;
    }

    @Override
    public void rentHouse(String info) {
        before();
        rentHouse.rentHouse(info);
        after();
    }

    private void before() {
        System.out.println("before");
    }

    private void after() {
        System.out.println("after");
    }
}

然后在主方法中调用

public class Main {
    public static void main(String[] args) {
        RentHouseProxy proxy = new RentHouseProxy(new RentHouse());
        proxy.rentHouse("Rent House");
    }
}

输出结果为

before
Rent House
after

这样实现了静态代理,但是静态代理也有缺陷,随着业务的增加,类也增多,那么Proxy这样的代理也会成倍的增长,最好只用一个类便可以解决所有代理。这就需要用到动态代理了。

动态代理

介绍两种动态代理,JDK 动态代理和 CGLib 动态代理

JDK 动态代理

创建一个 JDK 动态代理类,用来处理 before 和 after

import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.cglib.proxy.Proxy;

import java.lang.reflect.Method;

public class JdkDynamicProxy implements InvocationHandler {

    private Object target;

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

    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                this
        );
    }

    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("before");
    }

    private void after() {
        System.out.println("after");
    }
}

在主方法中

public class Main {
    public static void main(String[] args) {
        Owner owner = new JdkDynamicProxy(new RentHouse()).getProxy();
        owner.rentHouse("Rent House");
    }
}

这样所有的代理类都使用动态代理类处理了,但是 JDK 动态代理只能代理接口,却不能代理没有接口的类,这就需要用到 CGLib 动态代理了

CGLib 动态代理

CGLib可以代理没有接口的类,弥补了JDK动态代理的缺陷,首先创建一个动态代理类

import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLibDynamicProxy implements MethodInterceptor {

    private static CGLibDynamicProxy instance = new CGLibDynamicProxy();

    private CGLibDynamicProxy() {
    }

    public static CGLibDynamicProxy getInstance() {
        return instance;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> cls) {
        return (T) Enhancer.create(cls, (Callback) this);
    }

    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        before();
        Object result = proxy.invokeSuper(target, args);
        after();
        return result;
    }

    private void before() {
        System.out.println("before");
    }

    private void after() {
        System.out.println("after");
    }

}

然后在主方法中使用

public class Main {

    public static void main(String[] args) {
        Owner owner = CGLibDynamicProxy.getInstance().getProxy(RentHouse.class);
        owner.rentHouse("Rent House");
    }

}

可以看出CGLib动态代理类使用了单例模式。

JDK 动态代理和 CGLib 动态代理

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:

Spring实现动态代理

以上的before方法被称为Before Advice(前置增强),after方法被称为After Advice(后置增强),合并在一起称为环绕增强,此外还有Throws Advice(抛出增强),Introduction Advice(引入增强),那么在Spring的世界里,怎么使用Advice呢 ?其实非常简单

实现前后置增强,只需要创建一个实现MethodBeforeAdvice, AfterReturningAdvice接口的类

public class RentHouseBeforeAfterAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("before");
    }

    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("after");
    }
}

然后在主方法中

import com.ecin520.source.proxy.Owner;
import com.ecin520.source.proxy.RentHouse;
import com.ecin520.source.proxy.RentHouseBeforeAfterAdvice;
import org.springframework.aop.framework.ProxyFactory;

public class Main {

    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(new RentHouse());
        proxyFactory.addAdvice(new RentHouseBeforeAfterAdvice());

        Owner owner = (Owner) proxyFactory.getProxy();
        owner.rentHouse("Rent House");
    }
}

我们会发现,使用AOP框架会非常的简单,也可以使用环绕增强,注意不是Spring的Around Advice。

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class RentHouseAroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        before();
        Object result = methodInvocation.proceed();
        after();
        return result;
    }

    private void before() {
        System.out.println("before");
    }

    private void after() {
        System.out.println("after");
    }
    
}

使用

proxyFactory.addAdvice(new RentHouseAroundAdvice());

那么如何在Spring中使用它呢? 首先xml文件为

<?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"
       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">
    <context:component-scan base-package="com.ecin520.source.proxy"/>

    <bean id="rentHouseProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 需要代理的接口 -->
        <property name="interfaces" value="com.ecin520.source.proxy.Owner"/>
        <!-- 接口实现类 -->
        <property name="target" ref="rentHouse"/>
        <!-- 拦截器名称(也就是增强类名称,Spring Bean 的 id) -->
        <property name="interceptorNames" value="rentHouseAroundAdvice"/>
    </bean>
    <!-- 注册两个bean -->
    <bean id="rentHouse" class="com.ecin520.source.proxy.RentHouse"/>
    <bean id="rentHouseAroundAdvice" class="com.ecin520.source.proxy.RentHouseAroundAdvice"/>

</beans>

然后在主方法中

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Owner owner = (Owner) context.getBean("rentHouseProxy");
        owner.rentHouse("Rent House");
    }

}

Spring 实现 AOP

首先介绍一下AOP的一些概念

术语概念
横切关注点与业务逻辑无关但是需要关注的部分,如缓存、日志、事务等
连接点(JointPoint)类里面可以被增强的方法,这些方法称之为连接点
切入点(PointCut)定义要对哪些连接点进行拦截横切
通知/增强(Advice)拦截到连接点后所做的事情,有前置增强、后置增强、环绕增强、异常增强、引入增强等
切面(Aspect)PointCut和Advice的结合,把增强应用到具体方法上面的过程称为切面
引入(Introduction)一种特殊的Advice,可以在不修改类代码的前提下,引入可以在运行期为类动态地添加方法或者Field
目标对象(Target)代理的目标对象,需要增强的类
织入(Weaving)把增强应用到目标的过程,把Advice应用到Target的过程
代理(Proxy)一个类被AOP织入增强后,就产生一个代理类

通过以上动态代理的例子应该容易理解这些概念,现在在 Spring 中怎么实现 AOP 呢 ?通过一个打印方法信息的 Log 来举例:

(1)导入 AspectJ 包

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.4</version>
</dependency>

(2)编写 UserService 和其实现类 UserServiceImpl

public interface UserService {
    void insertUser();
    void updateUser();
    void queryUser();
}
public class UserServiceImpl implements UserService {
    @Override
    public void insertUser() {
        System.out.println("insert user success.");
    }

    @Override
    public void updateUser() {
        System.out.println("update user success.");
    }

    @Override
    public void queryUser() {
        System.out.println("query user success.");
    }
}

(3)然后写一个环绕通知类

public class LogAroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        before(methodInvocation.getMethod().getName());
        Object result = methodInvocation.proceed();
        after(methodInvocation.getMethod().getName());
        return result;
    }

    private void before(String info) {
        System.out.println("before " + info + " invoke.");
    }

    private void after(String info) {
        System.out.println("after " + info + " invoke.");
    }

}

(4)注册到 Spring 中

<?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 id="userServiceImpl" class="com.ecin520.source.service.UserServiceImpl"/>
    <bean id="log" class="com.ecin520.source.proxy.LogAroundAdvice"/>

    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(* com.ecin520.source.service.UserServiceImpl.*(..))"/>
        <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
    </aop:config>

</beans>

上面的pointcut也就是切点,表示需要对哪些方法进行拦截,这里表示UserServiceImpl中所有的方法

(5)在主方法中

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = (UserService) context.getBean("userServiceImpl");
        userService.insertUser();
    }
}

注解实现SpringAOP(1)

(1)XML文件改为,需要自动扫描@Component注解的包

<?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"
       xmlns:context="http://www.springframework.org/schema/context"
       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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.ecin520.source"/>
    <aop:aspectj-autoproxy/>
</beans>

(2)编写LogAspect类

@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(* com.ecin520.source.service.UserServiceImpl.*(..))")
    public void pointCut() {

    }

    @Around("pointCut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result;
        System.out.println(joinPoint.getTarget().getClass().getName() + "方法开始执行");
        result = joinPoint.proceed();
        System.out.println(joinPoint.getTarget().getClass().getName() + "方法执行完毕");
        return result;
    }

    @AfterThrowing(pointcut = "pointCut()", throwing = "e")
    public void logThrowing(JoinPoint joinPoint, Throwable e) {
        e.printStackTrace();
    }
}

(3)测试 因为是自动扫包,所以要在UserServiceImpl类中添加@Component注解,然后在主方法中

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = (UserService) context.getBean("userServiceImpl");
        userService.insertUser();
    }
}

(4)结果

com.ecin520.source.service.UserServiceImpl方法开始执行
insert user success.
com.ecin520.source.service.UserServiceImpl方法执行完毕

注解实现SpringAOP(2)

上面的方法使用需要扫面特定的切入点,如果切入点分散在各个角落,这样扫描就非常麻烦,因此,使用自己编写的特定的注解会方便许多,而且这是比较主流的写法,现在开始介绍这种方法

(1)自定义一个Log注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}

(2)在Aspect中

@Aspect
@Component
public class LogAspect {

    private ThreadLocal<Long> currentTime = new ThreadLocal<>();

    @Pointcut("@annotation(com.ecin520.source.annotation.Log)")
    public void pointCut() {

    }

    @Around("pointCut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result;

        currentTime.set(System.currentTimeMillis());

        result = joinPoint.proceed();

        Thread.sleep(10);

        Log log = joinPoint.getTarget().getClass().getMethod(joinPoint.getSignature().getName()).getAnnotation(Log.class);

        System.out.println("Log注解信息" + log.value());
        System.out.println(joinPoint.getSignature() + "方法执行了" + (System.currentTimeMillis() - currentTime.get()) + "ms");

        currentTime.remove();

        return result;
    }

    @AfterThrowing(pointcut = "pointCut()", throwing = "e")
    public void logThrowing(JoinPoint joinPoint, Throwable e) {
        e.printStackTrace();
    }
}

(3)编写一个测试类

@Component(value = "rand")
public class Rand {
    @Log("print")
    public void print() {
        System.out.println("print() 方法被执行了");
    }

    @Log("show")
    public void show() {
        System.out.println("show() 方法被执行了");
    }
}

(4)主方法中测试

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Rand rand = (Rand) context.getBean("rand");
        rand.print();
        rand.show();
    }
}

打印结果为

print() 方法被执行了
Log注解信息print
void com.ecin520.source.pojo.Rand.print()方法执行了28ms
show() 方法被执行了
Log注解信息show
void com.ecin520.source.pojo.Rand.show()方法执行了10ms

由此可见,使用注解非常的方便,上面例子中,还利用反射获取了注解信息,反射还用于动态代理,反射真是框架之魂