2024-12-18  阅读(1654)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/3218480666

回答

JDK 动态代理是基于 Java 反射机制,在运行时通过 Proxy.newProxyInstance() 动态生成代理类,代理类会实现目标类所实现的接口,并通过 InvocationHandler 接口拦截方法调用。而 CGLIB 则是基于字节码,它使用 ASM 库直接操作字节码,在运行时生成目标类的子类,重写目标类的非 final 方法,并在方法调用时进行拦截。它通过代理类的子类来对方法调用进行增强。

由于 JDK 动态代理是基于 Java 反射的,所以它的性能开销比较大,尤其是在方法频繁调用的场景下,性能可能成为瓶颈。而 CGLIB 的性能较高。

同时,JDK 动态代理只能作用于代理接口,不能对没有实现接口的类进行代理,而 CGLIB 是基于目标类生成对应的子类,它不要求目标类实现接口,比 JDK 动态代理灵活些,但是它不能代理 final 类和 final 方法。

详解

JDK 动态代理

JDK 动态代理主要涉及两个类:

java.lang.reflect.Proxy

Proxy 用于动态创建实现指定接口的代理类。通过它,我们可以在运行时生成代理对象,从而拦截并控制对目标对象的方法调用。

它的核心方法是 newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h),该方法是创建代理对象的关键方法,它接受三个参数:

  • ClassLoader loader:代理类的类加载器,通常使用目标类的类加载器。
  • Class<?>[] interfaces:被代理的接口数组,代理类会实现这些接口。
  • InvocationHandler h:处理接口方法调用的逻辑。

该方法返回一个实现了指定接口的代理对象,所有方法的调用都会被转发到指定的 InvocationHandler 实现。

java.lang.reflect.InvocationHandler

InvocationHandler 接口定义了代理对象调用方法时的处理逻辑。代理对象的所有方法调用都会转发到它的 invoke() 方法。在 JDK 动态代理中 InvocationHandler 充当了核心处理器,允许我们自定义方法调用的逻辑。invoke() 定义如下:

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

proxy:当前的代理对象。

method:当前被调用的方法对象,代表代理对象调用的具体方法。

args:方法调用时的参数数组,null 表示该方法无参数。

invoke() 内部,我们可以决定何时调用目标对象的方法,也可不调用目标对象的方法。

JDK 动态代理使用起来非常方便,三步就可以了。

  • 一、定义一个接口,然后实现它
public interface HelloService {
    void sayHello(String name);
}

public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello, " + name);
    }
}

  • 二、创建 InvocationHandler

实现 InvocationHandler 接口,在 invoke() 实现代理逻辑。

public class HelloServiceInvocationHandler implements InvocationHandler {
    private Object target;

    public HelloServiceInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 前置逻辑
        System.out.println("Before method: " + method.getName());
        
        // 调用实际的目标方法
        Object result = method.invoke(target, args);
        
        // 后置逻辑
        System.out.println("After method: " + method.getName());
        
        return result;
    }
}

  • 三、创建代理对象

通过 Proxy.newProxyInstance() 方法动态创建代理对象:

public class HelloServiceTest {
    public static void main(String[] args) {
        HelloService helloService = new HelloServiceImpl();
        
        // 创建代理对象
        HelloService proxy = (HelloService) Proxy.newProxyInstance(
            helloService.getClass().getClassLoader(),
            helloService.getClass().getInterfaces(),
            new HelloServiceInvocationHandler(helloService)
        );

        // 调用代理对象的方法
        proxy.sayHello("www.skjava.com");
    }
}

执行结果

Before method: sayHello
Hello, www.skjava.com
After method: sayHello

使用起来是简单,但是我们弄清楚底层原理我们就需要回答两个问题:

  1. 代理类是如何生成的
  2. 代理类的方法 invoke() 是什么时候调用的?

我们先看第一个问题,代理类是如何生成的。代理类由 Proxy.newProxyInstance() 生成,我们直接看对应源码就可以了:

    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);
        
        // 克隆代理类实现的所有接口
        final Class<?>[] intfs = interfaces.clone();
        
        // 安全检查
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * 生成代理类对象
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            // 拿到 InvocationHandler 代理对象的构造器
            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;
                    }
                });
            }
            // 生成 InvocationHandler 代理对象
            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);
        }
    }

这段源码很简单,其实就做了两件事情:

  1. 调用 getProxyClass0() 获取代理类对象
  2. 利用反射技术实例化 InvocationHandler 代理类对象:return cons.newInstance(new Object[]{h})

我们着重看 getProxyClass0()

    private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        // 接口类对象数组不能大于65535个
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // 直接从 proxyClassCache 缓冲中取
        return proxyClassCache.get(loader, interfaces);
    }

在这个方法中,是直接从 proxyClassCache 缓存中获取,如果根据提供的类加载器和接口数组能在缓存中找到代理类就直接返回该代理类。如果没有在缓存中找到,会调用ProxyClassFactory工厂去生成代理类。

proxyClassCache 是一个 WeakCache 缓存, 它是一个二级缓存。proxyClassCache 中一级缓存 key 是根据类加载器生成的,二级缓存 key 是根据接口数组生成的,如下:

private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

WeakCache 定义如下:

final class WeakCache<K, P, V> {
    
    private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
    // 二级缓存,key 为一级缓存, value 为二级缓存
    private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map = new ConcurrentHashMap<>();
    
    // reverseMap 记录了所有代理类生成器是否可用, 这是为了实现缓存的过期机制
    private final ConcurrentMap<Supplier<V>, Boolean> reverseMap = new ConcurrentHashMap<>();
    
    // 生成二级缓存key的工厂, 这里传入的是KeyFactory
    private final BiFunction<K, P, ?> subKeyFactory;
    
    // 生成二级缓存value的工厂, 这里传入的是ProxyClassFactory
    private final BiFunction<K, P, V> valueFactory;
    
    // 省略很多代码..
    
 }

我们直接看 WeakCacheget()

    public V get(K key, P parameter) {
        Objects.requireNonNull(parameter);

        expungeStaleEntries();

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

        // 懒加载,获取二级缓存
        ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
        if (valuesMap == null) {
            ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,valuesMap = new ConcurrentHashMap<>());
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }

        // 创建 subKey
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        // 通过subKey获取到二级缓存的值
        Supplier<V> supplier = valuesMap.get(subKey);
        Factory factory = null;

        while (true) {
            if (supplier != null) {
                // 核心代码Ⅰ
                // 从工厂里面获取代理对象
                // 这里其实是 ProxyClassFactory
                V value = supplier.get();
                if (value != null) {
                    return value;
                }
            }
            
            if (factory == null) {
                // facotry 为空,则新建一个 factory 
                factory = new Factory(key, parameter, subKey, valuesMap);
            }

            if (supplier == null) {
                // 将 facotry 保存到 valuesMap
                supplier = valuesMap.putIfAbsent(subKey, factory);
                // 核心代码Ⅱ
                if (supplier == null) {
                    supplier = factory;
                }
            } else {
                if (valuesMap.replace(subKey, supplier, factory)) {
                    supplier = factory;
                } else {
                    supplier = valuesMap.get(subKey);
                }
            }
        }
    }

在这个方法里面有很多并发的方面的控制,WeakCache 采用 ConcurrentMap 来保证,这里我们不关注,这里我们只需要关心两段代码即可(核心代码Ⅰ核心代码Ⅱ)。从 核心代码Ⅱ 我们可以确认 supplier 就是 Factory,Factory 是 WeakCache 的内部类。所以核心代码Ⅰget() 就是 Factory#get():

        public synchronized V get() { // serialize access
            // 从二级缓存中获取 supplier,判断是否为自己本身
            Supplier<V> supplier = valuesMap.get(subKey);
            if (supplier != this) {
                /*
                 * 在这里验证 supplier 是否是 Factory 本身, 如果不则返回 null让 调用者继续轮询重试
                 * 期间 supplier 可能替换成了 CacheValue, 或者由于生成代理类失败被从二级缓存中移除了
                 */
                return null;
            }

            V value = null;
            try {
                // 委托 valueFactory 生成代理类
                value = Objects.requireNonNull(valueFactory.apply(key, parameter));
            } finally {
                if (value == null) { // remove us on failure
                    valuesMap.remove(subKey, this);
                }
            }
            // ...
        }
    }

这里的核心代码是 valueFactory.apply(key, parameter),它是委托 valueFactory 去生成代理类,所以我们只需要去看看 valueFactory 是什么即可,它是在 WeakCache 构造函数中初始化的:

    public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                     BiFunction<K, P, V> valueFactory) {
        this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
        this.valueFactory = Objects.requireNonNull(valueFactory);
    }

还记得在 Proxy 中 proxyClassCache 的定义吗?

    private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

所以,我们可以确定 valueFactory 就是ProxyClassFactory,我们跟进去,apply() 如下:

       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 {
                    // 加载接口类,获得接口类的类对象,第二个参数为false表示不进行实例化
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
                }
                /*
                 * 判断 interfaceClass 是否为一个接口
                 */
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
                /*
                 * interfaceClass 中是否有重复
                 */
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
            }

            // 代理类包名
            String proxyPkg = null;    
            // 生成代理类的访问标志, 默认是 public final 的
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            // 验证所有非 public 的接口,如果接口的访问标志不是 public, 那么生成代理类的包名和接口包名相同
            for (Class<?> intf : interfaces) {
                // 接口的权限修饰符
                int flags = intf.getModifiers();
                // 非 public
                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");
                    }
                }
            }
            
            // 如果接口访问标志都是public的话,那么生成的代理类都放到默认的包下:com.sun.proxy
            if (proxyPkg == null) {
                // if no non-public proxy interfaces, use com.sun.proxy package
                proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
            }

            // 代理类的类名
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            /*
             * 生成代理类的类文件
             */
            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());
            }
        }
    }

这个方法最主要就是干两件事:

  1. 确定代理类的全限定名为 proxyPkg + proxyClassNamePrefix + num,即 com.sun.proxy.$Proxy0....
  2. 调用 ProxyGenerator.generateProxyClass() 生成代理类

生成代理类的工作由 ProxyGenerator.generateProxyClass() 负责,但是这个类是定义在 package sun.misc 下,所以我们只能通过 OpenJDK 的源码来找到这个类,在这个类中可以看到,它最终会去调用 generateClassFile() 来生成Class文件:

public class ProxyGenerator {
    private byte[] generateClassFile() {
  
        //第一步, 将所有的方法组装成ProxyMethod对象
        //首先为代理类生成toString, hashCode, equals等代理方法
        addProxyMethod(hashCodeMethod, Object.class);
        addProxyMethod(equalsMethod, Object.class);
        addProxyMethod(toStringMethod, Object.class);
        //遍历每一个接口的每一个方法, 并且为其生成ProxyMethod对象
        for (int i = 0; i < interfaces.length; i++) {
            Method[] methods = interfaces[i].getMethods();
            for (int j = 0; j < methods.length; j++) {
                addProxyMethod(methods[j], interfaces[i]);
            }
        }
        //对于具有相同签名的代理方法, 检验方法的返回值是否兼容
        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }
        //第二步, 组装要生成的class文件的所有的字段信息和方法信息
        try {
            //添加构造器方法
            methods.add(generateConstructor());
            //遍历缓存中的代理方法
            for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
                for (ProxyMethod pm : sigmethods) {
                    //添加代理类的静态字段, 例如:private static Method m1;
                    fields.add(new FieldInfo(pm.methodFieldName,
                            "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC));
                    //添加代理类的代理方法
                    methods.add(pm.generateMethod());
                }
            }
            //添加代理类的静态字段初始化方法
            methods.add(generateStaticInitializer());
        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }
        //验证方法和字段集合不能大于65535
        if (methods.size() > 65535) {
            throw new IllegalArgumentException("method limit exceeded");
        }
        if (fields.size() > 65535) {
            throw new IllegalArgumentException("field limit exceeded");
        }
        //第三步, 写入最终的class文件
        //验证常量池中存在代理类的全限定名
        cp.getClass(dotToSlash(className));
        //验证常量池中存在代理类父类的全限定名, 父类名为:"java/lang/reflect/Proxy"
        cp.getClass(superclassName);
        //验证常量池存在代理类接口的全限定名
        for (int i = 0; i < interfaces.length; i++) {
            cp.getClass(dotToSlash(interfaces[i].getName()));
        }
        //接下来要开始写入文件了,设置常量池只读
        cp.setReadOnly();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(bout);
        try {
            //1.写入魔数
            dout.writeInt(0xCAFEBABE);
            //2.写入次版本号
            dout.writeShort(CLASSFILE_MINOR_VERSION);
            //3.写入主版本号
            dout.writeShort(CLASSFILE_MAJOR_VERSION);
            //4.写入常量池
            cp.write(dout);
            //5.写入访问修饰符
            dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
            //6.写入类索引
            dout.writeShort(cp.getClass(dotToSlash(className)));
            //7.写入父类索引, 生成的代理类都继承自Proxy
            dout.writeShort(cp.getClass(superclassName));
            //8.写入接口计数值
            dout.writeShort(interfaces.length);
            //9.写入接口集合
            for (int i = 0; i < interfaces.length; i++) {
                dout.writeShort(cp.getClass(dotToSlash(interfaces[i].getName())));
            }
            //10.写入字段计数值
            dout.writeShort(fields.size());
            //11.写入字段集合
            for (FieldInfo f : fields) {
                f.write(dout);
            }
            //12.写入方法计数值
            dout.writeShort(methods.size());
            //13.写入方法集合
            for (MethodInfo m : methods) {
                m.write(dout);
            }
            //14.写入属性计数值, 代理类class文件没有属性所以为0
            dout.writeShort(0);
        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }
        //转换成二进制数组输出
        return bout.toByteArray();
    }
}

代理类生成了,那它到底长成什么样子呢?我们可以通过设置环境变量 sun.misc.ProxyGenerator.saveGeneratedFiles=true 来保存代理类,上面 HelloService 的代理类内容如下:

public final class $Proxy0 extends Proxy implements HelloService {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void sayHello(String var1) throws  {
        try {
            super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.zhaocai.system.api.test.HelloService").getMethod("sayHello", Class.forName("java.lang.String"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

从代理类中的sayHello(String var1) 我们就可以回答第二个问题了:代理类的方法 invoke() 是什么时候调用的?

CGLIB 动态代理

CGLIB 的底层是通过字节码技术,动态创建类的子类并在其中插入拦截代码。当方法调用发生时,代理对象会检查是否有拦截器(interceptor)来控制方法调用的行为。核心逻辑包括:

  • 字节码生成CGLIB 依赖于 ASM 框架生成字节码。ASM 是一个 Java 字节码操纵框架,它能被用来生成和修改 JVM 的字节码。
  • 代理类生成CGLIB 创建的代理类是目标类的子类,通过重写父类的方法,在方法中插入拦截逻辑。由于是子类继承的方式,因此 CGLIB 无法代理 final 类和 final 方法。
  • MethodInterceptor 拦截CGLIB 的代理核心是 MethodInterceptor 接口。代理类中调用的所有方法都会触发拦截器,通过 MethodInterceptorintercept 方法可以在方法执行前后进行逻辑的处理。

下面举一个简单的例子来演示如何使用它。

  • 一、引入 CGLIB 依赖
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>
  • 二、定义目标类
public class UserService {
    public void createUser(String username) {
        System.out.println("新增用户: " + username);
    }

    public void deleteUser(String username) {
        System.out.println("删除用户: " + username);
    }
}

  • 三、定义拦截器

定义一个实现 MethodInterceptor 接口的拦截器类,在其中加入方法拦截逻辑。

public class UserServiceInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("前置逻辑: " + method.getName());
        
        // 调用目标方法
        Object result = proxy.invokeSuper(obj, args);
        
        System.out.println("后置逻辑: " + method.getName());
        return result;
    }
}

  • 四、生成代理对象

使用 Enhancer 类生成代理对象。Enhancer 是 CGLIB 的核心类之一,用于创建代理类。

public class CGLIBTest {
    public static void main(String[] args) {
        // 设置 Enhancer
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);  // 设置目标类
        enhancer.setCallback(new UserServiceInterceptor());  // 设置拦截器

        // 生成代理对象
        UserService userService = (UserService) enhancer.create();

        // 调用代理对象的方法
        userService.createUser("大明哥");
        userService.deleteUser("小明第");
    }
}

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文