springboot debug bean初始化过程

直接来看AbstractApplicationContext类的refresh()方法的实现。这个方法是Spring容器启动的核心方法,用于初始化和刷新Spring应用程序上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (RuntimeException | Error ex ) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
contextRefresh.end();
}
}
finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}

第一个:prepareRefresh();从注释可以看出,这是做准备(为刷新上下文做准备,包括设置活跃标志、初始化事件发布者等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* Prepare this context for refreshing, setting its startup date and
* active flag as well as performing any initialization of property sources.
*/
protected void prepareRefresh() {
// Switch to active.
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);
if (logger.isDebugEnabled()) {
if (logger.isTraceEnabled()) {
logger.trace("Refreshing " + this);
}
else {
logger.debug("Refreshing " + getDisplayName());
}
}
// Initialize any placeholder property sources in the context environment.
// 初始化上下文环境中的任何占位符属性源
initPropertySources();
// Validate that all properties marked as required are
resolvable:seeConfigurablePropertyResolver#setRequiredProperties
// 验证所有标记为必需的属性是否可解析:参见 ConfigurablePropertyResolver#setRequiredProperties
getEnvironment().validateRequiredProperties();
// Store pre-refresh ApplicationListeners...
// 存储预刷新 ApplicationListeners
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);
}
// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<>();
}

第二步:刷新bean工厂准备beanfactory

1
2
3
4
5
6
// Tell the subclass to refresh the internal bean factory.
告诉子类刷新内部bean工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context
准备 bean 工厂以供在此上下文中使用
prepareBeanFactory(beanFactory);

第三步:允许在上下文子类中对 bean 工厂进行后处理

1
2
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

第四步:调用在上下文中注册为 bean 的工厂处理器

1
2
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

第五步:注册拦截 bean 创建的 bean 处理器

1
2
3
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();

第六步:初始化此上下文的消息源

1
2
// Initialize message source for this context.
initMessageSource();

第七步:为该上下文初始化事件多播器

1
2
// Initialize event multicaster for this context.
initApplicationEventMulticaster();

第八步:初始化特定上下文子类中的其他特殊 bean

1
2
// Initialize other special beans in specific context subclasses.
onRefresh()

第九步:检查监听器 bean 并注册它们

1
2
// Check for listener beans and register them.
registerListeners();

第十步:实例化所有剩余的(非延迟初始化)单例

1
2
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

十一步:最后一步,发布相应事件。

1
2
// Last step: publish corresponding event.
finishRefresh();

现在来看看bean的具体初始化,第十步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
方法注释是 完成此上下文的 bean 工厂的初始化,初始化所有剩余的单例 bean
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
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 BeanFactoryPostProcessor
// (such as a PropertySourcesPlaceholderConfigurer bean) registered any before:
// at this point, primarily for resolution in annotation attribute values.
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}

// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
try {
beanFactory.getBean(weaverAwareName, LoadTimeWeaverAware.class);
}
catch (BeanNotOfRequiredTypeException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to initialize LoadTimeWeaverAware bean '" + weaverAwareName +
"' due to unexpected type mismatch: " + ex.getMessage());
}
}
}

// Stop using the temporary ClassLoader for type matching.
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();
}

spring mvc解读

上篇自己手写了了一下mvc的简易版,这次解读一下springmvc官方源码,先大概根据图片来了解一下流程
springmvc.png
debug准备:实现debug的具体流程,创建springboot项目,添加web依赖,编写controller类,完成以上流程找到DispatcherServlet类(org.springframework.web.servlet包下),该类继承FrameworkServlet(抽象类),FrameworkServlet继承HttpServletBean,HttpServletBean继承HttpServlet;根据servlet的实现,肯定先执行init,找到该方法,init方法被重写了
截屏2025-01-03 09.26.01.png

可以看到调用了父类的init,父亲类是GenericServlet,GenericServlet和HttpServlet属于不属于springmvc包下先不讨论,但这个初始化过程还是要知道什么时候开始的;
回到DispatcherServlet,里面重要的组件,与上面对应上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private MultipartResolver multipartResolver;
@Nullable
private LocaleResolver localeResolver;
/** @deprecated */
@Deprecated
@Nullable
private ThemeResolver themeResolver;
@Nullable
private List<HandlerMapping> handlerMappings;
@Nullable
private List<HandlerAdapter> handlerAdapters;
@Nullable
private List<HandlerExceptionResolver> handlerExceptionResolvers;
@Nullable
private RequestToViewNameTranslator viewNameTranslator;
@Nullable
private FlashMapManager flashMapManager;
@Nullable
private List<ViewResolver> viewResolvers;

先来看两个构造方法,第一个构造方法打上断点

1
2
3
4
5
6
7
8
public DispatcherServlet() {
this.setDispatchOptionsRequest(true);
}

public DispatcherServlet(WebApplicationContext webApplicationContext) {
super(webApplicationContext);
this.setDispatchOptionsRequest(true);
}

往下走,可以看到执行到,截屏2025-01-03 09.48.38.png
这个自动配置,这里返回的就是我们今天要讨论的DispatcherServlet;写一个发送post请求的servlet,回到HttpServlet类,给doPost打上断点,postman发送请求,会发生什么,可以看到请求的一瞬间,进入debug,执行下一步就进入FrameworkServlet的doPost,执行processRequest方法
截屏2025-01-03 09.58.13.png

执行完毕后,走到DispatcherServlet的doDispatch方法,这里就是sprmvc执行的具体流程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
// 提供组件异步处理 HTTP 请求
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}

HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new ServletException("Handler dispatch failed: " + var21, var21);
}

this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
triggerAfterCompletion(processedRequest, response, mappedHandler, new ServletException("Handler processing failed: " + var23, var23));
}

} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}

asyncManager.setMultipartRequestParsed(multipartRequestParsed);
} else if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
this.cleanupMultipart(processedRequest);
}

}
}

根据上图
1.第一步请求进入DispatcherServlet
2.获取Handler,返回HandlerExecutionChain,包括
截屏2025-01-03 10.12.44.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mappedHandler = this.getHandler(processedRequest);

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();

while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}

3 .根据返回来的Handler来寻找HandlerAdapter,继续走

1
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());

4 . HandlerAdapter处理解析到的Handler

1
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

5.6.看作一步,handle方法内部,HandlerAdapter是一个接口
截屏2025-01-03 10.24.26.png
7 返回modelview(这里的属性名为mv)

1
2
3
4
// 用于给模型和视图(ModelAndView)对象mv应用默认的视图名称
this.applyDefaultViewName(processedRequest, mv);
// 用于执行所有后置拦截器(Interceptor)的postHandle方法
mappedHandler.applyPostHandle(processedRequest, response, mv);

8 9 10 可以看成一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);


private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
ModelAndViewDefiningException mavDefiningException = (ModelAndViewDefiningException)exception;
this.logger.debug("ModelAndViewDefiningException encountered", exception);
mv = mavDefiningException.getModelAndView();
} else {
Object handler = mappedHandler != null ? mappedHandler.getHandler() : null;
mv = this.processHandlerException(request, response, handler, exception);
errorView = mv != null;
}
}

if (mv != null && !mv.wasCleared()) {
this.render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace("No view rendering, null ModelAndView returned.");
}

if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, (Exception)null);
}

}
}

protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
LocaleResolver lr = this.localeResolver;
if (lr instanceof LocaleContextResolver localeContextResolver) {
return localeContextResolver.resolveLocaleContext(request);
} else {
return () -> {
return lr != null ? lr.resolveLocale(request) : request.getLocale();
};
}
}

可以看到这里对视图有好几种处理方式(异常,无视图,有视图),现在我们都是前后端分离,返回json,这种属于无视图的处理,有视图的话也是利用response流处理液乳到视图上,没有是直接放回response流;
截屏2025-01-03 10.52.52.png
现在都是springboot,springboot自动配置处理了DispatcherServlet,如下
截屏2025-01-03 09.48.38.png

手写SpringMVC,理解工作原理

SpringMVC是Spring家族中的元老之一,它是一个基于MVC三层架构模式的Web应用框架,它的出现也一统了JavaWEB应用开发的项目结构,从而避免将所有业务代码都糅合在同一个包下的复杂情况。在该框架中通过把Model、View、Controller分离,如下:

M/Model模型:由service、dao、entity等JavaBean构成,主要负责业务逻辑处理。
V/View视图:负责向用户进行界面的展示,由jsp、html、ftl….等组成。
C/Controller控制器:主要负责接收请求、调用业务服务、根据结果派发页面。

SpringMVC贯彻落实了MVC思想,以分层工作的模式,把整个较为复杂的web应用拆分成逻辑清晰的几部分,从很大程度上也简化了开发工作,减少了团队协作开发时的出错几率。
   回想最初的servlet开发,或者说最初我们学习Java时,如稚子般的操作,当时也不会划分模块、划分包,所有代码一股脑的全都放在少数的几个包下。但不知从何时起,慢慢的,每当有一个新的项目需求出现时,我们都会先对其划分模块,再划分层次,SpringMVC这个框架已经让每位Java开发彻底将MVC思想刻入到了DNA中,无论是最初的单体开发,亦或是如今主流的分布式、微服务开发,相信大家都已经遵守着这个思想。

SpringMVC框架的设计,是以请求为驱动,围绕Servlet设计的,将请求发给控制器,然后通过模型对象,分派器来展示请求结果的视图。SpringMVC的核心类是DispatcherServlet,它是一个Servlet子类,顶层是实现的Servlet接口。

当然,此刻暂且避开其原理不谈,先回想最初的SpringMVC是如何使用的呢?一起来看看。
1.1、SpringMVC的使用方式
   对于SpringMVC框架的原生使用方式,估计大部分小伙伴都已经忘了,尤其是近些年SpringBoot框架的流行,由于其简化配置的特性,让我们几乎无需再关注最初那些繁杂的XML配置。

说到这块就引起了我早些年那些痛苦的回忆,在SpringBoot还未那么流行之前,几乎所有的配置都是基于XML来弄的,而且每当引入一个新的技术栈,都需要配置一大堆文件,比如Spring、SpringMVC、MyBatis、Shiro、Quartz、EhCache….,这个整合过程无疑是痛苦的。

但随着后续的SpringBoot流行,这些问题则无需开发者再关注,不过成也SpringBoot,败也SpringBoot,尤其是近几年新入行的Java程序员,正是由于未曾有过之前那种繁重的XML配置经历,因此对于application.yml中很多技术栈的配置项也并不是特别理解,项目开发中需要引入一个新的技术栈时,几乎靠在网上copy他人的配置信息,也就成了“知其然而不知其所以然”,这对后续想要深入研究底层也成了一道新的屏障。

就此打住,感慨也不多说了,咱们先来回忆回忆最初SpringMVC的使用方式:基于最普通的maven-web工程构建。

在使用SpringMVC框架时,一般会首先配置它的核心文件:springmvc-servlet.xml,如下:

1
2
3
4
5
6
7
8
9
10
11
xml 代码解读复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
<!-- 通过context:component-scan元素扫描指定包下的控制器-->
<!-- 扫描com.xxx.xxx及子孙包下的控制器(扫描范围过大,耗时)-->
<context:component-scan base-package="com.xxx.controller"/>

<!-- ViewResolver -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <!-- viewClass需要在pom中引入两个包:standard.jar and jstl.jar -->
    <property name="viewClass"
              value="org.springframework.web.servlet.view.JstlView"></property>
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

<!-- 省略其他配置...... -->
</beans>

在springmvc-servlet.xml这个核心配置文件中,最重要的其实是配置Controller类所在的路径,即包扫描的路径,以及配置一个视图解析器,主要用于解析请求成功之后的视图数据。
OK~,配置好了springmvc-servlet.xml文件后,紧接着我们会再修改maven-web项目核心文件web.xml中的配置项:
xml 代码解读复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!-- 再这里会添加一个SpringMVC的servlet配置项 -->
<servlet>
<!-- 首先指定SpringMVC核心控制器所在的位置 -->
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- DispatcherServlet启动时,从哪个文件中加载组件的初始化信息 -->
<!--此参数可以不配置,默认值为:/WEB-INF/springmvc-servlet.xml-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/springmvc-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<!--web.xml 3.0的新特性,是否支持异步-->
<!--<async-supported>true</async-supported>-->
</servlet>
<!-- 配置路由匹配规则,/ 代表匹配所有,类似于nginx的location规则 -->
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

修改web.xml中的配置时,主要就干了一件事情,也就是为SpringMVC添加了一对servlet的配置项,主要指定了几个值:

①指定了SpringMVC中DispatcherServlet类的全路径。
②指定DispatcherServlet初始化组件时,从哪个文件中加载组件的配置信息。
③配置了一条值为/的路由匹配规则,/代表所有请求路径都匹配。

经过上述配置后,服务器启动后,所有的请求都会根据配置好的路由规则,先去到DispatcherServlet中处理。
至此,大概的配置就弄好了,紧接着是在前面配置的com.xxx.controller包中编写对应的Controller类,如下:
java 代码解读复制代码package com.xxx.controller;

@Controller(“/user”)
public class UserController{
// 省略……
}

一切就绪后,一般都会将WEB应用打成war包,然后放入到Tomcat中运行,而当Tomcat启动时,首先会找到对应的WEB程序,紧接着会去加载web.xml,加载web.xml时,由于前面在其中配置了DispatcherServlet,所以此时会先去加载DispatcherServlet,而加载这个类时,又会触发它的初始化方法,会调用initStrategies()方法对组件进行初始化,如下:
java 代码解读复制代码// DispatcherServlet类 → initStrategies()方法
protected void initStrategies(ApplicationContext context) {
// 在这里面初始化SpringMVC工作时,需要用到的各大组件
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

那初始化组件时,肯定需要一些加载一些对应的组件配置,这些配置信息从哪儿来呢?也就是根据我们指定的配置项,读取之前的核心文件:springmvc-servlet.xml中所配置的信息,对各大组件进行初始化。

所以,当Tomcat启动成功后,SpringMVC的各大组件也会初始化完成。

当然,DispatcherServlet除开是SpringMVC的初始化构建器外,还是SpringMVC的组件调用器,因为前面在web.xml还配置了一条路由规则,所有的请求都会先进入DispatcherServlet中处理,那既然所有的请求都进入了这个类,此时究竟该如何分发请求,就可以任由SpringMVC调度了。

但SpringMVC内部究竟是如何调用各大组件对请求进行处理的,这就涉及到了本文开头抛出的面试题了,也就是SpringMVC的工作原理,接下来我们简单聊一聊。

二、SpringMVC工作原理详解
   在了解SpringMVC的工作原理之前,首先认识一些常用组件:

DispatcherServlet前端控制器:接收请求,响应结果,相当于转发器,是整个流程控制的中心,由它调用其它组件处理用户的请求,因此也可称为中央处理器。有了它之后,可以很大程度上减少其它组件之间的耦合度。

HandlerMapping处理映射器:主要负责根据请求路径查找Handler处理器,也就是根据用户的请求路径找到具体的Java方法,具体是如何找到的呢?是根据映射关系查找的,SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

HandlerAdapter处理适配器:就是一个用于执行Handler处理器的组件,会根据客户端不同的请求方式(get/post/…),执行对应的Handler。说人话就是前面的组件定位到具体Java方法后,用来执行Java方法的组件。

Handler处理器:其实这也就是包含具体业务操作的Java方法,在SpringMVC中会被包装成一个Handler对象。

ViewResolver视图解析器::对业务代码执行完成之后的结果进行视图解析,根据逻辑视图名解析成真正的视图,比如controller方法执行完成之后,return的值是index,那么会对这个结果进行解析,将结果生成例如index.jsp这类的View视图。
ViewResolver工作时,会首先根据逻辑视图名解析成物理视图名,即具体的页面地址,然后再生成View视图对象,最后对视图进行渲染,将处理结果通过页面展示给用户。
SpringMVC提供了很多的View视图类型,如:jstlView、freemarkerView、pdfView等,前面我们配置的JSP视图解析器则是JstlView,这里也可以根据模板引擎的不同,选择不同的解析器。

View视图:View在SpringMVC中是一个接口,实现类支持不同的类型,例如jsp、freemarker、ftl…,不过现在一般都是前后端分离的项目,因此也很少再用到这块内容,视图一般都成了html页面,数据结果的渲染工作也交给了前端完成。

大致对于SpringMVC的核心组件有了了解之后,再上一张图:

对于这张图,相信大家都多多少少有在“面试八股文”中看到过,这也是涵盖了SpringMVC内部调度时的完整流程图,请求到来后都会经过这一系列步骤,如下:

①用户发送请求至会先进入DispatcherServlet控制器进行相应处理。
②DispatcherServlet会调用HandlerMapping根据请求路径查找Handler。
③处理器映射器找到具体的处理器后,生成Handler对象及Handler拦截器(如果有则生成),然后返回给DispatcherServlet。
④DispatcherServlet紧接着会调用HandlerAdapter,准备执行Handler。
⑤HandlerAdapter底层会利用反射机制,对前面生成的Handler对象进行执行。
⑥执行完对应的Java方法后,HandlerAdapter会得到一个ModelAndView对象。
⑦HandlerAdapter将ModelAndView再返回给DispatcherServlet控制器。
⑧DisPatcherServlet再调用ViewReslover,并将ModelAndView传递给它。
⑨ViewReslover视图解析器开始解析ModelAndView并返回解析出的View视图。
⑩解析出View视图后,对视图进行数据渲染(即将模型数据填充至视图中)。
⑪DispatcherServlet最终将渲染好的View视图响应给用户浏览器。

其实观察如上流程,SpringMVC中的其他组件几乎不存在太多的耦合关系,大部分的工作都是由DispatcherServlet来调度组件完成的,因此这也是它被称为“中央控制器”的原因,DispatcherServlet本质上并不会处理用户请求,它仅仅是作为请求统一的访问点,负责请求处理时的全局流程控制。

当然,最开始由于我们在springmvc-servlet.xml中配置了扫包路径,因此在项目启动时,就会去扫描对应目录下的所有类,然后将带有对应注解的类与方法,与注解上指定的请求路径生成映射关系,方便后续请求到来时能够精准定位(稍后看完手写案例大家就理解这点了)。

经过上述一系列分析后会发现,SpringMVC的核心就是DispatcherServlet,由它去调用各类组件完成工作。而DispatcherServlet其实本质上就是一个Servlet子类,一般WEB层框架本质上都离不开Servlet,就好比ORM框架离不开JDBC,比如Zuul、GateWay等框架,本质上也是依赖于Servlet技术作为底层的。
三、手写Mini版SpringMVC框架
   到目前为止,相对来说已经将SpringMVC的工作原理做了简单概述,接下来就来到本文的核心:自己手写一个Mini版的SpringMVC框架。步骤主要分为五步:

①自定义相关注解。
②实现核心组件。
③实现DispatcherServlet。
④编写相关的视图(jsp网页)。
⑤编写测试用例。

不过在手写之前,咱们得先创建一个普通的Maven-Web工程。
3.1、自定义相关注解
   SpringMVC中的注解实际上并不少,所以在这里不会全部实现,重点就自定义@Controller、@RequestMapping、@ResponseBody这几个常用的核心注解。
3.1.1、@Controller注解的定义
java 代码解读复制代码// 声明注解的生命周期:RUNTIME表示运行时期有效
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范围:只能生效于类上面
@Target(ElementType.TYPE)
public @interface Controller {
//@interface是元注解:JDK封装的专门用来实现自定义注解的注解
}

这个注解稍后会加载咱们要扫描的Controller类上,主要是为了标注出扫描时的目标类。
3.1.2、@RequestMapping注解的定义
java 代码解读复制代码// 声明注解的生命周期:RUNTIME表示运行时期有效
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范围:可应用在类上面、方法上面
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface RequestMapping {
// 允许该注解可以填String类型的参数,默认为空
String value() default “”;
}

这个注解可以加在类或方法上,主要是用来给类或方法映射请求路径。
3.1.3、@ResponseBody注解的定义
java 代码解读复制代码// 声明注解的生命周期:RUNTIME表示运行时期有效
@Retention(RetentionPolicy.RUNTIME)
// 注解的生效范围:只能应用在方法上面
@Target(ElementType.METHOD)
public @interface ResponseBody {
}

这个注解的作用是在于控制返回时的响应方式,不加该注解的方法,默认会跳转页面,也加了该注解的方法,则会直接响应数据。
OK~,在上面定义了三个注解,其中使用到了两个JDK提供的元注解:@Retention、@Target,前者用于控制注解的生命周期,表示自定义的注解在何时生效。后者则控制了注解的生效范围,可以控制自定义注解在类、方法、属性上生效。

不过在这里并未对这些注解进行处理,只是简单的定义,如果想要注解生效,一般有两种方式:①使用AOP切面对注解进行处理。②使用反射机制对注解进行处理。

稍后我们会采用上述的第二种方式对自定义的注解进行处理。
3.2、实现核心组件
自定义注解的工作完成后,紧接着再来实现一些运行时需要用到的核心组件。当然,这里也不会将之前SpringMVC拥有的所有组件全部实现,仅实现几个核心的组件,能够达到效果即可。(在完成之后,大家有兴趣可自行完善)。
3.2.1、InvocationHandler组件
InvocationHandler这个组件,主要是为了待会儿配合扫描包使用的,可以简单理解成Java方法的封装对象,如下:
java 代码解读复制代码

public class InvocationHandler {
    // 这里会存放方法对应的对象实例
    private Object object;
    // 这里会存放对应的Java方法
    private Method method;
// 构造方法:无参和全参构造
public InvocationHandler(){}
public InvocationHandler(Object object, Method method) {
    this.object = object;
    this.method = method;
}

// Get and Set方法
public Object getObject() {
    return object;
}
public void setObject(Object object) {
    this.object = object;
}
public Method getMethod() {
    return method;
}
public void setMethod(Method method) {
    this.method = method;
}

// 这里重写了toString()方法
@Override
public String toString() {
    return "InvocationHandler{" +
            "object=" + object +
            ", method=" + method +
            '}';
}

}

这个组件很简单,相信大家也能直接看明白,这也对应着之前SpringMVC中的Handler组件。
3.2.2、HandlerMapping组件
这个组件主要负责扫描包,在项目启动时,将指定的包目录下,所有的请求路径与Java方法形成映射关系。
typescript 代码解读复制代码

public class HandlerMapping {
    public Map<String,InvocationHandler> urlMapping(Set<Class<?>> classSet){
        // 初始化一个 Map 集合,用于存放映射关系
        HashMap<String, InvocationHandler> HandlerHashMap = new HashMap<>();
        // 遍历 Controller 集合(也就是所有带@Controller注解的类)
        for (Class<?> aClass : classSet) {
            //获取类上@RequestMapping注解的值
            String classReqPath = AnnotationUtil.
                    getAnnotationValue(aClass, RequestMapping.class);
            System.out.println("类的请求路径:" + classReqPath);
        // 获取这个 class 类中的所有方法
        Method[] methods = aClass.getDeclaredMethods();
        System.out.println("类中方法数量为:" + methods.length);

        // 如果这个类中方法数量不为空
        if (methods.length != 0) {
            // 开始遍历这个类中的所有方法
            for (Method method : methods) {
                // 判断每个方法上是否带有@RequestMapping注解
                boolean flag = method.isAnnotationPresent(RequestMapping.class);
                // 如果当前方法上带有这个注解
                if (flag){
                    // 获取方法上@RequestMapping注解的值
                    String methodReqPath = AnnotationUtil.
                            getAnnotationValue(method, RequestMapping.class);
                    // 判断得到的值是否为空,不为空则获取对应的值
                    String reqPath = methodReqPath == null ||
                            methodReqPath.equals("") ? "" : methodReqPath;
                    System.out.println("方法上的请求路径:" + reqPath);
                    // 将得到的值封装成 InvocationHandler 对象
                    try {
                        // 放入一个当前类的实例对象,用于执行后面的类方法
                        InvocationHandler invocationHandler = new 
                                InvocationHandler(aClass.newInstance(), method);
                        // 使用 类的请求路径 + 方法的请求路径 作为Key
                        HandlerHashMap.put(classReqPath + reqPath,
                                invocationHandler);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    // 将存放映射关系的Map集合返回
    return HandlerHashMap;
}

}

在这个类中,主要定义了一个urlMapping()方法,这个方法做的主要工作就是:对于所有存在@Controller注解的类做扫描,对于这些类中的方法进行判断,将所有带@RequestMapping注解的方法,全部封装成InvocationHandler对象作为Value,然后再以类的请求路径 + 方法的请求路径作为Key,放入到一个Map集合中保存。
3.3、实现DispatcherServlet中央控制器
自定义注解和组件的工作完成后,接下来再开始编写最核心的DispatcherServlet类,同样,在定义时记得继承HttpServlet:
java 代码解读复制代码public class DispacherServlet extends HttpServlet {

// 定义一个 Map 容器,存储映射关系
private static Map<String, InvocationHandler> HandlerMap;

@Override
public void init() throws ServletException {
    System.out.println("项目启动了.....");
    // 指定要扫描的包路径(原本是从xml文件中读取的)
    String packagePath = "com.xxx.controller";
    // 在指定的包路径下扫描带有@Controller注解的类
    Set<Class<?>> classSet = ClassUtil.
            scanPackageByAnnotation(packagePath, Controller.class);
    System.out.println("扫描到类的数量为:" + classSet.size());
    // 创建一个HandlerMapping并调用urlMapping()方法
    HandlerMapping handlerMapping = new HandlerMapping();
    HandlerMap = handlerMapping.urlMapping(classSet);
    // 最终获取到一个带有所有映射关系的 Map 集合
    System.out.println("HandlerMap的长度:" + HandlerMap.size());
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    doPost(req,resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    // 获取客户端的请求路径
    StringBuffer requestURL = req.getRequestURL();
    System.out.println("客户端请求路径:" + requestURL);
    // 判断请求路径中是否包含项目名,包含的话使用空字符替换掉
    String path = new String(requestURL).replace("http://" +
            req.getServerName() + ":" + req.getServerPort(), "");
    System.out.println("处理后的客户端请求路径:" + path);
    // 根据处理好的 path 作为条件去map中查找对应的方法
    InvocationHandler handler = HandlerMap.get(path);
    // 获取到对应的类实例对象和Java方法
    Object object = handler.getObject();
    Method method = handler.getMethod();

    // 判断该方法上是否添加了@ResponseBody注解:
    //      true:直接返回数据  false:跳转页面
    boolean f = method.isAnnotationPresent(ResponseBody.class);
    System.out.println("是否添加了@ResponseBody注解:" + f);
    // 如果方法上存在@ResponseBody注解
    if (f){
        try {
            // 通过反射的方式调用方法并执行
            Object invoke = method.invoke(object);
            // 将结果通过Response直接写回给客户端
            resp.getWriter().print(invoke.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else{
        // 获取客户端的请求路径作为返回时的前路径
        String URL = "http://" + req.getServerName() + ":" +
                req.getServerPort() + "/" + req.getContextPath();
        System.out.println("URL:" + URL);
        // 自定义的前后缀(原本也是在xml中读取)
        String prefix = "";
        String suffix = ".jsp";
        try {
            // 通过反射机制,执行对应的Java方法
            Object invoke = method.invoke(object);
            if(invoke instanceof ModelAndView){
                // 如果是返回的ModelAndView对象,这里做额外处理....
            } else{
                // 获取Java方法执行之后的返回结果
                String str = (String)invoke;
                // 如果指定了跳转方法为 forward: 转发
                if(str.contains("forward:")){
                    System.out.println("以转发的方式跳转页面...");
                    req.getRequestDispatcher("index.jsp").forward(req,resp);
                }
                // 如果指定了跳转方法为 redirect: 重定向
                if(str.contains("redirect:")){
                    System.out.println("以重定向的方式跳转页面...");
                    resp.sendRedirect(URL + prefix +
                        str.replace("redirect:","") + suffix);
                }
                // 如果没有指定,则默认使用转发的方式跳转页面
                if(!str.contains("forward:") && !str.contains("redirect:")){
                    resp.sendRedirect(URL + prefix + str + suffix);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

}

由于DispacherServlet实现了HttpServlet抽象类,因此也重写了它的三个方法:init()、doGet()、doPost(),其中init()方法会在项目启动时执行,而doGet()、doPost()则会在客户端请求时被触发。
总结一下上述DispacherServlet所做的工作:

①初始化所有请求路径与Java方法之间的映射关系。
②根据客户端的请求路径,查找对应的Java方法并执行。
③判断方法上是否添加了@ResponseBody注解:

添加了:直接向客户端返回数据。
未添加:跳转对应的页面。

④以重定向或转发的方式跳转对应的页面。

OK~,最后也不要忘了在web.xml配置一下我们自己的DispacherServlet:
xml 代码解读复制代码

Archetype Created Web Application dispacherServlet com.xxx.DispacherServlet 1 dispacherServlet /

3.4、编写View视图
当然,不追求外观了,简单编写两个视图页面:index.jsp、edit.jsp:
html 代码解读复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>

<head>
<title>首页</title>
<link href="favicon.ico" rel="shortcut icon">
</head>

<body>
<h1>欢迎来到熊猫高级会所,我是竹子一号!</h1>
</body>
</html>

<!-- edit.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>

<head>
<title>修改</title>
<link href="favicon.ico" rel="shortcut icon">
</head>

<body>
<h1>修改页面</h1>
<a href="#">跳转</a>
</body>
</html>

3.5、编写测试用例
为了方便测试,先写一个实体类:User.java,如下:
java 代码解读复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class User {
private Integer id;
private String name;
private String sex;
private Integer age;
public User(){}

public User(Integer id, String name, String sex, Integer age) {
this.id = id;
this.name = name;
this.sex = sex;
this.age = age;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
}

这个实体类主要方便为了待会儿测试@ResponseBody注解的功能,接下来写两个Controller类:
java 代码解读复制代码/* —— UserController类 ——- */

@Controller
@RequestMapping("/user")
public class UserController {
// 测试@ResponseBody的功效
@RequestMapping("/get")
@ResponseBody
public User get(){
    return new User(1,"竹子爱熊猫","男",18);
}

// 跳转首页的方法
@RequestMapping("/")
public String test(){
    return "index";
}

// 测试重定向的功效
@RequestMapping("/edit")
public String toEdit(){
    return "redirect:edit";
}

public String TEST(){
    return null;
}

}

/* ——OrderController类——- */
public class OrderController {
}

在上述测试案例中,编写了UserController、OrderController两个类,其中仅有UserController加了@Controller注解,下面来测试,首先将这个Maven工程打成war包部署在Tomcat中,然后启动,日志如下:
java 代码解读复制代码项目启动了…..
扫描到类的数量为:1
类的请求路径:/user
类中方法数量为:4
方法上的请求路径:/get
方法上的请求路径:/test
方法上的请求路径:/edit
HandlerMap的长度:3

从上述日志输出中,很明显可以看出,未添加@Controller注解的OrderController类并未被扫描,同时,UserController类中未添加@RequestMapping注解的TEST()方法,也没有被加入到HandlerMap集合中,该集合中仅存放了有映射关系的Java方法。
OK~,接下来使用浏览器测试我们手写的SpringMVC是否可以做到原本的效果:

测试首页跳转效果:http://localhost:80

参考文章:https://juejin.cn/post/7139807630024769549
源代码:https://github.com/Breeze1203/JavaAdvanced/tree/main/springboot-demo/spring-mvc-pt

SpringBoot执行异步任务

1.@Async注解
截屏2024-12-18 16.24.20.png
大概解释是异步执行的方法:

  • 你可以在方法上使用@Async注解,这样Spring会在调用这个方法时异步执行它。
    如果你在类级别使用@Async注解,那么这个类中的所有方法都会被视为异步方法。
    配置类中的使用限制:
  • @Async注解不能用于带有@Configuration注解的类中的方法。这是因为配置类通常用于定义Spring容器的配置,而不是作为业务逻辑的一部分。
    方法签名:
  • @Async注解的方法可以有任意类型的参数。
    返回类型必须是void或者Future类型的,包括ListenableFuture和CompletableFuture,这些类型提供了更丰富的异步任务交互能力,并且可以立即与后续的处理步骤组合。
  • 返回Future的处理:当方法被标记为@Async后,调用这个方法会返回一个Future对象,这个对象可以用来跟踪异步方法的执行结果。
  • 由于目标方法需要与代理方法有相同的签名,如果方法本身不返回Future,那么它需要返回一个临时的Future对象,这个对象只是简单地传递值。Spring提供了AsyncResult类来帮助实现这一点,或者你可以使用EJB 3.1的AsyncResult,或者Java的CompletableFuture.completedFuture(Object)方法。
    AnnotationAsyncExecutionInterceptor:这是一个拦截器,用于处理@Async注解的方法调用。
    AsyncAnnotationAdvisor:这是一个Spring AOP顾问,用于创建代理以支持@Async注解

截屏2024-12-18 16.29.58.png

  • 限定符值(Qualifier Value):这个属性是一个限定符值,用于确定执行异步操作时应该使用的目标执行器。
    它匹配具有特定限定符值(或bean名称)的java.util.concurrent.Executor或org.springframework.core.task.TaskExecutor bean定义。
    类级别和方法级别的使用:
  • 如果在类级别的@Async注解中指定了这个限定符值,那么表示类中所有的方法都应该使用指定的执行器。
    如果在方法级别使用了Async#value,则总是会覆盖在类级别配置的限定符值。
    动态解析:
  • 如果限定符值被提供为一个SpEL(Spring Expression Language)表达式或属性占位符,它将被动态解析。例如,可以使用SpEL表达式”#{environment[‘myExecutor’]}”来动态指定执行器,或者使用属性占位符”${my.app.myExecutor}”来从配置中读取执行器的名称。
1
2
3
4
5
@Async
public void asyncTaskTwo() {
// 异步任务逻辑
System.out.println("Async task two is running with thread: " + Thread.currentThread().getName());
}

2.使用 CompletableFuture 实现异步任务
CompletableFuture 是 Java 8 新增的一个异步编程工具,它可以方便地实现异步任务。使用 CompletableFuture 需要满足以下条件:
1.异步任务的返回值类型必须是 CompletableFuture 类型;
2.在异步任务中使用 CompletableFuture.supplyAsync() 或 CompletableFuture.runAsync() 方法来创建异步任务;
3.在主线程中使用 CompletableFuture.get() 方法获取异步任务的返回结果。
示例代码如下:

1
2
3
4
5
6
7
8
9
@Service
public class AsyncService {
public CompletableFuture<String> asyncTask() {
return CompletableFuture.supplyAsync(() -> {
// 异步任务执行的逻辑
return "异步任务执行完成";
});
}
}

3.利用Executor
在上下文中没有 Executor bean 的情况下, Spring Boot 会自动配置AsyncTaskExecutor。 启用虚拟线程(使用 Java 21+ 并设置为 )时,这将是使用虚拟线程的 SimpleAsyncTaskExecutor。 否则,它将是具有合理默认值的 ThreadPoolTaskExecutor。
如果您在上下文中定义了自定义 Executor,则常规任务执行(即 @EnableAsync)和 Spring for GraphQL 都将使用它。 但是,Spring MVC 和 Spring WebFlux 支持仅在它是 AsyncTaskExecutor 实现(名为 )时才会使用它。 根据你的目标安排,你可以将 Executor 更改为 AsyncTaskExecutor,或者同时定义 AsyncTaskExecutor 和 AsyncConfigurer 来包装你的自定义 Executor。applicationTaskExecutor
自动配置的 ThreadPoolTaskExecutorBuilder 允许您轻松创建实例,这些实例可以重现自动配置默认执行的操作。
当 ThreadPoolTaskExecutor 被自动配置时,线程池使用 8 个核心线程,这些线程可以根据负载进行扩展和收缩。 可以使用命名空间对这些默认设置进行微调,如以下示例所示:spring.task.execution
spring.task.execution.pool.max-size=16
spring.task.execution.pool.queue-capacity=100
spring.task.execution.pool.keep-alive=10s
这会将线程池更改为使用有界队列,以便在队列已满(100 个任务)时,线程池增加到最多 16 个线程。 池的收缩更加激进,因为线程在空闲 10 秒(而不是默认 60 秒)时被回收。
如果需要将计划程序与计划任务执行相关联(例如,使用 @EnableScheduling),也可以自动配置计划程序。
如果启用了虚拟线程(使用 Java 21+ 并设置为 ),这将是使用虚拟线程的 SimpleAsyncTaskScheduler。 此 SimpleAsyncTaskScheduler 将忽略任何与池相关的属性。spring.threads.virtual.enabledtrue
如果未启用虚拟线程,它将是具有合理默认值的 ThreadPoolTaskScheduler。 默认情况下,ThreadPoolTaskScheduler 使用一个线程,并且可以使用命名空间对其设置进行微调,如以下示例所示:spring.task.scheduling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableAsync
public class AppConfig implements AsyncConfigurer {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(7);
executor.setMaxPoolSize(42);
executor.setQueueCapacity(11);
executor.setThreadNamePrefix("MyExecutor-");
executor.initialize();
return executor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new MyAsyncUncaughtExceptionHandler();
}
}

这是配置Executor,其实可以跟1一起搭配使用,因为@Async注解,也是通过异步操作时应该使用的目标执行器,我们还可以这样做

1
2
3
4
5
6
7
8
@Configuration
public class AsyncConfig{

@Bean(value = "task")
public Executor getAsyncExecutor() {
return new ThreadPoolTaskExecutorBuilder().build();
}
}

然后业务类,依赖注入这个bean,类似于下面

1
2
3
4
5
public void asyncTask() {
task.execute(()->{
System.out.println("Async task is running with thread: " + Thread.currentThread().getName());
});
}

SpringBoot官方默认是配置 Executor的bean,搭配@Async注解

如何处理消费过程中的重复消息?

在 MQTT 协议中,给出了三种传递消息时能够提供的服务质量标准,这三种服务质量从低到高依次是:

  • At most once:至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景,使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。
  • At least once: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。
  • Exactly once:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级”
    用幂等性解决重复消息问题一般解决重复消息的办法是,在消费端,让我们消费消息的操作具备幂等性。幂等(Idempotence) 本来是一个数学上的概念,它是这样定义的:
    如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。
    这个概念被拓展到计算机领域,被用来描述一个操作、方法或者服务。一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同
    一个幂等的方法,使用同样的参数,对它进行多次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。我们举个例子来说明一下。在不考虑并发的情况下,“将账户 X 的余额设置为 100 元”,执行一次后对系统的影响是,账户 X 的余额变成了 100 元。只要提供的参数 100 元不变,那即使再执行多少次,账户 X 的余额始终都是 100 元,不会变化,这个操作就是一个幂等的操作。再举一个例子,“将账户 X 的余额加 100 元”,这个操作它就不是幂等的,每执行一次,账户余额就会增加 100 元,执行多次和执行一次对系统的影响(也就是账户的余额)是不一样的。“如果我们系统消费消息的业务逻辑具备幂等性,那就不用担心消息重复的问题了,因为同一条消息,消费一次和消费多次对系统的影响是完全一样的。也就可以认为,消费多次等于消费一次。
    从对系统的影响结果来说:At least once + 幂等消费 = Exactly once。那么如何实现幂等操作呢?
    最好的方式就是,从业务逻辑设计上入手,将消费的业务逻辑设计成具备幂等性的操作。但是,不是所有的业务都能设计成天然幂等的,这里就需要一些方法和技巧来实现幂等
    下面我给你介绍几种常用的设计幂等操作的方法:
  1. 利用数据库的唯一约束实现幂等,“例如我们刚刚提到的那个不具备幂等特性的转账的例子:将账户 X 的余额加 100 元。在这个例子中,我们可以通过改造业务逻辑,让它具备幂等性。首先,我们可以限定,对于每个转账单每个账户只可以执行一次变更操作,在分布式系统中,这个限制实现的方法非常多,最简单的是我们在数据库中建一张转账流水表,这个表有三个字段:转账单 ID、账户 ID 和变更金额,然后给转账单 ID 和账户 ID 这两个字段联合起来创建一个唯一约束,这样对于相同的转账单 ID 和账户 ID,表里至多只能存在一条记录。这样,我们消费消息的逻辑可以变为:“在转账流水表中增加一条转账记录,然后再根据转账记录,异步操作更新用户余额即可。”在转账流水表增加一条转账记录这个操作中,由于我们在这个表中预先定义了“账户 “ ID 转账单 ID”的唯一约束,对于同一个转账单同一个账户只能插入一条记录,后续重复的插入操作都会失败,这样就实现了一个幂等的操作。我们只要写一个 SQL,正确地实现它就可以了。基于这个思路,不光是可以使用关系型数据库,只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等,比如,你可以用 Redis 的 SETNX 命令来替代数据库中的唯一约束,来实现幂等消费。
  2. 为更新的数据设置前置条件另外一种实现幂等的思路是,给数据变更设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。这样,重复执行这个操作时,由于第一次更新数据的时候已经变更了前置条件,“中需要判断的数据,不满足前置条件,则不会重复执行更新数据操作。比如,刚刚我们说过,“将账户 X 的余额增加 100 元”这个操作并不满足幂等性,我们可以把这个操作加上一个前置条件,变为:“如果账户 X 当前的余额为 500 元,将余额加 100 元”,这个操作就具备了幂等性。对应到消息队列中的使用时,可以在发消息时在消息体中带上当前的余额,在消费的时候进行判断数据库中,当前余额是否与消息中的余额相等,只有相等才执行变更操作。但是,如果我们要更新的数据不是数值,或者我们要做一个比较复杂的更新操作怎么办?用什么作为前置判断条件呢?更加通用的方法是,给你的数据增加一个版本号属性,每次更数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时版本号 +1,一样可以实现幂等更新
  3. 记录并检查操作如果上面提到的两种实现幂等方法都不能适用于你的场景,我们还有一种通用性最强,适用范围最广的实现幂等性方法:记录并检查操作,也称为“Token 机制或者 GUID(全局唯一 ID)机制”,实现的思路特别简单:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费
    原理和实现是不是很简单?其实一点儿都不简单,在分布式系统中,这个方法其实是非常难实现的。首先,给每个消息指定一个全局唯一的 ID 就是一件不那么简单的事儿,方法有很多,但都不太好同时满足简单、高可用和高性能,或多或少都要有些牺牲。更加麻烦的是,在“检查消费状态,然后更新数据并且设置消费状态”中,三个操作必须作为一组操作保证原子性,才能真正实现幂等,否则就会出现 Bug。比如说,对于同一条消息:“全局 ID 为 8,操作为:给 ID 为 666 账户增加 100 元”,有可能出现这样的情况:
    t0 时刻:Consumer A 收到条消息,检查消息执行状态,发现消息未处理过,开始执行“账户增加 100 元”;
    t1 时刻:Consumer B 收到条消息,检查消息执行状态,发现消息未处理过,因为这个时刻“Consumer A 还未来得及更新消息执行状态。
    这样就会导致账户被错误地增加了两次 100 元,这是一个在分布式系统中非常容易犯的错误,一定要引以为戒。对于这个问题,当然我们可以用事务来实现,也可以用锁来实现,但是在分布式系统中,无论是分布式事务还是分布式锁都是比较难解决问题”

kafka无消息丢失怎么实现

kafka到底在什么情况下能保证消失不丢失

一句话概括,kafka只对“已提交”的消息做有限度的持久化保证,这句话有两个要素

  1. 第一个核心要素是“已提交的消息”。什么是已提交的消息?当 Kafka 的若干个 Broker 成功地接收到一条消息并写入到日志文件后,它们会告诉生产者程序这条消息已成功提交。此时,这条消息在 Kafka 看来就正式变为“已提交”消息了。那为什么是若干个 Broker 呢?这取决于你对“已提交”的定义。你可以选择只要有一个 Broker 成功保存该消息就算是已提交,也可以是令所有 Broker 都成功保存该消息才算是已提交。不论哪种情况,Kafka 只对已提交的消息做持久化保证这件事情是不变的。
  2. 第二个核心要素就是“有限度的持久化保证”,也就是说 Kafka 不可能保证在任何情况下都做到不丢失消息。举个极端点的例子,如果地球都不存在了,Kafka 还能保存任何消息吗?显然不能,“倘若这种情况下你依然还想要 Kafka 不丢消息,那么只能在别的星球部署 Kafka Broker 服务器了”
    “现在你应该能够稍微体会出这里的“有限度”的含义了吧,其实就是说 Kafka 不丢消息是有前提条件的。假如你的消息保存在 N 个 Kafka Broker 上,那么这个前提条件就是这 N 个 Broker 中至少有 1 个存活。只要这个条件成立,Kafka 就能保证你的这条消息永远不会丢失。总结一下,Kafka 是能做到不丢失消息的,只不过这些消息必须是已提交的消息,而且还要满足一定的条件。当然,说明这件事并不是要为 Kafka 推卸责任,而是为了在出现该类问题时我们能够明确责任边界”
消息丢失案例

案例 1:生产者程序丢失数据Producer 程序丢失消息
这应该算是被抱怨最多的数据丢失场景了。我来描述一个场景:你写了一个 Producer 应用向 Kafka 发送消息,最后发现 Kafka 没有保存,于是大骂:“Kafka 真烂,消息发送居然都能丢失,而且还不告诉我?!”如果你有过这样的经历,那么请先消消气,我们来分析下可能的原因。目前 Kafka Producer 是异步发送消息的,也就是说如果你调用的是 producer.send(msg) 这个 API,那么它通常会立即返回,但此时你不能认为消息发送已成功完成。这种发送方式有个有趣的名字,叫“fire and forget”,翻译一下就是“发射后不管”。这个术语原本属于导弹制导领域,后来被借鉴到计算机领域中,它的意思是,执行完一个操作后不去管它的结果是否成功。调用 producer.send(msg) 就属于典型的“fire and forget”,因此如果出现消息丢失,我们是无法知晓的。这个发送方式挺不靠谱吧,不过有些公司真的就是在使用这个 API 发送消息。如果用这个方式,可能会有哪些因素导致消息没有发送成功呢?其实原因有很多,例如网络抖动,导致消息压根就没有发送到 Broker 端;或者消息本身不合格导致 Broker 拒绝接收(比如消息太大了,超过了 Broker 的承受能力)等。这么来看,让 Kafka“背锅”就有点冤枉它了。就像前面说过的,Kafka 不认为消息是已提交的,因此也就没有 Kafka 丢失消息这一说了。不过,就算不是 Kafka 的“锅”,我们也要解决这个问题吧。实际上,解决此问题的方法非常简单:Producer 永远要使用带有回调通知的发送 API,也就是说不要使用 “producer.send(msg),而要使用 producer.send(msg, callback)。不要小瞧这里的 callback(回调),它能准确地告诉你消息是否真的提交成功了。一旦出现消息提交失败的情况,你就可以有针对性地进行处理。举例来说,如果是因为那些瞬时错误,那么仅仅让 Producer 重试就可以了;如果是消息不合格造成的,那么可以调整消息格式后再次发送。总之,处理发送失败的责任在 Producer 端而非 Broker 端。你可能会问,发送失败真的没可能是由 Broker 端的问题造成的吗?当然可能!如果你所有的 Broker 都宕机了,那么无论 Producer 端怎么重试都会失败的,此时你要做的是赶快处理 Broker 端的问题。但之前说的核心论据在这里依然是成立的:Kafka 依然不认为这条消息属于已提交消息,故对它不做任何持久化保证
案例 2:消费者程序丢失数据
“Consumer 端丢失数据主要体现在 Consumer 端要消费的消息不见了。Consumer 程序有个“位移”的概念,表示的是这个 Consumer 当前消费到的 Topic 分区的位置。下面这张图来自于官网,它清晰地展示了 Consumer 端的位移数据
截屏2024-12-12 15.01.44.png

比如对于 Consumer A 而言,它当前的位移值就是 9;Consumer B 的位移值是 11。这里的“位移”类似于我们看书时使用的书签,它会标记我们当前阅读了多少页,下次翻书的时候我们能直接跳到书签页继续阅读。正确使用书签有两个步骤:第一步是读书,第二步是更新书签页。如果这两步的顺序颠倒了,就可能出现这样的场景:当前的书签页是第 90 页,我先将书签放到第 100 页上,之后开始读书。当阅读到第 95 页时,我临时有事中止了阅读。那么问题来了,当我下次直接跳到书签页阅读时,我就丢失了第 96~99 页的内容,即这些消息就丢失了。同理,Kafka 中 Consumer 端的消息丢失就是这么一回事。要对抗这种消息丢失,办法很简单:维持先消费消息(阅读),再更新位移(书签)的顺序即可。这样就能最大限度地保证消息不丢失“当然,这种处理方式可能带来的问题是消息的重复处理,类似于同一页书被读了很多遍,但这不属于消息丢失的情形。在专栏后面的内容中,我会跟你分享如何应对重复消费的问题。除了上面所说的场景,其实还存在一种比较隐蔽的消息丢失场景。我们依然以看书为例。假设你花钱从网上租借了一本共有 10 章内容的电子书,该电子书的有效阅读时间是 1 天,过期后该电子书就无法打开,但如果在 1 天之内你完成阅读就退还租金。为了加快阅读速度,你把书中的 10 个章节分别委托给你的 10 个朋友,请他们帮你阅读,并拜托他们告诉你主旨大意。当电子书临近过期时,这 10 个人告诉你说他们读完了自己所负责的那个章节的内容,于是你放心地把该书还了回去。不料,在这 10 个人向你描述主旨大意时,你突然发现有一个人对你撒了谎,他并没有看完他负责的那个章节。那么很显然,你无法知道那一章的内容了。对于 Kafka 而言,这就好比 Consumer 程序从 Kafka 获取到消息后开启了多个线程异步处理消息,而 Consumer 程序自动地向前更新位移。假如其中某个线程运行失败了,它负责的消息没有被成功处理,但位移已经被更新了,因此这条消息对于 Consumer 而言实际上是丢失了。这里的关键在于 Consumer 自动提交位移,与你没有确认书籍内容被全部读完就将书归还类似,你没有真正地确认消息是否真的被消费就“盲目”地更新了位移。这个问题的解决方案也很简单:如果是多线程异步处理消费消息,Consumer 程序不要开启自动提交位移,而是要应用程序手动提交位移。在这里我要提醒你一下,单个 Consumer 程序使用多线程来消费消息说起来容易,写成代码却异常困难,因为你很难正确地处理位移的更新,也就是说避免无消费消息丢失很简单,但极易出现消息被消费了多次的情况

最佳实践

“看完这两个案例之后,我来分享一下 Kafka 无消息丢失的配置,每一个其实都能对应上面提到的问题。

  1. 不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。记住,一定要使用带有回调通知的 send 方法。
  2. 设置 acks = all。acks 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。
  3. 设置 retries 为一个较大的值。这里的 retries 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失”
  4. 设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。
  5. 设置 replication.factor >= 3。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
  6. 设置 min.insync.replicas > 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。
  7. 确保 replication.factor > min.insync.replicas。如果两者相等,那么只要有一个副本挂机整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 replication.factor = min.insync.replicas + 1。
  8. 确保消息消费完成再提交。Consumer 端有个参数 enable.auto.commit,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的”

1.Executors、Executor 和 ExecutorService

(1)Executors是一个帮助类,提供了创建几种预配置线程池实例的方法。如果你不需要应用任何自定义的微调,可以调用这些方法创建默认配置的线程池。
(2)Executor 和 ExecutorService接口则用于与 Java 中不同线程池的实现协同工作。
Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口
Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的对象。
Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。
Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。可以通过 《Java Concurrency in Practice》 一书了解更多关于关闭线程池和如何处理 pending 的任务的知识。
2.用Executors创建的通用线程池
(1)Executors.newFixedThreadPool(n)
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。超出的线程会在队列中等待,可控制线程最大并发数。创建的线程池 corePoolSize 和 maximumPoolSize 值是相等的,使用的是 LinkedBlockingQueue 阻塞队列。执行长期的任务,性能好很多。底层实现如下:

1
2
3
4
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());

(2)Executors.newSingleThreadExecutor()
创建一个单线程的线程池,这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。将 corePoolSize 和 maximumPoolSize 都设置为1,使用的是 LinkedBlockingQueue 阻塞队列。适合一个任务一个任务执行的场景。底层实现如下:

1
2
3
4
5
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

(3)Executors.newCachedThreadPool()
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收线程,则新建线程。将 corePoolSize 设置为0,maximumPoolSize 设置为Integer.MAX_VALUE ,使用的阻塞队列是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。适合执行很多短期异步的小程序或者负载较轻的服务器。

1
2
3
4
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

(4)Executors.newScheduledThreadPool(n)
创建一个定长线程池,支持定时及周期性任务执行。

1
2
3
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

(5) Executors.newWorkStealingPool()
JDK8引入,创建持有足够线程的线程池支持给定的并行度,并通过使用多个队列减少竞争。

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

禁止直接使用Executors创建线程池原因:
FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
3.ThreadPoolExecutor 创建线程池(推荐)

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

(1)参数解释
corePoolSize(核心线程数) : 线程池中常驻核心线程数。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。
maximumPoolSize (最大线程数): 线程池允许创建的最大线程数,此值>=1。如果队列满了,并且已创建的线程小于最大线程数,则线程池也会创建新的线程执行任务。
此值一般设置为多少?考虑这个问题首先要分析你的系统是 CPU 密集型,还是 IO 密集型的服务。再就是查看系统的内核数:Runtime.getRuntime().availableProcessors());
①、CPU 密集型:CPU 密集型任务只有在真正的多核 CPU 上才可能得到加速,CPU 一直全速运行。而在单核 CPU 上,无论你开几个模拟的多线程任务都不能得到加速,因为 CPU 总的运算能力就那些。一般公式:线程数=CPU核数+1
②、IO 密集型:IO 密集型的任务并不是一直在执行任务,则应配置尽可能多的线程。一般公式:线程数=CPU核数*2
③、IO 密集型:IO 密集型时,大部分线程都阻塞,故需多配置线程数。一般公式:线程数=CPU核数/1-阻塞系数。阻塞系数:一般阻塞系数取值在0.8~0.9 之间。
keepAliveTime (线程空闲时间): 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间
unit: keepAliveTime 的时间单位,可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微妙(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微妙)。
workQueue(缓存队列) : 用来储存等待执行任务的队列
threadFactory (线程工厂): 用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀、是否为守护线程以及优先级等。设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的。
使用开源框架guava 提供的 ThreadFactoryBuilder 可以快速给线程池里的线程设置有意义的名字,一般使用默认即可。如下:
new ThreadFactoryBuilder().setNameFormat(“XX-task-%d”).build();
handler: 执行拒绝策略对象。当达到任务缓存上限时(即超过workQueue参数能存储的任务数),执行拒接策略,创建线程执行任务,当线程数量等于corePoolSize时,请求加入阻塞队列里,当队列满了时,接着创建线程,线程数等于maximumPoolSize。 当任务处理不过来的时候,线程池开始执行拒绝策略。
(2)阻塞队列
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
(3)拒绝策略
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
(4)提交任务
可以向ThreadPoolExecutor提交两种任务:Callable和Runnable
Callable
该类任务有返回结果,可以抛出异常。
通过submit函数提交,返回Future对象。
可通过get获取执行结果。
Runnable
该类任务只执行,无法获取返回结果,并在执行过程中无法抛异常。
通过execute提交。
(5)线程池关闭
shutdown:将线程池状态置为SHUTDOWN,并不会立即停止:停止接收外部submit的任务,内部正在跑的任务和队列里等待的任务,会执行完后,才真正停止

shutdownNow:将线程池状态置为STOP。企图立即停止,事实上不一定:跟shutdown()一样,先停止接收外部提交的任务,忽略队列里等待的任务,尝试将正在跑的任务interrupt中断,返回未执行的任务列表。

它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它也可能必须要等待所有正在执行的任务都执行完成了才能退出。

awaitTermination(long timeOut, TimeUnit unit)当前线程阻塞,直到等所有已提交的任务(包括正在跑的和队列中等待的)执行完或者等超时时间到或者线程被中断,抛出InterruptedException,然后返回true(shutdown请求后所有任务执行完毕)或false(已超时)
4.自定义阻塞提交的MyThread(防止拒绝忽略,任务得不到处理)

MyThread.java

public class MyThread  implements  Runnable{
    private Integer number;

    public MyThread(int number){
        this.number = number;
    }

    public Integer getNumber() {
        return number;
    }

    @Override
    public void run() {
        try {
             //to do
            TimeUnit.SECONDS.sleep(1);
            System.out.println("Hello! ThreadPoolExecutor - " + getNumber());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
CustomBlockThreadPoolExecutor.java



/**
 * 自定义阻塞提交的ThreadPoolExecutor
 */
public class CustomBlockThreadPoolExecutor {

    private ThreadPoolExecutor pool = null;
    private final  int  poolSize=2;
    private final  int  maxPoolSize=4;
    private final  Long  keepAliveTime=30L;
    private final  int  arrayBlockingQueueSize=30;

    /**
     * 线程池初始化方法
     *
     * corePoolSize 核心线程池大小----2
     * maximumPoolSize 最大线程池大小----4
     * keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间----30+单位TimeUnit
     * TimeUnit keepAliveTime时间单位----TimeUnit.MINUTES
     * workQueue 阻塞队列----new ArrayBlockingQueue<Runnable>(10)==== 10容量的阻塞队列
     * threadFactory 新建线程工厂----new CustomThreadFactory()====定制的线程工厂
     * rejectedExecutionHandler 当提交任务数超过maxmumPoolSize+workQueue之和时,
     * 即当提交第15个任务时(前面线程都没有执行完,此测试方法中用sleep(100)),任务会交给RejectedExecutionHandler来处理
     */

    public void init() {
        pool = new ThreadPoolExecutor(poolSize,maxPoolSize,keepAliveTime,
                TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(arrayBlockingQueueSize),new CustomThreadFactory(), new CustomRejectedExecutionHandler());
    }

    public void destory() {
        if(pool !=null) {
            pool.shutdownNow();
        }
    }

    public ExecutorService getCustomThreadPoolExecutor() {
        return this.pool;
    }


    /**
     * 自定义线程工厂类
     * 生成的线程名词前缀、是否为守护线程以及优先级等
     */
    private class CustomThreadFactory implements ThreadFactory {

        private AtomicInteger count = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            String threadName =  CustomBlockThreadPoolExecutor.class.getSimpleName()+count.addAndGet(1);
            t.setName(threadName);
            return t;
        }
    }


    /**
     * 自定义拒绝策略对象
     */
    private class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            //核心改造点,将blockingqueue的offer改成put阻塞提交
            try {
                executor.getQueue().put(r);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 当提交任务被拒绝时,进入拒绝机制,我们实现拒绝方法,把任务重新用阻塞提交方法put提交,实现阻塞提交任务功能,防止队列过大,OOM
     */
    public static void main(String[] args){

        CustomBlockThreadPoolExecutor customlockThreadPoolExecutor = new CustomBlockThreadPoolExecutor();

        //初始化
        customlockThreadPoolExecutor.init();
        ExecutorService pool = customlockThreadPoolExecutor.getCustomThreadPoolExecutor();
        for(int i=1;i<51;i++) {
            MyThread myThread=new MyThread(i);
            System.out.println("提交第"+i+"个任务");
            pool.execute(myThread);
        }


        pool.shutdown();
        try {
            //阻塞,超时时间到或者线程被中断
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                //立即关闭
                pool.shutdownNow();
            }
        } catch (InterruptedException e) {
            pool.shutdownNow();
        }

    }

mybatis generator

每次配置mybatis都很痛苦,还容易出错,推荐使用mybatis generator
  1. 在maven里面添加插件依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
</dependencies>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
  1. 编写generatorConfig.xml文件,放在指定目录下src/main/resources/generatorConfig.xml
  1. 注意 :我们生成的xml文件放在src/main/java/org/apache/dubbo/springboot/demo/provider下面,所以需要配置resources目录下的文件为资源目录
1
2
3
4
5
6
7
8
9
10
11
 <resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
  1. 在启动类加上@MapperScan注解,指定src/main/java/org/apache/dubbo/springboot/demo/provider

$connection_upgrade在使用 Websockets 或使用 nginx 配置生成器时,您可能会在 nginx 配置中遇到变量。
默认情况下,此$connection_upgrade变量不可用。但是,建议在反向代理设置中定义并使用它。
本教程将向您展示如何修复与连接升级相关的 nginx 未知变量消息!
问题:未知的“$connection_upgrade”变量
您可能会在(更新及之后)使用以下命令检查 nginx 配置时遇到此问题nginx -t:

$ sudo nginx -t
nginx: [emerg] unknown “connection_upgrade” variable
nginx: configuration file /etc/nginx/nginx.conf test failed

该connection_upgrade变量不是全局 nginx 设置。然而,您会在整个互联网上的教程和代码片段中看到它。甚至nginx 公司也建议定义和使用connection_upgrade。让我们这样做吧!
close如果升级标头设置为 ,则此映射块告诉 nginx 正确设置相关的连接标头’’。
将 map 块放入http你的 nginx 配置块中。nginx 配置的默认文件路径是/etc/nginx/nginx.conf。
下面是我们使用 map 块定义“变量”的 nginx 配置示例$connection_upgrade。

/etc/nginx/nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
multi_accept on;
worker_connections 65535;
}

http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
##
# Connection header for WebSocket reverse proxy
##
map $http_upgrade $connection_upgrade {
default upgrade;
‘’ close;
}
# further configurations …
}

保存更新的 nginx 配置文件。然后,使用以下命令再次检查配置文件nginx -t:

1
2
3
4
$ sudo nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
0%