面试问到AOP就该这样回答

旧时模样 提交于 2020-07-28 17:43:06

前言

  相信各位小伙伴在准备面试的时候,AOP都是无法绕过的一个点,经常能看到动态代理、JDK动态代理、CGLIB动态代理这样的字眼。其实动态代理是代理模式的一种。代理模式有静态代理、强制代理、动态代理。所以在认识AOP之前需要了解代理模式。

代理模式定义

  代理模式(Proxy Pattern):为其他对象提供一种代理以控制这个对象的访问。

  • Subject抽象主题角色,也叫做抽象主题类。可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求。
  • RealSubject具体主题角色,也叫做被委托角色,被代理角色,是业务逻辑的具体执行者。
  • Proxy代理主题角色,也叫做委托类、代理类。他负责对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现,并且在真实主题角色处理完毕前后做到预处理和善后工作。

代理模式的优点

  • 职责清晰
  • 高扩展性
  • 智能化

UML

image

我的理解

  跳板机不管是对于运维老哥还是对于我们来讲,都是日常的工作中不可或缺的一个工具。为了保证生产服务器的安全,我们是无法通过xshell等工具直接进行连接的。如果需要操作生产的服务器,则需要通过跳板机。并且跳板机还能记录我们的操作,用来做安全审计。防止出现,某位老哥一气之下反手就是个sudo rm -rf /*直接凉凉。

  我们回过头看看代理模式的定义:为其他对象提供一种代理以控制这个对象的访问。实际上跳板机就是生产服务器的一个代理Proxy,为了实现控制生产服务器的访问权限。你需要通过跳板机来操作生产服务器。

  • 为其他对象提供一种代理以控制这个对象的访问
  • 给你提供一个跳板机来访问生产服务器,目的是来控制生产服务器的访问

  Proxy的职责:Proxy是对真实角色的应用,把所有抽象主题类定义的方法限制委托给真实主题角色实现。并且在真实主题角色处理完毕前后做到预处理善后工作。你通过操作跳板机。跳板机将你输入的命令在生产服务器上进行执行。并且能记录下执行的命令和执行的结果。

代码演示

Server

public interface Server {

    /**
     * 执行
     * @param command 命令
     */
    void exec(String command);

}

ProdServer

public class ProdServer implements Server {

    @Override
    public void exec(String command){
        System.out.println(command + ":执行成功");
    }

}

JumpServer

public class JumpServer {

    private Server server;

    public JumpServer(Server server) {
        this.server = server;
    }

    public void exec(String command){
        System.out.println("xxx在:" + LocalDateTime.now() + " 执行了:" + command);
        server.exec(command);
        System.out.println("xxx在:" + LocalDateTime.now() + " 执行完了:"+ command + " 结果是XXX");
    }

}

Client

public class Client {

    public static void main(String[] args) {
        JumpServer jumpServer = new JumpServer();
        jumpServer.exec("pwd");
    }
}

运行结果

xxx在:2020-04-04T16:43:19.277 执行了:pwd
pwd:执行成功
xxx在:2020-04-04T16:43:19.278 执行完了:pwd 结果是XXX

  通过上面的代码,简单的实现了代理模式。在网络上代理服务器设置分为透明代理普通代理

  • 透明代理就是用户不用设置代理服务器地址,就可以直接访问.也就是说代理服务器对用户来说是透明的,不用知道它存在的。
  • 普通代理则是需要用户自己设置代理服务器的IP地址,用户必须知道代理的存在。

  当运维老哥给了一台服务器的账号和密码,你成功登录,并完成了相应的操作。你以为给你的是生产的服务器。实际就可能是个跳板机。为了安全起见,是不可能将实际服务器的IP让你知道的。很显然跳板机对你来讲就是透明的。

所以我们调整一下代码,将其变成普通代理

JumpServer

public class JumpServer {

    private Server server;

    public JumpServer(Server server) {
        this.server = server;
    }

    public void exec(String command){
        server.exec(command);
    }

}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        JumpServer jumpServer = new JumpServer(prodServer);
        jumpServer.exec("pwd");
    }
    
}

执行结果

xxx在:2020-04-04T16:52:23.282 执行了:pwd
pwd:执行成功
xxx在:2020-04-04T16:52:23.283 执行完了:pwd 结果是XXX

强制代理

  对于现实情况,我们可以通过不开放公网访问的权限来实现,强制使用代理操作服务器。我们可以用代码简单的模拟下。

JumpServer

public class ProdServer implements Server {

    private JumpServer jumpServer;

    @Override
    public void exec(String command){
        hasProxy();
        System.out.println(command + ":执行成功");
    }

    private void hasProxy(){
        if(jumpServer == null){
            throw new RuntimeException("请使用跳板机!");
        }
    }

    public JumpServer setJumpServer() {
        this.jumpServer = new JumpServer(this);
        return this.jumpServer;
    }
}

Client未设置跳板机

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        prodServer.exec("pwd");
    }

}

不设置跳板机运行结果

Exception in thread "main" java.lang.RuntimeException: 请使用跳板机!
	at proxy.pattern.tmp.ProdServer.hasProxy(ProdServer.java:21)
	at proxy.pattern.tmp.ProdServer.exec(ProdServer.java:15)
	at proxy.pattern.tmp.Client.main(Client.java:13)

Client设置跳板机

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();
        prodServer.setJumpServer().exec("pwd");
    }

}

运行结果

xxx在:2020-04-05T15:01:10.944 执行了:pwd
pwd:执行成功
xxx在:2020-04-05T15:01:10.944 执行完了:pwd 结果是XXX

这个时候需要访问生产服务器,就需要先设置跳板机了,才能进行操作。

动态代理

  对静态代理来说,我们需要手动生成代理类。但是如果需要代理的类太多了,那这个肯定是不可取的。所以我们可以使用JDK动态代理来帮我们完成工作。

JDK动态代理

  JDK动态代理利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。

JumpServerInvocationHandler

public class JumpServerInvocationHandler implements InvocationHandler {

    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target,args);
    }
}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();

        JumpServerInvocationHandler handler = new JumpServerInvocationHandler(prodServer);
        ClassLoader classLoader = prodServer.getClass().getClassLoader();
        Server proxy = (Server) Proxy.newProxyInstance(classLoader, new Class[]{Server.class}, handler);

        proxy.exec("pwd");
    }

}

测试结果

pwd:执行成功

增加前置通知和后置通知

Advice

public interface Advice {

    void exec();

}

BeforeAdvice

public class BeforeAdvice implements Advice {
    @Override
    public void exec() {
        System.out.println("执行前置通知");
    }
}

AfterAdvice

public class AfterAdvice implements Advice {
    @Override
    public void exec() {
        System.out.println("执行后置通知");
    }
}

JumpServerInvocationHandler

public class JumpServerInvocationHandler implements InvocationHandler {

    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 执行前置通知
        new BeforeAdvice().exec();

        Object ret = method.invoke(target, args);

        // 执行后置通知
        new AfterAdvice().exec();
        return ret;

    }
}

Client

public class Client {

    public static void main(String[] args) {
        ProdServer prodServer = new ProdServer();

        JumpServerInvocationHandler handler = new JumpServerInvocationHandler(prodServer);
        ClassLoader classLoader = prodServer.getClass().getClassLoader();
        Server proxy = (Server) Proxy.newProxyInstance(classLoader, new Class[]{Server.class}, handler);

        proxy.exec("pwd");
    }

}

测试结果

执行前置通知
pwd:执行成功
执行后置通知

看到这里有没有点AOP的感觉了。

CGLIB动态代理

  CGLIB动态代理利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理,需要使用MethodInterceptor接口来进行实现。

JumpServerMethodInterceptor

public class JumpServerMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        new BeforeAdvice().exec();

        Object ret = methodProxy.invokeSuper(obj, args);

        new AfterAdvice().exec();

        return ret;
    }
}

注意的点:是使用invokeSuper()而不是invoke()

Client

public class Client {

    public static void main(String[] args) {

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ProdServer.class);
        enhancer.setCallback(new JumpServerMethodInterceptor());

        ProdServer proxy = (ProdServer) enhancer.create();

        proxy.exec("pwd");
    }

}

测试结果

执行前置通知
pwd:执行成功
执行后置通知

小结

  通过上面的一大堆的篇幅介绍代理模式就是为了能更加清晰的理解代理模式非常重要的一个应用场景AOP。

AOP

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

AOP相关术语

连接点(Joinpoint)
  一个类或一段程序代码拥有一些具有便捷性质的特定点。比如说类的某个方法调用前/调用后、方法抛出异常后。

切点(Pointcut)
  在AOP中用于定位连接点。如果将连接点作为数据库中的记录,切点即相当于查询条件。切点和连接点不是一对一的关系,一个切点可以匹配多个连接点。

增强(Advice)
  增强是织入目标类连接点上的一段程序代码。

在Spring中增强除了用于描述一段程序代码外,还可以拥有另一个和连接点相关的信息,这便是执行点的方位。通过执行点方位信息和切点信息,就可以找到特定的连接。正是因为增强即包含添加到连接点上的逻辑,包含定位连接点的方位信息,所以Spring提供的增强接口都是带方位名的。如BeforeAdviceAfterAdviceAroundAdvice

目标对象(Target)
  需要织入增强逻辑的目标类。比如说在使用AOP的时候配置的请求日志输出,目标对象就是对应的controller.

引介(Introduction)
  引介是一种特殊的增强,它为类添加一些属性和方法。这样,即使一个业务类没有原本没有实现某个接口,通过AOP可以动态的为某些业务类添加接口和实现方法,让业务类成为这个接口的实现类。

织入(Weaving)
  织入是将增强添加到目标类的具体连接点上的过程。AOP有3种织入方式:

  • 编译期织入,要求使用特殊的Java编译器。
  • 类装载期织入,要求使用特殊的类装载器。
  • 动态代理织入,在运行期,为目标类添加增强生成子类的方式。
    毫无疑问Spring是采用动态代理织入。

代理(Proxy)
  一个类被AOP织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。

切面(Proxy)
  切面由切点和增强(引介)组成,它即包括很切逻辑的定义,也包括连接点的定义。SpringAOP就是负责实施切面的框架,他将切面所定义的横切逻辑织入切面所指定的连接点中。

我们可以这样回答

  AOP翻译过来是:面向切面编程是一种设计思想。主要由连接点,切点,增强、切面组成。AOP依托于代理模式进行实现,所以AOP拥有代理模式的特性。可以在不改变原有类的情况下,能动态的添加某些功能。所以说比较适合来实现,打印请求日志,权限校验,等功能。针对不同的场景,AOP可以选择使用JDK动态代理或CGLIB代理来实现。由于CGLIB创建出来的代理类运行速度快于JDK动态代理,但是创建的过程太慢,所以可以将其创建出来的代理类交由IOC容器进行管理,免去了重复创建代理不必要的性能开销,来提高运行速度。

主要针对,AOP是什么、由什么组成、适合用场景、如何实现,不同实现的区别这些点去总结回答。

切点和切面的区别?

  切面包含切点,切点和增强组成了切面。SpringAOP通过切面将逻辑特定织入切面所指定的连接点中。

CGLIB 和 JDK 动态代理的区别

  jdk动态代理只能对实现了接口的类生成代理,而不能针对类。cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法。简而言之就是JDK动态代理基于接口实现,cglib基于类继承。因为是继承,所以该类或方法不能使用final进行修饰。

  在性能上,有研究表明cglib所创建的代理对象的性能要比jdk创建的高10倍,但是呢cglib创建代理对象时所花费的时间要比jdk8倍。所以单例的代理对象或者具有实例池的代理,无效频繁的创建对象,比较适合采用cglib,反正适合采用jdk

参考书籍

  • 设计模式之禅道第二版
  • 精通Spring 4.x企业应用开发实战

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!