【Spring aop】从静态代理、动态代理聊到aop

静态代理

概念

  • 在其他对象访问真实对象的时候,由代理对象来处理对真实对象的访问控制

特点

  1. ⭐️真实对象和代理对象都实现了同一个接口
  2. 真实对象负责实现接口的所有方法细节
  3. 代理对象通过组合的方式对真实对象进行控制,也就是代理对象有一个属性,类型是对真实对象的引用
  4. 真实对象的实例化由代理对象来控制
  5. 代理对象实现接口的方法并不需要真正完成接口的业务逻辑,而是调用真实对象实现了的这些方法,只是代理对象可以在调用这些方法的前后进行一些控制处理

举例

  1. 有一个图片接口,接口中规定了一个display()方法,显示图片
  2. 真实对象实现这个接口,只用负责把图片显示出来
  3. 代理对象实现这个方法,是通过调用真实对象的display()方法,为了体现代理模式对真实对象的控制这一特点,让代理对象对调用display()这个方法之前进行一些检查处理,即检查图片是否违规,如果图片违规则不进行显示
  4. 外部有一个客户端,调用代理对象的display()方法查看图片内容
  • 案例中几个对象的关系
几个对象的关系
  • Image 接口
package com.plasticine.service;

public interface Image {

    void display();

}
  • RealImage 真实对象
package com.plasticine.service;

import java.util.concurrent.TimeUnit;

public class RealImage implements Image {

    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk();
    }

    @Override
    public void display() {
        System.out.println("这是一张图片: " + this.fileName);
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j < 20; j++) {
                System.out.print("*");
            }
            System.out.println();
        }
    }

    private void loadFromDisk() {
        System.out.println("RealImage is loading from disk...");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("RealImage has loaded!");
    }
}
  • ProxyImage 代理对象
package com.plasticine.service;

public class ProxyImage implements Image {

    private String fileName;
    private RealImage realImage;
    private Boolean isAllowed;
    private String errMsg;

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    private void checkImage() {
        // 模拟图片出现违规的情况,生成一个随机数,如果是 1~5 则是正常显示,否则就是违规图片
        int num = (int) Math.floor((Math.random() * 10) % 10 + 1);
        if (num >= 1 && num <= 5) {
            this.isAllowed = true;
            this.errMsg = "图片检查通过,正常显示";
        } else {
            this.isAllowed = false;
            this.errMsg = "图片内容违规,不允许显示!";
        }
    }

    @Override
    public void display() {
        if (realImage == null) {
            this.realImage = new RealImage(this.fileName);
        }
        System.out.println("=========代理对象在调用真实对象之前做了一些事情=========");

        // 还没检查过图片时,进行检查,之后就不需要再检查了
        if (this.isAllowed == null) checkImage();

        System.out.println(this.errMsg);
        if (this.isAllowed) realImage.display();

        System.out.println("=========代理对象在调用真实对象之后做了一些事情=========");
        System.out.println("清空图片下载缓存...");
    }
}
  • ProxyPatternDemo 客户端
public class ProxyPatternDemo {

    public static void main(String[] args) {
        Image image = new ProxyImage("test.png");

        image.display();

        System.out.println("");

        image.display();
    }

}
  • 运行示例
RealImage is loading from disk...
RealImage has loaded!
=========代理对象在调用真实对象之前做了一些事情=========
图片检查通过,正常显示
这是一张图片: test.png
********************
********************
********************
********************
********************
********************
********************
********************
********************
********************
=========代理对象在调用真实对象之后做了一些事情=========
清空图片下载缓存...

=========代理对象在调用真实对象之前做了一些事情=========
图片检查通过,正常显示
这是一张图片: test.png
********************
********************
********************
********************
********************
********************
********************
********************
********************
********************
=========代理对象在调用真实对象之后做了一些事情=========
清空图片下载缓存...
RealImage is loading from disk...
RealImage has loaded!
=========代理对象在调用真实对象之前做了一些事情=========
图片内容违规,不允许显示!
=========代理对象在调用真实对象之后做了一些事情=========
清空图片下载缓存...

=========代理对象在调用真实对象之前做了一些事情=========
图片内容违规,不允许显示!
=========代理对象在调用真实对象之后做了一些事情=========
清空图片下载缓存...
  • 可以看到,客户端调用了两次display()方法,但实际上图片加载的过程只出现了一次,也就是真实对象实例化的过程只有一次,之后再调用display()方法时并不会对真实对象进行实例化,当然这个特点是自己控制的,也可以写成每次调用都去实例化真实对象,这个由编程人员自己决定,这也体现了代理对象对真实对象的控制

这种设计模式有什么好处?

  • 很明显能够看到的是,真实对象只用负责处理接口规定好的业务逻辑,而代理对象则负责对这些业务逻辑的其他方面的控制,比如网站上传图片,真实对象只用负责实现上传图片的接口,能把图片上传上去即可,不用管调用者是否是已登录的状态,这个检查调用者是否已登录可以交给代理对象去做,让代理对象对上传图片这一功能做出一些控制。
  • 类似的,可以将一些和业务逻辑关系不大的模块用代理模式的设计思想分离开来,实现解耦,比如记录日志、权限控制、事务、拦截器等

为什么叫"静态"代理?

  • 因为一个真实对象需要有一个代理对象对应实现相应的方法,如果能够有一个对象可以处理所有的真实对象的控制的话,那就是动态的了,相对应的,这种一个真实对象需要一个对应的代理对象来管理的方式,就是静态的
  • 一个代理对象只能服务于一个真实对象还是可以服务于所有的真实对象,这就是静态和动态的最大区别,也是最本质的区别

静态代理模式的缺点

  1. 代理对象需要实现和真实对象一样的接口的所有方法,这就导致了会有大量的代码重复,如果接口增加了一个方法,那么不仅仅真实对象需要实现这个方法,所有的代理对象也需要为这个新增的方法编写实现的代码
  2. 一个真实对象要有一个对应的代理对象,如果项目规模很大,有许许多多的真实对象,则意味着都要有相应的代理对象,这就导致项目的类的数目翻倍,维护成本高

动态代理

概念

  • 静态代理的一个代理只能代理一种类型,而且是在编译时就已经确定被代理的对象。而动态代理是在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象
  • 在Java中要想实现动态代理机制,需要java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy 类的支持

特点

  • 使用反射机制来动态加载类,这样就可以做到运行时获得类的方法去进行调用,这与静态代理是截然不同的,静态代理是预先写好代理类,并实现接口的方法,而动态代理是编写一个处理某一逻辑的通用处理器handler。

  • 比如有一个负责日志记录的handler,那么这个handler可以通过反射机制去加载被代理对象的方法,然后在调用被代理类的某个方法之前就进行日志记录,在方法调用完毕后也可以进行日志记录。

  • 与静态代理最大的不同在于静态代理需要给每一个真实对象写相应的接口方法,比如上面的显示图片的例子中,如果我要实现一个记录日志的功能的话,就需要给上面的RealImage这个类单独写一个代理类,然后下次有一个RealImage2这个类的话,它实现的接口多了一个下载图片的功能,然后我的日志想要记录相应的方法调用记录,就得单独再去写一个对应的代理类,并在下载的方法实现中加上对下载方法调用前后的记录,比如下面这样

    • 接口
    package com.plasticine.service;
    
    public interface Image {
    
        void display();
    
    }
    
    • 真实对象
    package com.plasticine.service;
    
    import java.util.concurrent.TimeUnit;
    
    public class RealImage implements Image {
    
        private String fileName;
    
        public RealImage(String fileName) {
            this.fileName = fileName;
            loadFromDisk();
        }
    
        @Override
        public void display() {
            ...
        }
    
        @Override
        public void download() {
            ...
        }
        
        private void loadFromDisk() {
            ...
        }
    }
    
    • 代理对象
    package com.plasticine.service;
    
    public class ProxyImage implements Image {
    
        private String fileName;
        private RealImage realImage;
        private Boolean isAllowed;
        private String errMsg;
    
        public ProxyImage(String fileName) {
            this.fileName = fileName;
        }
    
        @Override
        public void display() {
            logger.info("调用了 display() 方法");
            if (realImage != null) this.realImage = new realImage(this.fileName);
            realImage.display();
            logger.info("调用 display() 方法结束");
        }
        
        @Override
        public void download() {
            logger.info("调用了 download() 方法");
            if (realImage != null) this.realImage = new realImage(this.fileName);
            realImage.display();
            logger.info("调用 download() 方法结束");
        }
    }
    

    很明显,这里日志记录其实只用在调用方法前后记录相应的方法名称即可,不管调用的是什么方法都行,如果是用动态代理的话,就可以用反射来调用方法,并在调用方法前后记录日志,而静态代理就得像这样,每增加了一个方法都得去修改代理类的代码,加上一些很机械的代码

实现动态代理

首先介绍一下java.lang.reflect.InvocationHandler接口和java.lang.reflect.Proxy

  • java.lang.reflect.InvocationHandler接口

    • 所有的动态代理类都需要实现该接口,该接口中只有一个方法需要实现,就是invoke()方法,签名如下

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

      该方法是外部客户端调用被代理对象的方法之前会调用的,然后由invoke()方法去调用外部客户端调用的被代理对象的方法,这就意味着我们可以在该方法里面去写一些想让动态代理类做的事情,实际上可以理解为对调用真实对象方法之前,动态代理对象会进行拦截处理,这里就以记录日志为例

  • java.lang.reflect.Proxy

    • 这个类提供了几个静态方法
    java.lang.reflect.Proxy中的静态方法
  • 这里要用到newProxyInstance()这个方法,解释一下这里的三个参数

    • loader --> 这里要传入用于生成被代理对象的类加载器,可以用组合的方式在handler中保存一个被代理对象的引用,然后使用反射获取被代理对象的类加载器,即

      targetObj.getClass().getClassLoader()
      
    • interfaces --> 这个是用于给外部调用的被代理对象的方法,同样可以通过反射获取被代理对象的所有方法,然后传入,由于被代理对象实现了某一业务接口,所以要传入被代理对象的接口,即

      targetObj.getClass().getInterface()
      
    • h --> 表明在拦截方法之前,要调用哪个handler的invoke()方法去处理拦截的逻辑请求,一般用this,即表明使用当前的handler去拦截

  • 动态代理代码

package com.plasticine.handler;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LogHandler implements InvocationHandler {

    /**
     * 被代理的真实对象
     */
    private Object targetObj;

    public LogHandler(Object targetObj) {
        this.targetObj = targetObj;
    }

    /**
     * 用于获取代理对象 --> 使用 Proxy 的静态方法 newProxyInstance() 来创建代理对象
     *
     * @return 返回与真实对象相对应的代理对象
     */
    public Object getProxyObj() {
        return Proxy.newProxyInstance(targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(), this);
    }

    /**
     * 处理拦截逻辑 --> 这里的逻辑是调用被代理对象之前和之后打印调用的被代理对象的方法以及传入的参数
     *
     * @param proxy  代理对象
     * @param method 调用的代理对象的方法
     * @param args   对应方法的参数
     * @return 返回 method 被调用后的返回结果
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        // 日志记录调用的方法名和参数
        System.out.print("调用了" + method.getName());
        if (args != null) {
            System.out.print(" 参数为: ");
            for (int i = 0; i < args.length; i++) {
                System.out.print(args[i]);
                if (i + 1 != args.length) System.out.print(" ");
            }
        }
        System.out.println();

        // 调用代理对象的方法
        Object res = method.invoke(targetObj, args);

        // 日志记录调用的结果
        if (res != null)
            System.out.println("方法返回结果为: " + res.toString());
        else
            System.out.println("方法没有返回结果");

        return res;
    }

}
  • 客户端调用
public class TestCase {

    public static void main(String[] args) {

        // 真实对象
        RealImage realImageObj = new RealImage("test.png");

        // 使用日志 handler 获取代理对象
        LogHandler realImageLogHandler = new LogHandler(realImageObj);
        Image realImageProxy = (Image) realImageLogHandler.getProxyObj();

        // 通过代理对象调用真实对象的方法
        proxyObj.display();
    }

}

这里需要注意,获取到的代理对象应当强转为真实对象实现的接口引用类型,而不是强转为真实对象的引用类型,否则会报com.sun.proxy.$Proxy0 cannot be cast to com.plasticine.pojo.RealImage这个错误,这是因为动态代理是针对接口而言的,如果用实现了接口的实现类的引用类型去接收Proxy.newProxyInstance()的返回值,由于该返回值是对接口的引用,把一个对接口的引用赋值给一个实现类的引用,这肯定是不行的(据我所知最多也就只有父类引用指向子类对象,或者接口引用指向实现类对象这样的用法)

  • 执行结果
RealImage is loading from disk...
RealImage has loaded!
调用了display
这是一张图片: test.png
********************
********************
********************
********************
********************
********************
********************
********************
********************
********************
方法没有返回结果

可以看到日志成功记录了,此时如果我们想要记录别的类的调用方法日志记录,需要改动的仅仅只是实例化LogHandler时传给构造方法的真实对象罢了,其他的不需要任何变动!下面以一个简单的记录用户service层操作日志为例

  • User pojo 实体类

    package com.plasticine.service;
    
    public class User {
    
        private String username;
        private String password;
    
        public User(String username, String password) {
            this.username = username;
            this.password = password;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
    }
    
  • UserService 接口

    package com.plasticine.service;
    
    import com.plasticine.pojo.User;
    
    import java.util.List;
    
    public interface UserDao {
    
        Integer addUser(User user);
    
        Integer deleteUser(String username);
    
        Integer updateUser(User user);
    
        User retrieveUser(String username);
    
        List listUser();
    }
    
  • UserServiceImple 实现类

    package com.plasticine.service;
    
    import com.plasticine.pojo.User;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class UserDaoImpl implements UserDao {
    
    
        @Override
        public Integer addUser(User user) {
            System.out.println("新增一个用户");
            return 1;
        }
    
        @Override
        public Integer deleteUser(String username) {
            System.out.println("删除一个用户");
            return 1;
        }
    
        @Override
        public Integer updateUser(User user) {
            System.out.println("更新一个用户");
            return 1;
        }
    
        @Override
        public User retrieveUser(String username) {
            System.out.println("查询一个用户");
            return new User("plasticine", "abc123");
        }
    
        @Override
        public List listUser() {
            System.out.println("获取用户列表");
    
            List userList = new ArrayList<>();
            userList.add(new User("plasticine", "abc123"));
            userList.add(new User("法外狂徒张三", "def456"));
    
            return userList;
        }
    }
    
  • Controller 层调用

    public class TestCase {
    
        public static void main(String[] args) {
    
            // 真实对象 --> RealImage
            RealImage realImageObj = new RealImage("test.png");
    
            // 使用日志 handler 获取代理对象
            LogHandler realImageLogHandler = new LogHandler(realImageObj);
            Image realImageProxy = (Image) realImageLogHandler.getProxyObj();
    
            // 通过代理对象调用真实对象的方法
            // realImageProxy.display();
    
            // ========================================================================
            
            // 真实对象 --> UserServiceImpl
            UserServiceImpl userService = new UserServiceImpl();
    
            // 使用日志 handler 获取代理对象
            LogHandler userServiceLogHandler = new LogHandler(userService);
            UserService userServiceProxy = (UserService) userServiceLogHandler.getProxyObj();
    
            // 通过代理对象调用真实对象的方法
            User user = new User("plasticine", "abc123");
            userServiceProxy.addUser(user);
            userServiceProxy.deleteUser("plasticine");
            userServiceProxy.updateUser(user);
            userServiceProxy.retrieveUser("plasticine");
            userServiceProxy.listUser();
        }
    
    }
    
  • 执行结果

    调用了addUser 参数为: User{username='plasticine', password='abc123'}
    新增一个用户
    方法返回结果为: 1
    调用了deleteUser 参数为: plasticine
    删除一个用户
    方法返回结果为: 1
    调用了updateUser 参数为: User{username='plasticine', password='abc123'}
    更新一个用户
    方法返回结果为: 1
    调用了retrieveUser 参数为: plasticine
    查询一个用户
    方法返回结果为: User{username='plasticine', password='abc123'}
    调用了listUser
    获取用户列表
    方法返回结果为: [User{username='plasticine', password='abc123'}, User{username='法外狂徒张三', password='def456'}]
    
  • 可以看到,我改变的仅仅是传给日志处理器的真实对象,但是它依然能够正常地记录日志,这就是动态代理的好处,让类之间解耦合!

  • 即使以后想要修改日志打印的格式,也只需要去LogHandler中修改即可,不需要去改动别的类,如果不使用动态代理,而是直接在类的方法调用前后去打印日志输出,那维护成本极高,每个方法都要写上重复的日志记录,不同的仅仅是日志中的方法名和参数值,而且想要修改日志的格式的时候还得找出每个方法去逐一修改,效率极低,因此用好动态代理可以让开发业务逻辑的过程中事半功倍!

Spring aop使用

  • 首先要引入 aspectj maven依赖


    org.aspectj
    aspectjweaver
    1.9.8.M1

方式1:使用原生 Spring API 接口

  • 调用方法前要做的事,实现MethodBeforeAdvice接口
public class beforeLog implements MethodBeforeAdvice {

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.print("调用了" + method.getName());
        if (args != null) {
            System.out.print(" 参数为: ");
            for (int i = 0; i < args.length; i++) {
                System.out.print(args[i]);
                if (i + 1 != args.length) System.out.print(" ");
            }
        }
        System.out.println();
    }

}
  • 调用方法后要做的事,实现AfterReturningAdvice接口
public class afterLog implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        // 日志记录调用的结果
        if (returnValue != null)
            System.out.println("方法返回结果为: " + returnValue.toString());
        else
            System.out.println("方法没有返回结果");
    }

}
  • 配置aop







    
    
    
    
    

pointcut表达式的规则

  • 第一个参数是指方法的返回类型,用*表示所有类型
  • 第二个参数是要切入的方法,这里com.plasticine.service.UserServiceImpl.*(..)),表示要切入到UserServiceImpl这个类中的所有方法,(..)表示方法的参数

测试

public class TestCase {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = context.getBean("userService", UserService.class);

        User user = new User("plasticine", "abc123");
        userService.addUser(user);
        userService.deleteUser("plasticine");
        userService.updateUser(user);
        userService.retrieveUser("plasticine");
        userService.listUser();
    }

}
调用了addUser 参数为: User{username='plasticine', password='abc123'}
新增一个用户
方法返回结果为: 1
调用了deleteUser 参数为: plasticine
删除一个用户
方法返回结果为: 1
调用了updateUser 参数为: User{username='plasticine', password='abc123'}
更新一个用户
方法返回结果为: 1
调用了retrieveUser 参数为: plasticine
查询一个用户
方法返回结果为: User{username='plasticine', password='abc123'}
调用了listUser 参数为: 
获取用户列表
方法返回结果为: [User{username='plasticine', password='abc123'}, User{username='法外狂徒张三', password='def456'}]

和手写实现动态代理的效果一样

方式2:使用自定义的类

  • 前面方式1中的日志类有两个,分别实现了 Spring 提供的MethodBeforeAdviceAfterReturningAdvice接口,现在有另一种方式实现日志,就是用自己定义一个类的方式,但是这种方式比较局限,不能获取到被拦截方法的信息,比如方法名称,方法参数,方法的返回值等信息
  • 简易日志器 simpleLog
public class simpleLog {

    public void before() {
        System.out.println("-------------方法执行之前-------------");
    }

    public void after() {
        System.out.println("-------------方法执行之后-------------");
    }

}
  • 可以看到,这个类没有实现任何接口,仅仅是我自定义了两个简单的方法,现在尝试把他注册到aop中




    
    
        
        
        
        
        
    

  • 运行结果
-------------方法执行之前-------------
新增一个用户
-------------方法执行之后-------------
-------------方法执行之前-------------
删除一个用户
-------------方法执行之后-------------
-------------方法执行之前-------------
更新一个用户
-------------方法执行之后-------------
-------------方法执行之前-------------
查询一个用户
-------------方法执行之后-------------
-------------方法执行之前-------------
获取用户列表
-------------方法执行之后-------------

可以看到简易的日志记录也生效了

方式3:使用注解实现

  • 另外写一个日志类,并使用注解来替代方式2中在xml的声明
@Aspect
public class annotationSimpleLog {

    @Before("execution(* com.plasticine.service.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("-------------方法执行之前-------------");
    }

    @After("execution(* com.plasticine.service.UserServiceImpl.*(..))")
    public void after() {
        System.out.println("-------------方法执行之后-------------");
    }

    @Around("execution(* com.plasticine.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("-------------环绕前-------------");

        // 执行 joinPoint
        Object proceed = joinPoint.proceed();

        System.out.println("-------------环绕后-------------");
    }

}
  • @Aspect注解等价于xml中,即把这个日志类声明为一个切面
  • @Before@After注解等价于描述切入点以及使用哪个方法去处理切入点,也就是把描述切入点和通知两个配置合并成了一个注解来实现
  • @Around注解也是描述切入点和通知,只是它可以接收一个ProceedingJoinPoint类型的参数,这个参数允许我们手动调用proceed方法去执行被代理对象的方法,因此可以在调用proceed方法的前后来执行想要的处理逻辑,这也正是around这个注解的名字所包含的意思,可以环绕在一个方法的前后去做一些不同的事情

在xml中注册这个日志类bean,并开启注解支持






  • 运行结果
-------------环绕前-------------
-------------方法执行之前-------------
新增一个用户
-------------方法执行之后-------------
-------------环绕后-------------
-------------环绕前-------------
-------------方法执行之前-------------
删除一个用户
-------------方法执行之后-------------
-------------环绕后-------------
-------------环绕前-------------
-------------方法执行之前-------------
更新一个用户
-------------方法执行之后-------------
-------------环绕后-------------
-------------环绕前-------------
-------------方法执行之前-------------
查询一个用户
-------------方法执行之后-------------
-------------环绕后-------------
-------------环绕前-------------
-------------方法执行之前-------------
获取用户列表
-------------方法执行之后-------------
-------------环绕后-------------
  • 这个配置有一个选项可以选,proxy-target-class,是一个布尔类型的选项,默认值为false
    • true --> 底层使用cglib实现动态代理
    • false --> 底层使用 jdk 实现动态代理,默认就是false,也就是使用jdk实现

你可能感兴趣的:(【Spring aop】从静态代理、动态代理聊到aop)