【面试】代理&SPI相关-这一篇全了解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/w372426096/article/details/84617221

什么是Java中的代理模式(静态代理)

解:

所谓静态代理,就是代理类是由程序员自己编写的,在编译期就确定好了的。来看下下面的例子:

public interface HelloSerivice {
      public void say();
}

public class HelloSeriviceImpl implements HelloSerivice{
        @Override
        public void say() {
            System.out.println("hello world");
        }
} 

上面的代码比较简单,定义了一个接口和其实现类。这就是代理模式中的目标对象和目标对象的接口。接下类定义代理对象。
 

public class HelloSeriviceProxy implements HelloSerivice{
    private HelloSerivice target;
    public HelloSeriviceProxy(HelloSerivice target) {
         this.target = target;
    }

    @Override
    public void say() {
       System.out.println("记录日志");
       target.say();
       System.out.println("清理数据");
    }
} 

上面就是一个代理类,他也实现了目标对象的接口,并且扩展了say方法。下面是一个测试类:
 

public class Main {
    @Test
    public void testProxy(){
        //目标对象
        HelloSerivice target = new HelloSeriviceImpl();
        //代理对象
        HelloSeriviceProxy proxy = new HelloSeriviceProxy(target);
        proxy.say();
    }
}
// 记录日志
// hello world
// 清理数据 

这就是一个简单的静态的代理模式的实现。代理模式中的所有角色(代理对象、目标对象、目标对象的接口)等都是在编译期就确定好的。

静态代理的用途 控制真实对象的访问权限 通过代理对象控制对真实对象的使用权限。

避免创建大对象 通过使用一个代理小对象来代表一个真实的大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。 增强真实对象的功能 这个比较简单,通过代理可以在调用真实对象的方法的前后增加额外功能。

什么是动态代理,和反射有什么关系。

解:

前面介绍了静态代理,虽然静态代理模式很好用,但是静态代理还是存在一些局限性的,比如使用静态代理模式需要程序员手写很多代码,这个过程是比较浪费时间和精力的。一旦需要代理的类中方法比较多,或者需要同时代理多个对象的时候,这无疑会增加很大的复杂度。

扫描二维码关注公众号,回复: 4286343 查看本文章

有没有一种方法,可以不需要程序员自己手写代理类呢。

这就是动态代理啦。 动态代理中的代理类并不要求在编译期就确定,而是可以在运行期动态生成,从而实现对目标对象的代理功能。

反射是动态代理的一种实现方式。

Java中的动态代理有几种实现方式,各有什么优缺点?

解:

Java中,实现动态代理有两种方式:

1、JDK动态代理:java.lang.reflect 包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。

2、Cglib动态代理:Cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

关于这两种动态代理的写法本文就不深入展开了,读者感兴趣的话,后面我再写文章单独介绍。本文主要来简单说一下这两种动态代理的区别和用途。

JDK动态代理和Cglib动态代理的区别

JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。如果想代理没有实现接口的类,就可以使用CGLIB实现。

Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。

Cglib包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉。

Cglib与动态代理最大的区别就是:

使用动态代理的对象必须实现一个或多个接口

使用cglib代理的对象则无需实现接口,达到代理类无侵入。

Java中动态代理有哪些用途。

解:

Java的动态代理,在日常开发中可能并不经常使用,但是并不代表他不重要。Java的动态代理的最主要的用途就是应用在各种框架中。因为使用动态代理可以很方便的运行期生成代理类,通过代理类可以做很多事情,比如AOP,比如过滤器、拦截器等。

在我们平时使用的框架中,像servlet的filter、包括spring提供的aop以及struts2的拦截器都使用了动态代理功能。我们日常看到的mybatis分页插件,以及日志拦截、事务拦截、权限拦截这些几乎全部由动态代理的身影。

Java实现动态代理的大致步骤是怎样的。

1、定义一个委托类和公共接口。

2、自己定义一个类(调用处理器类,即实现 InvocationHandler 接口),这个类的目的是指定运行时将生成的代理类需要完成的具体任务(包括Preprocess和Postprocess),即代理类调用任何方法都会经过这个调用处理器类(在本文最后一节对此进行解释)。

3、生成代理对象(当然也会生成代理类),需要为他指定(1)委托对象(2)实现的一系列接口(3)调用处理器类的实例。因此可以看出一个代理对象对应一个委托对象,对应一个调用处理器实例。

Java 实现动态代理主要涉及哪几个类?

解:

java.lang.reflect.Proxy: 这是生成代理类的主类,通过 Proxy 类生成的代理类都继承了 Proxy 类,即 DynamicProxyClass extends Proxy。

java.lang.reflect.InvocationHandler: 这里称他为"调用处理器",他是一个接口,我们动态生成的代理类需要完成的具体内容需要自己定义一个类,而这个类必须实现 InvocationHandler 接口。

使用动态代理实现功能:不改变Test类的情况下,在方法target 之前打印一句话,之后打印一句话。

public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        // TODO Auto-generated method stub
        System.out.println("--------------------add----------------------");
    }
} 

解:
 

public class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target) {
    super();
    this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        PerformanceMonior.begin(target.getClass().getName()+"."+method.getName());
        //System.out.println("-----------------begin "+method.getName()+"-----------------");
    Object result = method.invoke(target, args);
        //System.out.println("-----------------end "+method.getName()+"-----------------");
        PerformanceMonior.end();
        return result;
    }

    public Object getProxy(){
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), target.getClass().getInterfaces(), this);
    }
}

public static void main(String[] args) {
    UserService service = new UserServiceImpl();
    MyInvocationHandler handler = new MyInvocationHandler(service);
    UserService proxy = (UserService) handler.getProxy(); proxy.add();
}

使用CGLIB动态实现功能:不改变Test类的情况下,在方法target 之前打印一句话,之后打印一句话。

public class UserServiceImpl implements UserService {
    @Override
    public void add() {
        // TODO Auto-generated method stub System.out.println("--------------------add----------------------");
    }
}

解:

public class CglibProxy implements MethodInterceptor{
    private Enhancer enhancer = new Enhancer();
    public Object getProxy(Class clazz){
    //设置需要创建子类的类
    enhancer.setSuperclass(clazz);
    enhancer.setCallback(this);
    //通过字节码技术动态创建子类实例
    return enhancer.create();
}
//实现MethodInterceptor接口方法
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    System.out.println("前置代理");
    //通过代理类调用父类中的方法
    Object result = proxy.invokeSuper(obj, args);
    System.out.println("后置代理"); return result;
    }
}

public class DoCGLib {
    public static void main(String[] args) {
    CglibProxy proxy = new CglibProxy();
    //通过生成子类的方式创建代理类
    UserServiceImpl proxyImp = (UserServiceImpl)proxy.getProxy(UserServiceImpl.class);
    proxyImp.add();
    }
}

Spring的AOP是怎么实现的?

解:

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。

JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。

CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

什么是SPI?和API有什么区别?

解:

Java 中区分 API 和 SPI,通俗的讲:API 和 SPI 都是相对的概念,他们的差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩展人员使用

理解

API Application Programming Interface 大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。

SPI Service Provider Interface

而如果是调用方来制定接口,实现方来针对接口来实现不同的实现。调用方来选择自己需要的实现方。

如何定义一个SPI?

解:

步骤1、定义一组接口 (假设是org.foo.demo.IShout),并写出接口的一个或多个实现,(假设是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)。

public interface IShout {
    void shout();
}

public class Cat implements IShout {
    @Override
    public void shout() {
    System.out.println("miao miao");
    }
}

public class Dog implements IShout {
    @Override
    public void shout() {
        System.out.println("wang wang");
    }
} 

步骤2、在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (org.foo.demo.IShout文件),内容是要应用的实现类(这里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一个类)。

org.foo.demo.animal.Dog

org.foo.demo.animal.Cat

步骤3、使用 ServiceLoader 来加载配置文件中指定的实现。
 

public class SPIMain {
    public static void main(String[] args) {
        ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
        for (IShout s : shouts) {
            s.shout();
        }
    }
} 

代码输出:

wang wang

miao miao

SPI有哪些应用场景?

解:

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略 比较常见的例子:

数据库驱动加载接口实现类的加载

JDBC加载不同类型数据库的驱动

日志门面接口实现类加载

SLF4J加载不同提供商的日志实现类

Spring

Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

Dubbo

Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

SPI的实现原理

解:

看ServiceLoader类的签名类的成员变量:

public final class ServiceLoader<S> implements Iterable<S>{
    private static final String PREFIX = "META-INF/services/";
    // 代表被加载的类或者接口
    private final Class<S> service;
    // 用于定位,加载和实例化providers的类加载器
    private final ClassLoader loader;
    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;
    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 懒查找迭代器
    private LazyIterator lookupIterator;
    ......
} 

参考具体源码,梳理了一下,实现的流程如下:

1 应用程序调用ServiceLoader.load方法

ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:

loader(ClassLoader类型,类加载器)

acc(AccessControlContext类型,访问控制器)

providers(LinkedHashMap类型,用于缓存加载成功的类)

lookupIterator(实现迭代器功能)

2 应用程序通过迭代器接口获取对象实例

ServiceLoader先判断成员变量providers对象中(LinkedHashMap类型)是否有缓存实例对象,如果有缓存,直接返回。

如果没有缓存,执行类的装载:

读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称

通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化

把实例化后的类缓存到providers对象中(LinkedHashMap类型)

然后返回实例对象。

猜你喜欢

转载自blog.csdn.net/w372426096/article/details/84617221
今日推荐