面试问到AOP就该这样回答

前言

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

代理模式定义

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

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

代理模式的优势

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

UML

image

个人理解

  跳板机不论是对于运维老哥仍是对于咱们来说,都是平常的工做中不可或缺的一个工具。为了保证生产服务器的安全,咱们是没法经过xshell等工具直接进行链接的。若是须要操做生产的服务器,则须要经过跳板机。而且跳板机还能记录咱们的操做,用来作安全审计。防止出现,某位老哥一气之下反手就是个sudo rm -rf /*直接凉凉。shell

  咱们回过头看看代理模式的定义:为其余对象提供一种代理以控制这个对象的访问。实际上跳板机就是生产服务器的一个代理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企业应用开发实战

结尾

  若是以为对你有帮助,能够多多评论,多多点赞哦,也能够到个人主页看看,说不定有你喜欢的文章,也能够随手点个关注哦,谢谢。

相关文章
相关标签/搜索