关于jdk:金九银十想面BAT那这些JDK-动态代理的面试点你一定要知道

5次阅读

共计 16501 个字符,预计需要花费 42 分钟才能阅读完成。

一、什么是代理

代理是一种罕用的设计模式,其目标就是为其余对象提供一个代理以管制对某个对象的拜访。代理类负责为委托类预处理音讯,过滤音讯并转发音讯,以及进行音讯被委托类执行后的后续解决。

代理模式 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 相干技术文章或行业资讯,欢送大家关注和转发文章!

正文完
 0