Java——动态代理

动态代理

1、 静态代理

代理是一个比较通用的词,作为一个软件设计模式,它在《设计模式》一书中被提出,基本概念和日常生活中的概念是类似的。代理背后一般至少有一个实际对象,代理的外部功能和实际对象一般是一样的,用户与代理打交道,不直接接触实际对象。虽然外部功能和实际对象一样,但代理有它存在的价值,比如:

  1. 节省成本比较高的实际对象的创建开销,按需延迟加载,创建代理时并不真正创建实际对象,而只是保存实际对象的地址,在需要时再加载或创建。
  2. 执行权限检查,代理检查权限后,再调用实际对象。
  3. 屏蔽网络差异和复杂性,代理在本地,而实际对象在其他服务器上,调用本地代理时,本地代理请求其他服务器。
    代理模式的代码结构也比较简单,我们看个简单的例子,如代码所示。
java 复制代码
public class SimpleStaticProxyDemo {
    static interface IService {
        public void sayHello();
    }
    static class RealService implements IService {
        @Override
        public void sayHello() {
            System.out.println("hello");
        }
    }
    static class TraceProxy implements IService {
        private IService realService;
        public TraceProxy(IService realService) {
            this.realService = realService;
        }
        @Override
        public void sayHello() {
            System.out.println("entering sayHello");
            this.realService.sayHello();
            System.out.println("leaving sayHello");
        }
    }
    public static void main(String[] args) {
        IService realService = new RealService();
        IService proxyService = new TraceProxy(realService);
        proxyService.sayHello();
    }
}

代理和实际对象一般有相同的接口,在这个例子中,共同的接口是IService,实际对象是RealService,代理是TraceProxy。TraceProxy内部有一个IService的成员变量,指向实际对象,在构造方法中被初始化,对于方法sayHello的调用,它转发给了实际对象,在调用前后输出了一些跟踪调试信息,程序输出为:

复制代码
entering sayHello
 hello
 leaving sayHello

适配器和装饰器,它们与代理模式有点类似,它们的背后都有一个别的实际对象,都是通过组合的方式指向该对象,不同之处在于,适配器是提供了一个不一样的新接口,装饰器是对原接口起到了"装饰"作用,可能是增加了新接口、修改了原有的行为等,代理一般不改变接口。不过,我们并不想强调它们的差别,可以将它们看作代理的变体,统一看待。

在上面的例子中,我们想达到的目的是在实际对象的方法调用前后加一些调试语句。为了在不修改原类的情况下达到这个目的,我们在代码中创建了一个代理类TraceProxy,它的代码是在写程序时固定的,所以称为静态代理。

输出跟踪调试信息是一个通用需求,可以想象,如果每个类都需要,而又不希望修改类定义,我们需要为每个类创建代理,实现所有接口,这个工作就太烦琐了,如果再有其他的切面需求,整个工作可能又要重来一遍。

这时,就需要动态代理了,主要有两种方式实现动态代理:Java SDK和第三方库cglib,我们先来介绍Java SDK。

2、Java SDK动态代理

2.1、用法

在静态代理中,代理类是直接定义在代码中的,在动态代理中,代理类是动态生成的。

java 复制代码
public class SimpleJDKDynamicProxyDemo {
    static interface IService {
        public void sayHello();
    }

    static class RealService implements IService {

        @Override
        public void sayHello() {
            System.out.println("hello");
        }
    }

    static class SimpleInvocationHandler implements InvocationHandler {
        private Object realObj;

        public SimpleInvocationHandler(Object realObj) {
            this.realObj = realObj;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("entering " + method.getName());
            Object result = method.invoke(realObj, args);
            System.out.println("leaving " + method.getName());
            return result;
        }
    }

    public static void main(String[] args) {
        IService realService = new RealService();
        IService proxyService = (IService) Proxy.newProxyInstance(IService.class.getClassLoader(),
                new Class[]{IService.class}, new SimpleInvocationHandler(realService));
        proxyService.sayHello();
    }

}

代码看起来更为复杂了,这有什么用呢?别着急,我们慢慢解释。IService和Real-Service的定义不变,程序的输出也没变,但代理对象proxyService的创建方式变了,它使用java.lang.reflect包中的Proxy类的静态方法newProxyInstance来创建代理对象,这个方法的声明如下:

java 复制代码
public static Object newProxyInstance(ClassLoader loader,
    Class<? >[] interfaces, InvocationHandler h)

它有三个参数,具体如下。

  1. loader表示类加载器
  2. interfaces表示代理类要实现的接口列表,是一个数组,元素的类型只能是接口,不能是普通的类,例子中只有一个IService。
  3. h的类型为InvocationHandler,它是一个接口,也定义在java.lang.reflect包中,它只定义了一个方法invoke,对代理接口所有方法的调用都会转给该方法。

newProxyInstance的返回值类型为Object,可以强制转换为interfaces数组中的某个接口类型。这里我们强制转换为了IService类型,需要注意的是,它不能强制转换为某个类类型,比如RealService,即使它实际代理的对象类型为RealService。

SimpleInvocationHandler实现了InvocationHandler,它的构造方法接受一个参数realObj表示被代理的对象,invoke方法处理所有的接口调用,它有三个参数:

  1. proxy表示代理对象本身,需要注意,它不是被代理的对象,这个参数一般用处不大。
  2. method表示正在被调用的方法。
  3. args表示方法的参数。

在SimpleInvocationHandler的invoke实现中,我们调用了method的invoke方法,传递了实际对象realObj作为参数,达到了调用实际对象对应方法的目的,在调用任何方法前后,我们输出了跟踪调试语句。需要注意的是,不能将proxy作为参数传递给method. invoke,比如:

java 复制代码
Object result = method.invoke(proxy, args);

上面的语句会出现死循环,因为proxy表示当前代理对象,这又会调用到SimpleIn-vocationHandler的invoke方法。

2.2、基本原理

Proxy.newProxyInstance的内部:

java 复制代码
Class<? > proxyCls = Proxy.getProxyClass(IService.class.getClassLoader(),
        new Class<? >[] { IService.class });
Constructor<? > ctor = proxyCls.getConstructor(
    new Class<? >[] { InvocationHandler.class });
InvocationHandler handler = new SimpleInvocationHandler(realService);
IService proxyService = (IService) ctor.newInstance(handler);
  1. 通过Proxy.getProxyClass创建代理类定义,类定义会被缓存;
  2. 获取代理类的构造方法,构造方法有一个InvocationHandler类型的参数;
  3. 创建InvocationHandler对象,创建代理类对象。

Proxy.getProxyClass需要两个参数:一个是ClassLoader;另一个是接口数组。它会动态生成一个类,类名以$Proxy开头,后跟一个数字。对于上面的例子,动态生成的类定义如代码所示,为简化起见,我们忽略了异常处理的代码。

java 复制代码
final class $Proxy0 extends Proxy implements
        SimpleJDKDynamicProxyDemo.IService {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
    public $Proxy0(InvocationHandler paramInvocationHandler) {
        super(paramInvocationHandler);
    }
    public final boolean equals(Object paramObject) {
        return((Boolean) this.h.invoke(this, m1,
                new Object[] { paramObject })).booleanValue();
    }
    public final void sayHello() {
        this.h.invoke(this, m3, null);
    }
    public final String toString() {
        return (String) this.h.invoke(this, m2, null);
    }
    public final int hashCode() {
        return ((Integer) this.h.invoke(this, m0, null)).intValue();
      }
      static {
          m1 = Class.forName("java.lang.Object").getMethod("equals",
                  new Class[] { Class.forName("java.lang.Object") });
          m3 = Class.forName(
                  "laoma.demo.proxy.SimpleJDKDynamicProxyDemo$IService")
                  .getMethod("sayHello", new Class[0]);
          m2 = Class.forName("java.lang.Object")
                  .getMethod("toString", new Class[0]);
          m0 = Class.forName("java.lang.Object")
                  .getMethod("hashCode", new Class[0]);
      }
  }

Proxy0的父类是Proxy,它有一个构造方法,接受一个InvocationHandler类型的参数,保存为了实例变量h, h定义在父类Proxy中,它实现了接口IService,对于每个方法,如sayHello,它调用InvocationHandler的invoke方法,对于Object中的方法,如hash-Code、equals和toString, Proxy0同样转发给了InvocationHandler。

可以看出,这个类定义本身与被代理的对象没有关系,与InvocationHandler的具体实现也没有关系,而主要与接口数组有关,给定这个接口数组,它动态创建每个接口的实现代码,实现就是转发给InvocationHandler,与被代理对象的关系以及对它的调用由InvocationHandler的实现管理。

我们是怎么知道$Proxy0的定义的呢?对于Oracle的JVM,可以配置java的一个属性得到,比如:

java 复制代码
java  -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true  shuo.laoma.dynamic.c86.
    SimpleJDKDynamicProxyDemo

以上命令会把动态生成的代理类 P r o x y 0 保存到文件 Proxy0保存到文件 Proxy0保存到文件Proxy0.class中,通过一些反编译器工具比如JD-GUI(http://jd.benow.ca/)就可以得到源码。

2.3、动态代理的优点

相比静态代理,动态代理看起来麻烦了很多,它有什么好处呢?使用动态代理,可以编写通用的代理逻辑,用于各种类型的被代理对象,而不需要为每个被代理的类型都创建一个静态代理类。看个简单的示例,如代码所示。

java 复制代码
public class GeneralProxyDemo {
    static interface IServiceA {
        public void sayHello();
    }
      static class ServiceAImpl implements IServiceA {
          @Override
          public void sayHello() {
              System.out.println("hello");
          }
      }
      static interface IServiceB {
          public void fly();
      }
      static class ServiceBImpl implements IServiceB {
          @Override
          public void fly() {
              System.out.println("flying");
          }
      }
      static class SimpleInvocationHandler implements InvocationHandler {
          private Object realObj;
          public SimpleInvocationHandler(Object realObj) {
              this.realObj = realObj;
          }
          @Override
          public Object invoke(Object proxy, Method method, Object[] args)
                  throws Throwable {
              System.out.println("entering " + realObj.getClass()
                  .getSimpleName() + "::" + method.getName());
              Object result = method.invoke(realObj, args);
              System.out.println("leaving " + realObj.getClass()
                  .getSimpleName() + "::" + method.getName());
              return result;
          }
      }
      private static <T> T getProxy(Class<T> intf, T realObj) {
          return (T) Proxy.newProxyInstance(intf.getClassLoader(),
                  new Class<? >[] { intf }, new SimpleInvocationHandler(realObj));
      }
      public static void main(String[] args) throws Exception {
          IServiceA a = new ServiceAImpl();
          IServiceA aProxy = getProxy(IServiceA.class, a);
          aProxy.sayHello();
          IServiceB b = new ServiceBImpl();
          IServiceB bProxy = getProxy(IServiceB.class, b);
          bProxy.fly();
      }
  }

在这个例子中,有两个接口IServiceA和IServiceB,它们对应的实现类是Service-AImpl和ServiceBImpl,虽然它们的接口和实现不同,但利用动态代理,它们可以调用同样的方法getProxy获取代理对象,共享同样的代理逻辑SimpleInvocationHandler,即在每个方法调用前后输出一条跟踪调试语句。程序输出为:

java 复制代码
entering ServiceAImpl::sayHello
hello
leaving ServiceAImpl::sayHello
entering ServiceBImpl::fly
flying
leaving ServiceBImpl::fly

3、cglib动态代理

Java SDK动态代理的局限在于,它只能为接口创建代理,返回的代理对象也只能转换到某个接口类型,如果一个类没有接口,或者希望代理非接口中定义的方法,那就没有办法了。有一个第三方的类库cglib(https://github.com/cglib/cglib)​,可以做到这一点,Spring、Hibernate等都使用该类库。我们看个简单的例子,如代码所示。

java 复制代码
public class SimpleCGLibDemo {
    static class RealService {
        public void sayHello() {
            System.out.println("hello");
        }
    }
    static class SimpleInterceptor implements MethodInterceptor {
        @Override
        public Object intercept(Object object, Method method,
                Object[] args, MethodProxy proxy) throws Throwable {
            System.out.println("entering " + method.getName());
            Object result = proxy.invokeSuper(object, args);
            System.out.println("leaving " + method.getName());
            return result;
        }
    }
      private static <T> T getProxy(Class<T> cls) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(cls);
        enhancer.setCallback(new SimpleInterceptor());
        return (T) enhancer.create();
    }
    public static void main(String[] args) throws Exception {
        RealService proxyService = getProxy(RealService.class);
        proxyService.sayHello();
    }
}

RealService表示被代理的类,它没有接口。getProxy()为一个类生成代理对象,这个代理对象可以安全地转换为被代理类的类型,它使用了cglib的Enhancer类。Enhancer类的setSuperclass设置被代理的类,setCallback设置被代理类的public非final方法被调用时的处理类。Enhancer支持多种类型,这里使用的类实现了MethodInterceptor接口,它与Java SDK中的InvocationHandler有点类似,方法名称变成了intercept,多了一个MethodProxy类型的参数。

与前面的InvocationHandler不同,SimpleInterceptor中没有被代理的对象,它通过MethodProxy的invokeSuper方法调用被代理类的方法:

java 复制代码
Object result = proxy.invokeSuper(object, args);

注意,它不能这样调用被代理类的方法:

java 复制代码
Object result = method.invoke(object, args);

object是代理对象,调用这个方法还会调用到SimpleInterceptor的intercept方法,造成死循环。

在main方法中,我们也没有创建被代理的对象,创建的对象直接就是代理对象。

cglib的实现机制与Java SDK不同,它是通过继承实现的,它也是动态创建了一个类,但这个类的父类是被代理的类,代理类重写了父类的所有public非final方法,改为调用Callback中的相关方法,在上例中,调用SimpleInterceptor的intercept方法。

4、Java SDK代理与cglib代理比较

Java SDK代理面向的是一组接口,它为这些接口动态创建了一个实现类。接口的具体实现逻辑是通过自定义的InvocationHandler实现的,这个实现是自定义的,也就是说,其背后都不一定有真正被代理的对象,也可能有多个实际对象,根据情况动态选择。cglib代理面向的是一个具体的类,它动态创建了一个新类,继承了该类,重写了其方法。

从代理的角度看,Java SDK代理的是对象,需要先有一个实际对象,自定义的InvocationHandler引用该对象,然后创建一个代理类和代理对象,客户端访问的是代理对象,代理对象最后再调用实际对象的方法;cglib代理的是类,创建的对象只有一个。

如果目的都是为一个类的方法增强功能,Java SDK要求该类必须有接口,且只能处理接口中的方法,cglib没有这个限制。

5、动态代理的应用:AOP

5.1、用法

我们添加一个新的注解@Aspect,其定义为:

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {
    Class<?>[] value();
}

它用于注解切面类,它有一个参数,可以指定要增强的类,比如:

java 复制代码
@Aspect({ServiceA.class, ServiceB.class})
public class ServiceLogAspect

ServiceLogAspect就是一个切面,它负责类ServiceA和ServiceB的日志切面,即为这两个类增加日志功能。再如:

java 复制代码
@Aspect({ServiceB.class})
public class ExceptionAspect

ExceptionAspect也是一个切面,它负责类ServiceB的异常切面。

这些切面类与主体类怎么协作呢?我们约定,切面类可以声明三个方法before/after/exception,在主体类的方法调用前/调用后/出现异常时分别调用这三个方法,这三个方法的声明需符合如下签名:

java 复制代码
public static void before(Object object, Method method, Object[] args)
public static void after(Object object, Method method,
    Object[] args, Object result)
public static void exception(Object object, Method method,
    Object[] args, Throwable e)

object、method和args与cglib MethodInterceptor中的invoke参 数 一 样,after中 的result表示方法执行的结果,exception中的e表示发生的异常类型。

ServiceLogAspect实现了before和after方法,加了一些日志,如代码所示。

java 复制代码
@Aspect({ServiceA.class, ServiceB.class})
public class ServiceLogAspect {
    public static void before(Object object, Method method, Object[] args) {
        System.out.println("entering " + method.getDeclaringClass().getSimpleName()
            + "::" + method.getName() + ", args:" + Arrays.toString(args));
    }
    
    public static void after(Object object, Method method, Object[] args, Object result) {
        System.out.println("leaving " + method.getDeclaringClass().getSimpleName()
            + "::" + method.getName() + ", result:" + result);
    }
}

ExceptionAspect只实现exception方法,在异常发生时,输出一些信息,如代码所示。

java 复制代码
@Aspect({ServiceB.class})
public class ExceptionAspect {
    public static void exception(Object object, Method method, Object[] args, Throwable e) {
        System.out.println("exception when calling: " + method.getName() 
            + ", " + Arrays.toString(args));
    }
}

ServiceLogAspect的目的是在类ServiceA和ServiceB所有方法的执行前后加一些日志,而ExceptionAspect的目的是在类ServiceB的方法执行出现异常时收到通知并输出一些信息。它们都没有修改类ServiceA和ServiceB本身,本身做的事是比较通用的,与ServiceA和ServiceB的具体逻辑关系也不密切,但又想改变ServiceA/ServiceB的行为,这就是AOP的思维。

只是声明一个切面类是不起作用的,我们需要与DI容器结合起来。我们实现一个新的容器CGLibContainer,它有一个方法:

java 复制代码
public static <T> T getInstance(Class<T> cls)

通过该方法获取ServiceA或ServiceB,它们的行为就会被改变,ServiceA和ServiceB的定义如下:

java 复制代码
public class ServiceA {
    ServiceB b;
    public void callB() {
        b.action();
    }
}

public class ServiceB {
    public void action() {
        System.out.println("I'm B");
    }
}

通过CGLibContainer获取ServiceA,会自动应用ServiceLogAspect,比如:

java 复制代码
ServiceA a = CGLibContainer.getInstance(ServiceA.class);
a.callB();

输出为:

java 复制代码
entering ServiceA::callB, args: []
entering ServiceB::action, args: []
I'm B
leaving ServiceB::action, result: null
leaving ServiceA::callB, result: null

5.2、实现原理

这是怎么做到的呢?CGLibContainer在初始化的时候,会分析带有@Aspect注解的类,分析出每个类的方法在调用前/调用后/出现异常时应该调用哪些方法,在创建该类的对象时,如果有需要被调用的方法,则创建一个动态代理对象,下面我们具体来看下代码。

为简化起见,即每次获取对象时都创建一个,不支持单例。我们定义一个枚举InterceptPoint,表示切点(调用前/调用后/出现异常)​:

java 复制代码
public static enum InterceptPoint {
    BEFORE, AFTER, EXCEPTION
}

在CGLibContainer中定义一个静态变量,表示每个类的每个切点的方法列表,定义如下:

java 复制代码
static Map<Class<? >, Map<InterceptPoint, List<Method>>> interceptMethodsMap
    = new HashMap<>();

我们在CGLibContainer的类初始化过程中初始化该对象,方法是分析每个带有@Aspect注解的类,这些类一般可以通过扫描所有的类得到,为简化起见,我们将它们写在代码中,如下所示:

java 复制代码
static Class<? >[] aspects = new Class<? >[] {
    ServiceLogAspect.class, ExceptionAspect.class };

分析这些带@Aspect注解的类,并初始化interceptMethodsMap的代码如下所示:

java 复制代码
static {
   init();
}
private static void init() {
   for(Class<? > cls : aspects) {
       Aspect aspect = cls.getAnnotation(Aspect.class);
       if(aspect ! = null) {
           Method before = getMethod(cls, "before", new Class<? >[] {
               Object.class, Method.class, Object[].class });
           Method after = getMethod(cls, "after", new Class<? >[] {
               Object.class, Method.class, Object[].class, Object.class });
           Method exception = getMethod(cls, "exception", new Class<? >[] {
               Object.class, Method.class, Object[].class, Throwable.class });
           Class<? >[] intercepttedArr = aspect.value();
           for(Class<? > interceptted : intercepttedArr) {
               addInterceptMethod(interceptted,
                         InterceptPoint.BEFORE, before);
               addInterceptMethod(interceptted, InterceptPoint.AFTER, after);
               addInterceptMethod(interceptted,
                         InterceptPoint.EXCEPTION, exception);
           }
       }
   }
}

对每个切面,即带有@Aspect注解的类cls,查找其before/after/exception方法,调用方法addInterceptMethod将其加入目标类的切点方法列表中,addInterceptMethod的代码为:

java 复制代码
private static void addInterceptMethod(Class<? > cls,
       InterceptPoint point, Method method) {
   if(method == null) {
       return;
   }
     Map<InterceptPoint, List<Method>> map = interceptMethodsMap.get(cls);
     if(map == null) {
         map = new HashMap<>();
         interceptMethodsMap.put(cls, map);
     }
     List<Method> methods = map.get(point);
     if(methods == null) {
         methods = new ArrayList<>();
         map.put(point, methods);
     }
     methods.add(method);
 }

准备好了每个类的每个切点的方法列表,我们来看根据类型创建实例的代码:

java 复制代码
private static <T> T createInstance(Class<T> cls)
        throws InstantiationException, IllegalAccessException {
    if(! interceptMethodsMap.containsKey(cls)) {
        return (T) cls.newInstance();
    }
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(cls);
    enhancer.setCallback(new AspectInterceptor());
    return (T) enhancer.create();
}

如果类型cls不需要增强,则直接调用cls.newInstance(),否则使用cglib创建动态代理,callback为AspectInterceptor,其代码为:

java 复制代码
static class AspectInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method,
            Object[] args, MethodProxy proxy) throws Throwable {
        //执行before方法
        List<Method> beforeMethods = getInterceptMethods(
                object.getClass().getSuperclass(), InterceptPoint.BEFORE);
        for(Method m : beforeMethods) {
            m.invoke(null, new Object[] { object, method, args });
        }
        try {
            //调用原始方法
            Object result = proxy.invokeSuper(object, args);
            //执行after方法
            List<Method> afterMethods = getInterceptMethods(
                    object.getClass().getSuperclass(), InterceptPoint.AFTER);
            for(Method m : afterMethods) {
                m.invoke(null, new Object[] { object, method, args, result });
            }
            return result;
        } catch (Throwable e) {
            //执行exception方法
List<Method> exceptionMethods = getInterceptMethods(
    object.getClass().getSuperclass(), InterceptPoint.EXCEPTION);
for(Method m : exceptionMethods) {
    m.invoke(null, new Object[] { object, method, args, e });
}
throw e;
}
}
}

这段代码也容易理解,它根据原始类的实际类型查找应该执行的before/after/exception方法列表,在调用原始方法前执行before方法,执行后执行after方法,出现异常时执行exception方法。getInterceptMethods方法的代码为:

java 复制代码
static List<Method> getInterceptMethods(Class<? > cls,
       InterceptPoint point) {
   Map<InterceptPoint, List<Method>> map = interceptMethodsMap.get(cls);
   if(map == null) {
       return Collections.emptyList();
   }
   List<Method> methods = map.get(point);
   if(methods == null) {
       return Collections.emptyList();
   }
   return methods;
}

这段代码也容易理解。CGLibContainer最终的getInstance方法就简单了,它调用create-Instance创建实例,代码如下所示:

java 复制代码
public static <T> T getInstance(Class<T> cls) {
    try {
        T obj = createInstance(cls);
        Field[] fields = cls.getDeclaredFields();
        for(Field f : fields) {
            if(f.isAnnotationPresent(SimpleInject.class)) {
                if(! f.isAccessible()) {
                    f.setAccessible(true);
                }
                Class<? > fieldCls = f.getType();
                f.set(obj, getInstance(fieldCls));
            }
        }
        return obj;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
相关推荐
存在morning1 小时前
【GO语言开发实践】一 GO 语法快速上手
开发语言·python·golang
晨曦中的暮雨1 小时前
Python 并发模型理解:GIL、线程、async 到底是什么关系
开发语言·python
星恒随风1 小时前
四天学完前端基础三件套(JavaScript webAPI篇)
开发语言·前端·javascript
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第59题】【JVM篇】第19题:并发标记过程中会出现什么问题?
java·开发语言·jvm
平行侠1 小时前
40希尔排序 - 以递减间距进行插入排序
java·算法·排序算法
摇滚侠1 小时前
Mybatis 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·开发语言·mybatis
林熙蕾LXL1 小时前
进程处理操作
开发语言·c++·算法
淘矿人1 小时前
Claude助力前端开发
java·数据库·git·python·sql·spring·database
砍材农夫1 小时前
物联网 基于netty心跳和ack机制
java·物联网·netty