Spring - 基本用法参考

Spring 官方文档

Spring容器启动流程(源码解读)

  1. BeanFactoryPostProcessor vs BeanPostProcessor vs BeanDefinitionRegistryPostProcessor:

From java doc:
BeanFactoryPostProcessor may interact with and modify bean definitions, but never bean instances. Doing so may cause premature bean instantiation, violating the container and causing unintended side-effects. If bean instance interaction is required, consider implementing BeanPostProcessor instead.

BeanDefinitionRegistryPostProcessor allowing for the registration of further bean definitions before regular BeanFactoryPostProcessor detection kicks in. In particular, BeanDefinitionRegistryPostProcessor may registe further bean definitions which in turn define BeanFactoryPostProcessor instances.

BeanFactoryPostProcessor 是 BeanDefinitionRegistryPostProcessor 的父接口。

在Spring Bean的生命周期中各方法的执行顺序

  1. BeanPostProcessor(针对 Spring 容器中管理的所有 Bean 生效):postProcessBeforeInitialization(bean 初始化之前执行)、postProcessAfterInitialization(bean 初始化后执行)
  2. InitializingBean(针对实现了该接口的 Bean 生效):afterPropertiesSet
  3. 执行顺序:postProcessBeforeInitialization、@postConstruct、afterPropertiesSet、postProcessAfterInitialization

聊透Spring bean的生命周期
Bean 生命周期

Spring Bean 对象创建过程: 扫描包(@ComponentScan) -> 解析 class 文件,生成 BeanDefinition 对象,并将其缓存到 Map 中 -> 推断构造器 -> 创建 Bean 实例普通对象 -> 依赖注入 -> 执行 Aware 接口 -> 初始化(BeanPostProcessor + InitializingBean) -> [ AOP -> 代理对象 ] -> 放入 Map 单例池

  1. 推断构造器:若 Bean 类有无参构造器,则调用无参构造器;若只有一个无参构造器,则调用该构造器;若有多个无参构造器,且没有使用 @AutoWired 指定则报错,否则调用 @AutoWired 指定的那个构造器
  2. 依赖注入:byType -> byName
  3. 一些 Aware 回调接口是通过 BeanPostProcessor 实现的(比如: ApplicationContextAwareProcessor)
  4. 代理对象(AOP 实现原理)
    例:
/***
	// 除了 Spring-Boot, 额外需要的 pom 依赖:
    <dependency>
       <groupId>org.aspectj</groupId>
       <artifactId>aspectjrt</artifactId>
       <version>1.8.13</version>
   </dependency>
   <dependency>
       <groupId>org.aspectj</groupId>
       <artifactId>aspectjweaver</artifactId>
       <version>1.9.6</version>
   </dependency>
*/
// bean 类
@Component
public class UserService {
   
    public void test() {
   
        System.out.println("userService test() running");
    }
}

// 切面类
@Aspect
@Component
public class MyAspect {
   

    @Before("execution(public * UserService.*(..))")
    public void doBefore() {
   
        System.out.println("MyAspect before...");
    }
}

// 测试类
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
   UserService.class, MyAspect.class})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopTest {
   
    @Autowired
    private UserService userService;

    @Test
    public void testAopProxy() {
   
        userService.test();
    }
}

在执行 AopTest 时,自动注入的 UserService 为 CGLib 生成的代理类对象,其样式大概为:

public class UserService$Proxy extends UserService{
   
	private UserService target; // Spring 会自动注入被代理的 Bean 实例普通对象
	
	@Autowired
	public void test(){
   
		// 先执行 MyAspect 的切面逻辑
		target.test();
	}
}
// 从 CGLib 的实现原理可以看出,当在被代理方法 A 中调用 A 所在类的其它方法 B 时,此时 B 并不是被代理后的方法。

即代理类持有一个原对象(spring用的两种代理,Proxy和CGlib都一样):先创建 bean 对象,再创建代理类,再初始化代理对象中的 bean 对象。

  1. Bean 实例的创建过程模板定义在:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
    在这里插入图片描述

doCreateBean 模板流程为: 创建 bean 对象、依赖注入、BeanNameAware / BeanFactoryAware 、BeanPostProceessor#postProcessBeforeInitilization(@Resource、@PostConstruct 等)、InitializeBean#afterProperties、BeanPostProceessor#postProcessAfterInitilization(AOP、事务、@Async 等)

@PostConstruct

该注解由 JSR-250规范提供

  1. 到Java 9及以后,J2EE弃用了 @PostConstruct和@PreDestroy这两个注解,并计划在Java 11中将其删除。如果你使用的是Spring的项目,则可考虑另外一种方式,基于Spring的InitializingBean和DisposableBean接口来实现同样的功能.
  2. BeanPostProcessor有个实现类CommonAnnotationBeanPostProcessor,就是专门处理@PostConstruct、@PreDestroy、@Resource 这些定义在 JDK 里面的注解.

@Resource 的具体处理逻辑定义在 CommonAnnotationBeanPostProcessor 的 postProcessProperties 方法中,在“Bean 的依赖注入阶段 populateBean” 被执行。另外,Spring 定义的注解 @Autowired、@Value 则是在 AutowiredAnnotationBeanPostProcessor 的 postProcessProperties 方法中被处理的。

@PostConstruct、@PreDestroy 具体的处理逻辑则定义在 InitDestroyAnnotationBeanPostProcessor 的 postProcessBeforeInitialization 方法中,在 “Bean 初始化阶段 initializeBean” 被执行。

CommonAnnotationBeanPostProcessor、AutowiredAnnotationBeanPostProcessor 则由 Spring 容器默认注入。

// org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry, java.lang.Object)
if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
   
	RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
	def.setSource(source);
	beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}
if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
   
	RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
	def.setSource(source);
	beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
}

InitializingBean 接口作用

JDK 已经有了代码块,为什么 Spring 还需要提供 afterPropertiesSet 方法

The static initializer block is only executed when the class is loaded by the class loader. There is no instance of that class at that moment and you will only be able to access class level (static) variables at that point and not instance variables.

The non-static initializer block is when the object is constructed but before any properties are injected. The non-static initializer block is actually copied to the every constructor.

The afterPropertiesSet or @PostConstruct annotated method is called after an instance of class is created and all the properties have been set. For instance if you would like to preload some data that can be done in this method as all the dependencies have been set.

注意:InitializingBean 对所有实现该接口的类,且由 Spring 容器管理的对象都会生效,即

  1. 生效方式一:
@Component
public class User implements InitializingBean {
   
	@Override
	public void afterPropertiesSet(){
   }
}
  1. 生效方式二:
// 代码来自 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
   
		
	@Override
	public void afterPropertiesSet() {
   
		// Do this first, it may add ResponseBody advice beans
		initControllerAdviceCache();

		if (this.argumentResolvers == null) {
   
			List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
			this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		if (this.initBinderArgumentResolvers == null) {
   
			List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
			this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
		}
		if (this.returnValueHandlers == null) {
   
			List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
			this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
		}
	}
}

// 代码来自 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

// RequestMappingHandlerAdapter 类上并没有使用 @Componenet、@Configuration 等注解,但这里通过 @Bean 将其注入到了 Spring 容器中,其 afterPropertiesSet 方法也会被调用
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
		@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
		@Qualifier("mvcConversionService") FormattingConversionService conversionService,
		@Qualifier("mvcValidator") Validator validator) {
   

	RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
	adapter.setContentNegotiationManager(contentNegotiationManager);
	adapter.setMessageConverters(getMessageConverters());
	adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
	adapter.setCustomArgumentResolvers(getArgumentResolvers());
	adapter.setCustomReturnValueHandlers(getReturnValueHandlers());

	if (jackson2Present) {
   
		adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
		adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
	}

	AsyncSupportConfigurer configurer = getAsyncSupportConfigurer();
	if (configurer.getTaskExecutor() != null) {
   
		adapter.setTaskExecutor(configurer.getTaskExecutor());
	}
	if (configurer.getTimeout() != null) {
   
		adapter.setAsyncRequestTimeout(configurer.getTimeout());
	}
	adapter.setCallableInterceptors(configurer.getCallableInterceptors());
	adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());

	return adapter;
}

todo: afterProperties 与 Warmup 的 tryWarmup() 的区别是啥?

循环依赖

Bean 对象创建三级缓存:org.springframework.beans.factory.support.DefaultSingletonBeanRegistry

/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级缓存,缓存完成依赖注入的 Bean 实例

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存,在出现循环依赖时,缓存未完成依赖注入的 Bean 的普通对象

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存,在出现循环依赖时,缓存未完成依赖注入的 Bean 的代理对象

三级缓存解决循环依赖的代码定义在:org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean
在这里插入图片描述

org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

在这里插入图片描述
总结过程:
创建 AService 普通对象 -> 注入 BService 对象 -> 创建 BService 普通对象 -> 注入 AService 对象,存在循环依赖 -> 将 aService 放入到三级缓存 -> 从一级缓存取 aService -> 从二级缓存取 aService -> 根据三级缓存,创建 AService 代理对象放入到二级缓存
为什么需要使用三级缓存解决循环依赖
在这里插入图片描述

  1. 一级缓存 singletonObjects 存的是完全创建完成的 Bean 对象.
  2. 假如去掉三级缓存,直接将代理对象放到二级缓存 earlySingletonObjects。这会导致一个问题:在实例化阶段就得执行后置处理器,判断有 AnnotationAwareAspectJAutoProxyCreator 并创建代理对象。这样会对 Bean 的生命周期(依赖注入完成,即被代理对象完全创建成功后再对其进行代理)造成影响。(拿上面的例子:A 的代理对象是在初始化动作(initializeBean 方法) 之前创建的,破坏了其正常的生命周期,但 B 的代理对象则是在初始化动作之后创建的,其生命周期过程得到了保证)同样,先创建 singletonFactory 的好处就是:在真正需要实例化的时候,再使用 singletonFactory.getObject() 获取 Bean 或者 Bean 的代理。相当于是延迟实例化。
  3. 假如去掉二级缓存,那有这么一种场景,B 和 C 都依赖了 A。要知道在有代理的情况下,(从三级缓存中取对象,将)多次调用 singletonFactory.getObject() 返回的代理对象是不同的,就会导致 B 和 C 依赖了不同的 A。

Spring 容器完成依赖注入:

  1. org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons:遍历 beanDefinitionNames,依次调用容器的 getBean 方法
    在这里插入图片描述
  2. 通过 AutowiredAnnotationBeanPostProcessor 解析 @Autowired、@Value,通过 CommonAnnotationBeanPostProcessor 解析 @Resource ,从而完成 Bean 之间的依赖注入。主要是借助 org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency 能力
    在这里插入图片描述

todo:applicationContext.getBean(beanName)还需要再梳理下.

扩展知识

  1. Spring的Lifecycle和SmartLifecycle

SmartLifecycle 定义了三个方法,任何Bean实现了Lifecycle方法,当ApplicationContext收到start、stop和restart等信号时,就会调用对应的方法。因此可以通过实现Lifecycle接口获得容器生命周期的回调,实现业务扩展

  1. Spring扩展-3-SmartLifecycle

SmartLifecycle#start() 会在 Spring 容器中的 Bean 对象完全创建完成后,才会被调用。即在 AbstractApplicationContext#finishRefresh 阶段(此时容器中的 Bean 已经完全创建完成)通过委托过 LifeCycleProcessor 执行。

比较好的规范格式参考:

标准返回对象
反面案例

后端历史返回给前端的数据格式为 Map,这种写法至少存在两种缺点:

  1. 会写冗余代码:map.put(“result”, resultCode);map.put(“data”, beanData);
  2. 规范性不强,对于后来者,可以继续往 map 中筛入其它值。
@RestController
@RequestMapping("/app")
public class AppController {
   
	@RequestMapping("/detail", method = RequestMethod.POST)
	public Object appDetail(@RequestParam Long appId) {
   
		AppInfo appInfo = new AppInfo();

		Map<String, Object> map = new HashMap<>();
		map.put("result", "1");
		map.put("data", appInfo);
		return map;
	}
}
最佳实践

将返回数据以泛型类型定义,收敛冗余代码,Controller 中的代码看上去更为简洁。

@Data
@NoArgsConstructor
public class ResponseObject<T> {
   

    private static final int DEFAULT_SUCCESS_CODE = 1;
    private static final int DEFAULT_FAIL_CODE = -1;

    @JsonProperty("host-name")
    private String hostName = HostInfo.getHostName(); // 当前容器云实例,方便排查问题

    private Integer result;

    @JsonProperty("error_msg")
    private String errorMsg;

    private T data;

    public static final String RESPONSE_MESSAGE_OK = "ok";


    public ResponseObject(Integer responseCode, String responseMsg) {
   
        this.result = responseCode;
        this.errorMsg = responseMsg;
    }

    public ResponseObject(Integer responseCode, String responseMsg, T responseData) {
   
        this.result = responseCode;
        this.errorMsg = responseMsg;
        this.data = responseData;
    }

    public static <T> ResponseObject<T> success() {
   
        ResponseObject<T> responseObject = new ResponseObject<>();
        responseObject.setResult(DEFAULT_SUCCESS_CODE);
        responseObject.setErrorMsg("");
        return responseObject;
    }

    public static <T> ResponseObject<T> success(T data) {
   
        ResponseObject<T> responseObject = new ResponseObject<>();
        responseObject.setResult(DEFAULT_SUCCESS_CODE);
        responseObject.setData(data);
        return responseObject;
    }


    public static <T> ResponseObject<T> ofError(String msg) {
   
        ResponseObject<T> responseObject = new ResponseObject<>();
        responseObject.setResult(DEFAULT_FAIL_CODE);
        responseObject.setErrorMsg(msg);
        return responseObject;
    }
}
@RestController
@RequestMapping("/app")
public class AppController {
   
	@RequestMapping("/detail", method = RequestMethod.POST)
	public ResponseObject<AppInfo> appDetail(@RequestParam Long appId) {
   
		AppInfo appInfo = new AppInfo();
		return ResponseObject.success(appInfo);
	}
}
统一异常处理
@ControllerAdvice
@Slf4j
public class WebApiExceptionHandler extends BaseExceptionHandler {
   
	@ResponseBody
    @ExceptionHandler(IllegalArgumentException.class)
    public Map<String, Object> handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException exception) {
   
        Sentry.capture(exception);
        Map<String, Object> resultMap = Result.success();
        resultMap.put("result", PARAM_ERR.getCode());
        resultMap.put("error_msg", Optional.ofNullable(exception)
                .map(IllegalArgumentException::getMessage).orElse("参数错误"));
        return resultMap;
    }
}

BeanDefinition

  1. 想真正玩懂Spring,先搞定让你眼花缭乱的BeanDefinition吧

GenericBeanDefinition替代了低版本Spring的ChildBeanDefinition,GenericBeanDefinition比ChildBeanDefinition、RootBeanDefinition更加灵活,既可以单独作为BeanDefinition,也可以作为父BeanDefinition,还可以作为子GenericBeanDefinition。

RootBeanDefinition可以作为其他BeanDefinition的父BeanDefinition,也可以单独作为BeanDefinition,但是不能作为其他BeanDefinition的子BeanDefinition,在 org.springframework.beans.factory.support.AbstractBeanFactory#mergedBeanDefinitions(GenericApplicationContext.getBeanFactory()#getMergedBeanDefinition 返回的就是 mergedBeanDefinitions 这个 map 存储的数据)存储的都是RootBeanDefinition。

  1. Spring的bean定义 4 : 合并了的bean定义–MergedBeanDefinition

在Spring中,关于bean定义,其Java建模模型是接口BeanDefinition, 其变种有RootBeanDefinition,ChildBeanDefinition,还有GenericBeanDefinition,AnnotatedGenericBeanDefinition,ScannedGenericBeanDefinition等等。这些概念模型抽象了不同的关注点。关于这些概念模型,除了有概念,也有相应的Java建模模型,甚至还有通用的实现部分AbstractBeanDefinition。但事实上,关于BeanDefinition,还有一个概念也很重要,这就是MergedBeanDefinition, 但这个概念并没有相应的Java模型对应。但是它确实存在,并且Spring专门为它提供了一个生命周期回调定义接口MergedBeanDefinitionPostProcessor用于扩展。

从 org.springframework.beans.factory.support.AbstractBeanFactory#getMergedBeanDefinition 方法可以看出:一个MergedBeanDefinition其实就是一个"合并了的BeanDefinition",最终以RootBeanDefinition的类型存在。

BeanPostProcessor

Factory hook that allows for custom modification of new bean instances, e.g. checking for marker interfaces or wrapping them with proxies.
ApplicationContexts can autodetect BeanPostProcessor beans in their bean definitions and apply them to any beans subsequently created. Plain bean factories allow for programmatic registration of post-processors, applying to all beans created through this factory. Typically, post-processors that populate beans via marker interfaces or the like will implement {@link #postProcessBeforeInitialization}, while post-processors that wrap beans with proxies will normally implement {@link #postProcessAfterInitialization}.

InstantiationAwareBeanPostProcessor

Subinterface of BeanPostProcessor that adds a before-instantiation callback, and a callback after instantiation but before explicit properties are set or autowiring occurs.Typically used to suppress default instantiation for specific target beans, for example to create proxies with special TargetSources (pooling targets, lazily initializing targets, etc), or to implement additional injection strategies such as field injection. This interface is a special purpose interface, mainly for internal use within the framework. It is recommended to implement the plain BeanPostProcessor interface as far as possible, or to derive from InstantiationAwareBeanPostProcessorAdapter in order to be shielded from extensions to this interface.

执行流程模板定义(AbstractAutowireCapableBeanFactory#doCreateBean)
在这里插入图片描述

MergedBeanDefinitionPostProcessor

Allow post-processors to modify the merged bean definition

在 Spring 生命周期方法 doGetBean 中,会首先根据 beanName 获取得到「 MergedBeanDefinition」,然后对其执行 MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition:
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyMergedBeanDefinitionPostProcessors


其实现子接口有:

  1. AutowiredAnnotationBeanPostProcessor:处理 @Autowired 注解
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
		implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
   
	// ...		

	@Override
	public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, 
		Class<?> beanType, String beanName) {
   
		// 获取指定 bean (标注了 @Autowired,@Inject,@Value 等注解)的属性注入元数据
		InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
		// 将注入的 bean 添加到 RootBeanDefinition#externallyManagedConfigMembers 中。注入过程则在 AutowiredAnnotationBeanPostProcessor#postProcessProperties 方法中
		metadata.checkConfigMembers(beanDefinition);
	}
	
	// ...
}
  1. ReferenceAnnotationBeanPostProcessor:Dubbo 中处理 @Reference 注解

FactoryBean

FactoryBean——Spring的扩展点之一

  1. FactoryBean的特殊之处在于它可以向容器中注册两个Bean,一个是它本身,一个是FactoryBean.getObject()方法返回值所代表的Bean。
  2. 在容器启动阶段,会先通过getBean()方法来创建XxxFactoryBean的实例对象。如果实现了SmartFactoryBean接口,且isEagerInit()方法返回true,那么在容器启动阶段,就会调用getObject()方法,向容器中注册getObject()方法返回值的对象。否则,只有当第一次获取getObject()返回值的对象时,才会去回调getObject()方法。过程定义在: org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons

注解

注解解析/处理器注入容器的位置: org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors(org.springframework.beans.factory.support.BeanDefinitionRegistry, java.lang.Object)

@AliasFor

Annotation Type AliasFor:官方 doc 文档

  • 在同一个注解中为两个属性互相设置别名(@RequestMapping 中的 value、path 属性)
  • 给元注解中的属性设定别名(@PostMapping)
MergedAnnotations

Interface MergedAnnotations 官方文档:@AliasFor 的配套类,

@RestController
@Slf4j
public class HelloController {
   
	@PostMapping("/hello/v2")
	public String helloV2(HttpServletRequest request) {
   
	    return "helloV2";
	}

	public static void main(String[] args] {
   
		/*
			@RequestMapping 是 @PostMapping 的元注解, RequestMapping mapping =  AnnotatedElementUtils.findMergedAnnotation(PostMapping.class, RequestMapping.class) 的属性值:
			1. method 来自定义 @PostMapping 时给定的值
			2. value、path 来自 @PostMapping 的 value 属性(通过 @AliasFor 将 @PostMapping 的 value 指向了 @RequestMapping 的 value, 而在  @RequestMapping 中,value 和 path 又互为别名)
		*/
		RequesetMapping requestMapping =  AnnotatedElementUtils.findMergedAnnotation(HelloController.class, RequesetMapping.class);
	}
}

@Resource & @Autowired

  1. 如果@Resource注解中指定了name属性,那么则只会根据name属性的值去找bean,如果找不到则报错。
  2. 如果@Resource注解没有指定name属性,那么会先判断当前注入点名字(属性名字或方法参数名字)是不是存在Bean, 如果存在,则直接根据注入点名字取获取bean,如果不存在,则会走@Autowired注解的逻辑,会根据注入点类型去找Bean
  1. 从实现代码 AutowiredAnnotationBeanPostProcessor#buildAutowiringMetadata 来看,
do {
   
	final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();

	ReflectionUtils.doWithLocalFields(targetClass, field -> {
   
		MergedAnnotation<?> ann = findAutowiredAnnotation(field);
		if (ann != null) {
   
			if (Modifier.isStatic(field.getModifiers())) {
   
				if (logger.isInfoEnabled()) {
   
					logger.info("Autowired annotation is not supported on static fields: " + field);
				}
				return;
			}
			boolean required = determineRequiredStatus(ann);
			currElements.add(new AutowiredFieldElement(field, required));
		}
	});

	// 省略寻找 AutowiredMethodElement 部分的代码

	targetClass = targetClass.getSuperclass();

} while (targetClass != null && targetClass != Object.class); // 向上遍历找到类层次体系中所有代码 @Autowirted、@Value 的方法或字段

抽象类中标注有 @Autowired 的字段也会自动 Spring 被注入,结合模板方法模式抽象出公共的代码从而提高代码的可复用性。示例代码如下:

public abstract class AbstractCreateService {
   
	@Autowired
	public List<Validator> validators;
	
	public Long create(Param param) {
   
		// 通用的校验逻辑放在模板方法中
		validators.forEach(validator -> validator.validate(param));
		
		return process(param);
	}
	
	public abstract Long process(Param param);
}

@Service
public class CampaignCreateService extends AbstractCreateService {
   
	@Autowired
	public CampaignDao dao;
	public Long process(Param param) {
   
		// 省略业务逻辑处理,模型转化(Param -> Model)
		
		return dao.insert(model);
	}
}

@Service
public class UnitCreateService extends AbstractCreateService {
   
	@Autowired
	public UnitDao dao;
	public Long process(Param param) {
   
		// 省略业务逻辑处理,模型转化(Param -> Model)
		
		return dao.insert(model);
	}
}
实现原理

Spring IOC 自动注入流程

@Configuration

todo: @Configuration、@Component 什么时候被解析的? scan 时候?
Spring: FactoryBean vs @Configuration

聊透spring @Configuration配置类

  1. Spring会保证多次调用 @Configuration 标注类下的@Bean 产生的对象时单例的.
  2. 在任何能够被Spring管理的类(比如加了@Component的类)中,定义@Bean方法,都能将自定义对象放入到Spring容器
    @Configuration 解析全过程

博客中的错误:

  1. @Bean 工厂方法的调用时机是在创建 Bean 实例阶段: org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance
    在这里插入图片描述

详细流程为:

  1. 在初始化 AnnotationConfigApplicationContext 扫描包时,注册 ConfigurationClassPostProcessor 对象:org.springframework.context.annotation.ClassPathBeanDefinitionScanner#scan,并将 @Configuration 类解析为 BeanDefinition。
  2. 在刷新容器执行 org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors,执行 org.springframework.context.annotation.ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry 将 @Bean 解析成 ConfigurationClassBeanDefinition。这里还会将 @Configuration 类对应的 BeanDefinition 中的 class 替换为使用 org.springframework.context.annotation.ConfigurationClassEnhancer 增强过的类。
  3. 执行 org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization 将 @Configuration 和 @Bean 对应的 Bean 对象创建完成并放入容器中。

@ComponentScan

Either basePackageClasses or basePackages (or its alias value) may be specified to define specific packages to scan. If specific packages are not defined, scanning will occur from the package of the class that declares this annotation.

原理

在 Spring 的 refresh 阶段,通过 BeanFactoryPostProcessor 的实现类 ConfigurationClassPostProcessor 借助 ConfigurationClassParser 封装的能力实现的。

// From ConfigurationClassParser#doProcessConfigurationClass

protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {
   

		// 省略代码 ...

		// Process any @ComponentScan annotations
		Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
				sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
		if (!componentScans.isEmpty() &&
				!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
   
			for (AnnotationAttributes componentScan : componentScans) {
   
				// The config class is annotated with @ComponentScan -> perform the scan immediately
				Set<BeanDefinitionHolder> scannedBeanDefinitions =
						this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
				// Check the set of scanned definitions for any further config classes and parse recursively if needed
				for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
   
					BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
					if (bdCand == null) {
   
						bdCand = holder.getBeanDefinition();
					}
					if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
   
						parse(bdCand.getBeanClassName(), holder.getBeanName());
					}
				}
			}
		}
		// 省略代码 ...
}

扫描包(todo:如何扫描的? 带有注解 @Component 的类?)下的类,并将其封装成 BeanDefinition

@Bean

@Bean 等注解的实现原理

@Lazy

org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons:当 beanDefinition 为 lazy 时,就不会主动调用 beanFactory.getBean(beanName) 完成 bean 的创建和初始化。
在这里插入图片描述
但当该 bean 被其它 bean 通过 @Resource、@Autowired、@Value 等方式依赖时,依然会创建并初始化该 Bean。

@Async

  1. 对于异步方法调用,从Spring3开始提供了@Async注解,该注解可以被标注在方法上,以便异步地调用该方法。调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行。
源码解析

Spring 的 @EnableXXX,通常用来开启某种能力,比如 @EnableScheduling 开启定时任务调度、
@EnableAsync 开启异步调用等。其实现原理也很简单,都是借助 @Import 导入某些 BeanPostProcessor ,通过生成代理类,对原 bean 对象进行功能增强。

从注解 @EnableAsync 的元注解 @Import(AsyncConfigurationSelector.class) 入手, 最终可以定位到 @Async 功能的解析入口为 ProxyAsyncConfiguration。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
   
	// ...
}

从 ProxyAsyncConfiguration 中可以看到,标有 @Async 的类或方法生成代理类实现功能增强的处理器为:AsyncAnnotationBeanPostProcessor,其继承体系为:
AsyncAnnotationBeanPostProcessor 继承体系
可以看出其增强逻辑的实现主要是 Spring 两大顶级扩展接口 BeanFactoryAware 与 BeanPostProcessor 的实现。而这两个接口的调用顺序定义在 bean 实例的初始化方法(AbstractAutowireCapableBeanFactory#initializeBean)中。
在这里插入图片描述
所以需要先看 BeanFactoryAware#setBeanFactory,初始化成员变量 advisor 为 AsyncAnnotationAdvisor。

public void setBeanFactory(BeanFactory beanFactory) {
   
	super.setBeanFactory(beanFactory);

	AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
	if (this.asyncAnnotationType != null) {
   
		advisor.setAsyncAnnotationType(this.asyncAnnotationType);
	}
	advisor.setBeanFactory(beanFactory);
	this.advisor = advisor;
}

AsyncAnnotationAdvisor 中定义了 pointcut(@Async 注解的类下所有方法 或 @Async 注解的方法),定义的 advice 为 AnnotationAsyncExecutionInterceptor。

public Object invoke(final MethodInvocation invocation) throws Throwable {
   
	Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
	Method specificMethod = ClassUtils.getMostSpecificMethod(invocation.getMethod(), targetClass);
	final Method userDeclaredMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);

	/*
	执行线程池确认过程:
		1. 去容器寻找 Executor 类型,<bean_name> 为 @Async 的 value 的 Bean 对象
		2. 使用容器中类型为 AsyncConfigurer 的 Bean 对象
		3. 使用容器中 TaskExecutor 类型的 Bean 或名为 taskExecutor 的 Executor 对象作为兜底线程池
		4. 使用 SimpleAsyncTaskExecutor (为每个任务都新开一个线程池)作为最终的兜底线程池
	注意:「SpringBoot」 开启 EnableAutoConfiguration 后,会通过 TaskExecutionAutoConfiguration 配置自动注入名为 applicationTaskExecutor、taskExecutor 的线程池。(线程池的等待队列、最大线程数都是无限大,核心线程数为 8),具体的配置类为:org.springframework.boot.autoconfigure.task.TaskExecutionProperties
	*/
	AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
	if (executor == null) {
   
		throw new IllegalStateException(
				"No executor specified and no default executor set on AsyncExecutionInterceptor either");
	}

	// 2. invocation 被封装成 Callable,然后将其提交给线程池执行
	Callable<Object> task = () -> {
   
		try {
   
			//
			Object result = invocation.proceed();
			if (result instanceof Future) {
   
				return ((Future<?>) result).get();
			}
		}
		catch (ExecutionException ex) {
   
			handleError(ex.getCause(), userDeclaredMethod, invocation.getArguments());
		}
		catch (Throwable ex) {
   
			handleError(ex, userDeclaredMethod, invocation.getArguments());
		}
		return null;
	};

	return doSubmit(task, executor, invocation.getMethod().getReturnType());
}

再看 BeanPostProcessor#postProcessAfterInitialization,

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
   
	// ...

	// 使用 AsyncAnnotationAdvisor 中的 Pointcut 判断是否需要对当前 bean 进行增强
	if (isEligible(bean, beanName)) {
   
		ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
		if (!proxyFactory.isProxyTargetClass()) {
   
			evaluateProxyInterfaces(bean.getClass(), proxyFactory);
		}
		
		// 将增强逻辑(AsyncAnnotationAdvisor 中的 Advice)放入到 proxyFactory 中
		proxyFactory.addAdvisor(this.advisor);
		customizeProxyFactory(proxyFactory);

		// Use original ClassLoader if bean class not locally loaded in overriding class loader
		ClassLoader classLoader = getProxyClassLoader();
		if (classLoader instanceof SmartClassLoader && classLoader != bean.getClass().getClassLoader()) {
   
			classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader();
		}
		
		// 返回代理 bean
		return proxyFactory.getProxy(classLoader);
	}

	// No proxy needed.
	return bean;
}
线程池

By default, Spring will be searching for an associated thread pool definition: either a unique {@link org.springframework.core.task.TaskExecutor} bean in the context, or an {@link java.util.concurrent.Executor} bean named “taskExecutor” otherwise. If neither of the two is resolvable, a {@link org.springframework.core.task.SimpleAsyncTaskExecutor} will be used to process async method invocations. Besides, annotated methods having a {@code void} return type cannot transmit any exception back to the caller. By default, such uncaught exceptions are only logged.To customize all this, implement {@link AsyncConfigurer}. NOTE: {@link AsyncConfigurer} configuration classes get initialized early in the application context bootstrap. If you need any dependencies on other beans there, make sure to declare them ‘lazy’ as far as possible in order to let them go through other post-processors as well.

  1. AsyncConfigurer 在 AbstractAsyncConfiguration,通过 setter 注入。
@Autowired(required = false)
void setConfigurers(Collection<AsyncConfigurer> configurers) {
   
	if (CollectionUtils.isEmpty(configurers)) {
   
		return;
	}
	if (configurers.size() > 1) {
   
		throw new IllegalStateException("Only one AsyncConfigurer may exist");
	}
	AsyncConfigurer configurer = configurers.iterator().next();
	this.executor = configurer::getAsyncExecutor;
	this.exceptionHandler = configurer::getAsyncUncaughtExceptionHandler;
}
  1. 当应用中没有 AsyncConfigurer 实现类时,将使用 Spring 容器中的 TaskExecutor 作为兜底。
// org.springframework.scheduling.annotation.AsyncAnnotationAdvisor#buildAdvice
public void configure(@Nullable Supplier<Executor> defaultExecutor,
		@Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
   

	// 使用容器 beanFactory 中 TaskExecutor 类型的 Bean 或名为 taskExecutor 的 Executor 对象作为兜底线程池
	this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory));
	this.exceptionHandler = new SingletonSupplier<>(exceptionHandler, SimpleAsyncUncaughtExceptionHandler::new);
	}

@Async 如何做到在 方法、类上声明时都能生效

@Import

Provides functionality equivalent to the <import/> element in Spring XML. Allows for importing @Configuration classes, ImportSelector and ImportBeanDefinitionRegistrar implementations, as well as regular component classes (as of 4.2; analogous to AnnotationConfigApplicationContext#register).
May be declared at the class level or as a meta-annotation.

ImportBeanDefinitionRegistrar

Interface to be implemented by types that register additional bean definitions when processing @{@link Configuration} classes. Useful when operating at the bean definition level (as opposed to {@code @Bean} method/instance level) is desired or necessary.

应用
  1. springboot实现多租户动态路由代码gitee 地址: tenant-spring-boot-starter
ImportSelector

Interface to be implemented by types that determine which @{@link Configuration} class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.

应用
  1. SpringBoot Web 应用中使用该注解决定向 Spring 容器中注入的 Servlet 容器类型(Tomcat、Jetty、Undertow):
    ServletWebServerFactoryAutoConfiguration.java
原理

在 Spring 的 refresh 阶段,通过 BeanFactoryPostProcessor 的实现类 ConfigurationClassPostProcessor 借助 ConfigurationClassParser 封装的能力实现的。

调用栈

在这里插入图片描述

ConfigurationClassParser
// From ConfigurationClassParser.java

// 递归收集 @Import 导入的所有类
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
		throws IOException {
   

	if (visited.add(sourceClass)) {
   
		for (SourceClass annotation : sourceClass.getAnnotations()) {
   
			String annName = annotation.getMetadata().getClassName();
			if (!annName.equals(Import.class.getName())) {
   
				// 递归「注解上的注解」
				collectImports(annotation, imports, visited);
			}
		}
		imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
	}
}

// 递归对导入的 class 进行处理,将导入的类放入到 ConfigurationClassParser#configurationClasses 中
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
		Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
		boolean checkForCircularImports) {
   

	if (importCandidates.isEmpty()) {
   
		return;
	}

	if (checkForCircularImports && isChainedImportOnStack(configClass)) {
   
		this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
	}
	else {
   
		this.importStack.push(configClass);
		try {
   
			for (SourceClass candidate : importCandidates) {
   
				if (candidate.isAssignable(ImportSelector.class)) {
   
					// Candidate class is an ImportSelector -> delegate to it to determine imports
					Class<?> candidateClass = candidate.loadClass();
					ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
							this.environment, this.resourceLoader, this.registry);
					Predicate<String> selectorFilter = selector.getExclusionFilter();
					if (selectorFilter != null) {
   
						exclusionFilter = exclusionFilter.or(selectorFilter);
					}
					if (selector instanceof DeferredImportSelector) {
   
						this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
					}
					else {
   
						String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
						Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
						processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
					}
				}
				else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
   
					// Candidate class is an ImportBeanDefinitionRegistrar ->
					// delegate to it to register additional bean definitions
					Class<?> candidateClass = candidate.loadClass();
					ImportBeanDefinitionRegistrar registrar =
							ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
									this.environment, this.resourceLoader, this.registry);
					configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
				}
				else {
   
					// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
					// process it as an @Configuration class
					this.importStack.registerImport(
							currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
					processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
				}
			}
		}
		catch (BeanDefinitionStoreException ex) {
   
			throw ex;
		}
		catch (Throwable ex) {
   
			throw new BeanDefinitionStoreException(
					"Failed to process import candidates for configuration class [" +
					configClass.getMetadata().getClassName() + "]", ex);
		}
		finally {
   
			this.importStack.pop();
		}
	}
}
ConfigurationClassPostProcessor
// From ConfigurationClassPostProcessor#processConfigBeanDefinitions

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
   
		// 省略代码 ...
		
		// Parse each @Configuration class
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);

		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
		do {
   
			StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
			// 递归 @Import,将相关的类放入 Map 中(configurationClasses)
			parser.parse(candidates);
			parser.validate();

			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
			configClasses.removeAll(alreadyParsed);

			// Read the model and create bean definitions based on its content
			if (this.reader == null) {
   
				this.reader = new ConfigurationClassBeanDefinitionReader(
						registry, this.sourceExtractor, this.resourceLoader, this.environment,
						this.importBeanNameGenerator, parser.getImportRegistry());
			}

			// 将通过 @Import 导入的类包装成 BeanDefinition 放入 Spring 容器中
			this.reader.loadBeanDefinitions(configClasses);
			alreadyParsed.addAll(configClasses);
			processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();

			candidates.clear();
			if (registry.getBeanDefinitionCount() > candidateNames.length) {
   
				String[] newCandidateNames = registry.getBeanDefinitionNames();
				Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
				Set<String> alreadyParsedClasses = new HashSet<>();
				for (ConfigurationClass configurationClass : alreadyParsed) {
   
					alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
				}
				for (String candidateName : newCandidateNames) {
   
					if (!oldCandidateNames.contains(candidateName)) {
   
						BeanDefinition bd = registry.getBeanDefinition(candidateName);
						if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
								!alreadyParsedClasses.contains(bd.getBeanClassName())) {
   
							candidates.add(new BeanDefinitionHolder(bd, candidateName));
						}
					}
				}
				candidateNames = newCandidateNames;
			}
		}
		while (!candidates.isEmpty());

		// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
		if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
   
			sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
		}

		if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
   
			// Clear cache in externally provided MetadataReaderFactory; this is a no-op
			// for a shared cache since it'll be cleared by the ApplicationContext.
			((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
		}
}

@Import注解的使用和实现原理
@Import 处理流程图

其它
  1. SpringBoot之@Import注解正确使用方式

@Cachable

@Conidtional

Indicates that a component is only eligible for registration when all specified conditions match.

The @Conditional annotation may be used in any of the following ways:

  1. as a type-level annotation on any class directly or indirectly annotated with @Component, including @Configuration classes
  2. as a meta-annotation, for the purpose of composing custom stereotype annotations
  3. as a method-level annotation on any @Bean method

If a @Configuration class is marked with @Conditional, all of the @Bean methods, @Import annotations, and @ComponentScan annotations associated with that class will be subject to the conditions.

原理

@Conditional 的解析封装在 ConditionEvaluator#shouldSkip 方法中

@ConditionalOnClass
@ConditionalOnProperty

{@link Conditional} that checks if the specified properties have a specific value. By default the properties must be present in the {@link Environment} and not equal to {@code false}. The {@link #havingValue()} and {@link #matchIfMissing()} attributes allow further customizations.

应用
// SpringBoot 自动开启 AOP 的配置类

@Configuration(proxyBeanMethods = false)
// Spring 容器未 spring.aop.auto,或设置的 spring.aop.auto=true 时,才会向容器中注入 AopAutoConfiguration 配置
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
//自动配置类
public class AopAutoConfiguration {
   
	// 省略代码 ...
}

@AfterThrowing

aspectjrt

AbstractAnnotationBeanPostProcessor

位于包 com.alibaba.spring:spring-context-support 下

用于处理自定义注解。如果使用「自定义注解」注解了属性或者方法,并且需要创建对象将其设置到属性或者方法入参,可以继承AbstractAnnotationBeanPostProcessor,并实现doGetInjectedBean 以创建需要注入的对象。

当 Bean创建时,会遍历其所有的属性和方法,判断是否被 ReferenceAnnotationBeanPostProcessor#annotationTypes 修饰

使用
  1. dubbo解析-客户端启动入口:流程图
  2. Dubbo笔记 ㉕ : Spring 执行流程概述:详细代码解析
  3. Krpc

日志

logging

logging.level.root=trace 设置整个工程日志级别高于 trace 的日志都会被打印输出。

nt

  1. ConfigurableApplicationContext重在对各种属性的配置,而ApplicationContext接口主要各种属性的get方法。Spring这种将get和set分开到两个接口的设计增大了属性设置和获取的灵活性,将两者分开也更加清晰。在以后的解决方案设计中,可以参考,将配置信息和获取信息分开,两者互不干扰,在保证重要的基础属性不变的情况,可以按需进行拓展。
  2. MethodInvokingFactoryBean

相关推荐

  1. Spring框架中用于处理请求参数的注解

    2024-01-29 09:40:04       20 阅读
  2. Git基本用法教程

    2024-01-29 09:40:04       27 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-29 09:40:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-29 09:40:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-29 09:40:04       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-29 09:40:04       20 阅读

热门阅读

  1. 无穷大与无穷小【高数笔记】

    2024-01-29 09:40:04       35 阅读
  2. DAY_10(区间dp)

    2024-01-29 09:40:04       32 阅读
  3. 上线服务器流程用法及说明

    2024-01-29 09:40:04       36 阅读
  4. Anaconda中安装包下载超时

    2024-01-29 09:40:04       43 阅读
  5. G1与ZGC

    G1与ZGC

    2024-01-29 09:40:04      42 阅读
  6. 第二百九十三回

    2024-01-29 09:40:04       41 阅读