开发者社区> 技术小甜> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

Spring AOP 和 动态代理技术

简介:
+关注继续查看










AOP 是什么东西

首先来说 AOP 并不是 Spring 框架的核心技术之一,AOP 全称 Aspect Orient Programming,即面向切面的编程。其要解决的问题就是在不改变源代码的情况下,实现对逻辑功能的修改。常用的场景包括记录日志、异常处理、性能监控、安全控制(例如拦截器)等,总结起来就是,凡是想对当前功能做变更,但是又不想修改源代码的情况下,都可以考虑是否可以用 AOP 实现。

为什么要面向切面呢,我直接改源代码不是很好吗?当然没有问题,如果情况允许。但是考虑到下面这些情况,我本来写好了1000个方法,有一天,我想加入一些控制,我想在执行方法逻辑之前,检查一些系统参数,参数检查没问题再执行逻辑,否则不执行。这种情况怎么办呢,难道要修改这1000个方法吗,那简直就是灾难。还有,有些线上逻辑执行缓慢,但我又不想重新部署环境,因为那样会影响线上业务,这种情况下,也可以考虑 AOP 方式,Btrace 就是这样一个线上性能排查的神器。

 

Spring AOP 的用法

面向切面编程,名字好像很炫酷,但是使用方式已经被 Spring 封装的非常简单,只需要简单的配置即可实现。使用方式不是本文介绍的重点,下面仅演示最简单最基础的使用,实现对调用的方法进行耗时计算,并打印出来。

环境说明: JDK 1.8 ,Spring mvc 版本 4.3.2.RELEASE

1. 首先引用 Spring mvc 相关的 maven 包,太多了,就不列了,只列出 Spring-aop 相关的

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

<dependency>

    <groupId>org.springframework</groupId>

    <artifactId>spring-aop</artifactId>

    <version> 4.3.2.RELEASE </version>

</dependency>

<dependency>

    <groupId>org.aspectj</groupId>

    <artifactId>aspectjrt</artifactId>

    <version>1.8.9</version>

</dependency>

<dependency>

    <groupId>org.aspectj</groupId>

    <artifactId>aspectjweaver</artifactId>

    <version>1.8.9</version>

</dependency>

2. 在 Spring mvc 配置文件中增加关于 AOP 的配置,内容如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

<?xml version="1.0" encoding="UTF-8"?>

<beans default-lazy-init="true"

    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:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"

       xmlns:aop="http://www.springframework.org/schema/aop"

    xsi:schemaLocation=" 

       http://www.springframework.org/schema/beans  

       http://www.springframework.org/schema/beans/spring-beans-4.3.xsd

       http://www.springframework.org/schema/mvc  

       http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd

       http://www.springframework.org/schema/context 

       http://www.springframework.org/schema/context/spring-context-4.3.xsd

       http://www.springframework.org/schema/aop

    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

 

    <!-- 自动扫描与装配bean -->

    <context:component-scan base-package="kite.lab.spring"></context:component-scan>

           <!-- 启动 @AspectJ 支持 -->

    <aop:aspectj-autoproxy proxy-target-class="true" />

</bean>

3. 创建切面类,并在 kite.lab.spring.service 包下的方法设置切面,使用 @Around 注解监控,实现执行时间的计算并输出,内容如下:

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

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.*;

import org.springframework.stereotype.Component;

import org.springframework.util.StopWatch;

 

@Component

@Aspect

public class PerformanceMonitor {

     

    //配置切入点,该方法无方法体,主要为方便同类中其他方法使用此处配置的切入点

    @Pointcut("execution(* kite.lab.spring.service..*(..))")

    public void aspect(){   }

 

    @Around("aspect()")

    public Object methodTime(ProceedingJoinPoint pjp) throws Throwable {

        StopWatch stopWatch = new StopWatch();

        stopWatch.start();

        // 开始

        Object retVal = pjp.proceed();

        stopWatch.stop();

        // 结束

        System.out.println(String.format("方法 %s 耗时 %s ms!",pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis()));

        return retVal;

    }

}

4. 被切面监控的类定义如下:

1

2

3

4

5

6

7

8

package kite.lab.spring.service;

 

public class Worker {

    public String dowork(){

        System.out.println("生活向来不易,我正在工作!");

        return "";

    }

}

5. 加载 Spring mvc 配置文件,并调用 Worker 类的方法  

1

2

3

4

5

6

public static void main(String[] args) {

        String filePath = "spring-servlet.xml";

        ApplicationContext ac = new FileSystemXmlApplicationContext(filePath);

        Worker worker = (Worker) ac.getBean("worker");

        worker.dowork();

}

6. 显示结果如下:

  

说完用法,接下来说一下实现原理,知其然也要知其所以然。  

Spring AOP 原理

AOP 的实现原理就是动态的生成代理类,代理类的执行过程为:执行我们增加的代码(例如方法日志记录)—-> 回调原方法 ——> 增加的代码逻辑。看图比较好理解:

Spring AOP 动态代理可能采用 JDK 动态代理或 CGlib 动态生成代理类两种方式中的一种, 决定用哪一种方式的判断标准就是被切面的类是否有其实现的接口,如果有对应的接口,则采用 JDK 动态代理,否则采用 CGlib 字节码生成机制动态代理方式。

代理模式是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。代理类和委托类实现相同的接口,所以调用者调用代理类和调用委托类几乎感觉不到差别。

是不是看完了定义,感觉正好可以解决切面编程方式要解决的问题。下图是基本的静态代理模式图:

而动态代理的意思是运行时动态生成代理实现类,由于 JVM 的机制,需要直接操作字节码,生成新的字节码文件,也就是 .class 文件。

JDK 动态代理

JDK 动态代理模式采用 sun 的 ProxyGenerator 的字节码框架。要说明的是,只有实现了接口的类才能使用 JDK 动态代理技术,实现起来也比较简单。

1. 只要实现 InvocationHandler 接口,并覆写 invoke方法即可。具体实现代码如下:

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

package kite.lab.spring.aop.jdkaop;

 

import java.lang.reflect.InvocationHandler;

import java.lang.reflect.Method;

import java.lang.reflect.Proxy;

 

/**

 * JdkProxy

 *

 * @author fengzheng

 */

public class JdkProxy implements InvocationHandler {

 

    private Object target;

 

    /**

     * 绑定委托对象并返回一个代理类

     *

     * @param target

     * @return

     */

    public Object bind(Object target) {

        this.target = target;

        //取得代理对象

        return Proxy.newProxyInstance(target.getClass().getClassLoader(),

                target.getClass().getInterfaces(), this);

    }

 

 

    /**

     * 调用方法

     */

    @Override

    public Object invoke(Object proxy, Method method, Object[] args)

            throws Throwable {

        Object result = null;

        System.out.println("事物开始");

        //执行方法

        result = method.invoke(target, args);

        System.out.println("事物结束");

        return result;

    }

}

Proxy.newProxyInstance 方法用于动态生成实际生成的代理类,三个参数依次为被代理类的类加载器、被代理类所实现的接口和当前代理拦截器。

覆写的 invoke 中可以加入我们增加的业务逻辑,然后回调原方法。

2. 被代理的类仍然用的前面 spring aop 介绍的那个worker 类,只不过我们需要让这个类实现自接口,接口定义如下:

1

2

3

4

5

6

7

8

9

package kite.lab.spring.service;

 

/**

 * IWorker

 *

**/

public interface IWorker {

    String dowork();

}

3. 实际调用如下:

1

2

3

4

5

public static void main(String[] args) {

        JdkProxy jdkProxy = new JdkProxy();

        IWorker worker = (IWorker) jdkProxy.bind(new Worker());

        worker.dowork();

    }

原理说明: jdkProxy.bind 会生成一个实际的代理类,这个生成过程是利用的字节码生成技术,生成的代理类实现了IWorker 接口,我们调用这个代理类的 dowork 方法的时候,实际在代理类中是调用了 JdkProxy (也就是我们实现的这个代理拦截器)的 invoke 方法,接着执行我们实现的 invoke 方法,也就执行了我们加入的逻辑,从而实现了切面编程的需求。

我们把动态生成的代理类字节码文件反编译一下,也就明白了。由于代码较长,只摘出相关部分。

首先看到类的接口和继承关系:

public final class $Proxy0 extends Proxy implements IWorker 代理类被命名为 $Proxy0 ,继承了 Proxy ,并且实现了IWorker ,这是关键点。

找到 dowork 方法,代码如下:

1

2

3

4

5

6

7

8

9

public final String dowork() throws  {

        try {

            return (String)super.h.invoke(this, m3, (Object[])null);

        } catch (RuntimeException | Error var2) {

            throw var2;

        } catch (Throwable var3) {

            throw new UndeclaredThrowableException(var3);

        }

    }

super.h 就是我们实现的JdkProxy 这个类,可以看到调用了这个类的 invoke 方法,并且传入了参数 m3 ,再来看 m3 是什么

 m3 = Class.forName("kite.lab.spring.service.IWorker").getMethod("dowork", new Class0);   

看到了吧,m3 就是 dowork 方法,是不是流程就明确了。

但是,并不是所有的被代理的类(要被切面的类)都实现了某个接口,没有实现接口的情况下,JDK 动态代理就不行了,这时候就要用到 CGlib 字节码框架了。

CGLIB 动态代理

CGlib库使用了ASM这一个轻量但高性能的字节码操作框架来转化字节码,它可以在运行时基于一个类动态生成它的子类。厉害了吧,不管有没有接口,凡是类都可以被继承,拥有这样的特点,原则上来说,它可以对任何类进行代码拦截,从而达到切面编程的目的。

CGlib 不需要我们非常了解字节码文件(.class 文件)的格式,通过简单的 API 即可实现字节码操作。

基于这样的特点,CGlib 被广泛用于如 Spring AOP 等基于 代理模式的AOP框架中。

下面就基于 CGlib 实现一个简单的动态代理模式。

1. 创建拦截类实现 MethodInterceptor接口,并覆intercept 方法,在此方法中加入我们增加的逻辑,代码如下:

1

2

3

4

5

6

7

8

public class MyAopWithCGlib implements MethodInterceptor {

    @Override

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        System.out.println("嘿,你在干嘛?");

        methodProxy.invokeSuper(o, objects);

        System.out.println("是的,你说的没错。");

        return null;

    }

2. 被代理的类依然是上面的 Worker 类,并且不需要接口。

3. 客户端调用代理方法的代码如下:

1

2

3

4

5

6

7

8

9

public static void main(String[] args) {

        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib");

        MyAopWithCGlib aop = new MyAopWithCGlib();

        Enhancer enhancer = new Enhancer();

        enhancer.setSuperclass(Worker.class);

        enhancer.setCallback(aop);

        Worker worker = (Worker) enhancer.create();

        worker.dowork();

    }

代码第一行是要将动态生成的字节码文件持久化到磁盘,方便反编译观察。

利用 CGlib 的 Enhancer 对象,设置它的继承父类,设置回调类,即上面实现的 拦截类,然后用create 方法创造一个 Worker 类,实际这个类是 Worker 类的子类,然后调用dowork方法。执行结果如下:

  

可以看到我们织入的代码起作用了。

4. 上面功能比较简单,它会横向切入被代理类的所有方法中,下来我们稍微做的复杂一点。控制一下,让有些方法被织入代码,有些不被织入代码,模仿 Spring aop ,我们新增一个注解,用于注解哪些方法要被横向切入。注解如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

package kite.lab.spring.aop.AopWithCGlib;

 

import java.lang.annotation.*;

 

/**

 * CGLIB

 *

 * @author fengzheng

 */

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

@Documented

public @interface CGLIB {

    String value() default "";

}

5. 然后再 Worker 中增加一个方法,并应用上面的注解  

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

package kite.lab.spring.service;

 

import kite.lab.spring.aop.AopWithCGlib.CGLIB;

 

/**

 * Worker

 *

 * @author fengzheng

 */

public class Worker  {

    public String dowork(){

        System.out.println("生活向来不易,我正在工作!");

        return "";

    }

 

    @CGLIB(value = "cglib")

    public void dowork2(){

        System.out.println("生活如此艰难,我在奔命!");

    }

}

我们在 dowrok2 上应用了上面的注解

6. 在拦截方法中加入注解判断逻辑,如果加了上面的注解,就加入织入的代码逻辑,否则不加入,代码如下:  

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@Override

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        Annotation[] annotations = method.getDeclaredAnnotations();

        boolean isCglib = false;

        for(Annotation annotation: annotations){

            if (annotation.annotationType().getName().equals("kite.lab.spring.aop.AopWithCGlib.CGLIB")){

                isCglib = true;

            }

        }

        if(isCglib) {

            System.out.println("嘿,你在干嘛?");

            methodProxy.invokeSuper(o, objects);

            System.out.println("是的,你说的没错。");

        }

        return null;

    }

7. 调用方法如下:

1

2

3

4

5

6

7

8

9

10

11

public static void main(String[] args) {

        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib");

        MyAopWithCGlib aop = new MyAopWithCGlib();

        Enhancer enhancer = new Enhancer();

        enhancer.setSuperclass(Worker.class);

        enhancer.setCallback(aop);

        Worker worker = (Worker) enhancer.create();

        worker.dowork();

 

        worker.dowork2();

    }

执行结果应该为 dowork 不执行被织入的逻辑,dowork2 执行被织入的代码逻辑,执行结果如下:

  

另外说一下,CGlib 不支持 final 类, CGlib 的执行速度比较快,但是创建速度比较慢,所以如果两种动态代理都适用的场景下,有大量动态代理类创建的场景下,用 JDK 动态代理模式,否则可以用 CGlib 。

标准的 Spring MVC 框架,一般都是一个服务接口类对应一个实现类,所以根据Spring AOP 的判断逻辑,应该大部分情况下都是使用的 JDK 动态代理模式。当然也可以手动改成 CGlib 模式。









本文转自xsster51CTO博客,原文链接: http://blog.51cto.com/12945177/1950704,如需转载请自行联系原作者










版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
《Spring Boot 2.5开发实战》电子版地址
本书全面覆盖了 Spring Boot 2.5 新特性、自动化配置原理、如何开发 REST API、 实战 MySQL 数据库、Redis 高并发缓存、MongoDB 数据库、MQ 消息队列、安全机制、 性能监控、高级面试题等热门知识点。
0 0
「Spring」Boot Docker 认证指南(下)
「Spring」Boot Docker 认证指南(下)
0 0
「Spring」Boot Docker 认证指南(上)
「Spring」Boot Docker 认证指南(上)
0 0
Spring Boot demo系列 :Redis缓存
目录 Spring Boot demo系列 :Redis缓存 12.2.1 手动添加`@class` 12.2.2 将实体类设置为`open`
0 0
Spring Boot 操作 Redis 的各种实现
Spring Boot 操作 Redis 的各种实现
0 0
Spring Boot 的 HTTP 客户端框架
Spring Boot 的 HTTP 客户端框架
0 0
Spring Boot 项目 启动 端口经常被占用 彻底解决方案
Spring Boot 项目 启动 端口经常被占用 彻底解决方案
0 0
每日一面 - Spring Boot 中的 ApplicationContext 的分层是什么意思?
每日一面 - Spring Boot 中的 ApplicationContext 的分层是什么意思?
0 0
Spring Boot 注解收集 - @SpringBootApplication
Spring Boot 注解收集 - @SpringBootApplication
0 0
+关注
文章
问答
文章排行榜
最热
最新
相关电子书
更多
微服务架构模式与原理Spring Cloud开发实战
立即下载
Java Spring Boot开发实战系列课程【第7讲】:Spring Boot 2.0安全机制与MVC身份验证实战(Java面试题)
立即下载
Spring框架入门
立即下载