面试官问我:什么是静态代理?什么是动态代理?注解、反射你会吗?

197 阅读10分钟

欢迎关注同名公众号《Java鱼仔》,更多知识点你值得拥有

开场

一位穿着蓝色衬衫,牛仔裤,拿着一个白色保温杯的中年男子急匆匆地坐在你对面,看样子是项目上的东西很急,估摸面试时间不会太长,这样一想心情放松了许多......(后来我就被打脸了)

面试开始

面试官:小伙子,我看你的简历上说精通java基础对吧,那我先简单来问几个java基础。

好的好的,面试官你问。(一听到简单两个字就内心窃喜......)

面试官:你知道Java中有个东西叫代理吗?

知道知道,代理就是通过代理对象去访问实际的目标对象,比如我们在生活中租房,可以直接找房东,也可以通过某些租房平台去租房,通过租房平台的这种方式就是代理。在java中这种租房平台就被叫做代理类,代理类不仅能实现目标对象,还能增加一些额外的功能。据我所知java中的代理方式有静态代理和动态代理。(这个时候面试官很大概率会问你这两种代理模式)。

面试官:没想到你还能通过生活中的现象去理解代码,不错不错,我看你提到了静态代理和动态代理,那你给我说说什么是静态代理吧

(果然问了,还好我做了准备)静态代理就是在代码运行之前,这个代理类就已经存在了。还是以上面的租房为例,在代码中会首先创建一个通用的租房接口:

public interface Room {
    void rent();
}

然后需要有一个被代理的类(或者称为真实的类)和一个代理类:

public class RealRoom implements Room {
    private String roomname;
    public RealRoom(String roomname) {
        this.roomname = roomname;
    }
    public void rent() {
        System.out.println("租了"+roomname);
    }
}

代理类如下:

public class ProxyClass implements Room {
    RealRoom realRoom;
    public ProxyClass(RealRoom realRoom) {
        this.realRoom = realRoom;
    }
    public void rent() {
        System.out.println("租房前收取中介费");
        realRoom.rent();
        System.out.println("租房后收取服务费");
    }
}

代理类可以在不改变被代理对象的情况下增加功能,最后我们测试一下这个静态代理:

public class Main {
    public static void main(String[] args) {
        RealRoom realRoom =new RealRoom("碧桂园");
        ProxyClass proxyClass=new ProxyClass(realRoom);
        proxyClass.rent();
    }
}

然后观察结果:

租房前收取中介费
租了碧桂园
租房后收取服务费

面试官:既然静态代理那么强大,那他有什么缺点吗?

由于静态代理在代码运行之前就已经存在代理类,因此对于每一个代理对象都需要建一个代理类去代理,当需要代理的对象很多时就需要创建很多的代理类,严重降低程序的可维护性。用动态代理就可以解决这个问题。

面试官:那你给我讲一讲动态代理吧

动态代理是指代理类不是写在代码中,而是在运行过程中产生的,java提供了两种实现动态代理的方式,分别是基于Jdk的动态代理和基于Cglib的动态代理。

面试官:基于JDK的动态代理我忘了,你给我复习复习。

(我???算了算了) 实现Jdk的动态代理需要实现InvocationHandler接口,然后实现其中的invoke方法。如果代理的方法被调用,那么代理便会通知和转发给内部的 InvocationHandler 实现类invoke,由它实现处理内容。

public class ProxyHandler implements InvocationHandler {
    Object object;
    public ProxyHandler(Object object) {
        this.object = object;
    }
    //proxy 代理对象
    //method 要实现的方法
    //args 方法的参数    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理执行之前:"+method.getName());
        Object invoke = method.invoke(object, args);
        System.out.println("代理执行之后:"+method.getName());
        return invoke;
    }
}

接下来在main方法中执行动态代理

public static void main(String[] args) {
    Room room=new RealRoom("碧桂园");
    //obj.getClass().getClassLoader()类加载器
    //obj.getClass().getInterfaces() 目标类实现的接口
    //InvocationHandler对象
    InvocationHandler invocationHandler=new ProxyHandler(room);
    Room proxyRoom = (Room) Proxy.newProxyInstance(room.getClass().getClassLoader(), room.getClass().getInterfaces(), invocationHandler);
    proxyRoom.rent();
}

这段代码的核心是Proxy.newProxyInstance,目的是运行期间生成代理类,最后通过代理类执行被代理的方法。最后结果如下:

代理执行之前:rent
租了碧桂园
代理执行之后:rent

面试官:被你这么一说我想起来动态代理了,那他的优势呢?

之前我讲静态代理的时候说静态代理的缺点在于对于每一个被代理的对象,都需要建一个代理类。因为静态代理是在项目运行前就写好的。但是动态代理就不是这样,由于动态代理在运行时才创建代理类,因此只需要写一个动态代理类就好。比如我再创建一个被代理的对象卖房:

写一个通用接口Sell

public interface Sell {
    void sellRoom();
}

接着还是写一个被代理对象的类:

public class RealSell implements Sell {
    public void sellRoom() {
        System.out.println("卖房了");
    }
}

接下来在main方法中执行动态代理

    public static void main(String[] args) {
        Sell sell=new RealSell();
        InvocationHandler invocationHandler=new ProxyHandler(sell);
        Sell proxysell= (Sell) Proxy.newProxyInstance(sell.getClass().getClassLoader(),sell.getClass().getInterfaces(),invocationHandler);
        proxysell.sellRoom();
    }

最终实现结果如下:

代理执行之前:sellRoom
卖房了
代理执行之后:sellRoom

通过动态代理,我可以通过一个动态代理类,去代理多个对象。

面试官:如果我记的没错,通过这种方式只能代理接口吧,我看你上面的例子也都是代理接口,那我如果想代理类该怎么办呢?

jdk动态代理确实只能代理接口,JDK动态代理是基于接口的方式,换句话来说就是代理类和目标类都实现同一个接口。如果想要代理类的话可以使用CGLib,CGLib动态代理是代理类去继承目标类,然后实现目标类的方法。

创建一个目标类CGRoom

public class CGRoom {
    public void rent(String roomName){
        System.out.println("租了"+roomName);
    }
}

创建cglib的动态代理类,继承MethodInterceptor ,实现其中的intercept方法

public class MyMethodInterceptor implements MethodInterceptor {

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("代理执行之前:"+method.getName());
        Object object=methodProxy.invokeSuper(o,objects);
        System.out.println("代理执行之后:"+method.getName());
        return object;
    }
}

最后通过enhance对象来创建代理类

public static void main(String[] args) {
    //创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
    Enhancer enhancer=new Enhancer();
    //设置目标类的字节码文件
    enhancer.setSuperclass(CGRoom.class);
    //设置回调函数
    enhancer.setCallback(new MyMethodInterceptor());
    //创建代理对象
    CGRoom proxy= (CGRoom) enhancer.create();
    proxy.rent("碧桂园");
}

最终实现以下结果:

代理执行之前:rent
租了碧桂园
代理执行之后:rent

面试官:既然动态代理被你说的这么牛,那你平常工作中有使用到吗?

平常我的业务代码中虽然几乎没有使用过动态代理,但是我工作中使用的Spring系列框架中的AOP,以及RPC框架中都用到了动态代理,以AOP为例,AOP通过动态代理对目标对象进行了增强,比如我们最常用的前置通知、后置通知等。

面试官:不错!下面再考你几个基础,说说你对注解的理解,注解又解决了哪些问题?

Java语言中的类、方法、变量、参数和包都可以用注解标记,程序运行过程中我们可以获取到相应的注解以及注解中定义的内容,比如说 Spring 中如果检测到说你的类被 @Component注解标记的话,Spring 容器在启动的时候就会把这个类归为自己管理,这样你就可以通过 @Autowired注解注入这个对象了。

面试官:那你知道如何自己去定义注解吗?

知道知道,自定义注解主要有以下四步:

第一步通过@interface声明注解:

public @interface Myannotation {
    String key() default "";
}

第二步通过四种元注解修饰注解:(面试的时候说出这四种注解就可以了)

元注解的作用就是负责其他注解,java中一共有四个元注解,分别是@Target,@Retention,@Documented,@Inherited,下面先介绍以下四种注解的作用:

@Target:Target说明了注解所修饰的对象范围,取值(ElementType)有:

  1. 用于描述构造器

  2. 用于描述属性

  3. 用于描述局部变量

  4. 用于描述方法

  5. 用于描述包

  6. 用于描述参数

  7. 用于描述类、接口(包括注解类型)或者enum声明

@Retention:Retention定义了注解的保留范围,取值(RetentionPoicy)有:

  1. 在源文件中有效(即源文件保留)

  2. 在class文件中有效(即class保留)

  3. 在运行时有效(即运行时保留)

@Documented:Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。

@Inherited:Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Myannotation {
    String key() default "";
}

第三步使用注解,因为定义Target时定义了MEHTOD和FIELD,因此可以在属性和方法中使用这个注解:

public class MyannotationTest {
    @Myannotation(key = "javayz")
    private String username;
}

第四步利用反射解析注解

public static void main(String[] args) {
    Class myclass=MyannotationTest.class;
    Field[] fields = myclass.getDeclaredFields();
    for (Field field :fields){
        if (field.isAnnotationPresent(Myannotation.class)){
            System.out.println("配置了自定义注解");
            Myannotation annotation = field.getAnnotation(Myannotation.class);
            System.out.println("属性:"+field.getName()+"上的注解key为"+annotation.key());
        }
    }
}

输出结果:

配置了自定义注解
属性:username上的注解key为javayz

面试官:我看你上面第四步提到了反射是吧?那你给我讲讲什么是反射,它有啥特点:

(我晕,我就说了反射两个字啊,还好有准备)JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。

在上面第四步利用反射解析注解中,我通过MyannotationTest.class获取到了MyannotationTest的类对象,又用myclass.getDeclaredFields();获取到了所有的属性。这就是反射。

结束

面试官:不错,这几块的基础算你过关了,下面我要开始真正的技术面试了!

天呐!竟然这才算开始,还好我关注了公众号《Java鱼仔》,每天在地铁公交上都能学到知识点!