2020年10月

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

SpringMVC 源码剖析

本文基于当前上游最新版 SpringBoot-2.3.4.RELEASE, 和 SpringMVC-5.2.9.RELEASE 讲解

1) DispatcherServlet

一个普通的 SpringMVC 项目中, 需要在 web.xml 中定义 DispatcherServlet顶层 Servlet, 那么代表 DispatcherServlet继承HttpServlet 的.
继承关系如下:

1.1) SpringMVC 初始化

1.1.1) SpringMVC 九大组件初始化链路

仔细翻阅源码之后发现, SpringMVC 的初始化入口是在父类的 init 方法中, 也就是 Servlet 第一次被请求才初始化调用的 init 方法, 被定义在父类 HttpServletBean 中.

    public final void init() throws ServletException {
        // 1. 启动参数加载
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        if (!pvs.isEmpty()) {
            try {
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            }
            catch (BeansException ex) {
                if (logger.isErrorEnabled()) {
                    logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
                }
                throw ex;
            }
        }

        initServletBean();
    }

我们可以看到, 方法中都是一些启动参数配置的方法, 最后调用了 initServletBean(), 接着往下看看, 是在 FrameworkServlet中实现的:

    @Override
    protected final void initServletBean() throws ServletException {
        // ...
            this.webApplicationContext = initWebApplicationContext();
            initFrameworkServlet();
        // ...
    }

去掉一些日志输出和 catch 代码, 最终 initServletBean 方法中剩下这两句, 看了一下第二行 initFrameworkServlet 方法中是一个模板方法, 但是没有子类实现, 所以就只剩下WebApplicationContext 的初始化逻辑, 也是在本类实现的, 进入康康:

    protected WebApplicationContext initWebApplicationContext() {
        // 1. 包装 ServletContext 为 wac
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;
        if (this.webApplicationContext != null) {
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    if (cwac.getParent() == null) {
                        cwac.setParent(rootContext);
                    }
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        if (wac == null) {
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            wac = createWebApplicationContext(rootContext);
        }

        if (!this.refreshEventReceived) {
            synchronized (this.onRefreshMonitor) {
                // 2. **核心方法**
                onRefresh(wac);
            }
        }

        if (this.publishContext) {
            // Publish the context as a servlet context attribute.
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
        }

        return wac;
    }

发现没有什么有价值的代码, 都是一些判断换包装, 最后有一句 onRefresh(wac), 像是 Spring 的命名, 应该是个重点方法, 发现是在子类 DispatcherServlet 中实现的, 进入康康:

    @Override
    protected void onRefresh(ApplicationContext context) {
        initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }

接着就能发现 SpringMVC 中, 9 大组件的初始化方法啦, 这个方法的调用链大概就是这样:

  1. HttpServletBean#init
  2. FrameworkServlet#initServletBean
  3. FrameworkServlet#initWebApplicationContext
  4. DispatcherServlet#onRefresh

1.1.2) SpringMVC 九大组件初始化

只挑两个最重要的讲, initHandlerMappingsinitHandlerAdapters 方法.

1.1.2.1) initHandlerMappings

    private boolean detectAllHandlerMappings = true;
    public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";

    private void initHandlerMappings(ApplicationContext context) {
        this.handlerMappings = null;

        if (this.detectAllHandlerMappings) { // 默认 True
            // 1. 从 Spring 容器中获取 HandlerMapping 接口的实现
            Map<String, HandlerMapping> matchingBeans =
                    BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
            if (!matchingBeans.isEmpty()) { // 2. 如不为空则拿 value 转成 List
                this.handlerMappings = new ArrayList<>(matchingBeans.values());
                // 3. 再根据 @Order 注解排序
                AnnotationAwareOrderComparator.sort(this.handlerMappings);
            }
        }
        else { // 可以在 web.xml 或 SpringBoot 配置文件中修改 detectAllHandlerMappings
            try {
                // 只获取一个 name 为 handlerMapping 的实现
                HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
                this.handlerMappings = Collections.singletonList(hm);
            }
            catch (NoSuchBeanDefinitionException ex) {
                // Ignore, we'll add a default HandlerMapping later.
            }
        }

        // 4. 如果未获取到 HandlerMapping 的实现, 则获取默认实现
        if (this.handlerMappings == null) {
            this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
            if (logger.isTraceEnabled()) {
                logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
                        "': using default strategies from DispatcherServlet.properties");
            }
        }
    }

方法上部没啥可以深入的代码, 最后获取默认实现的方法 getDefaultStrategies 我们进去康康:

    private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
    private static final Properties defaultStrategies;

    static {
        try {
            ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        }
        catch (IOException ex) {
            throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
        }
    }

    protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
        String key = strategyInterface.getName(); // 1. HandlerMapping
        String value = defaultStrategies.getProperty(key); // 2. 从 DispatcherServlet.properties 中获取 key 为 HandlerMapping 的值
        if (value != null) {
            String[] classNames = StringUtils.commaDelimitedListToStringArray(value); // 3. 分割成字符串数组
            // 4. 创建实例并加入容器和 List 集合
            List<T> strategies = new ArrayList<>(classNames.length);
            for (String className : classNames) {
                try {
                    Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
                    Object strategy = createDefaultStrategy(context, clazz); // 使用 BeanFactory 创建实例
                    strategies.add((T) strategy); // 加入数组
                }
                catch (ClassNotFoundException ex) {
                    throw new BeanInitializationException(
                            "Could not find DispatcherServlet's default strategy class [" + className +
                            "] for interface [" + key + "]", ex);
                }
                catch (LinkageError err) {
                    throw new BeanInitializationException(
                            "Unresolvable class definition for DispatcherServlet's default strategy class [" +
                            className + "] for interface [" + key + "]", err);
                }
            }
            // 5. 返回
            return strategies;
        }
        else {
            return new LinkedList<>();
        }
    }

最终默认的 HandlerMapping 如下:

  • BeanNameUrlHandlerMapping: 处理配置文件中定义的映射
  • RequestMappingHandlerMapping: 处理 @RequestMapping 注解定义的映射
  • RouterFunctionMapping: 处理另一种配置文件中定义的映射

1.1.2.2) initHandlerAdapters

initHandlerAdapters 的代码和 initHandlerMapping 几乎一样, 直接上默认的 Adapters:

  • HttpRequestHandlerAdapter
  • SimpleControllerHandlerAdapter
  • RequestMappingHandlerAdapter
  • HandlerFunctionAdapter

1.2) 请求处理

我们知道, 正常一个请求发起, 首先会进入 Servlet 中的 service 方法, 然后被 service 方法根据请求的 Method 分发给 doGet, doPost, doPut 等方法, 我们试着康康继承关系中哪些子类重写了 service 方法:
好的, 在继承关系中, 只发现 FrameworkServlet 重写了它, 康康代码:

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
        if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
            processRequest(request, response);
        }
        else { // 调用父类 HTTPServlet 的 service 方法
            super.service(request, response);
        }
    }

我们通过代码可以发现, SpringMVCServlet 的基础上, 额外实现了对 HTTP PATCH 的支持, 平常一般用的不多, 就不深入看了.
然后对于普通的请求, 还是交由父类处理, 那么我们还是得从 doGet, doPost 之类的方法下手, 看看子类实现.
发现还是由 FrameworkServlet 重写了它们:

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

    @Override
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }

    @Override
    protected final void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }
    // ...

最终都是由本类中 processRequest 方法处理, 康康代码:

    protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        // Locale 国际化相关
        //  获取 ThreadLocal 中 Locale 缓存
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        //  从请求中获取新的 Locale
        LocaleContext localeContext = buildLocaleContext(request);

        // 获取 request 域中的参数缓存
        //  获取 ThreadLocal 中 request 域参数缓存
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        //  从请求中获取新的 request 域参数
        ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

        // 异步相关
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

        // 将新的 Locale 和 request 域参数更新到 ThreadLocal 中
        initContextHolders(request, localeContext, requestAttributes);

        try {
            doService(request, response); // **核心方法**
        }
        catch (ServletException | IOException ex) {
            failureCause = ex;
            throw ex;
        }
        catch (Throwable ex) {
            failureCause = ex;
            throw new NestedServletException("Request processing failed", ex);
        }

        finally {
            resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }
            logResult(request, response, failureCause, asyncManager);
            publishRequestHandledEvent(request, response, startTime, failureCause);
        }
    }

最终我们发现, 调用了一个 doService 方法来实际处理请求响应, 本类方法中为抽象方法, 是子类 DispatcherServlet 实现的, 提到这个类就熟悉了吧:

public class DispatcherServlet extends FrameworkServlet {

    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        logRequest(request);
        // 保存请求快照, 可用于之后恢复
        Map<String, Object> attributesSnapshot = null;
        if (WebUtils.isIncludeRequest(request)) {
            attributesSnapshot = new HashMap<>();
            Enumeration<?> attrNames = request.getAttributeNames();
            while (attrNames.hasMoreElements()) {
                String attrName = (String) attrNames.nextElement();
                if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                    attributesSnapshot.put(attrName, request.getAttribute(attrName));
                }
            }
        }
        // request 域中设置了些参数
        request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
        request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
        request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
        request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
        // 重定向相关
        if (this.flashMapManager != null) {
            FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
            if (inputFlashMap != null) {
                request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
            }
            request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
            request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
        }
        // 请求域中 request_uri 处理
        RequestPath requestPath = null;
        if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) {
            requestPath = ServletRequestPathUtils.parseAndCache(request);
        }

        try {
            doDispatch(request, response); // **核心方法**
        }
        finally {
            if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
                if (attributesSnapshot != null) {
                    restoreAttributesAfterInclude(request, attributesSnapshot);
                }
            }
            if (requestPath != null) {
                ServletRequestPathUtils.clearParsedRequestPath(request);
            }
        }
    }

DispatcherServlet 中, 最后将请求响应交给了 doDispatch, 此方法即 SpringMVC 的核心方法:

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

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

            try {
                // 1. 文件上传判断
                processedRequest = checkMultipart(request);
                multipartRequestParsed = (processedRequest != request);

                // 2. 根据 request 获取 HandlerExecutionChain(包含 Controller 和拦截器链)
                mappedHandler = getHandler(processedRequest);
                if (mappedHandler == null) { // 404
                    noHandlerFound(processedRequest, response);
                    return;
                }

                // 3. 获取 HandlerAdapter(Controller)
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // 处理 Last-Modified 请求头返回 304
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }

                // 4. 拦截器链前置执行
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // 5. Handler 实际处理, 返回 ModelAndView
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }

                applyDefaultViewName(processedRequest, mv); // 6. 如没指定视图名, 给一个默认的
                // 7. 拦截器链后置执行
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
            catch (Exception ex) {
                dispatchException = ex;
            }
            catch (Throwable err) {
                // As of 4.3, we're processing Errors thrown from handler methods as well,
                // making them available for @ExceptionHandler methods and other scenarios.
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            // 8. 视图渲染
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        }
        catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        }
        catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        }
        finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                // Instead of postHandle and afterCompletion
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            }
            else {
                // Clean up any resources used by a multipart request.
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

DispatcherServlet 中的 doDispatch 方法基本流程:

  1. 根据 getHandler 获取匹配的 HandlerExecutionChain(Handler + 拦截器链)
  2. 拦截器链执行
  3. Handler 执行
  4. 拦截器链执行
  5. 视图渲染响应

接着来一步一步的康康源码

1.1) getHandler

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

可以看到, getHandler 中会遍历 DispatcherServlet 中的 handlerMappings 处理器映射器, 尝试获取 HandlerExecutionChain 处理器执行链, 如果获取到则返回.
handlerMappings 是 Spring 容器初始化时构建的, 那么一般有哪几种呢:

  1. RequestMappingHandlerMapping: 映射通过 @RequestMapping 定义的 Handler
  2. BeanNameUrlHandlerMapping: 映射通过 Spring 配置文件定义的 Handler
  3. SimpleUrlHandlerMapping: 也可以通过 Spring 配置文件定义映射, 一般在 Spring 初始化时会往里加载 ResourceHttpRequestHandler 用于处理资源文件请求.
  4. WelcomePageHandlerMapping: 不常用, 一般用于处理映射 / 路径

1.2) getHandlerAdapter

1.2) 前置拦截器执行

1.3) Handler 执行

1.4) 后置拦截器执行

1.5) 视图渲染

1.6) 响应

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

SpringBoot 源码分析

本文基于 SpringBoot 2.3.4.STABLE 版本撰写

1) 依赖管理

2) Maven parent 依赖引入

2.1) spring-boot-starter-parent 分析

一个最基本的 SpringBoot 工程中, 会在 pom.xml 中引入一个 parent 依赖: spring-boot-starter-parent, 观察此依赖包内容如下:

  • spring-boot-starter-parent-2.3.4.RELEASE.pom
  1. 只有一个 pom 文件, 是一个 pom 类型的依赖, 打开: spring-boot-starter-parent-2.3.4.RELEASE.pom 后可以发现引入了一些 Plugin, 没有啥有用的信息.
  2. 翻到头部找找有无父依赖, 发现其还有一个父依赖 spring-boot-dependencies, 此依赖便是 spring-boot-starter-parent 的核心实现:
    <parent>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-dependencies</artifactId>
     <version>2.3.4.RELEASE</version>
    </parent>
2.1.1) spring-boot-dependencies 分析

老样子先来研究一下目录结构:

  • spring-boot-dependencies-2.3.4.RELEASE.pom

发现也是一个 pom 依赖, 打开 spring-boot-dependencies-2.3.4.RELEASE.pom 后一幕明了了, 结构大致如下:

<!-->1. 首先定义了一些 properties 的版本号</-->
  <properties>
    <activemq.version>5.15.13</activemq.version>
    <antlr2.version>2.7.7</antlr2.version>
    <appengine-sdk.version>1.9.82</appengine-sdk.version>
    <!-->.......</-->
    <!-->.......</-->
  </properties>

<!-->2. 接着是基于 properties 中的版本号, 利用 maven-dependency-management 管理相应包的依赖版本</-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.apache.activemq</groupId>
        <artifactId>activemq-amqp</artifactId>
        <version>${activemq.version}</version>
      </dependency>
      <dependency>
        <groupId>org.apache.activemq</groupId>
        <artifactId>activemq-blueprint</artifactId>
        <version>${activemq.version}</version>
      </dependency>
      <!-->.......</-->
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>2.3.4.RELEASE</version>
      </dependency>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-activemq</artifactId>
        <version>2.3.4.RELEASE</version>
      </dependency>
      <!-->.......</-->
    </dependencies>
  </dependencyManagement>
<!-->3. 最后利用 maven-plugin-management 管理插件依赖的版本</-->
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>build-helper-maven-plugin</artifactId>
          <version>${build-helper-maven-plugin.version}</version>
        </plugin>
        <plugin>
          <groupId>org.codehaus.mojo</groupId>
          <artifactId>exec-maven-plugin</artifactId>
          <version>${exec-maven-plugin.version}</version>
        </plugin>
        <!-->.......</-->
        <!-->.......</-->
    </pluginManagement>
  </build>

总结: spring-boot-starter-parent 中, 利用 maven 的依赖管理, 管理了一些常用包以及 starter 的依赖版本号, 起到了版本号统一管理的作用, 版本号可以提供给 SpringBoot 的其他 starter使用, 起到了让我们引入一些基础依赖或 starter 时, 不需要手动填写版本号的作用.

2.2) spring-boot-starter-web 分析

上一节讲到 spring-boot-starter-parent 主要起到统一管理常用包的一些版本号, 我们拿最常用的依赖包 spring-boot-starter-web 来接着分析一下, 找到包目录中文件如下:

  • spring-boot-starter-web-2.3.4.RELEASE.jar
  • spring-boot-starter-web-2.3.4.RELEASE.pom

jar 包中只有一些 License 文件, 打开 pom 文件康康带进了啥依赖:

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>2.3.4.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-json</artifactId>
      <version>2.3.4.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <version>2.3.4.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.2.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

观察发现带入了以下几种传递依赖:

  1. SpringBoot 基础库
  2. Spring json 相关库
  3. Tomcat
  4. SpringMVC

总结: spring-boot-starter-web 将 WEB 相关的依赖做了一个打包, 简化项目中 pom 文件的长度, 增强可读性.

3) SpringBoot 自动配置

从一个最基础的启动类开始讲起吧, 本节先讲讲 @SpringBootApplication 注解, 其注解最主要的作用时自动配置, 或者叫自动装配:

@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication application = new SpringApplication(Application.class);
    application.run(args);
  }
}

将不重要或基础的代码移除后, @SpringBootApplication 中代码大致如下, 将分为三个小节讲解:

// 注解作用域等
@SpringBootConfiguration // 3.1
@EnableAutoConfiguration // 3.2 
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  // 注解字段属性
}

3.1) @SpringBootConfiguration

在当前版本中, 此注解只是为我们的启动类, 加上了 @Configuration 注解, 标记为 SpringFramework 的配置类, 代码:

@Configuration
public @interface SpringBootConfiguration {
  // ...
}

3.2) @EnableAutoConfiguration

@EnableAutoConfiguration 中的代码就是重头戏了, SpringBoot 自动配置主要就是基于此注解实现, 来康康代码, 两句注解将通过 3.2.1, 3.2.2 两小节讲解:

@AutoConfigurationPackage // 3.2.1
@Import(AutoConfigurationImportSelector.class) // 3.2.2
public @interface EnableAutoConfiguration {
  // ...
}
3.2.1) @AutoConfigurationPackage

我们先来康康代码:

@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
  // ...
}

代码量很少, @Import 注解可以将类导入 Spring 容器, 我们接着看看这个内部类 Registrar:

  // 实现了 ImportBeanDefinitionRegistrar, 代表此类使用个性化加载, 并不将 Registrar 本身注入容器
    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
      // 1. new PackageImports(metadata).getPackageNames().toArray(new String[0])
      // 返回了当前启动类所在的包的包名
      // 2. 接着将 registry Bean 注册表, 和上一步获取的包名传入 register 方法
            register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
        }

    // determineImports 方法在 SpringBoot 启动过程中没有调用, 国内外也基本查不到相关作用, 就不讲了
    // 官方 Doc 里注释此方法返回一个需要注入到容器的对象数组
        @Override
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new PackageImports(metadata));
        }
    }

我们接着来康康 register 方法:

  // 1. org.springframework.boot.autoconfigure.AutoConfigurationPackages
    private static final String BEAN = AutoConfigurationPackages.class.getName();

    public static void register(BeanDefinitionRegistry registry, String... packageNames) {
    // 2. Spring 容器中是否包含 AutoConfigurationPackages
        if (registry.containsBeanDefinition(BEAN)) { // 启动过程中并不包含, 继续走 else
            BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
            ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
            constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
        }
        else {
      // 3. 定义一个标准 Bean, 相当于需要注入容器的 Bean 的包装类
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
      // 4. 定义需要注入容器的类
            beanDefinition.setBeanClass(BasePackages.class);
      // 5. 根据其第 1 个构造方法, 传入启动类所在包名作为构造器参数, 后续会根据此构造方法反射构建 Bean
            beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
            beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); // 4. 表明此 Bean 为 Spring 后台注册而非人为注册
      // 5. 注入容器, name 为 org.springframework.boot.autoconfigure.AutoConfigurationPackages
            registry.registerBeanDefinition(BEAN, beanDefinition);
        }
    }

总结: 很多人以为 register 这个方法会扫描启动类所有子包, 并将 @Component 等注解类标注的类加入容器, 实际上只是将启动类所在包名的包装类, 加入了容器中, BeanName 为: org.springframework.boot.autoconfigure.AutoConfigurationPackages

是的, @Import(AutoConfigurationPackages.Registrar.class) 注解几乎啥也没做

3.2.1) @Import(AutoConfigurationImportSelector.class)

熟悉的 @Import, 我们进入 Selector 康康:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
        ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    // ...
}
  1. 发现此类实现的是 DeferredImportSelector, 它也是一个个性化加载接口, 实现它会在所有的 @Configuration 注解类处理完才执行导入操作.
  2. 接着实现了其他四个接口: BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, 从这四个接口实现的方法会比 selectImports 方法(DeferredImportSelector)先执行.
  3. 最后还有一个 Ordered, 实现了其 getOrder 方法: public int getOrder() { return Ordered.LOWEST_PRECEDENCE - 1; }, 这里也应证了第 1 点.

我们先来康康从第 2 点中实现下来的方法吧:

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory);
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.beanClassLoader = classLoader;
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

发现没啥信息, 很基础的实现, 没做啥特殊操作, 我们接着康康从 Selector 中实现的方法:

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 1. 默认为 True, 可以通过在 SpringBoot 配置文件中将 `spring.boot.enableautoconfiguration=false` 关闭自动装配
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }

先进入 getAutoConfigurationEntry 康康:

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    // 1. 默认为 True, 可以通过在 SpringBoot 配置文件中将 `spring.boot.enableautoconfiguration=false` 关闭自动装配
        if (!isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        }
    // 2. 获取 @EnableAutoConfiguration 注解中的属性
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 3.3 中讲解
        List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        configurations = removeDuplicates(configurations);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = getConfigurationClassFilter().filter(configurations);
        fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }

代码行数过多, 3.3 中详细讲解:

3.3) getAutoConfigurationEntry

  1. getCandidateConfigurations
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    我们进入方法内部康康:
     protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
       // 加载 spring.factories 中, key 为 EnableAutoConfiguration 全类名的 value 们
       List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
           getBeanClassLoader());
       Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
           + "are using a custom packaging, make sure that file is correct.");
       return configurations;
     }
     protected Class<?> getSpringFactoriesLoaderFactoryClass() {
       return EnableAutoConfiguration.class;
     }

    阅读过 SpringBoot 启动源码的童鞋可能对这行代码比较熟悉, 这里不深入讲了, 稍微来看看 loadFactoryNames 的源码:

     public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
       String factoryTypeName = factoryType.getName(); // 拿到 EnableAutoConfiguration 的全类名
       // 从 spring.factories 中读取 key 为 EnableAutoConfiguration 的全类名的值
       return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
     }

    最后返回一个自动配置类的 List 集合.

  2. removeDuplicates
    configurations = removeDuplicates(configurations);
    进入方法内部看看:
     protected final <T> List<T> removeDuplicates(List<T> list) {
       return new ArrayList<>(new LinkedHashSet<>(list));
     }

    这就比较简单了 =.=, 用一个 HashSet 对 List 做了一个去重, 然后重新转成一个 List 集合, removeDuplicates 中主要是对我们的自动配置类做一个去重工作.

  3. getExclusions
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    上面说到, annotationMetadata 是我们启动类的一些元数据信息, attributes 是 @EnableAutoConfiguration 注解中的属性, 我们进入 getExclusions 方法康康:
    protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
         Set<String> excluded = new LinkedHashSet<>(); // 去重集合
     // 获取 @EnableAutoConfiguration 中的 exclude 属性
         excluded.addAll(asList(attributes, "exclude"));
     // 获取 @EnableAutoConfiguration 中的 excludeName 属性
         excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
     // 获取 SpringBoot 配置文件中的 spring.autoconfigure.exclude 配置的排除类, 这里不深入讲解了
         excluded.addAll(getExcludeAutoConfigurationsProperty());
         return excluded;
     }

    最后返回一个去重后自动配置类的排除类集合, 我们接着往下看.

  4. checkExcludeClasses
    checkExcludedClasses(configurations, exclusions);
    private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
         List<String> invalidExcludes = new ArrayList<>(exclusions.size());
         for (String exclusion : exclusions) {
       // 1. 排除类如果存在, 且从 spring.factories 中读取的配置类数组中**不包含**
             if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
                 invalidExcludes.add(exclusion); // 2. 塞入 invalidExcludes 异常排除类集合
             }
         }
     // 3. 如有异常排除类进入 handleInvalidExcludes 方法抛出一个异常, 这里应该会终止 SpringBoot 的启动
         if (!invalidExcludes.isEmpty()) {
             handleInvalidExcludes(invalidExcludes); // 不深入讲了
         }
     }
  5. removeALl
    configurations.removeAll(exclusions);
    从自动配置类集合中, 移除掉我们配置的排除类.
  6. getConfigurationClassFilter
    configurations = getConfigurationClassFilter().filter(configurations);
    先来看看 getConfigurationClassFilter() 方法
     private ConfigurationClassFilter getConfigurationClassFilter() {
         if (this.configurationClassFilter == null) {
       // 1. 获取 spring.factories 中的过滤器
             List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
             for (AutoConfigurationImportFilter filter : filters) {
                 invokeAwareMethods(filter); // 2. 遍历过滤器赋值注入一些环境参数
             }
       // 3. 将过滤器赋值到局部变量
             this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
         }
         return this.configurationClassFilter;
     }

    过滤器的初始化我们分三步来讲解:

  7. 我们先来康康 getAutoConfigurationImportFilters 的过程
    protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
    // 还是熟悉的 loadFactories, 略过不讲了
    // 从 spring.factories 中读取 key 为 AutoConfigurationImportFilter.class 的过滤器
       return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader);
    }
  8. 然后是执行过滤器 invokeAwareMethods(filter);, 康康代码:
    private void invokeAwareMethods(Object instance) {
       if (instance instanceof Aware) {
           if (instance instanceof BeanClassLoaderAware) {
               ((BeanClassLoaderAware) instance).setBeanClassLoader(this.beanClassLoader);
           }
           if (instance instanceof BeanFactoryAware) {
               ((BeanFactoryAware) instance).setBeanFactory(this.beanFactory);
           }
           if (instance instanceof EnvironmentAware) {
               ((EnvironmentAware) instance).setEnvironment(this.environment);
           }
           if (instance instanceof ResourceLoaderAware) {
               ((ResourceLoaderAware) instance).setResourceLoader(this.resourceLoader);
           }
       }
    }

    发现只是判断过滤器实现了哪些接口, 注入了一些基本环境参数, 我们回到上一步代码接着往下看看

  9. new ConfigurationClassFilter
    this.configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
       ConfigurationClassFilter(ClassLoader classLoader, List<AutoConfigurationImportFilter> filters) {
           this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader);
           this.filters = filters;
       }

发现只是将 filters 集合包装了一下, 还执行了一个 loadMetadata 方法:

  protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";

    static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
        return loadMetadata(classLoader, PATH); // 1. 调用了下方的重载方法
    }

  static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
        try {
      // 2. 加载 spring-autoconfigure-metadata.properties 文件
            Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
                    : ClassLoader.getSystemResources(path);
      // 3. 将 metadata 文件转成 Properties 格式对象
            Properties properties = new Properties();
            while (urls.hasMoreElements()) {
                properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
            }
            return loadMetadata(properties); // 4. 再次调用重载方法
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
        }
    }

  static AutoConfigurationMetadata loadMetadata(Properties properties) {
    // 5. 最后创建了一个 porperties 的包装类返回
        return new PropertiesAutoConfigurationMetadata(properties);
    }

  private static class PropertiesAutoConfigurationMetadata implements AutoConfigurationMetadata {

    private final Properties properties;

    PropertiesAutoConfigurationMetadata(Properties properties) {
      this.properties = properties;
    }
    // ...
  }

稍微总结一下过滤器的初始化 getConfigurationClassFilter, 将 spring.factories 中的 filter, 和 spring-autoconfigure-metadata.properties 中的每一行数据读取了出来, 包装成了一个对象.
7. filter
configurations = getConfigurationClassFilter().filter(configurations);
那我们接着康康 filter 这个方法:

    List<String> filter(List<String> configurations) {
            long startTime = System.nanoTime();
            String[] candidates = StringUtils.toStringArray(configurations); // 1. 将自动配置类转成 String 数组
            boolean skipped = false;
            for (AutoConfigurationImportFilter filter : this.filters) { // 2. 遍历每一个 filter
        // 3. 将每个 filter 中的 match 方法, 判断自动配置类是否满足启动条件
        //   filter 是一个过滤器, metadata 则是过滤器过滤自动配置类需要依赖的数据
        //   结合过滤器和 metadata 则可以得出符合条件的自动配置类
                boolean[] match = filter.match(candidates, this.autoConfigurationMetadata); // 4. 返回自动配置类是否满足条件的 bool 数组
                for (int i = 0; i < match.length; i++) {
                    if (!match[i]) {
                        candidates[i] = null; // 5. 将不满足自动配置条件的自动配置类删除
                        skipped = true;
                    }
                }
            }
            if (!skipped) {
                return configurations;
            }
      // 6. 最后将满足条件的自动配置类返回
            List<String> result = new ArrayList<>(candidates.length);
            for (String candidate : candidates) {
                if (candidate != null) {
                    result.add(candidate);
                }
            }
            if (logger.isTraceEnabled()) {
                int numberFiltered = configurations.size() - result.size();
                logger.trace("Filtered " + numberFiltered + " auto configuration class in "
                        + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
            }
            return result;
        }

看完简介, 我们结合 spring.factoriesspring-autoconfigure-metadata.properties 接着分析一下 3 和 4 之间的代码流程
factoriesmetadata 中的自动配置类数据起到一一对应, 从 factories 中读取出的配置类, 在 metadata 中会有一条匹配规则:

  org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration=
  org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.ConditionalOnClass=com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplaten
  org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$MessagingTemplateConfiguration=
  org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$MessagingTemplateConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.core.RabbitMessagingTemplate

比如我需要找 RabbitAutoConfiguration 自动配置类的启动条件, 会从 metadata 中读取出这一行:

  org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.ConditionalOnClass=com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplaten

匹配规则大概格式这样: 自动配置类全类名.匹配条件=匹配条件参数
所以, RabbitAutoConfiguraion 需要满足以下条件才加载: ClassPath 中包含 com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplaten 这两个类.
匹配条件不止一种, 一共如下:

  • ConditionalOnBean: Spring 容器中是否包含 Bean
  • ConditionalOnClass: ClassPath 中是否包含 Bean
  • ConditionalOnCloudPlatform: 是否运行在指定的云平台上
  • ConditionalOnExpression: SpEL 表达式结果为 true 时
  • ConditionalOnJava: 是否满足指定的 java 版本
  • ConditionalOnJndi: 满足指定的 Jndi
  • ConditionalOnMissingBean: Spring 容器中不存在某些 Bean 时生效
  • ConditionalOnMissingClass: ClassPath 中不存在某些 Class 时生效
  • ConditionalOnNotWebApplication: 非 Web 环境下 SpringBoot 运行时生效
  • ConditionalOnProperty: SpringBoot 参数设置值相等生效
  • ConditionalOnResource: 指定的文件存在时生效
  • ConditionalOnSingleCandidate: 当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
  • ConditionalOnWarDeployment: 传统 WAR 包部署时生效
  • ConditionalOnWebApplication: SpringBoot 以 Web type 运行时生效
    深入 match 的代码就不讲了, 太过底层繁杂, 略过.
  1. fireAutoConfigurationImportEvents
     private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
     // 1. 从 spring.factories 中读取 key 为 AutoConfigurationImportListener 的监听器集合
         List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
         if (!listeners.isEmpty()) {
       // 2. 将已过滤的自动配置类集合, 和排除类集合(没错,第3次了)构建一个 Spring 事件
             AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
             for (AutoConfigurationImportListener listener : listeners) {
                 invokeAwareMethods(listener); // 3. 监听器环境参数注入
                 listener.onAutoConfigurationImportEvent(event); // 4. 让监听器执行事件, 没有广播
             }
         }
     }
  2. return
    最后返回自动配置类和排除类集合的包装类.
     return new AutoConfigurationEntry(configurations, exclusions);

    最终向 Spring 返回了过滤后自动配置类的 String 数组, 交由 Spring 执行自动配置类.

         return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());

SpringBoot 自动配置/装配的源码大概就这样写完了.

4) 总结

  • Maven 相关
    • parent 中定义了基础包和官方 starter 的版本号
    • 一个 starter 中传递依赖了很多子包
  • @SpringApplication
    1. @SpringBootConfiguration: 传递注解 @Configuration
    2. @EnableAutoConfiguration:
      1. @AutoConfigurationPackage: 将启动类所在包名包装成 BasePackages, 加入 DI 容器中.
      2. @Import(AutoConfigurationImportSelector.class):
        1. spring.factories 中加载 keyEnableAutoConfiguration 的自动配置类 String 集合.
        2. 对自动配置类 String 集合去重, 排除.
        3. spring.factories 中加载 keyAutoConfigurationImportFilter 的过滤器.
        4. 结合过滤器, 和 META-INF 下的 metadata 中的条件数据, 对自动配置类做一个去重.
        5. 将过滤后的自动配置类交由 Spring 容器执行.
    3. @ScanComponent 基于上一步中得到的 BasePackage, 扫描启动类路径下的所有组件加入容器.

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

SpringCloud 优化

1) Eureka Server 优化

1.1) 自我保护

有两个 EurekaClient 集群, A 集群服务量 10, B 集群服务量 1000.
A B 集群中各有 3 台机器失去心跳.
A 集群服务少网络抖动概率相对少, 自我保护可能把宕机节点保护下来导致服务不可用;
B 集群大, 保护阈值高, 一般不会出现大量服务宕机, 且由于集群大网络抖动概率大, 建议开启.
总结:

  • 服务关闭自我保护
  • 服务开启自我保护
    eureka:
    server:
      enable-self-preservation: false # 关闭自我保护
      renewal-parcent-thresshold: 0.85 # 自我保护阈值
      eviction-interval-timer-in-ms: 1000 # 剔除服务时间间隔
      use-read-only-response-cache: false # 关闭三层缓存
      response-cache-update-interval-ms: 1000 # 设置 readWrite 和 readOnly 同步时间间隔

1.2) 服务下线监控

阅读 EurekaServer 源码可得知, 发生服务下线时, 会发送 EurekaInstanceCanceledEvent 事件, 我们可以监听这个 Spring 事件, 来发送做一些处理(如发邮件)

1.3) Eureka 三级缓存

这块具体可以参考 com.netflix.eureka.registry.ResponseCacheImpl 的源码.

  • register(ConcurrentHashMap)
  • readWriteCacheMap(Guava LoadingCache)
  • readOnlyCacheMap(ConcurrentHashMap)

注册流程:

  1. client 发起注册请求时, 首先注册至 register, readWriteCacheMap 二级缓存失效
  2. Timmer 定时器任务每隔 response-cache-update-interval-ms 毫秒, 三级缓存从二级缓存中更新已存在的数据, 如为二级缓存失效状态, 触发 guava 回调从同步 register

Get 服务流程:

  1. 从三级缓存找, 找不到就从二级缓存取并更新, 若二级缓存失效, 则触发 Guava 回调从一级缓存同步.

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

JVM 笔记

本文都基于 Hotspot 编写

1) JVM 内存

1.1) JVM 内存结构

  • 线程共享
    • 方法区
  • 线程隔离
    • 虚拟机栈
    • 本地方法栈
    • 程序计数器

1.2) 堆

1.2.1) JDK 1.8 以前
  • 新生代
    • Eden
    • Survivor 1
    • Survivor 2
  • 老年代
    • Tenured
  • 持久代
    • Perm Gen
1.2.1) JDK 1.8 后

1.8 后将 Perm Gen 持久代替换成了 Metaspace 元空间:
从使用堆内存 升级为 直接使用 OS 内存资源

  • 新生代
    • Eden
    • Survivor 1
    • Survivor 2
  • 老年代
    • Tenured
  • 元空间
    • Metaspace

1.3) 方法区

有两个陌生的常量池: Class 常量池主要存放类加载到内存之后的常量等, 然后将这些常量继续加载至运行时常量池

1.4) 虚拟机栈

当创建一个线程时, 会建立对应此线程的虚拟机栈, 管理本地方法, 一图流:

1.5) 本地方法栈

与虚拟机栈类似, 但只管理 Native 方法, 这里不细讲了.

1.6) 程序计数器

与前两者一致, 都是线程级的, 主要记录当前线程的执行到了哪个位置, 供于多线程切换后运行状态的恢复.

2) 类加载

Hotspot 1.8 类加载过程如下图:

如果确认项目能正常加载, 可以用 -Xverify:none 关闭链接阶段验证步骤提高启动速度, 可应用在 IDEA 之类的 Java IDE 上.

3) 编译器优化

3.1) 运行模式优化

JVM 一共有三种运行模式, 默认 Mixed 混合模式:

  1. -Xint: 解释模式
  2. -Xcomp: 编译模式, 优先以编译模式运行
  3. -mixed: 混合模式, 用 JIT(即时编译器) 将热点代码编译提高启动速度
  • 分层编译优化

3.2) JIT 优化

JIT(Just-In-Time Compiler) 即时编译器, 由 JIT 来即时决定调用代码是否需要编译, Hotspot 中提供两个 JIT, C1 和 C2

3.2.1) JIT_C1(Client Compiler)
  • 只局部优化, 简单快速
  • 启动速度高, 适合做 GUI 等 Client 应用
3.2.2) JIT_C2(Server Compiler)
  • 相比 C1 优化更全面
  • 启动速度慢, 适合做执行时间较长或追求性能的程序

3.3) 热点代码优化

被判断为热点代码则会被 JIT 编译成字节码执行, 反之则解释执行.

以下的代码为热点代码:

  • 重复出现在栈顶的方法
  • 计数器探测
    • 方法调用计数器(Invocation Counter): 可通过 -XX:CompileThreshold=x 设置阈值
      1. 统计方法被调用次数, 不开启分层编译时, C1 阈值 1500, C2 阈值 10000 次
      2. 每过一段时间会在 GC 时, 顺便发生热度衰减导致阈值减半
        • -XX:-UseCounterDecay 关闭热度衰减
        • -XX:COunterHalfLifeTime=x 设置热度衰减周期
    • 回边计数器(Back Edge Counter): 可通过 -XX:OnStackReplacePercentage=X 设置阈值
      1. 统计循环代码执行次数, 不开启分层编译时, C1 阈值 13995, C2 阈值 10700 次

当开启分层编译时, JVM 根据当前待编译方法数, 编译线程数动态调整阈值, 上述两个 JVM 参数会失效

3.4) 分层编译优化

分层编译一共有 5 种级别, 根据代码的热点程度使用相应级别优化代码:
0. 解释执行

  1. 简单 C1 编译, 不开启 Profiling JVM 性能监控
  2. 受限的 C1 编译, Profiling 只监控方法调用次数, 循环回边执行次数
  3. 完全 C1 编译, 会使用 C1 的所有 Profiling 监控
  4. C2 编译, 某些情况会根据性能监控信息进行一些非常激进的优化

可以通过以下 JVM 参数限制级别:

  • 仅使用 C1: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
    • TieredStopAtLevel 即分层停止级别, 若设置为 3 则只使用 0,1,2,3 级编译
  • 仅使用 C2: -XX:-TieredCompilation , 关闭分层编译, 仅使用 0,4 级别优化

3.5) 方法内联优化

JVM 会使用内联优化, 将满足条件的目标代码, 尝试内联(复制)至调用处, 减少入栈出栈开销:

  1. 方法体足够小
    • 热点方法体阈值 325 字节, 可用 -XX:FreqInlineSize=x 修改
    • 非热点方法体阈值 35 字节, 可用 -XX:MaxInlineSize=x 修改
  2. 目标方法运行时的实现可以被唯一确定

方法内联带来的问题: 方法内联实际上是空间换时间, 如果内联过多可能导致 CodeCache 溢出, 使得 JVM 降级解释模式运行

以下还有一些其他的方法内联参数:
| 参数名 | 默认 | 说明 |
| --- | --- | --- |
| -XX:+Printlnlining | - | 打印内联详情, 该参数需和 -XX:+UnlockDiagnosticVMOptions 配合使用 |
| -XX:+UnlockDiagnosticVMOptions | - | 打印 JVM 诊断相关的信息 |
| -XX:MaxInlineSize=n | 35 | 如果非热点方法的字节码超过该值, 则无法内联, 单位字节 |
| -XX:FreqInlineSize=n | 325 | 如果热点方法的字节码超过该值, 则无法内联, 单位字节 |
| -XX:InlineSmallCode=n | 1000 | 目标编译后生成的机器码代销大于该值则无法内联, 单位字节 |
| -XX:MaxInlineLevel=n | 9 | 内联方法的最大调用帧数(嵌套调用的最大内联深度)|
| -XX:MaxTrivialSize=n | 6 | 如果方法的字节码少于该值,则直接内联,单位字节 |
| -XX:MinInliningThreshold=n | 250 | 如果目标方法的调用次数低于该值,则不去内联 |
| -XX:LiveNodeCountlnliningCutoff=n | 40000 | 编译过程中最大活动节点数(IR节点)的上限,仅对C2编译器有效 |
| -XX:InlineFrequencyCount=n | 100 | 如果方法的调用点(call site)的执行次数超过该值,则触发内联 |
| -XX:MaxRecursiveInlineLevel=n | 1 | 递归调用大于这么多次就不内联 |
| -XX:+InlineSynchronizedMethods | 开启 | 是否允许内联同步方法 |

3.5) 标量替换 / 栈上分配

| -XX:+DoEscapeAnalysis | 开启 | 是否开启逃逸分析 |
| -XX:+EliminateAllocations | 开启 | 是否开启标量替换 |
| -XX:+EliminateLocks | 开启 | 是否开启锁消除 |

4) GC 优化

我们可以根据各种场景下的需求, 来选择垃圾回收策略, 如:

  • 内存不足场景: 提高对象的回收效率, 腾出更多内存
  • CPU 资源不足: 降低高并发时垃圾回收频率, 充分利用 CPU 资源提高并发量

4.1) GC 在哪回收

上文提到 JVM 的内存结构可以得知, (虚拟机栈,本地方法栈,程序计数器) 都是线程隔离的, 对象会随着栈入栈出自动销毁, 所以它们不需要考虑线程隔离.
而线程共享的(堆,方法区), 则是会发生 GC 的部分:

  • : 回收对象
  • 方法区: 回收常量和未被使用的类

4.2) 垃圾计算算法

Java 默认使用的垃圾计算算法是可达性分析算法, 我们还可以了解以下引用计数法:

4.2.1) 引用计数法

记录对象的被引用的次数, 当计数器归 0, 即回收, 无法解决循环引用问题

4.2.1) 可达性分析

可达性分析即: 被 GCRoots 直接或间接引用则可达, 反之不可达, 可以回收.

GCRoots 有以下几类, 可达性可以理解为堆外指向堆内的引用:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI 引用的对象

引用又有以下几种:

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

不可达对象回收流程:

  1. 不可达的对象首先会被标记死缓
    1. 判断该对象有无必要执行(重写) finalize(), 如不需要则回收
  2. 若有必要执行 finalize(), JVM 会创建一个低优先级线程执行其
    1. finalize() 中的代码重新使该对象建立引用, 则放弃回收

4.3) 垃圾回收算法

三种常用基础算法:

  1. 标记清除算法: 会产生碎片, 放不下大对象导致溢出
    1. 标记需要被回收的对象
    2. 清理需要被回收的对象
  2. 标记整理/压缩算法: 相比标记清除避免内存碎片
    1. 标记需要被回收的对象
    2. 把存活对象移动到一起
    3. 剩余区域回收
  3. 复制算法:
    1. 需要两块一样大小的内存区域 AB, 只使用其中一块
    2. 将 A 存活对象复制到 B, 切换使用空间至 B
    3. 清空 A
    4. 将 B 存活对象复制到 A, 切换使用空间至 A
    5. 清空 B
    6. ....

三种常用基础算法对比:

算法 优点 缺点
标记清除 实现简单 存在内存碎片, 分配内存开销
标记整理 无碎片 整理开销
复制 性能好, 无碎片 内存利用率低

两种综合算法:

  1. Java 分代收集算法:
  2. 增量算法:

4.4) 垃圾收集器

新生代收集器(复制算法):

  1. Serial (Client 模式默认收集器):
    • 单线程
    • 简单, 高效
    • 收集全程 Stop The World
  2. ParNew (Serial 多线程版)
    • 多线程
    • 线程数: -XX:ParallelGCThreads=x
    • 主要和 CMS 配合使用
  3. Parallel Scavenge (吞吐量优先收集器)
    • 多线程
    • 可控制回收吞吐量
      • 回收最大停顿时间(尽量保证): -XX:MaxGCPauseMilis
      • 吞吐量大小, 设置回收时间不超过运行时间的 1/(1+n): -XX:GCTimeRatio
      • 自适应 GC: -XX:+UseAdptiveSizePolicy
        • 开启后无需手动设置新生代大小(-Xmn), Eden/Survivor 区比例(-XX:SurvivorRatio) 等参数

老年代收集器(标记清除算法):

  1. Seria Old
    • CMS 收集器的后备收集器
  2. Parallel Old
  3. CMS(Concurrent Mark Sweep)
    • 并发收集器
    • 老年代占比触发阈值: -XX:CMSInitiatingOccupancyFraction=-1

CMS 执行流程:

  1. 初始标记
    • 标记 GC Roots 直接关联对象
    • Stop The World
  2. 并发标记
    • 标记 GC Roots 关联的所有对象
    • 并发执行, 无 Stop The World
  3. 并发预清理
    • 重新标记上阶段中, 引用被更新的对象
    • 并发执行
    • 关闭此阶段: -XX:-CMSPrecleaningEnable=true
  4. 并发可终止预清理
    • 与上阶段一致
    • 当 Eden 的使用量大于阈值: -XX:CMSScheduleRemarkEdenSizeThreshold=2M才执行
    • 控制预清理阶段结束时机
      • 扫描时间阈值(s): -XX:CMSMaxAbortablePrecleanTime=5
      • Eden 占比阈值: -XX:CmsScheduleRemarkEdenPenetration=50
  5. 重新标记
    • 修正并发标记期间, 标记发生变动的对象标记
    • Stop The World
  6. 并发清理
    • 直接清除被标记对象
    • 并发执行
  7. 并发重置
    • 清理本次 CMS GC 的长下文信息, 为下一次 GC 做准备

G1 收集器

  • Region 式内存区块分布
  • CMS 替代品
  • Java9 后删除了 CMS
  • 复制算法, 没有碎片

本文采用知识共享 署名-相同方式共享 4.0 国际 许可协议进行许可。
访问 https://creativecommons.org/licenses/by-sa/4.0/ 查看该许可协议。

DB 调优

本文基于 MySQL 编写,兼容版本 5.7+

1) 调优数据库纬度

从上往下成本依次递减, 从上往下效果依次递增, 尽量从下往上优化, 提高投入产出比

  • 硬件和系统调优
    • 硬件
      • 硬件配置
    • OS 配置
      • 内核参数如 swappiness 等
  • MySQL 自身调优
    • 数据库参数配置
      • 性能参数如 buffer 等
    • 表结构
      • 良好的表结构
    • SQL 以及索引
      • 良好 SQL
      • 高效索引
  • 架构调优
    • 系统架构
      • 读写分离
      • 高可用
      • 实例个数
      • 分库分表
      • 数据库选择
    • 业务需求
      • 拒绝不合理的需求, 提出优化方案

2) 性能分析

2.0) 数据准备

基于 MySQL 官方测试数据库, 可按照 README 安装:
https://github.com/datacharmer/test_db

2.1) 慢查询分析

2.1.1) MySQL 慢查询日志配置

加入 my.cnf 中 mysqld 重启; 或在 clinet 中 set global 临时生效.

  • slow_query_log: 开启日志输出
  • slow_query_log_file: 默认如 /var/log/mysql/xxxx.slow_log, 慢日志存放路径
  • log_output(FILE, TABLE): 默认 FILE, 输出至 mysql.slow_log, 可以组合使用如: FILE,TABLE
  • long_query_time: 默认 10, 即执行时间超过 10 秒记录为慢查询
  • long_queries_not_using_indexes(OFF, ON): 默认 OFF, 是否将未使用索引 SQL 同时记录
  • long_throttle_queries_not_using_indexes: 默认 0, 与 long_queries_not_using_indexes 搭配使用, 限制每分钟写入未使用索引 SQL 数量
  • min_examined_row_limit: 默认 0, 慢查询 SQL 行数超过此阈值才记录
  • log_slow_admin_statements(OFF, ON): 默认 OFF, 是否记录管理语句(ALTER TABLE, ANALYZE TABLE, CHECK TABLE, CREATE INDEX, DROP INDEX, OPTIMIZE TABLE, REPAIR TABLE)
  • log_slow_slave_statements(OFF, ON): 默认 OFF, 是否记录 Slave 节点做主从复制时, 超过 long_query_time 时间的复制查询
  • log_slow_extra(OFF, ON): 默认 OFF, 仅当日志输出为文件时有效, 额外输出一些额外 log

2.1.2) MySQL 慢查询日志分析

2.1.3) TABLE 类型日志分析
SELECT * FROM `mysql`.slow_log;

终端查询 SQL 会显示如下, 可以使用其他工具如 DataGrip 查看具体 SQL

+----------------------------+---------------------------+-----------------+-----------------+-----------+---------------+-----------+----------------+-----------+-----------+------------------------------------------------------------+-----------+
| start_time                 | user_host                 | query_time      | lock_time       | rows_sent | rows_examined | db        | last_insert_id | insert_id | server_id | sql_text                                                   | thread_id |
+----------------------------+---------------------------+-----------------+-----------------+-----------+---------------+-----------+----------------+-----------+-----------+------------------------------------------------------------+-----------+
| 2020-10-17 22:56:21.693516 | root[root] @ localhost [] | 00:00:00.004481 | 00:00:00.000000 |         0 |             0 | employees |              0 |         0 |         1 | 0x73657420676C6F62616C20736C6F775F71756572795F6C6F673D4F4E |        22 |
| 2020-10-17 22:56:28.742718 | root[root] @ localhost [] | 00:00:00.165174 | 00:00:00.000109 |    300024 |        300024 | employees |              0 |         0 |         1 | 0x73656C656374202A2066726F6D20656D706C6F79656573           |        22 |
+----------------------------+---------------------------+-----------------+-----------------+-----------+---------------+-----------+----------------+-----------+-----------+------------------------------------------------------------+-----------+
2.1.4) FILE 类型日志分析

日志文件存放路径可以使用如下 SQL 查询:

show variables LIKE '%slow_query_log%';

日志文件不好直接浏览, 可以使用 MySQL 自带工具 mysqldumpslow 分析, 使用方法可以参考 mysqldumpslow --help:

mysqldumpslow -s t -t 10 -g "ORDER BY" /usr/local/var/mysql/warsdeiMac-slow.log

2.1.3) EXPLAIN 分析 SQL 执行计划

使用 EXPLAIN 加上 SQL 即可分析 SQL 的执行计划如: explain SELECT * FROM employees;
输出格式一共有三种, 可以在 EXPLAIN 后拼上 FORMAT=(TREE,JSON) 使用其他两种.

默认输出的信息列含义解释如下:

还可以在 SQL 结尾使用 SHOW WARNING, 查看扩展信息, 这里不深究.

2.1.4) SHOW PROFILE 分析各阶段开销

SHOW PROFILE 已经被废弃, 但是 PERFORMANCE_SCHEMA 使用过于繁琐, 依然建议使用 SHOW PROFILE.

  • SELECT @@have_profiling;
    是否支持 SHOW PROFILE
  • SELECT @@PROFILING;
    是否已启用
  • SET profiling=1/0;
    开启或关闭, 分析完成请关闭此功能降低性能损耗
  • SHOW PROFILES;
    查看最近执行 15 条 SQL 耗时, 可通过 SET profiling_history_size=x 调整数量
  • SHOW PROFILE [type, ...] FOR QUERY {Query_ID}
    • Query_ID 通过 SHOW PROFILES 获得
    • type:
      • ALL
      • BLOCK IO
      • CONTEXT SWITCHES
      • CPU
      • IPC
      • MEMORY
      • PAGE FAULTS
      • SOURCE
      • SWAPS

2.1.5) OPTIMIZER_TRACE

待完善

3) 索引

3.1) 常见 Tree 数据结构

3.1.1) 二叉树

左边叶子节点始终比右边节点小, 因为无法保证左右平衡, 所以上界 O(n)

3.1.2) 平衡二叉树

左边叶子节点始终比右边节点小, 加入平衡算法改变树结构保证平衡, 所以上界 O(logn)

3.1.3) B-tree

算法 平均 最差
空间 O(n) O(n)
搜索 O(log n) O(log n)
插入 O(log n) O(log n)
删除 O(log n) O(log n)

图有点难画, 转自维基百科:

  • m 为树的层数
  • 根节点的子节点个数为 2 <= x <= m
  • 中间节点的子节点个数为 m/2 <= y <= m
  • 有 k 个子节点的非叶子节点有 k - 1 个键

3.1.4) B+tree

相对于 B-tree, 适合范围查找

图有点难画, 转自维基百科:

  • 与 B-tree 最大的区别是各节点中包含了所有父节点的关键字, 有序链表存储
  • 所有叶子节点中间有指针相连

3.2) MySQL 索引类型

3.2.1) InnoDB vs MyISAM

InnoDB 和 MyISAM 数据结构都默认使用 B+tree 实现

  • InnoDB: 聚簇索引
    • 叶子节点索引和数据存储在一起
  • MyISAM: 非聚簇索引
    • 的叶子节点只存储数据指针

3.2.2) Hash

上界 O(1)

转自慕课网架构师直通车:

3.2.2.1) MySQL Hash 索引

默认只支持 Memory 引擎, InnoDB 可以通过 innodb-adaptive_hash_index 参数开启 ‘自适应 Hash 索引’, 默认打开

CREATE TABLE hash_test(
  name varchar(55) not null,
  age tinyint(4) not null,
  key using hash(name)
)engine = memory;

3.2.3) 空间索引

MySQL 5.7 后支持 InnoDB, 之前只支持 MyISAM, 建议使用 PostgreSQL 玩空间索引

3.2.4) 全文索引

MySQl 5.7 后支持中文,之前通常搭配搜索引擎使用, 建议使用搜索引擎

3.3) 索引限制

3.3.1) 匹配规则支持

  • 完全匹配: WHERE name = 'wars'
  • 范围匹配: WHERE age > 18
  • 前缀匹配: WHERE name LIKE 'w%'

3.3.1) B-tree / B+tree 组合索引限制

index(name, age, sex)

  • 组合索引查询条件不包括最左列(name),则无法使用索引
  • 组合索引若不连续使用(WHERE name='a' AND sex=1),只能使用到 name 索引
  • 组合索引查询中如有列范围(模糊)查询(WHERE age>1 AND sex=1), 右边列都玩法使用索引(sex)

3.3.2) Hash 索引限制

  • 无法使用排序
  • 不支持范围/模糊查询
  • 不支持部分索引列匹配查找

3.4) 创建索引原则

建议创建场景:

  • SELECT 中频繁 WHERE 字段
  • UPDATE/DELETE 中非主键 WHERE 条件
  • ORDER BY/GROUP BY 字段
  • DISTINCT 字段
  • 唯一约束字段
  • 多表连接字段,务必类型一致(避免隐式转换)
    不建议创建场景:
  • WHERE 中用不到的字段
  • 表记录过少
  • 表中大量重复数据
  • 频繁更新的字段,会产生索引维护开销

3.5) 索引失效

  • WHERE 中对索引列使用了表达式或函数
  • 尽量避免使用左模糊, 可考虑转搜索引擎
  • OR 条件左右侧有无索引字段,引起全表扫描
  • WHERE 条件和索引列类型不一致
  • WHERE 条件字段含有 NULL 值, 无法索引, 建议将表字段都定义成 NOT NULL

3.6) 索引调优

3.6.1) 长字段索引调优

对于长字段列, 可以新建一列 Hash 列作为索引, 在插入时, 可以计算该字段值的 Hash, 然后与该字段一同插入表中.
查询时直接计算 Hash 值直接查 Hash 列即可.

3.6.1.1) 无法模糊查询问题

但是这种 Hash 索引无法模糊查询, 所以可以引进前缀索引:

-- 5 代表使用该列的前几个字符进行索引
ALTER TABLE employees ADD KEY(first_name(5));

那么前缀多少比较好呢, 可以使用一个完整列选择性公式计算:

-- 计算此列的最大选择性积分
SELECT COUNT(DISTINCT first_name) / COUNT(*) FROM employees;
-- 计算当前缀索引长度为 x 时, 选择性积分, 可以依次递增计算次列合适的长度
SELECT COUNT(DISTINCT LEFT(first_name, x)) / COUNT(*) FROM employees;

还可以新增一个字段反转列, 建立前缀索引, 即可实现后缀索引

Title - Artist
0:00