IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring的动态代理原理 -> 正文阅读

[Java知识库]Spring的动态代理原理

Spring动态代理原理

Spring动态代理有两种:CGLIB动态代理和JDK动态代理。

JDK动态代理可以代理接口,不能代理没有实现接口的类;而CGLIB通过字节码技术可以动态生成被代理类的子类,从而可以代理没有实现接口的类;但是不能代理非public和final的方法,因为子类不可见。

Spring框架中使用DefaultAopProxyFactory来确定使用CGLIB或者JDK的动态代理

一般满足以下条件之一 就会进行CGLIB动态代理:

  • optimize标志位被设定
  • proxyTargetClass标志位被设定
  • 设置了不代理接口

除此之外使用JDK动态代理

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (!NativeDetector.inNativeImage() &&(config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                // 目标对象为接口或者是代理类时,使用JDK动态代理
				return new JdkDynamicAopProxy(config);
			}
            // 使用CGLIB动态代理
			return new ObjenesisCglibAopProxy(config);
		} else {
			return new JdkDynamicAopProxy(config);
		}
	}

	/**
	 * 确定代理的是不是接口
	 */
	private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        // 获取代理的接口
		Class<?>[] ifcs = config.getProxiedInterfaces();
		return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
	}
}

CGLIB动态代理

生成代理对象

在这里插入图片描述

ProxyFactory主要用来创建AOP代理,它继承了ProxyCreatorSupport,它的getProxy方法可以创建动态代理,并获取代理实例

public Object getProxy() {
    // 调用父类ProxyCreatorSupport的方法
   return createAopProxy().getProxy();
}

ProxyCreatorSupport是代理工厂的基类,通过他可以比较方便的访问可配置的AopProxyFactory

protected final synchronized AopProxy createAopProxy() {
   if (!this.active) {
      activate();
   }
    // 调用DefaultAopProxyFactory#createAopProxy创建AOP代理
   return getAopProxyFactory().createAopProxy(this);
}

ProxyCreatorSupport#createAopProxy方法会调用DefaultAopProxyFactory#createAopProxy创建AOP代理,他会根据入参AdvisedSupport选择使用JDK或者CGLIB动态代理;

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
   if (!NativeDetector.inNativeImage() &&
         (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
      Class<?> targetClass = config.getTargetClass();
      if (targetClass == null) {
         throw new AopConfigException("TargetSource cannot determine target class: " +
               "Either an interface or a target is required for proxy creation.");
      }
      if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { 
          // JDK动态代理
         return new JdkDynamicAopProxy(config);
      }
       // CGLIB动态代理
      return new ObjenesisCglibAopProxy(config);
   } else { // JDK动态代理
      return new JdkDynamicAopProxy(config);
   }
}

Spring AOP实现CGLIB动态代理主要的实现类是CglibAopProxy。首先DefaultAopProxyFactory#createAopProxy会调用CglibAopProxy构造方法创建CglibAopProxy实例

public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
    // 必要的校验
   Assert.notNull(config, "AdvisedSupport must not be null");
   if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
      throw new AopConfigException("No advisors and no TargetSource specified");
   }
    // 设置通知配置信息
   this.advised = config;
    // 设置通知调度器
   this.advisedDispatcher = new AdvisedDispatcher(this.advised);
}

ProxyFactory创建CglibAopProxy之后,接着调用它的getProxy方法创建代理。 由于CglibAopProxy实现了AopProxy接口,因此需要实现它的getProxy方法。在该方法中会获取代理类的信息,然后创建Enhancer并配置相关信息,再通过createProxyClassAndInstance方法来创建代理类和实例;

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
   if (logger.isTraceEnabled()) {
      logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
   }

   try {
       // 获取要代理的类
      Class<?> rootClass = this.advised.getTargetClass();
      Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

       // 因为要生成代理类的子类,所以这里为proxySuperClass
      Class<?> proxySuperClass = rootClass;
       // CGLIB_CLASS_SEPARATOR=$$
      if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
          // 获取代理类的父类
         proxySuperClass = rootClass.getSuperclass();
          // 获取附加接口后将其加入到advise中
         Class<?>[] additionalInterfaces = rootClass.getInterfaces();
         for (Class<?> additionalInterface : additionalInterfaces) {
            this.advised.addInterface(additionalInterface);
         }
      }

      // 校验类信息
      validateClassIfNecessary(proxySuperClass, classLoader);

      // 配置 CGLIB Enhancer
      Enhancer enhancer = createEnhancer();
      if (classLoader != null) {
         enhancer.setClassLoader(classLoader);
         if (classLoader instanceof SmartClassLoader &&
               ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
            enhancer.setUseCache(false);
         }
      }
      enhancer.setSuperclass(proxySuperClass);
      enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
      enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
      enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

       // 获取回调,并将其Class复制到types数组中
      Callback[] callbacks = getCallbacks(rootClass);
      Class<?>[] types = new Class<?>[callbacks.length];
      for (int x = 0; x < types.length; x++) {
         types[x] = callbacks[x].getClass();
      }
      // Encher设置回调过滤器和回调类型
      enhancer.setCallbackFilter(new ProxyCallbackFilter(
            this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
      enhancer.setCallbackTypes(types);

       // 产生代理类并创建代理实例
      return createProxyClassAndInstance(enhancer, callbacks);
   } catch (CodeGenerationException | IllegalArgumentException ex) { // final类或非public类,都不会创建代理
      throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
            ": Common causes of this problem include using a final class or a non-visible class",
            ex);
   } catch (Throwable ex) {
      // TargetSource.getTarget() failed
      throw new AopConfigException("Unexpected AOP exception", ex);
   }
}

getProxy方法中会获取回调getCallbacks,该方法会设置AOP拦截器、目标拦截器、通知调度器等作为回调

private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
    // 用来设置优化的参数
   boolean exposeProxy = this.advised.isExposeProxy();
   boolean isFrozen = this.advised.isFrozen();
   boolean isStatic = this.advised.getTargetSource().isStatic();

    // AOP代理拦截器:DynamicAdvisedInterceptor,拦截AOP的调用
   Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
    // 目标拦截器
   Callback targetInterceptor;
    // 根据exposeProxy参数设置不同的目标拦截器
   if (exposeProxy) {
      targetInterceptor = (isStatic ?
            new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) :
            new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()));
   } else {
      targetInterceptor = (isStatic ?
            new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) :
            new DynamicUnadvisedInterceptor(this.advised.getTargetSource()));
   }

    // 目标调度器
   Callback targetDispatcher = (isStatic ?
         new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp());

    // 主要的回调,拦截器数组
   Callback[] mainCallbacks = new Callback[] {
         aopInterceptor,  // 正常的拦截器
         targetInterceptor,  // 如果被优化,可以不考虑advice直接调用目标
         new SerializableNoOp(),  // 不覆写映射到this的方法
         targetDispatcher, 
         this.advisedDispatcher,
         new EqualsInterceptor(this.advised),
         new HashCodeInterceptor(this.advised)
   };

   Callback[] callbacks;

    // 如果目标是静态的并且advice链被冻结了,我们可以通过使用那个方法的固定链直接调用目标对象,以此来优化调用
   if (isStatic && isFrozen) {
      Method[] methods = rootClass.getMethods();
      Callback[] fixedCallbacks = new Callback[methods.length];
      this.fixedInterceptorMap = CollectionUtils.newHashMap(methods.length);
       
       // 一些小的内存优化(可以通过无advice来跳过方法的创建)
      for (int x = 0; x < methods.length; x++) {
         Method method = methods[x];
         List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, rootClass);
         fixedCallbacks[x] = new FixedChainStaticTargetInterceptor(
               chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass());
         this.fixedInterceptorMap.put(method, x);
      }
       
       // 把mainCallbacks和fixedCallbacks复制到callbacks数组
      callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length];
      System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length);
      System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length);
      this.fixedInterceptorOffset = mainCallbacks.length;
   } else {
      callbacks = mainCallbacks;
   }
   return callbacks;
}

ObjenesisCglibAopProxy是CglibAopProxy的子类,它覆写了其 createProxyClassAndInstance方法。该方法里面首先通过enhancer创建代理类,再创建代理类的实例对象

protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    // 利用enhancer创建代理类
   Class<?> proxyClass = enhancer.createClass();
   Object proxyInstance = null;
    
    // 如果在当前JVM上,配置的Objenesis实例化策略已经被识别为不工作或者spring.objenesis.ignore属性被设置为true,objenesis.isWorthTrying()会返回false
   if (objenesis.isWorthTrying()) {
      try {
          // 使用objenesis实例化策略进行实例化
         proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
      } catch (Throwable ex) {
         logger.debug("Unable to instantiate proxy using Objenesis, falling back to regular proxy construction", ex);
      }
   }

   if (proxyInstance == null) {
       // 通过默认构造器实例化
      try {
          // 根据参数获取有参或者是无参构造器
         Constructor<?> ctor = (this.constructorArgs != null ?
               proxyClass.getDeclaredConstructor(this.constructorArgTypes) :
               proxyClass.getDeclaredConstructor());
          // 设置构造器为可访问
         ReflectionUtils.makeAccessible(ctor);
          // 使用构造器实例化
         proxyInstance = (this.constructorArgs != null ? ctor.newInstance(this.constructorArgs) : ctor.newInstance());
      } catch (Throwable ex) {
         throw new AopConfigException("Unable to instantiate proxy using Objenesis, " +
               "and regular proxy instantiation via default constructor fails as well", ex);
      }
   }
   // 设置回调
   ((Factory) proxyInstance).setCallbacks(callbacks);
   return proxyInstance;
}

目标方法的拦截

CglibAopProxy的静态内部类DynamicAdvisedInterceptor实现了MethodInterceptor接口,它的intercept方法会在对目标方法进行调用时拦截方法,获取动态代理实例,进行目标方法的调用

private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
    
   @Override
   @Nullable
   public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      Object oldProxy = null;
      boolean setProxyContext = false;
      Object target = null;
      TargetSource targetSource = this.advised.getTargetSource();
      try {
         if (this.advised.exposeProxy) {
            // Make invocation available if necessary.
            oldProxy = AopContext.setCurrentProxy(proxy);
            setProxyContext = true;
         }
         // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
         target = targetSource.getTarget();
          // 获取目标类
         Class<?> targetClass = (target != null ? target.getClass() : null);
          // 获取拦截器和动态拦截advice
         List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
         Object retVal;
          // 检查是否拥有一个调用拦截器InvokerInterceptor:即,没有实际的advice,但是仅仅反射目标对象的调用。这种情况直接调用目标
         if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
             // 适配参数。可以跳过创建一个MethodInvocation:直接调用目标。
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
             // 调用目标方法
            retVal = methodProxy.invoke(target, argsToUse);
         } else {
             // 一般都会 创建CglibMethodInvocation并调用目标方法
            retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
         }
          // 处理返回的结果
         retVal = processReturnType(proxy, target, method, retVal);
         return retVal;
      } finally {
          // 释放目标对象
         if (target != null && !targetSource.isStatic()) { 
            targetSource.releaseTarget(target);
         }
          // 恢复老的代理
         if (setProxyContext) {
            // Restore old proxy.
            AopContext.setCurrentProxy(oldProxy);
         }
      }
   }
}

一般都会创建CglibMethodInvocation并调用它的proceed方法,CglibMethodInvocation也是CglibAopProxy的静态内部类,它继承了ReflectiveMethodInvocation,会调用父类的proceed方法处理目标方法调用,继承关系如下:
image-20220509200251854

public Object proceed() throws Throwable {
    try {
        // 调用父类ReflectiveMethodInvocation方法
        return super.proceed();
    } catch (RuntimeException ex) { // 运行时异常直接抛出
        throw ex;
    } catch (Exception ex) { 
        if (ReflectionUtils.declaresException(getMethod(), ex.getClass()) ||
            KotlinDetector.isKotlinType(getMethod().getDeclaringClass())) {
            // Propagate original exception if declared on the target method
            // (with callers expecting it). Always propagate it for Kotlin code
            // since checked exceptions do not have to be explicitly declared there.
            throw ex;
        } else {
            // Checked exception thrown in the interceptor but not declared on the
            // target method signature -> apply an UndeclaredThrowableException,
            // aligned with standard JDK dynamic proxy behavior.
            throw new UndeclaredThrowableException(ex);
        }
    }
}

? 父类ReflectiveMethodInvocation#proceed方法,其实就是采用的责任链模式,递归遍历目标方法的所有拦截器,然后再调用方法本身

public Object proceed() throws Throwable {
    // index从-1开始,递归调用过程中会累加
   if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
       // 调用连接点
      return invokeJoinpoint();
   }

    // 获取拦截器或者拦截advice
   Object interceptorOrInterceptionAdvice =
         this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    // 如果是拦截器和动态方法匹配器
   if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
       // 转化为指定的类型
      InterceptorAndDynamicMethodMatcher dm =
            (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
      Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
      if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
          // 匹配上了,调用方法
         return dm.interceptor.invoke(this);
      } else {
          // 没有匹配上,递归下一个
         return proceed();
      }
   } else {
       // 方法拦截器,可以调用它。Spring的事务拦截器就是在这里调用的
      return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
   }
}

JDK动态代理

生成代理对象

Spring AOP的JDK动态代理只能代理接口,它和CGLIB动态代理都是使用DefaultAopProxyFactory#createAopProxy创建AOP代理;

public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
    // 必要的校验
   Assert.notNull(config, "AdvisedSupport must not be null");
   if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
      throw new AopConfigException("No advisors and no TargetSource specified");
   }
    // 通知配置
   this.advised = config;
    // 获取完整的代理接口,会额外增加Advised,SpringProxy等接口的代理
   this.proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
    // 根据equals和hashCode寻找代理的接口
   findDefinedEqualsAndHashCodeMethods(this.proxiedInterfaces);
}

不同的是,JDK动态代理的的实现类是JdkDynamicAopProxy;
在这里插入图片描述

如上图,JdkDynamicAopProxy实现了InvocationHandler和AopProxy接口,AopProxy#getProxy可以生成代理实例,InvocationHandler#invoke拦截目标方法,并发起调用;先来看看getProxy方法获取代理实例;

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
    if (logger.isTraceEnabled()) {
        logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
    }
    // 生成代理实例
    return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
}

Proxy#newProxyInstance方法可以根据获取当前代理类的构造方法,然后调用它的构造方法完成对象的实例化

@CallerSensitive
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);

    // 使用指定的调用handler调用它的构造器
    try {
        // 安全校验
        if (sm != null) {
            checkNewProxyPermission(Reflection.getCallerClass(), cl);
        }

        // 获取构造方法
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
        if (!Modifier.isPublic(cl.getModifiers())) { // 非public方法,授权访问
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    cons.setAccessible(true);
                    return null;
                }
            });
        }
        // 使用构造方法构造实例对象
        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);
    }
}

目标方法的拦截

当调用目标方法时,JdkDynamicAopProxy#invoke会识别目标方法,然后获取它的拦截器链,调用拦截链,然后调用方法本身

@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   Object oldProxy = null;
   boolean setProxyContext = false;

   TargetSource targetSource = this.advised.targetSource;
   Object target = null;

   try {
      if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
          // 目标对象没有自己实现equals
         return equals(args[0]);
      } else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
          // 目标对象没有自己实现hashCode
         return hashCode();
      } else if (method.getDeclaringClass() == DecoratingProxy.class) {
          // 声明类为DecoratingProxy,返回最终的代理对象
         return AopProxyUtils.ultimateTargetClass(this.advised);
      } else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
            method.getDeclaringClass().isAssignableFrom(Advised.class)) {
          // 使用代理配置在ProxyConfig上的服务调用,通过反射调用目标对象
         return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
      }

      Object retVal;

      if (this.advised.exposeProxy) {
         // 使调用可用
         oldProxy = AopContext.setCurrentProxy(proxy);
         setProxyContext = true;
      }

       // 获取目标类
      target = targetSource.getTarget();
      Class<?> targetClass = (target != null ? target.getClass() : null);

       // 获取当前方法对的拦截链(实现了MethodInterceptor的拦截器设置了该方法作为切入点)
      List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

 
      if (chain.isEmpty()) { //当前方法的拦截链为空
          // 获取目标方法参数之后使用反射调用方法
         Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
         retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
      } else { // 当前方法有拦截链——》拦截器存在
          // 创建方法的调用对象
         MethodInvocation invocation =
               new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
          // 通过拦截链处理到连接点
         retVal = invocation.proceed();
      }

       // 获取返回类型
      Class<?> returnType = method.getReturnType();
      if (retVal != null && retVal == target && returnType != Object.class && returnType.isInstance(proxy) &&
            !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
         retVal = proxy;
      } else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
         throw new AopInvocationException("Null return value from advice does not match primitive return type for: " + method);
      }
      return retVal;
   } finally {
      if (target != null && !targetSource.isStatic()) {
         // Must have come from TargetSource.
         targetSource.releaseTarget(target);
      }
      if (setProxyContext) {
         // Restore old proxy.
         AopContext.setCurrentProxy(oldProxy);
      }
   }
}

ReflectiveMethodInvocation#proceed的处理过程和CGLIB一样,会根据责任链模式,递归遍历所有的方法拦截器,然后调用方法本身

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-10 11:43:27  更:2022-05-10 11:43:43 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 23:07:24-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码