一、什么是代理

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

代理模式UML图:

结构示意图:

为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。

二、Java动态代理

Java动态代理类位于java.lang.reflect包下,一般主要涉及到以下两个类:
(1)Interface InvocationHandler:该接口中仅定义了一个方法

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

在实际使用时,第一个参数proxy一般是指代理类,method是被代理的方法,如上例中的request( ),args为该方法的参数数组。这个抽象方法在代理类中动态实现。
(2)Proxy:该类即为动态代理类,其中主要包含以下内容(API):

//所在包
package java.lang.reflect;

//Proxy类的定义
public class Proxy implements java.io.Serializable {}

Proxy提供了用于创建对象的静态方法,这些对象充当接口实例但允许自定义方法调用。 要为接口Foo创建代理实例:

InvocationHandler handler = new MyInvocationHandler(...); 
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class<?>[] { Foo.class }, handler);  

1、构造函数,用于给内部的h赋值

protected Proxy(InvocationHandler h)

2、获得一个代理类,其中loader是类装载器,interfaces是真实类所拥有的全部接口的数组。

static Class getProxyClass (ClassLoaderloader, Class[] interfaces)

3、返回代理类的一个实例,返回后的代理类可以当作被代理类使用(可使用被代理类的在Subject接口中声明过的方法):

static Object newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)

所谓DynamicProxy是这样一种class:它是在运行时生成的class,在生成它时你必须提供一组interface给它,然后该class就宣称它实现了这些 interface。你当然可以把该class的实例当作这些interface中的任何一个来用。当然,这个DynamicProxy其实就是一个Proxy,它不会替你作实质性的工作,在生成它的实例时你必须提供一个handler,由它接管实际的工作。
在使用动态代理类时,我们必须实现InvocationHandler接口
通过这种方式,被代理的对象(RealSubject)可以在运行时动态改变,需要控制的接口(Subject接口)可以在运行时改变,控制的方式(DynamicSubject类)也可以动态改变,从而实现了非常灵活的动态代理关系。

动态代理的步骤:

  1. 创建一个实现接口InvocationHandler的类,它必须实现invoke方法
  2. 创建被代理的类以及接口
  3. 通过Proxy的静态方法newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)创建一个代理
  4. 通过代理调用方法

三、具体使用

1、需要动态代理的接口:

package CSDN.jdkProxy;

public interface Subject {
    /**
     * @param name 
     * @return
     */
    public String SayHello(String name);

    /**
     * @return 
     */
    public String SayGoodBye();
}

2、需要代理的实际对象

package CSDN.jdkProxy;

public class RealSubject implements Subject {
    /**
     * 你好
     *
     * @param name
     * @return
     */
    public String SayHello(String name)
    {
        return "hello " + name;
    }

    /**
     * 再见
     *
     * @return
     */
    public String SayGoodBye()
    {
        return " good bye ";
    }
}

3、调用处理器实现类(有木有感觉这里就是传说中的AOP啊)

package CSDN.jdkProxy;

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

public class InvocationHandlerImpl implements InvocationHandler {
    /**
     * 这个就是我们要代理的真实对象
     */
    private Object subject;

    /**
     * 构造方法,给我们要代理的真实对象赋初值
     *
     * @param subject
     */
    public InvocationHandlerImpl(Object subject)
    {
        this.subject = subject;
    }

    /**
     * 该方法负责集中处理动态代理类上的所有方法调用。
     * 调用处理器根据这三个参数进行预处理或分派到委托类实例上反射执行
     *
     * @param proxy 代理类实例
     * @param method 被调用的方法对象
     * @param args 调用参数
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在代理真实对象前我们可以添加一些自己的操作
        System.out.println("在调用之前,我要干点啥呢?");

        System.out.println("Method:" + method);

        //当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
        Object returnValue = method.invoke(subject, args);

        //在代理真实对象后我们也可以添加一些自己的操作
        System.out.println("在调用之后,我要干点啥呢?");

        return returnValue;
    }
}

4、测试

package CSDN.jdkProxy;

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

public class DynamicProxyDemonstration {
    public static void main(String[] args) {
        //代理的真实对象
        Subject realSubject = new RealSubject();

        /**
         * InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
         * 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用.
         * 即:要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法
         */
        InvocationHandler handler = new InvocationHandlerImpl(realSubject);

        ClassLoader loader = realSubject.getClass().getClassLoader();
        Class[] interfaces = realSubject.getClass().getInterfaces();
        /**
         * 该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
         */
        Subject subject = (Subject) Proxy.newProxyInstance(loader, interfaces, handler);

        System.out.println("动态代理对象的类型:"+subject.getClass().getName());

        String hello = subject.SayHello("jiankunking");
        System.out.println(hello);
        String goodbye = subject.SayGoodBye();
        System.out.println(goodbye);

    }
}

5、结果输出

动态代理对象的类型:com.sun.proxy.$Proxy0
在调用之前,我要干点啥呢?
Method:public abstract java.lang.String CSDN.jdkProxy.Subject.SayHello(java.lang.String)
在调用之后,我要干点啥呢?
hello jiankunking
在调用之前,我要干点啥呢?
Method:public abstract java.lang.String CSDN.jdkProxy.Subject.SayGoodBye()
在调用之后,我要干点啥呢?
 good bye 

四、动态代理实现原理

从使用代码中可以看出,关键点在:

Subject subject = (Subject) Proxy.newProxyInstance(loader, interfaces, handler); 

通过跟踪提示代码可以看出:当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用。

先看看newProxyInstance( )方法源码:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
        throws IllegalArgumentException{
        //参数 h 的非空判断
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         * 获得(查找或者实现)与指定类装载器和一组接口相关的代理类类型的对象,也就是Class对象
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         * 通过反射获取构造函数对象并生成代理类实例
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
            //获取代理对象的构造方法(也就是$Proxy0(InvocationHandler h))
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            //生成代理类的实例并把InvocationHandlerImpl的实例传给它的构造方法
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

再看看getProxyClass0( )方法源码:

/**
* Generate a proxy class.  Must call the checkProxyAccess method
* to perform permission checks before calling this.
* 生成一个代理类。但是在调用getProxyClass0()方法前必须先调用checkProxyAccess()方法进行权限检查
*/
private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    // 如果通过给定的接口的loader已经实现了代理类,则只返回缓存副本;否则,它将通过ProxyClassFactory创建代理类
    return proxyClassCache.get(loader, interfaces);
}

真相还是没有来到,继续,看一下proxyClassCache

/**
 * a cache of proxy classes
 */
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

原来用了一下缓存啊,那么它对应的get方法啥样呢?

/** 
 * Look-up the value through the cache. This always evaluates the 
 * {@code subKeyFactory} function and optionally evaluates 
 * {@code valueFactory} function if there is no entry in the cache for given 
 * pair of (key, subKey) or the entry has already been cleared. 
 * 
 * @param key possibly null key 
 * @param parameter parameter used together with key to create sub-key and 
 *  value (should not be null) 
 * @return the cached value (never null) 
 * @throws NullPointerException if {@code parameter} passed in or 
 *  {@code sub-key} calculated by 
 *  {@code subKeyFactory} or {@code value} 
 *  calculated by {@code valueFactory} is null. 
 */ 
public V get(K key, P parameter) { 
    Objects.requireNonNull(parameter); 

    expungeStaleEntries(); 

    Object cacheKey = CacheKey.valueOf(key, refQueue); 

    // lazily install the 2nd level valuesMap for the particular cacheKey 
    ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey); 
    if (valuesMap == null) { 
        //putIfAbsent这个方法在key不存在的时候加入一个值,如果key存在就不放入 
        ConcurrentMap<Object, Supplier<V>> oldValuesMap 
        = map.putIfAbsent(cacheKey, 
        valuesMap = new ConcurrentHashMap<>()); 

        if (oldValuesMap != null) { 
            valuesMap = oldValuesMap; 
        } 
    } 

    // create subKey and retrieve the possible Supplier<V> stored by that 
    // subKey from valuesMap 
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter)); 
    Supplier<V> supplier = valuesMap.get(subKey); 
    Factory factory = null; 

    while (true) { 
        if (supplier != null) { 
            // supplier might be a Factory or a CacheValue<V> instance 
            V value = supplier.get(); 
            if (value != null) { 
                return value; 
            } 
        } 
        // else no supplier in cache 
        // or a supplier that returned null (could be a cleared CacheValue 
        // or a Factory that wasn't successful in installing the CacheValue) 

        // lazily construct a Factory 
        if (factory == null) { 
            factory = new Factory(key, parameter, subKey, valuesMap); 
        } 

        if (supplier == null) { 
            supplier = valuesMap.putIfAbsent(subKey, factory); 
            if (supplier == null) { 
                // successfully installed Factory 
                supplier = factory; 
            } 
        // else retry with winning supplier 
        } else { 
            if (valuesMap.replace(subKey, supplier, factory)) { 
                // successfully replaced 
                // cleared CacheEntry / unsuccessful Factory 
                // with our Factory 
                supplier = factory; 
            } else { 
                // retry with current supplier 
                supplier = valuesMap.get(subKey); 
            } 
        } 
    } 
} 

我们可以看到它调用了 supplier.get(); 获取动态代理类,其中supplier是Factory,这个类定义在WeakCach的内部。
来瞅瞅,get里面又做了什么?

public synchronized V get() { 
    // serialize access 
    // re-check 
    Supplier<V> supplier = valuesMap.get(subKey); 
    if (supplier != this) { 
        // something changed while we were waiting: 
        // might be that we were replaced by a CacheValue 
        // or were removed because of failure -> 
        // return null to signal WeakCache.get() to retry 
        // the loop 
        return null; 
    } 
    // else still us (supplier == this) 

    // create new value 
    V value = null; 
    try { 
        value = Objects.requireNonNull(valueFactory.apply(key, parameter)); 
    } finally { 
        if (value == null) { // remove us on failure 
            valuesMap.remove(subKey, this); 
        } 
    } 
    // the only path to reach here is with non-null value 
    assert value != null; 

    // wrap value with CacheValue (WeakReference) 
    CacheValue<V> cacheValue = new CacheValue<>(value); 

    // try replacing us with CacheValue (this should always succeed) 
    if (valuesMap.replace(subKey, this, cacheValue)) { 
        // put also in reverseMap 
        reverseMap.put(cacheValue, Boolean.TRUE); 
    } else { 
        throw new AssertionError("Should not reach here"); 
    } 

    // successfully replaced us with new CacheValue -> return the value 
    // wrapped by it 
    return value; 
    } 
} 

发现重点还是木有出现,但我们可以看到它调用了valueFactory.apply(key, parameter)方法:

/** 
* A factory function that generates, defines and returns the proxy class given 
* the ClassLoader and array of interfaces. 
*/ 
private static final class ProxyClassFactory 
implements BiFunction<ClassLoader, Class<?>[], Class<?>> 
{ 
    // prefix for all proxy class names 
    private static final String proxyClassNamePrefix = "$Proxy"; 

    // next number to use for generation of unique proxy class names 
    private static final AtomicLong nextUniqueNumber = new AtomicLong(); 

    @Override 
    public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) { 

        Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length); 
        for (Class<?> intf : interfaces) { 
            /* 
            * Verify that the class loader resolves the name of this 
            * interface to the same Class object. 
            */ 
            Class<?> interfaceClass = null; 
            try { 
                interfaceClass = Class.forName(intf.getName(), false, loader); 
            } catch (ClassNotFoundException e) { 
        } 
        if (interfaceClass != intf) { 
            throw new IllegalArgumentException( 
            intf + " is not visible from class loader"); 
        } 
        /* 
        * Verify that the Class object actually represents an 
        * interface. 
        */ 
        if (!interfaceClass.isInterface()) { 
            throw new IllegalArgumentException( 
            interfaceClass.getName() + " is not an interface"); 
        } 
        /* 
        * Verify that this interface is not a duplicate. 
        */ 
        if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { 
            throw new IllegalArgumentException( 
            "repeated interface: " + interfaceClass.getName()); 
        } 
    } 

    String proxyPkg = null; // package to define proxy class in 
    int accessFlags = Modifier.PUBLIC | Modifier.FINAL; 

    /* 
    * Record the package of a non-public proxy interface so that the 
    * proxy class will be defined in the same package. Verify that 
    * all non-public proxy interfaces are in the same package. 
    */ 
    for (Class<?> intf : interfaces) { 
        int flags = intf.getModifiers(); 

        if (!Modifier.isPublic(flags)) { 
            accessFlags = Modifier.FINAL; 
            String name = intf.getName(); 
            int n = name.lastIndexOf('.'); 
            String pkg = ((n == -1) ? "" : name.substring(0, n + 1)); 

            if (proxyPkg == null) { 
                proxyPkg = pkg; 
            } else if (!pkg.equals(proxyPkg)) { 
                throw new IllegalArgumentException("non-public interfaces from different packages"); 
            } 
        } 
    } 

    if (proxyPkg == null) { 
        // if no non-public proxy interfaces, use com.sun.proxy package 
        proxyPkg = ReflectUtil.PROXY_PACKAGE + "."; 
    } 

    /* 
    * Choose a name for the proxy class to generate. 
    */ 
    long num = nextUniqueNumber.getAndIncrement(); 
    String proxyName = proxyPkg + proxyClassNamePrefix + num; 

    /* 
    * Generate the specified proxy class. 
    */ 
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); 
    try { 
        return defineClass0(loader, proxyName, 
         proxyClassFile, 0, proxyClassFile.length); 
        } catch (ClassFormatError e) { 
            /* 
            * A ClassFormatError here means that (barring bugs in the 
            * proxy class generation code) there was some other 
            * invalid aspect of the arguments supplied to the proxy 
            * class creation (such as virtual machine limitations 
            * exceeded). 
            */ 
            throw new IllegalArgumentException(e.toString()); 
        }
    } 
} 

通过看代码终于找到了重点:

//生成字节码 
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags); 

那么接下来我们也使用测试一下,使用这个方法生成的字节码是个什么样子:

import sun.misc.ProxyGenerator; 

import java.io.File; 
import java.io.FileNotFoundException; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.lang.reflect.InvocationHandler; 
import java.lang.reflect.Proxy; 

/** 
* 动态代理演示 
*/ 
public class DynamicProxyDemonstration 
{ 
    public static void main(String[] args) 
    { 
        //代理的真实对象 
        Subject realSubject = new RealSubject(); 

        /** 
        * InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发 
        * 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用. 
        * 即:要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法 
        */ 
        InvocationHandler handler = new InvocationHandlerImpl(realSubject); 

        ClassLoader loader = handler.getClass().getClassLoader(); 
        Class[] interfaces = realSubject.getClass().getInterfaces(); 
        /** 
        * 该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例 
        */ 
        Subject subject = (Subject) Proxy.newProxyInstance(loader, interfaces, handler); 

        System.out.println("动态代理对象的类型:"+subject.getClass().getName()); 

        String hello = subject.SayHello("jiankunking"); 
        System.out.println(hello); 
        // 将生成的字节码保存到本地, 
        createProxyClassFile(); 
    } 

    private static void createProxyClassFile(){ 
        String name = "ProxySubject"; 
        byte[] data = ProxyGenerator.generateProxyClass(name,new Class[]{Subject.class}); 
        FileOutputStream out =null; 
        try { 
            out = new FileOutputStream(name+".class"); 
            System.out.println((new File("hello")).getAbsolutePath()); 
            out.write(data); 
        } catch (FileNotFoundException e) { 
            e.printStackTrace(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        }finally { 
            if(null!=out) try { 
                out.close(); 
            } catch (IOException e) { 
                e.printStackTrace(); 
            } 
        } 
    } 
} 

可以看一下这里代理对象的类型:

我们用jd-jui 工具将生成的字节码反编译:

import java.lang.reflect.InvocationHandler; 
import java.lang.reflect.Method; 
import java.lang.reflect.Proxy; 
import java.lang.reflect.UndeclaredThrowableException; 
import jiankunking.Subject; 

public final class ProxySubject 
extends Proxy 
implements Subject { 
    private static Method m1; 
    private static Method m3; 
    private static Method m4; 
    private static Method m2; 
    private static Method m0; 

    public ProxySubject(InvocationHandler paramInvocationHandler) 
    { 
        super(paramInvocationHandler); 
    } 

    public final boolean equals(Object paramObject) 
    { 
        try { 
            return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue(); 
        } 
        catch (Error|RuntimeException localError) 
        { 
            throw localError; 
        } 
        catch (Throwable localThrowable) 
        { 
            throw new UndeclaredThrowableException(localThrowable); 
        } 
    } 

    public final String SayGoodBye() 
    { 
        try 
        { 
            return (String)this.h.invoke(this, m3, null); 
        } 
        catch (Error|RuntimeException localError) 
        { 
            throw localError; 
        } 
        catch (Throwable localThrowable) 
        { 
            throw new UndeclaredThrowableException(localThrowable); 
        } 
    } 

    public final String SayHello(String paramString) 
    { 
        try 
        { 
            return (String)this.h.invoke(this, m4, new Object[] { paramString }); 
        } 
        catch (Error|RuntimeException localError) 
        { 
            throw localError; 
        } 
        catch (Throwable localThrowable) 
        { 
            throw new UndeclaredThrowableException(localThrowable); 
        } 
    } 

    public final String toString() 
    { 
        try 
        { 
            return (String)this.h.invoke(this, m2, null); 
        } 
        catch (Error|RuntimeException localError) 
        { 
            throw localError; 
        } 
        catch (Throwable localThrowable) 
        { 
            throw new UndeclaredThrowableException(localThrowable); 
        } 
    } 

    public final int hashCode() 
    { 
        try 
        { 
            return ((Integer)this.h.invoke(this, m0, null)).intValue(); 
        } 
        catch (Error|RuntimeException localError) 
        { 
            throw localError; 
        } 
        catch (Throwable localThrowable) 
        { 
            throw new UndeclaredThrowableException(localThrowable); 
        } 
    } 

    static 
    { 
        try 
        { 
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") }); 
            m3 = Class.forName("jiankunking.Subject").getMethod("SayGoodBye", new Class[0]); 
            m4 = Class.forName("jiankunking.Subject").getMethod("SayHello", new Class[] { Class.forName("java.lang.String") }); 
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); 
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); 
            return; 
        } 
        catch (NoSuchMethodException localNoSuchMethodException) 
        { 
            throw new NoSuchMethodError(localNoSuchMethodException.getMessage()); 
        } 
        catch (ClassNotFoundException localClassNotFoundException) 
        { 
            throw new NoClassDefFoundError(localClassNotFoundException.getMessage()); 
        } 
    } 
} 

这就是最终真正的代理类,它继承自Proxy并实现了我们定义的Subject接口
也就是说:

Subject subject = (Subject) Proxy.newProxyInstance(loader, interfaces, handler); 

这里的subject实际是这个类的一个实例,那么我们调用它的:

public final String SayHello(String paramString) 

就是调用我们定义的InvocationHandlerImpl的 invoke方法:

五、结论

到了这里,终于解答了:
subject.SayHello("jiankunking")这句话时,为什么会自动调用InvocationHandlerImpl的invoke( )方法?

因为JDK生成的最终真正的代理类,它继承自Proxy并实现了我们定义的Subject接口,在实现Subject接口方法的内部,通过反射调用了InvocationHandlerImpl的invoke方法。

通过分析代码可以看出Java 动态代理,具体有如下四步骤:

  • 通过实现 InvocationHandler 接口创建自己的调用处理器;
  • 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
  • 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
  • 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。

最后

感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!


前程有光
936 声望618 粉丝