【Spring】AbstractApplicationContext源码解读

这个类源码一打开,我滴妈有一种我不卷了,我送外卖去了的感觉1500行····

但是还是对你自己说 坚持坚持再坚持。35岁再送外卖也不迟。

这里我们先看这个抽象类的注释:

Abstract implementation of the ApplicationContext interface. Doesn't mandate the type of storage used for configuration; simply implements common context functionality. Uses the Template Method design pattern, requiring concrete subclasses to implement abstract methods.
In contrast to a plain BeanFactory, an ApplicationContext is supposed to detect special beans defined in its internal bean factory: Therefore, this class automatically registers BeanFactoryPostProcessors, BeanPostProcessors, and ApplicationListeners which are defined as beans in the context.
A MessageSource may also be supplied as a bean in the context, with the name "messageSource"; otherwise, message resolution is delegated to the parent context. Furthermore, a multicaster for application events can be supplied as an "applicationEventMulticaster" bean of type ApplicationEventMulticaster in the context; otherwise, a default multicaster of type SimpleApplicationEventMulticaster will be used.
Implements resource loading by extending DefaultResourceLoader. Consequently treats non-URL resource paths as class path resources (supporting full class path resource names that include the package path, e.g. "mypackage/myresource.dat"), unless the getResourceByPath method is overridden in a subclass.
Since:
January 21, 2001
See Also:
refreshBeanFactory, getBeanFactory, BeanFactoryPostProcessor, org.springframework.beans.factory.config.BeanPostProcessor, ApplicationEventMulticaster, ApplicationListener, MessageSource
Author:
Rod Johnson, Juergen Hoeller, Mark Fisher, Stephane Nicoll, Sam Brannen, Sebastien Deleuze, Brian Clozel

翻译:

ApplicationContext接口的抽象实现。不强制要求用于配置的存储类型;简单地实现通用上下文功能。使用模板方法设计模式需要具体的子类来实现抽象方法
与普通BeanFactory相比,ApplicationContext应该检测其内部bean工厂中定义的特殊bean:因此,此类自动注册BeanFactoryPostProcessorsBeanPostProcessorsApplicationListeners,它们在上下文中定义为bean。
MessageSource也可以在上下文中作为bean提供,名称为“MessageSource”;否则,消息解析将委托给父上下文。

此外,用于应用程序事件的多主机可以作为上下文中ApplicationEventMultimaster类型的“applicationEventMultimaster”bean提供;

否则,将使用类型为SimpleApplicationEventMulticaster的默认多播器。
通过扩展DefaultResourceLoader实现资源加载。

因此,将非URL资源路径视为类路径资源(支持包括包路径的完整类路径资源名称,例如“mypackage/myresource.dat”),除非在子类中重写getResourceByPath方法。

这里需要对标红的内容有个基本的印象,后面很多知识围绕这些名称展开。

1.源码阅读(以下为完整源码):

1.1四个类常量属性

看不懂没事,先不管

工厂中MessageSource bean的名称。如果没有提供,则将消息解析委派给父级。
public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource";

工厂中的LifecycleProcessor bean的名称。如果没有提供,则使用DefaultLifecycleProcessor。
public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor";

工厂中ApplicationEventMulticastbean的名称。如果没有提供,则使用默认的SimpleApplicationEventMultimaster。
public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";

由spring.spel.ignore系统属性控制的布尔标志,该属性指示spring忽略spel,即不初始化spel基础结构。
默认值为“false”。
private static final boolean shouldIgnoreSpel = SpringProperties.getFlag("spring.spel.ignore");

1.2.一个静态代码块

看不懂继续跳过

//急切地加载ContextClosedEvent类以避免奇怪的类加载器问题
//关于WebLogic 8.1中的应用程序关闭。(达斯汀·伍兹报道)
static {
    // Eagerly load the ContextClosedEvent class to avoid weird classloader issues
    // on application shutdown in WebLogic 8.1. (Reported by Dustin Woods.)
    ContextClosedEvent.class.getName();
}

1.3.日志打印

/** Logger used by this class. Available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());

1.4.唯一标识这个上下文。这个对象。

/** Unique id for this context, if any. */
private String id = ObjectUtils.identityToString(this);

/** Display name. */
private String displayName = ObjectUtils.identityToString(this);

1.5.两个接口引用,应用上下文接口,可配置环境接口,这里先跳过。有个印象

/** Parent context. */
@Nullable
private ApplicationContext parent;

/** Environment used by this context. */
@Nullable
private ConfigurableEnvironment environment;

1.6.BeanFactoryPostProcessor数组

这里有个BeanFactoryPostProcessor数组,一个bean工厂的后置处理器数组。这里很重要,我们先对BeanFactoryPostProcessor这个名称有个记忆。如果有一点aop动态代理基础的朋友,应该可以联想到前置处理器和后置处理器的东西了。这里先知道这里有个数组。

/** BeanFactoryPostProcessors to apply on refresh. */
private final List<BeanFactoryPostProcessor> beanFactoryPostProcessors = new ArrayList<>();

1.7.状态信息

这里主要是一些对这个上下文定义的一些状态信息。

上下文启动时的系统时间(以毫秒为单位)。
private long startupDate;

上下文当前是否处于活动状态的标志。
private final AtomicBoolean active = new AtomicBoolean();

指示此上下文是否已关闭的标志。
private final AtomicBoolean closed = new AtomicBoolean();

“刷新”和“销毁”的同步监视器。
private final Object startupShutdownMonitor = new Object();

1.8.JVM相关

先不管

对JVM关闭回调的引用(如果已注册)。
@Nullable
private Thread shutdownHook;

1.9.上下文的一些高级应用

看不懂没事,先跳过

此上下文使用的ResourcePatternResolver。
private ResourcePatternResolver resourcePatternResolver;

LifecycleProcessor,用于在此上下文中管理bean的生命周期。
@Nullable
private LifecycleProcessor lifecycleProcessor;

我们将此接口的实现委托给MessageSource。
@Nullable
private MessageSource messageSource;

事件发布中使用的帮助程序类。
@Nullable
private ApplicationEventMulticaster applicationEventMulticaster;

应用程序启动指标。
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;

静态指定的侦听器。
private final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();

在刷新前已注册的本地侦听器。
@Nullable
private Set<ApplicationListener<?>> earlyApplicationListeners;

在多主机安装之前发布的ApplicationEvents。
@Nullable
private Set<ApplicationEvent> earlyApplicationEvents;

1.10. 两个构造方法

不用管

/**
 * 创建一个没有父级的新AbstractApplicationContext。
 */
public AbstractApplicationContext() {
    this.resourcePatternResolver = getResourcePatternResolver();
}

/**
 * 使用给定的父上下文创建一个新的AbstractApplicationContext。
 * @param parent the parent context
 */
public AbstractApplicationContext(@Nullable ApplicationContext parent) {
    this();
    setParent(parent);
}

1.11.一堆set,get方法

先不管

//---------------------------------------------------------------------
// Implementation of ApplicationContext interface
//---------------------------------------------------------------------

/**
 * Set the unique id of this application context.
 * <p>Default is the object id of the context instance, or the name
 * of the context bean if the context is itself defined as a bean.
 * @param id the unique id of the context
 */
@Override
public void setId(String id) {
    this.id = id;
}

@Override
public String getId() {
    return this.id;
}

@Override
public String getApplicationName() {
    return "";
}

/**
 * Set a friendly name for this context.
 * Typically done during initialization of concrete context implementations.
 * <p>Default is the object id of the context instance.
 */
public void setDisplayName(String displayName) {
    Assert.hasLength(displayName, "Display name must not be empty");
    this.displayName = displayName;
}

/**
 * Return a friendly name for this context.
 * @return a display name for this context (never {@code null})
 */
@Override
public String getDisplayName() {
    return this.displayName;
}

/**
 * Return the parent context, or {@code null} if there is no parent
 * (that is, this context is the root of the context hierarchy).
 */
@Override
@Nullable
public ApplicationContext getParent() {
    return this.parent;
}

/**
 * Set the {@code Environment} for this application context.
 * <p>Default value is determined by {@link #createEnvironment()}. Replacing the
 * default with this method is one option but configuration through {@link
 * #getEnvironment()} should also be considered. In either case, such modifications
 * should be performed <em>before</em> {@link #refresh()}.
 * @see org.springframework.context.support.AbstractApplicationContext#createEnvironment
 */
@Override
public void setEnvironment(ConfigurableEnvironment environment) {
    this.environment = environment;
}

/**
 * Return the {@code Environment} for this application context in configurable
 * form, allowing for further customization.
 * <p>If none specified, a default environment will be initialized via
 * {@link #createEnvironment()}.
 */
@Override
public ConfigurableEnvironment getEnvironment() {
    if (this.environment == null) {
       this.environment = createEnvironment();
    }
    return this.environment;
}

/**
 * Create and return a new {@link StandardEnvironment}.
 * <p>Subclasses may override this method in order to supply
 * a custom {@link ConfigurableEnvironment} implementation.
 */
protected ConfigurableEnvironment createEnvironment() {
    return new StandardEnvironment();
}

/**
 * Return this context's internal bean factory as AutowireCapableBeanFactory,
 * if already available.
 * @see #getBeanFactory()
 */
@Override
public AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException {
    return getBeanFactory();
}

/**
 * Return the timestamp (ms) when this context was first loaded.
 */
@Override
public long getStartupDate() {
    return this.startupDate;
}

/**
 * Publish the given event to all listeners.
 * <p>Note: Listeners get initialized after the MessageSource, to be able
 * to access it within listener implementations. Thus, MessageSource
 * implementations cannot publish events.
 * @param event the event to publish (may be application-specific or a
 * standard framework event)
 */
@Override
public void publishEvent(ApplicationEvent event) {
    publishEvent(event, null);
}

/**
 * Publish the given event to all listeners.
 * <p>Note: Listeners get initialized after the MessageSource, to be able
 * to access it within listener implementations. Thus, MessageSource
 * implementations cannot publish events.
 * @param event the event to publish (may be an {@link ApplicationEvent}
 * or a payload object to be turned into a {@link PayloadApplicationEvent})
 */
@Override
public void publishEvent(Object event) {
    publishEvent(event, null);
}

/**
 * 将给定的事件发布给所有侦听器。
 * @param event the event to publish (may be an {@link ApplicationEvent}
 * or a payload object to be turned into a {@link PayloadApplicationEvent})
 * @param eventType the resolved event type, if known
 * @since 4.2
 */
protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
    Assert.notNull(event, "Event must not be null");

    // Decorate event as an ApplicationEvent if necessary
    ApplicationEvent applicationEvent;
    if (event instanceof ApplicationEvent) {
       applicationEvent = (ApplicationEvent) event;
    }
    else {
       applicationEvent = new PayloadApplicationEvent<>(this, event);
       if (eventType == null) {
          eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
       }
    }

    // Multicast right now if possible - or lazily once the multicaster is initialized
    if (this.earlyApplicationEvents != null) {
       this.earlyApplicationEvents.add(applicationEvent);
    }
    else {
       getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
    }

    // Publish event via parent context as well...
    if (this.parent != null) {
       if (this.parent instanceof AbstractApplicationContext) {
          ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
       }
       else {
          this.parent.publishEvent(event);
       }
    }
}

/**
 * Return the internal ApplicationEventMulticaster used by the context.
 * @return the internal ApplicationEventMulticaster (never {@code null})
 * @throws IllegalStateException if the context has not been initialized yet
 */
ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {
    if (this.applicationEventMulticaster == null) {
       throw new IllegalStateException("ApplicationEventMulticaster not initialized - " +
             "call 'refresh' before multicasting events via the context: " + this);
    }
    return this.applicationEventMulticaster;
}

@Override
public void setApplicationStartup(ApplicationStartup applicationStartup) {
    Assert.notNull(applicationStartup, "applicationStartup should not be null");
    this.applicationStartup = applicationStartup;
}

@Override
public ApplicationStartup getApplicationStartup() {
    return this.applicationStartup;
}

/**
 * Return the internal LifecycleProcessor used by the context.
 * @return the internal LifecycleProcessor (never {@code null})
 * @throws IllegalStateException if the context has not been initialized yet
 */
LifecycleProcessor getLifecycleProcessor() throws IllegalStateException {
    if (this.lifecycleProcessor == null) {
       throw new IllegalStateException("LifecycleProcessor not initialized - " +
             "call 'refresh' before invoking lifecycle methods via the context: " + this);
    }
    return this.lifecycleProcessor;
}

/**
 * Return the ResourcePatternResolver to use for resolving location patterns
 * into Resource instances. Default is a
 * {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver},
 * supporting Ant-style location patterns.
 * <p>Can be overridden in subclasses, for extended resolution strategies,
 * for example in a web environment.
 * <p><b>Do not call this when needing to resolve a location pattern.</b>
 * Call the context's {@code getResources} method instead, which
 * will delegate to the ResourcePatternResolver.
 * @return the ResourcePatternResolver for this context
 * @see #getResources
 * @see org.springframework.core.io.support.PathMatchingResourcePatternResolver
 */
protected ResourcePatternResolver getResourcePatternResolver() {
    return new PathMatchingResourcePatternResolver(this);
}


//---------------------------------------------------------------------
// Implementation of ConfigurableApplicationContext interface
//---------------------------------------------------------------------

/**
 * Set the parent of this application context.
 * <p>The parent {@linkplain ApplicationContext#getEnvironment() environment} is
 * {@linkplain ConfigurableEnvironment#merge(ConfigurableEnvironment) merged} with
 * this (child) application context environment if the parent is non-{@code null} and
 * its environment is an instance of {@link ConfigurableEnvironment}.
 * @see ConfigurableEnvironment#merge(ConfigurableEnvironment)
 */
@Override
public void setParent(@Nullable ApplicationContext parent) {
    this.parent = parent;
    if (parent != null) {
       Environment parentEnvironment = parent.getEnvironment();
       if (parentEnvironment instanceof ConfigurableEnvironment) {
          getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
       }
    }
}

@Override
public void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor) {
    Assert.notNull(postProcessor, "BeanFactoryPostProcessor must not be null");
    this.beanFactoryPostProcessors.add(postProcessor);
}

/**
 * Return the list of BeanFactoryPostProcessors that will get applied
 * to the internal BeanFactory.
 */
public List<BeanFactoryPostProcessor> getBeanFactoryPostProcessors() {
    return this.beanFactoryPostProcessors;
}

@Override
public void addApplicationListener(ApplicationListener<?> listener) {
    Assert.notNull(listener, "ApplicationListener must not be null");
    if (this.applicationEventMulticaster != null) {
       this.applicationEventMulticaster.addApplicationListener(listener);
    }
    this.applicationListeners.add(listener);
}

/**
 * Return the list of statically specified ApplicationListeners.
 */
public Collection<ApplicationListener<?>> getApplicationListeners() {
    return this.applicationListeners;
}

1.12 Refresh()

他来了。这个方法非常的关键

这个方法是ConfigurableApplicationContext接口中定义的,并在本抽象类进行了实现,在这个接口中,它对这个方法的定义如下:

加载刷新配置的持久表示,它可能来自基于Java的配置、XML文件、属性文件、关系数据库模式或其他格式。
由于这是一种启动方法,如果失败,它应该销毁已经创建的singleton,以避免挂起资源。换句话说,在调用此方法之后,应该实例化所有Bean或不实例化singleton。

这个方法的意思就是

@Override
public void refresh() throws BeansException, IllegalStateException {
//锁住启动监视器同步器,避免同一时刻多次执行refresh操作
    synchronized (this.startupShutdownMonitor) {
       StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

       //准备此上下文以进行刷新。
       prepareRefresh();

       //告诉子类刷新内部bean工厂。
       ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

       //准备bean工厂以便在此上下文中使用。
       prepareBeanFactory(beanFactory);

       try {
          // 允许在上下文子类中对bean工厂进行后处理。
          postProcessBeanFactory(beanFactory);

          StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
          // 调用在上下文中注册为bean的工厂处理器。
          invokeBeanFactoryPostProcessors(beanFactory);

          // 注册拦截bean创建的bean处理器。
          registerBeanPostProcessors(beanFactory);
          beanPostProcess.end();

          // 初始化此上下文的消息源。
          initMessageSource();

          //初始化此上下文的事件多播器。
          initApplicationEventMulticaster();

          //初始化特定上下文子类中的其他特殊bean。
          onRefresh();

          //检查侦听器bean并注册它们。
          registerListeners();

          // 实例化所有剩余的(非惰性init)singleton。
          finishBeanFactoryInitialization(beanFactory);

          // 最后一步:发布相应的事件。
          finishRefresh();
       }

       catch (BeansException ex) {
          if (logger.isWarnEnabled()) {
             logger.warn("Exception encountered during context initialization - " +
                   "cancelling refresh attempt: " + ex);
          }

          // 销毁已创建的singleton以避免挂起资源。
          destroyBeans();

          // 重置“活动”标志。
          cancelRefresh(ex);

          //向调用方传播异常。
          throw ex;
       }

       finally {
          // 重置Spring核心中的常见内省缓存,因为我们可能不再需要单例bean的元数据了
          resetCommonCaches();
          contextRefresh.end();
       }
    }
}

 这个方法就是一个完整的Bean的生命周期操作,学习Bean的生命周期,也就是对这个方法的解读。

先来一行一行的看一遍:

表示当前加载或刷新bean进行到哪一步,不重要

StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

做一些准备工作,比如切换上下文执行状态,打印日志,验证属性有效性,注册一些事件监听器等。不重要

prepareRefresh();

内部:

protected void prepareRefresh() {
    // 切换为活跃状态.
    this.startupDate = System.currentTimeMillis();
    this.closed.set(false);
    this.active.set(true);

    //打印正在执行refresh操作的上下文日志
    if (logger.isDebugEnabled()) {
       if (logger.isTraceEnabled()) {
          logger.trace("Refreshing " + this);
       }
       else {
          logger.debug("Refreshing " + getDisplayName());
       }
    }

    // 初始化上下文环境中的任何占位符属性源。在这个抽象类中没有对这个方法进行实现。
    initPropertySources();

    // 验证标记为必需的所有属性是否可解析。PS:这里你回想一下有些类中的属性,我们需要加上Required注解来表示这个属性必须被加载。
    // 请参阅ConfigurationPropertyResolver#setRequiredProperties
    getEnvironment().validateRequiredProperties();

    // 存储refresh操作之前的事件监听器
    if (this.earlyApplicationListeners == null) {
       this.earlyApplicationListeners = new LinkedHashSet<>(this.applicationListeners);
    }
    else {
       // Reset local application listeners to pre-refresh state.
       this.applicationListeners.clear();
       this.applicationListeners.addAll(this.earlyApplicationListeners);
    }

    // 允许收集早期ApplicationEvents,以便在多播主机可用后发布
    this.earlyApplicationEvents = new LinkedHashSet<>();
}

让子类刷新内部bean工厂。并且有旧的工厂时,销毁旧工厂,重新加载新工厂,并获取bean的定义,从注解或者xml中获取bean的定义。返回生产这些bean的工厂对象。

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
1.12.1 onRefresh()

这个方法在这个抽象类中没有实现

/**
 * Template method which can be overridden to add context-specific refresh work.
 * Called on initialization of special beans, before instantiation of singletons.
 * <p>This implementation is empty.
 * @throws BeansException in case of errors
 * @see #refresh()
 */
protected void onRefresh() throws BeansException {
    // For subclasses: do nothing by default.
}

这个是一个模版设计模式。虽然在这个抽象类中没有实现,我们找到一个实现了这个抽象类的类来说明这个抽象方法可以做哪些事:

 这个onRefresh()方法在spring中有五个类实现了这个方法。

我们看看这个类:ServletWebServerApplicationContext

ServletWeb服务器应用上下文、熟悉吧?不熟悉的话我们看看他做了什么

进入createWebServer,创建web服务器

进入这段代码ServletWebServerFactory factory = getWebServerFactory();

获取web服务器工厂

进入:String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);

这段代码意思是从ServletWebServerFactory这个接口的类型中获取bean的name,

ServletWebServerFactory接口代码:

那么我们来看看这个ServletWebServerFactory.class接口到底有哪些类型:

!!!Tomcat的工厂。!!!Jetty工厂。!!!Undertow

 Springboot我们知道默认内置了Tomcat,当然,也可以使用其他的web服务器,这里就是一个模版设计模式。通过实现抽象类中的方法,在应用启动中,运行用户自定义的启动逻辑。这里这个类自定义的启动逻辑就是去加载运行一个tomcat服务器。

本博客是AbstractApplicationContext的源码解析,spring的基础代码跟你具体业务是没有关系的,因此抽象应用上下文抽象类,实际是做的事就是加载某个应用的上下文,加载应用的上下文所依赖的bean的工厂对象, 加载bean的对象等等功能,还有一些事件监听器的调用。比如你在应用启动的时候注册了事件监听器,那么应用启动成功后,会给你发送一个事件触发的操作。

然后具体应用的业务逻辑实现在onfresh中去完成。

1.13 BeanPostProcessors

前文提到本抽象类中会做一些BeanPostProcessors的注册。那么这部分代码到底做了什么工作我们来看看。

我们先看看什么是BeanPostProcessors

 注释翻译:

工厂回调,允许对新的bean实例进行自定义修改——例如,检查标记接口或用代理包装bean。
通常,通过标记接口等填充bean的后处理器将实现postProcessBeforeInitialization,而用代理封装bean的后处理程序通常将实现postProcessAfterInitialization
登记
ApplicationContext可以在其bean定义中自动检测BeanPostProcessor bean,并将这些后处理器应用于随后创建的任何bean。普通BeanFactory允许对后处理器进行编程注册,将它们应用于通过bean工厂创建的所有bean。
在ApplicationContext中自动检测到的BeanPostProcessor bean将根据org.springframework.core进行排序。PriorityOrdered和org.springframework.core。有序语义。相反,以编程方式向BeanFactory注册的BeanPostProcessor bean将按注册顺序应用;对于编程注册的后处理器,通过实现PriorityOrdered或Ordered接口表达的任何排序语义都将被忽略。此外,BeanPostProcessor bean没有考虑@Order注释。

我们可以理解为,既然名字叫bean的后置处理器。那么一定是对bean进行功能的增强,那么我们可以想起AOP动态代理就是对对象功能的增强。那么这里一定会用到AOP。我们继续往下看

postProcessAfterInitialization这个接口方法在类中AbstractAutoProxyCreator进行了实现

 

或者抽象类:AbstractAdvisingBeanPostProcessor

 这两个类的调用链中,最后都会指向

proxyFactory.getProxy(classLoader);

 

 

 看到这里!立马想到AOP动态代理的两种方式JDK和Cglib。

读完了依赖~我很快就离开

现在我们知道并理解为,Bean的后置处理器BeanPostProcessors的相关注册其实就是在抽象应用上下文中去使用AOP去加载注册bean的增强类,以便后续的使用。

相关推荐

  1. ffmplay 解读

    2024-05-16 11:54:08       6 阅读
  2. MetaGPT部分解读

    2024-05-16 11:54:08       25 阅读
  3. 【iOS】—— SDWebImage学习(2)(解读

    2024-05-16 11:54:08       16 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-05-16 11:54:08       19 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-05-16 11:54:08       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-05-16 11:54:08       20 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-05-16 11:54:08       20 阅读

热门阅读

  1. flutter 嵌套 StatefulWidget 不刷新

    2024-05-16 11:54:08       12 阅读
  2. logstach+elasticsearch+kibana整合后台.log文件

    2024-05-16 11:54:08       10 阅读
  3. 解密 Unix 中的 “rc“ 后缀:自定义你的工作环境

    2024-05-16 11:54:08       10 阅读
  4. oracle 临时表 在sql 里面用完要删除吗

    2024-05-16 11:54:08       11 阅读
  5. 简单上手SpringBean的整个装配过程

    2024-05-16 11:54:08       13 阅读
  6. Oracle 数据块之变化时的SCN

    2024-05-16 11:54:08       12 阅读
  7. bert 的MLM框架任务-梯度累积

    2024-05-16 11:54:08       14 阅读