Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理

前言

上一篇分析了BeanFactoryPostProcessor的作用,那么这一篇继续在refresh()方法里游荡,相信对Spring熟悉点的朋友,在看完BeanFactoryPostProcessor后,一定会想到Spring里面还有个BeanPostProcessor,那这个东西是什么作用呢?下面会进行介绍,同时由于注册BeanPostProcessor的逻辑比较简单,这里会穿插一下BeanPostProcessor生效的时机和源码逻辑,实际上这部分应该是Bean实例化出现的逻辑。

介绍完这部分之后,会介绍Spring的消息源初始化、广播器的初始以及监听器的初始化。这部分工作完成之后,Spring容器就会进入到Bean的创建过程,因为准备工作已经做得差不多了,容器已经准备好,接下来就是初始化Bean放进去容器里面。

BeanFactoryPostProcessor和BeanPostProcessor之间的区别

这两个的区别还是很显而易见的,主要表现在应用的阶段不同BeanFactoryPostProcessor是对BeanDefinition直接生效的,这更加底层,也更加原始,所以直接使用BeanFactoryPostProcessor会比较少。BeanPostProcessor是对bean实例生效的,相对于对BeanDefinition的处理,这个阶段更加靠后,BeanFactoryPostProcessor阶段bean是尚未初始化出来的,BeanPostProcessor处理的时候已经生成了实例对象,BeanPostProcessor会在对象的实例基础上进行一个更进一步的加工。

不熟悉的朋友看起来可能有点抽象,那么这里举一个例子吧。

BeanFactoryPostProcessor的类比:

假设你要造一个杯子,那么杯子需要一份原材料列表,材质你可以选择铁、铜、金、银等等,样式你可以选择圆型、方形、椭圆等等。假设开始原料选择铁,形状为圆形,那么这一份原料列表对应的就是一个BeanDefinition。原料列表出来后,没什么问题就会按照这一份列表去创建一个杯子。但是有时候需要一些额外的操作,例如对某些BeanDefinition进行检查,假设有一个检查员BeanFactoryPostProcessor去检查每个BeanDefinition。他看到杯子的材质是铁,觉得有失身份,于是把材料改成了金子,于是后续再去创建杯子的时候,就是个金杯了。

BeanPostProcessor的类比:

BeanPostProcessor的处理阶段则要靠后,在上面杯子创建完成之后,才到了BeanPostProcessor出场。BeanPostProcessor会在实例的基础上进行一些加工,拿杯子来举例,上一个阶段拿到的是一个粗糙的杯子,这里会进行一些处理,例如给杯子加点花纹样式,给杯子抛光等等。**注意这些操作都是在一个已有的杯子上进行的,但是请注意,这不是绝对的。**BeanPostProcessor除了能对Bean进行深加工外,还能直接进行Bean替换,类比来说,就是换了个杯子,偷梁换柱。Spring Aop的功能就是这样实现的,把经过代理的Bean放了进去,替换了原有的Bean。

所以比较一下得出一个很明显的结论:

  • BeanFactoryPostProcessor对BeanDefinition生效

  • BeanPostProcessor对bean实例生效

源码分析

registerBeanPostProcessors(beanFactory)

话不多说,下面继续分析refresh()方法里面的子方法,上一篇分析到了第五个子方法,那这篇从第六个registerBeanPostProcessors(beanFactory)开始。

跟进代码,可以看到实现都委托给了PostProcessorRegistrationDelegate#registerBeanPostProcessors(beanFactory, this)方法。

	/**
* Instantiate and register all BeanPostProcessor beans,
* respecting explicit order if given.
* <p>Must be called before any instantiation of application beans.
*
* 实例化并注册所有 BeanPostProcessor bean,如果给定顺序,则按照顺序排序。
* <p>必须在应用程序 bean 的任何实例化之前调用。
*/
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}

继续跟进,这个方法的逻辑也是比较简单的,跟上篇的BeanFactoryPostProcessor注册类似,这里也会按照优先级去对BeanPostProcessor进行排序然后按顺序进行注册。都是些家常套路了,可以跟着注释去看一下。值得注意的是,这里会额外加入两个BeanPostProcessor,分别为BeanPostProcessorCheckerApplicationListenerDetectorBeanPostProcessorChecker主要是用来记录一些日志,ApplicationListenerDetector是用来检测实现了ApplicationListener但是getBeanNamesForType()没探测出来的漏网之鱼。这里的漏网之鱼可能是一些动态注册的bean或者一些内部类,这里再次获取后会放入到applicationListeners集合里。

	public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) { // 获取容器中所有的 BeanPostProcessor
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); // Register BeanPostProcessorChecker that logs an info message when
// a bean is created during BeanPostProcessor instantiation, i.e. when
// a bean is not eligible for getting processed by all BeanPostProcessors.
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
// 注册 BeanPostProcessorChecker,当 bean 不符合所有 BeanPostProcessor 处理的条件时,它会在 BeanPostProcessor 实例化期间创建 bean 时记录一条信息消息
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); // Separate between BeanPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
// 按照顺序分类区分 BeanPostProcessor
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<>();
List<BeanPostProcessor> internalPostProcessors = new ArrayList<>();
List<String> orderedPostProcessorNames = new ArrayList<>();
List<String> nonOrderedPostProcessorNames = new ArrayList<>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
} // First, register the BeanPostProcessors that implement PriorityOrdered.
// 首先注册实现了 PriorityOrdered 接口的 BeanPostProcessor
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors); // Next, register the BeanPostProcessors that implement Ordered.
// 其次注册实现了 Ordered 接口的 BeanPostProcessor
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<>(orderedPostProcessorNames.size());
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, orderedPostProcessors); // Now, register all regular BeanPostProcessors.
// 现在到了注册没有实现上述接口的 BeanPostProcessor
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<>(nonOrderedPostProcessorNames.size());
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors); // Finally, re-register all internal BeanPostProcessors.
// 最后,重新注册所有内部 BeanPostProcessor MergedBeanDefinitionPostProcessor。
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors); // Re-register post-processor for detecting inner beans as ApplicationListeners,
// moving it to the end of the processor chain (for picking up proxies etc).
// 重新注册用于将内部 bean 检测为 ApplicationListeners 的后处理器,将其移动到处理器链的末尾(用于拾取代理等)。
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}

initMessageSource()

接下来继续进行下一步的准备工作,初始化消息源。这里是默认使用了父类的消息源,如果没有就初始化一个DelegatingMessageSource,这个DelegatingMessageSource会默认将所有的调用都委派到父容器的消息源去解析,如果没有父容器的消息源,那么它不会解析任何消息。

	/**
* Initialize the MessageSource.
* Use parent's if none defined in this context.
* 初始化消息源。如果没有在此上下文中定义,则使用父容器的。
*/
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
// 返回当前容器职工是否存在 messageSource,忽略祖先容器
if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
// Make MessageSource aware of parent MessageSource.
// 如果存在祖先,并且 messageSource 类型是 HierarchicalMessageSource,则获取祖先的 messageSource 设置到当前 messageSource 里。
if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
if (hms.getParentMessageSource() == null) {
// Only set parent context as parent MessageSource if no parent MessageSource
// registered already.
hms.setParentMessageSource(getInternalParentMessageSource());
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using MessageSource [" + this.messageSource + "]");
}
}
else {
// Use empty MessageSource to be able to accept getMessage calls.
// 本地不存在 messageSource,使用空 MessageSource 能够接受 getMessage 调用
DelegatingMessageSource dms = new DelegatingMessageSource();
dms.setParentMessageSource(getInternalParentMessageSource());
this.messageSource = dms;
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
if (logger.isTraceEnabled()) {
logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");
}
}
}

initApplicationEventMulticaster()

初始化 ApplicationEventMulticaster,如果上下文中没有定义,则使用 SimpleApplicationEventMulticaster

	/**
* Initialize the ApplicationEventMulticaster.
* Uses SimpleApplicationEventMulticaster if none defined in the context.
*
* 初始化 ApplicationEventMulticaster。
* 如果上下文中没有定义,则使用 SimpleApplicationEventMulticaster。
*
* @see org.springframework.context.event.SimpleApplicationEventMulticaster
*/
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
// 如果本地容器里存在 applicationEventMulticaster,直接使用本地容器里的 applicationEventMulticaster
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isTraceEnabled()) {
logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
// 否则使用 SimpleApplicationEventMulticaster 广播器
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
// 将给定的单例对象添加到该工厂的单例缓存中
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isTraceEnabled()) {
logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
"[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
}
}
}

这个事件广播器是干什么的呢?其实很简单,就是把一个事件广播到所有的ApplicationListener上。可以看一下里面的关键方法SimpleApplicationEventMulticaster#multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType),这里就是获取所有的listener,如果有异步线程池,则异步执行,否则逐个调用。

	@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
// 解析事件的类型,这个type会用于后续的 ListenerCacheKey 缓存 key 构建
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
// 获取线程池
Executor executor = getTaskExecutor();
// 逐个广播事件到 listener,就是将 listener 都遍历调用一遍
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
} protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
// 错误处理器,记录任务处理期间发生的错误
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
} private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
// 将事件传入listener,完成事件的监听回调
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
// ...
}
}

onRefresh()

这是个空方法,交给子类实现。这里可以用来初始化特定上下文子类中的其他特殊 bean,也是留出来的一个扩展口。

	/**
* Template method which can be overridden to add context-specific refresh work.
* Called on initialization of special beans, before instantiation of singletons.
*
* 可以重写以添加特定于上下文的刷新工作的模板方法。在单例实例化之前调用特殊 bean 的初始化。
*
* <p>This implementation is empty.
* @throws BeansException in case of errors
* @see #refresh()
*/
protected void onRefresh() throws BeansException {
// For subclasses: do nothing by default.
}

registerListeners()

上一步已经初始化完成了广播器,那接下来就是检查侦听器并注册它们。

事件可以按照注册的类型进行区分,可以分为以下三种:

  • 通过addApplicationListener()手动添加进去的
  • 容器里实现了ApplicationListener接口的
  • 容器启动早期需要的事件earlyApplicationEvents,早期事件是需要在这里直接发布的
	/**
* Add beans that implement ApplicationListener as listeners.
* Doesn't affect other listeners, which can be added without being beans.
*
* 添加实现 ApplicationListener 的 bean作为侦听器。
* 不影响其他监听器,可以添加而不是 bean。
*/
protected void registerListeners() {
// Register statically specified listeners first.
// 首先注册静态指定的监听器,也就是通过addApplicationListener(ApplicationListener<?> listener) 注册的listener。
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
} // Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them!
// 这里只是获取beanName,是为了避免初始化 bean 导致后置处理器失效
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
// 逐个注册listener
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
} // Publish early application events now that we finally have a multicaster...
// 发布早期应用程序事件,因为我们终于有了一个广播器......
// 忍辱负重,早期事件存到了这里才能进行发布,因为之前没有广播器
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
// 逐个发布事件
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}

finishBeanFactoryInitialization(beanFactory)

准备工作已经基本完成,接下来就到了finishBeanFactoryInitialization(beanFactory)方法了。从方法名可以看到,这个方法是负责完成此上下文的 bean 工厂的初始化,初始化所有剩余的单例 bean。可以看到这个方法开始也进行了一些准备工作,例如注册类型装换器、占位符处理器以及LoadTimeWeaverAware加载等。最后会调用beanFactory.preInstantiateSingletons()进行对象创建,由于这里是比较复杂的过程,会分几篇文章去详细分析,这篇文章就是大概从表面上走完refresh()方法的源码。

	/**
* Finish the initialization of this context's bean factory,
* initializing all remaining singleton beans.
*
* 完成此上下文的 bean 工厂的初始化,初始化所有剩余的单例 bean。
*
*/
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
// 初始化一个ConversionService用于类型转换,这个ConversionService会在实例化对象的时候用到
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
} // Register a default embedded value resolver if no bean post-processor
// (such as a PropertyPlaceholderConfigurer bean) registered any before:
// at this point, primarily for resolution in annotation attribute values.
// 添加一个StringValueResolver,用于处理占位符,可以看到,默认情况下就是使用环境中的属性值来替代占位符中的属性
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
} // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
// 创建所有的LoadTimeWeaverAware
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
} // Stop using the temporary ClassLoader for type matching.
// 静态织入完成后将临时的类加载器设置为null,所以除了创建LoadTimeWeaverAware时可能会用到临时类加载器,其余情况下都为空
beanFactory.setTempClassLoader(null); // Allow for caching all bean definition metadata, not expecting further changes.
// 将所有的配置信息冻结
beanFactory.freezeConfiguration(); // Instantiate all remaining (non-lazy-init) singletons.
// 开始进行真正的创建
beanFactory.preInstantiateSingletons();
}

finishRefresh()

到这里容器已经准备好了,bean也已经实例化完成,就差最后的一些事件通知和后续的兜底处理。这里比较重要的是会调用到所有实现了LifecycleProcessor#onRefresh()的Bean,在这里可以让生命周期Bean实现很多扩展。其次比较重要的是会发布一个ContextRefreshedEvent事件,通知所有监听器容器已经启动完成,这里就可以实现一些容器启动完成后的回调或者是一些任务等,任君发挥。

	/**
* Finish the refresh of this context, invoking the LifecycleProcessor's
* onRefresh() method and publishing the
* {@link org.springframework.context.event.ContextRefreshedEvent}.
*
* 完成容器的刷新启动,调用所有 LifecycleProcessor#onRefresh() 方法来发布 ContextRefreshedEvent 事件
*
*/
protected void finishRefresh() {
// Clear context-level resource caches (such as ASM metadata from scanning).
// 清除容器上下文级别的资源缓存(例如ASM扫描的元数据)
clearResourceCaches(); // Initialize lifecycle processor for this context.
// 初始化上下文的 lifecycle processor
initLifecycleProcessor(); // Propagate refresh to lifecycle processor first.
// 首先将刷新传播到生命周期处理器。
getLifecycleProcessor().onRefresh(); // Publish the final event.
// 发布最终事件。
publishEvent(new ContextRefreshedEvent(this)); // Participate in LiveBeansView MBean, if active.
// 参与 LiveBeansView MBean(如果处于活动状态)。
LiveBeansView.registerApplicationContext(this);
}

总结

本文的重点有点分散,更像是走马观花,但是分散里的重点毫无疑问是理解和区分BeanFactoryPostProcessorBeanPostProcessor之间的区别,文章开头通过一个例子去类比了一下这二者的作用阶段和分别可以完成什么工作,个人觉得还是比较贴切的,希望能够帮助到理解。

到这里已经基本把refresh()方法走了一遍,当然这里看到的大部分都是一些基础准备工作,最关键的Bean实例化是还没有开始分析的,Bean的实例化会后续分好几篇文章继续去分析。

今天这篇文章是比较简单的,没有太多逻辑,基本上都是一个一个小方法,嵌套不深,因为深入的我都不写了哈哈。

这系列写到这里,才完成了准备工作,接下来的Bean创建才是真正开始了重头戏。那接下来继续慢慢分析吧。

如果有人看到这里,那在这里老话重提。与君共勉,路漫漫其修远兮,吾将上下而求索。

Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理的更多相关文章

  1. Spring Ioc源码分析系列--Ioc容器BeanFactoryPostProcessor后置处理器分析

    Spring Ioc源码分析系列--Ioc容器BeanFactoryPostProcessor后置处理器分析 前言 上一篇文章Spring Ioc源码分析系列--Ioc源码入口分析已经介绍到Ioc容器 ...

  2. Spring Ioc源码分析系列--Ioc的基础知识准备

    Spring Ioc源码分析系列--Ioc的基础知识准备 本系列文章代码基于Spring Framework 5.2.x Ioc的概念 在Spring里,Ioc的定义为The IoC Containe ...

  3. Spring Ioc源码分析系列--Ioc源码入口分析

    Spring Ioc源码分析系列--Ioc源码入口分析 本系列文章代码基于Spring Framework 5.2.x 前言 上一篇文章Spring Ioc源码分析系列--Ioc的基础知识准备介绍了I ...

  4. Spring Ioc源码分析系列--Bean实例化过程(一)

    Spring Ioc源码分析系列--Bean实例化过程(一) 前言 上一篇文章Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理已经完成了对 ...

  5. Spring Ioc源码分析系列--容器实例化Bean的四种方法

    Spring Ioc源码分析系列--实例化Bean的几种方法 前言 前面的文章Spring Ioc源码分析系列--Bean实例化过程(二)在讲解到bean真正通过那些方式实例化出来的时候,并没有继续分 ...

  6. Spring Ioc源码分析系列--前言

    Spring Ioc源码分析系列--前言 为什么要写这个系列文章 首先这是我个人很久之前的一个计划,拖了很久没有实施,现在算是填坑了.其次,作为一个Java开发者,Spring是绕不开的课题.在Spr ...

  7. Spring Ioc源码分析系列--Bean实例化过程(二)

    Spring Ioc源码分析系列--Bean实例化过程(二) 前言 上篇文章Spring Ioc源码分析系列--Bean实例化过程(一)简单分析了getBean()方法,还记得分析了什么吗?不记得了才 ...

  8. Spring Ioc源码分析系列--@Autowired注解的实现原理

    Spring Ioc源码分析系列--@Autowired注解的实现原理 前言 前面系列文章分析了一把Spring Ioc的源码,是不是云里雾里,感觉并没有跟实际开发搭上半毛钱关系?看了一遍下来,对我的 ...

  9. Spring Ioc源码分析系列--自动注入循环依赖的处理

    Spring Ioc源码分析系列--自动注入循环依赖的处理 前言 前面的文章Spring Ioc源码分析系列--Bean实例化过程(二)在讲解到Spring创建bean出现循环依赖的时候并没有深入去分 ...

随机推荐

  1. css3中user-select的用法详解

    css3中user-select的用法详解 user-select属性是css3新增的属性,用于设置用户是否能够选中文本.可用于除替换元素外的所有元素,以下是user-select的主要用法和注意事项 ...

  2. (SSM框架)实现小程序图片上传(配小程序源码)

    阅读本文约"2分钟" 又是一个开源小组件啦! 因为刚好做到这个小功能,所以就整理了一下,针对微信小程序的图片(文件)上传! 原业务是针对用户反馈的图片上传.(没错,本次还提供小程序 ...

  3. java中抽象类和抽象方法到底有什么用呢?

    抽象类和抽象方法有什么用呢?马克-to-win:当初sun公司为什么要设计抽象类和抽象方法呢?当你在做车的系统设计时,当你设计车这个通用类时,假如你确认别人实例化车这个通用类没有意义时(不知道是bik ...

  4. PAT A1001 A+B Format

    Calculate a+b and output the sum in standard format -- that is, the digits must be separated into gr ...

  5. struts token令牌机制

    利用Struts同步令牌(Token)机制来解决Web应用中的重复提交问题.该方法的基本原理是:服务器端在处理到达的request之前,会将request中的Token值与保存在当前用户session ...

  6. 微信小程序人脸识别

    参考:https://cloud.tencent.com/document/product/1007/31071

  7. Centos搭建 LAMP 服务器教程

    搭建 LAMP 服务 搭建 MySQL 数据库 安装 MySQL 使用 yum 安装 MySQL: yum install mysql-server -y 安装完成后,启动 MySQL 服务: ser ...

  8. 服务器的cpu 核心、线程

    此版本有大范围改动,因为cpu作为一个大脑,所以更细致的进行了,相关的分析和阐述. 1.版本1. 2022.1.242.版本2: 2022.3.2 采集数据: ht2机器为物理机,cpu是4颗cpu, ...

  9. java数组算法——数组元素的赋值2

    java数组算法--数组元素的赋值2--java经典面试题:创建一个长度为6的int型数组,要求数组元素的值都在1-30之间,且是随机赋值.同时要求元素时的值各不相同

  10. 如何写好一份晋升PPT(附PPT模板)

    又到了每年晋升述职的时间,在过去的5.6年里,我以评委身份参与了大量的晋升述职(主要是前端,也包括客户端和测试),也辅导了许多(100+)组内外的同学,指导他们书写和完善PPT.过程中我发现大家有许多 ...