java大厂技术面试第四课 spring/mabatis/springboot相关

2023-10-24 01:59

本文主要是介绍java大厂技术面试第四课 spring/mabatis/springboot相关,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

第11讲:底层源码分析 Spring 的核心功能和执行流程?(上)

Spring Framework 已是公认的 Java 标配开发框架了,甚至还有人说 Java 编程就是面向 Spring 编程的,可见 Spring 在整个 Java 体系中的重要位置。

Spring 中包含了众多的功能和相关模块,比如 spring-core、spring-beans、spring-aop、spring-context、spring-expression、spring-test 等,本课时先从面试中必问的问题出发,来帮你更好的 Spring 框架。

我们本课时的面试题是,Spring Bean 的作用域有哪些?它的注册方式有几种?

典型回答

在 Spring 容器中管理一个或多个 Bean,这些 Bean 的定义表示为 BeanDefinition 对象,这些对象包含以下重要信息:

  • Bean 的实际实现类
  • Bean 的作用范围
  • Bean 的引用或者依赖项

Bean 的注册方式有三种:

  • XML 配置文件的注册方式
  • Java 注解的注册方式
  • Java API 的注册方式
1. XML 配置文件注册方式
<bean id="person" class="org.springframework.beans.Person"><property name="id" value="1"/><property name="name" value="Java"/>
</bean>
2. Java 注解注册方式

可以使用 @Component 注解方式来注册 Bean,代码如下:

@Component
public class Person {private Integer id;private String name// 忽略其他方法
}

也可以使用 @Bean 注解方式来注册 Bean,代码如下:

@Configuration
public class Person {@Beanpublic Person  person(){return new Person();}// 忽略其他方法
}

其中 @Configuration 可理解为 XML 配置里的 <beans> 标签,而 @Bean 可理解为用 XML 配置里面的 <bean> 标签。

3. Java API 注册方式

使用 BeanDefinitionRegistry.registerBeanDefinition() 方法的方式注册 Bean,代码如下:

public class CustomBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor {@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {RootBeanDefinition personBean = new RootBeanDefinition(Person.class);// 新增 Beanregistry.registerBeanDefinition("person", personBean);}
}

Bean 的作用域一共有 5 个。

(1)singleton 作用域:表示在 Spring 容器中只有一个 Bean 实例,以单例的形式存在,是默认的 Bean 作用域。

配置方式,缺省即可,XML 的配置方式如下:

<bean class="..."></bean>

(2)prototype 作用域:原型作用域,每次调用 Bean 时都会创建一个新实例,也就是说每次调用 getBean() 方法时,相当于执行了 new Bean()。

XML 的配置方式如下:

<bean class="..." scope="prototype"></bean>

(3)request 作用域:每次 Http 请求时都会创建一个新的 Bean,该作用域仅适应于 WebApplicationContext 环境。

XML 的配置方式如下:

<bean class="..." scope="request"></bean>

Java 注解的配置方式如下:

@Scope(WebApplicationContext.SCOPE_REQUEST)

或是:

@RequestScope(WebApplicationContext.SCOPE_REQUEST)

(4)session 作用域:同一个 Http Session 共享一个 Bean 对象,不同的 Session 拥有不同的 Bean 对象,仅适用于 WebApplicationContext 环境。

XML 的配置方式如下:

<bean class="..." scope="session"></bean>

Java 注解的配置方式如下:

@Scope(WebApplicationContext.SCOPE_SESSION)

或是:

@RequestScope(WebApplicationContext.SCOPE_SESSION)

(5)application 作用域:全局的 Web 作用域,类似于 Servlet 中的 Application。

XML 的配置方式如下:

<bean class="..." scope="application"></bean>

Java 注解的配置方式如下:

@Scope(WebApplicationContext.SCOPE_APPLICATION)

或是:

@RequestScope(WebApplicationContext.SCOPE_APPLICATION)

考点分析

在 Spring 中最核心的概念是 AOP(面向切面编程)、IoC(控制反转)、DI(依赖注入)等(此内容将会在下一课时中讲到),而最实用的功能则是 Bean,他们是概念和具体实现的关系。和 Bean 相关的面试题,还有以下几个:

  • 什么是同名 Bean?它是如何产生的?应该如何避免?
  • 聊一聊 Bean 的生命周期。

知识扩展

1.同名 Bean 问题

每个 Bean 拥有一个或多个标识符,在基于 XML 的配置中,我们可以使用 id 或者 name 来作为 Bean 的标识符。通常 Bean 的标识符由字母组成,允许使用特殊字符。

同一个 Spring 配置文件中 Bean 的 id 和 name 是不能够重复的,否则 Spring 容器启动时会报错。但如果 Spring 加载了多个配置文件的话,可能会出现同名 Bean 的问题。同名 Bean 指的是多个 Bean 有相同的 name 或者 id。

Spring 对待同名 Bean 的处理规则是使用最后面的 Bean 覆盖前面的 Bean,所以我们在定义 Bean 时,尽量使用长命名非重复的方式来定义,避免产生同名 Bean 的问题。

Bean 的 id 或 name 属性并非必须指定,如果留空的话,容器会为 Bean 自动生成一个唯一的

名称,这样也不会出现同名 Bean 的问题。

2.Bean 生命周期

对于 Spring Bean 来说,并不是启动阶段就会触发 Bean 的实例化,只有当客户端通过显式或者隐式的方式调用 BeanFactory 的 getBean() 方法时,它才会触发该类的实例化方法。当然对于 BeanFactory 来说,也不是所有的 getBean() 方法都会实例化 Bean 对象,例如作用域为 singleton 时,只会在第一次,实例化该 Bean 对象,之后会直接返回该对象。但如果使用的是 ApplicationContext 容器,则会在该容器启动的时候,立即调用注册到该容器所有 Bean 的实例化方法。

getBean() 既然是 Bean 对象的入口,我们就先从这个方法说起,getBean() 方法是属于 BeanFactory 接口的,它的真正实现是 AbstractAutowireCapableBeanFactory 的 createBean() 方法,而 createBean() 是通过 doCreateBean() 来实现的,具体源码实现如下:

@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {if (logger.isTraceEnabled()) {logger.trace("Creating instance of bean '" + beanName + "'");}RootBeanDefinition mbdToUse = mbd;// 确定并加载 Bean 的 classClass<?> resolvedClass = resolveBeanClass(mbd, beanName);if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {mbdToUse = new RootBeanDefinition(mbd);mbdToUse.setBeanClass(resolvedClass);}// 验证以及准备需要覆盖的方法try {mbdToUse.prepareMethodOverrides();}catch (BeanDefinitionValidationException ex) {throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),beanName, "Validation of method overrides failed", ex);}try {// 给BeanPostProcessors 一个机会来返回代理对象来代替真正的 Bean 实例,在这里实现创建代理对象功能Object bean = resolveBeforeInstantiation(beanName, mbdToUse);if (bean != null) {return bean;}}catch (Throwable ex) {throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,"BeanPostProcessor before instantiation of bean failed", ex);}try {// 创建 BeanObject beanInstance = doCreateBean(beanName, mbdToUse, args);if (logger.isTraceEnabled()) {logger.trace("Finished creating instance of bean '" + beanName + "'");}return beanInstance;}catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {throw ex;}catch (Throwable ex) {throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex);}
}

doCreateBean 源码如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {// 实例化 bean,BeanWrapper 对象提供了设置和获取属性值的功能BeanWrapper instanceWrapper = null;// 如果 RootBeanDefinition 是单例,则移除未完成的 FactoryBean 实例的缓存if (mbd.isSingleton()) {instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);}if (instanceWrapper == null) {// 创建 bean 实例instanceWrapper = createBeanInstance(beanName, mbd, args);}// 获取 BeanWrapper 中封装的 Object 对象,其实就是 bean 对象的实例final Object bean = instanceWrapper.getWrappedInstance();// 获取 BeanWrapper 中封装 bean 的 ClassClass<?> beanType = instanceWrapper.getWrappedClass();if (beanType != NullBean.class{mbd.resolvedTargetType = beanType;}// 应用 MergedBeanDefinitionPostProcessor 后处理器,合并 bean 的定义信息// Autowire 等注解信息就是在这一步完成预解析,并且将注解需要的信息放入缓存synchronized (mbd.postProcessingLock) {if (!mbd.postProcessed) {try {applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);} catch (Throwable ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName,"Post-processing of merged bean definition failed", ex);}mbd.postProcessed = true;}}boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}// 为了避免循环依赖,在 bean 初始化完成前,就将创建 bean 实例的 ObjectFactory 放入工厂缓存(singletonFactories)addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}// 对 bean 属性进行填充Object exposedObject = bean;try {populateBean(beanName, mbd, instanceWrapper);// 调用初始化方法,如 init-method 注入 Aware 对象exposedObject = initializeBean(beanName, exposedObject, mbd);} catch (Throwable ex) {if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {throw (BeanCreationException) ex;} else {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);}}if (earlySingletonExposure) {// 如果存在循环依赖,也就是说该 bean 已经被其他 bean 递归加载过,放入了提早公布的 bean 缓存中Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {// 如果 exposedObject 没有在 initializeBean 初始化方法中被增强if (exposedObject == bean) {exposedObject = earlySingletonReference;} else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {// 依赖检测String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}// 如果 actualDependentBeans 不为空,则表示依赖的 bean 并没有被创建完,即存在循环依赖if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");}}}}try {// 注册 DisposableBean 以便在销毁时调用registerDisposableBeanIfNecessary(beanName, bean, mbd);} catch (BeanDefinitionValidationException ex) {throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);}return exposedObject;
}

从上述源码中可以看出,在 doCreateBean() 方法中,首先对 Bean 进行了实例化工作,它是通过调用 createBeanInstance() 方法来实现的,该方法返回一个 BeanWrapper 对象。BeanWrapper 对象是 Spring 中一个基础的 Bean 结构接口,说它是基础接口是因为它连基本的属性都没有。

BeanWrapper 接口有一个默认实现类 BeanWrapperImpl,其主要作用是对 Bean 进行填充,比如填充和注入 Bean 的属性等。

当 Spring 完成 Bean 对象实例化并且设置完相关属性和依赖后,则会调用 Bean 的初始化方法 initializeBean(),初始化第一个阶段是检查当前 Bean 对象是否实现了 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 等接口,源码如下:

private void invokeAwareMethods(final String beanName, final Object bean) {if (bean instanceof Aware) {if (bean instanceof BeanNameAware) {((BeanNameAware) bean).setBeanName(beanName);}if (bean instanceof BeanClassLoaderAware) {ClassLoader bcl = getBeanClassLoader();if (bcl != null) {((BeanClassLoaderAware) bean).setBeanClassLoader(bcl);}}if (bean instanceof BeanFactoryAware) {((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);}}
}

其中,BeanNameAware 是把 Bean 对象定义的 beanName 设置到当前对象实例中;BeanClassLoaderAware 是将当前 Bean 对象相应的 ClassLoader 注入到当前对象实例中;BeanFactoryAware 是 BeanFactory 容器会将自身注入到当前对象实例中,这样当前对象就会拥有一个 BeanFactory 容器的引用。

初始化第二个阶段则是 BeanPostProcessor 增强处理,它主要是对 Spring 容器提供的 Bean 实例对象进行有效的扩展,允许 Spring 在初始化 Bean 阶段对其进行定制化修改,比如处理标记接口或者为其提供代理实现。

在初始化的前置处理完成之后就会检查和执行 InitializingBean 和 init-method 方法。

InitializingBean 是一个接口,它有一个 afterPropertiesSet() 方法,在 Bean 初始化时会判断当前 Bean 是否实现了 InitializingBean,如果实现了则调用 afterPropertiesSet() 方法,进行初始化工作;然后再检查是否也指定了 init-method,如果指定了则通过反射机制调用指定的 init-method 方法,它的实现源码如下:

protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd)throws Throwable {// 判断当前 Bean 是否实现了 InitializingBean,如果是的话需要调用 afterPropertiesSet()boolean isInitializingBean = (bean instanceof InitializingBean);if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {if (logger.isTraceEnabled()) {logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");}if (System.getSecurityManager() != null) { // 安全模式try {AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {((InitializingBean) bean).afterPropertiesSet(); // 属性初始化return null;}, getAccessControlContext());} catch (PrivilegedActionException pae) {throw pae.getException();}} else {((InitializingBean) bean).afterPropertiesSet(); // 属性初始化}}// 判断是否指定了 init-method()if (mbd != null && bean.getClass() != NullBean.class{String initMethodName = mbd.getInitMethodName();if (StringUtils.hasLength(initMethodName) &&!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&!mbd.isExternallyManagedInitMethod(initMethodName)) {// 利用反射机制执行指定方法invokeCustomInitMethod(beanName, bean, mbd);}}
}

初始化完成之后就可以正常的使用 Bean 对象了,在 Spring 容器关闭时会执行销毁方法,但是 Spring 容器不会自动去调用销毁方法,而是需要我们主动的调用。

如果是 BeanFactory 容器,那么我们需要主动调用 destroySingletons() 方法,通知 BeanFactory 容器去执行相应的销毁方法;如果是 ApplicationContext 容器,那么我们需要主动调用 registerShutdownHook() 方法,告知 ApplicationContext 容器执行相应的销毁方法。

注:本课时源码基于 Spring 5.2.2.RELEASE。

小结

本课时我们讲了 Bean 的三种注册方式:XML、Java 注解和 JavaAPI,以及 Bean 的五个作用域:singleton、prototype、request、session 和 application;还讲了读取多个配置文件可能会出现同名 Bean 的问题,以及通过源码讲了 Bean 执行的生命周期,它的生命周期如下图所示:


第12讲:底层源码分析 Spring 的核心功能和执行流程?(下)

上一课时我们讲了 Bean 相关的内容,它其实也是属于 IoC 的具体实现之一,本课时我们就来讲讲 Spring 中其他几个高频的面试点,希望能起到抛砖引玉的作用,能为你理解 Spring 打开一扇门。因为 Spring 涉及的内容和知识点太多了,用它来写一本书也绰绰有余,因此这里我们只讲核心的内容,希望下来你能查漏补缺,完善自己的 Spring 技术栈。

我们本课时的面试题是,谈一谈你对 IoC 和 DI 的理解。

典型回答

IoC(Inversion of Control,翻译为“控制反转”)不是一个具体的技术,而是一种设计思想。与传统控制流相比,IoC 会颠倒控制流,在传统的编程中需要开发者自行创建并销毁对象,而在 IoC 中会把这些操作交给框架来处理,这样开发者就不用关注具体的实现细节了,拿来直接用就可以了,这就是控制反转

IoC 很好的体现出了面向对象的设计法则之一——好莱坞法则:“别找我们,我们找你”。即由 IoC 容器帮对象找到相应的依赖对象并注入,而不是由对象主动去找。

举个例子,比如说传统找对象,先要设定好你的要求,如身高、体重、长相等,然后再一个一个的主动去找符合要求的对象,而 IoC 相当于,你把这些要求直接告诉婚介中心,由他们直接给你匹配到符合要求的对象,理想情况下是直接会帮你找到合适的对象,这就是传统编程模式和 IoC 的区别。

DI(Dependency Injection,翻译为“依赖注入”)表示组件间的依赖关系交由容器在运行期自动生成,也就是说,由容器动态的将某个依赖关系注入到组件之中,这样就能提升组件的重用频率。通过依赖注入机制,我们只需要通过简单的配置,就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心资源来自哪里、由谁实现等问题。

IoC 和 DI 其实是同一个概念从不同角度的描述的,由于控制反转这个概念比较含糊(可能只理解成了容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以 2004 年被开发者尊称为“教父”的 Martin Fowler(世界顶级专家,敏捷开发方法的创始人之一)又给出了一个新的名字“依赖注入”,相对 IoC 而言,“依赖注入”明确描述了“被注入对象依赖 IoC 容器配置依赖对象”。

考点分析

IoC 和 DI 为 Spring 框架设计的精髓所在,也是面试中必问的考点之一,这个优秀的设计思想对于初学者来说可能理解起来比较困难,但对于 Spring 的使用者来说可以很快的看懂。因此如果对于此概念还有疑问的话,建议先上手使用 Spring 实现几个小功能再回头来看这些概念,相信你会豁然开朗。

Spring 相关的高频面试题,还有以下这些:

  • Spring IoC 有哪些优势?
  • IoC 的注入方式有哪些?
  • 谈一谈你对 AOP 的理解。

知识扩展

1.Spring IoC 的优点

IoC 的优点有以下几个:

  • 使用更方便,拿来即用,无需显式的创建和销毁的过程;
  • 可以很容易提供众多服务,比如事务管理、消息服务等;
  • 提供了单例模式的支持;
  • 提供了 AOP 抽象,利用它很容易实现权限拦截、运行期监控等功能;
  • 更符合面向对象的设计法则;
  • 低侵入式设计,代码的污染极低,降低了业务对象替换的复杂性。
2.Spring IoC 注入方式汇总

IoC 的注入方式有三种:构造方法注入、Setter 注入和接口注入。

① 构造方法注入

构造方法注入主要是依赖于构造方法去实现,构造方法可以是有参的也可以是无参的,我们平时 new 对象时就是通过类的构造方法来创建类对象的,每个类对象默认会有一个无参的构造方法,Spring 通过构造方法注入的代码示例如下:

public class Person {public Person() {}public Person(int id, String name) {this.id = id;this.name = name;}private int id;private String name;// 忽略 Setter、Getter 的方法
}

applicationContext.xml 配置如下:

<bean id="person" class="org.springframework.beans.Person"><constructor-arg value="1" type="int"></constructor-arg><constructor-arg value="Java" type="java.lang.String"></constructor-arg>
</bean>

② Setter 注入

Setter 方法注入的方式是目前 Spring 主流的注入方式,它可以利用 Java Bean 规范所定义的 Setter/Getter 方法来完成注入,可读性和灵活性都很高,它不需要使用声明式构造方法,而是使用 Setter 注入直接设置相关的值,实现示例如下:

<bean id="person" class="org.springframework.beans.Person"><property name="id" value="1"/><property name="name" value="Java"/>
</bean>

③ 接口注入

接口注入方式是比较古老的注入方式,因为它需要被依赖的对象实现不必要的接口,带有侵入性,因此现在已经被完全舍弃了,所以本文也不打算做过多的描述,大家只要知道有这回事就行了。

3.Spring AOP

AOP(Aspect-OrientedProgramming,面向切面编程)可以说是 OOP(Object-Oriented Programing,面向对象编程)的补充和完善,OOP 引入封装、继承和多态性等概念来建立一种公共对象处理的能力,当我们需要处理公共行为的时候,OOP 就会显得无能为力,而 AOP 的出现正好解决了这个问题。比如统一的日志处理模块、授权验证模块等都可以使用 AOP 很轻松的处理。

Spring AOP 目前提供了三种配置方式:

  • 基于 Java API 的方式;
  • 基于 @AspectJ(Java)注解的方式;
  • 基于 XML <aop /> 标签的方式。

① 基于 Java API 的方式

此配置方式需要实现相关的接口,例如 MethodBeforeAdvice 和 AfterReturningAdvice,并且在 XML 配置中定义相应的规则即可实现。

我们先来定义一个实体类,代码如下:

package org.springframework.beans;

public class Person {
   public Person findPerson() {
      Person person = new Person(1“JDK”);
      System.out.println(“findPerson 被执行”);
      return person;
   }
   public Person() {
   }
   public Person(Integer id, String name) {
      this.id = id;
      this.name = name;
   }
   private Integer id;
   private String name;
   // 忽略 Getter、Setter 方法
}

再定义一个 advice 类,用于对拦截方法的调用之前和调用之后进行相关的业务处理,实现代码如下:

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class MyAdvice implements MethodBeforeAdviceAfterReturningAdvice {
   @Override
   public void before(Method method, Object[] args, Object target) throws Throwable {
      System.out.println("准备执行方法: " + method.getName());
   }

   @Override
   public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
      System.out.println(method.getName() + " 方法执行结束");
   }

然后需要在 application.xml 文件中配置相应的拦截规则,配置如下:

<!-- 定义 advisor -->
<bean id="myAdvice" class="org.springframework.advice.MyAdvice"></bean>
<!-- 配置规则,拦截方法名称为 find* -->
<bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"><property name="advice" ref="myAdvice"></property><property name="pattern" value="org.springframework.beans.*.find.*"></property>
</bean>

<!-- 定义 DefaultAdvisorAutoProxyCreator 使所有的 advisor 配置自动生效 -->
<bean class=“org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator”></bean>

从以上配置中可以看出,我们需要配置一个拦截方法的规则,然后定义一个 DefaultAdvisorAutoProxyCreator 让所有的 advisor 配置自动生效。

最后,我们使用测试代码来完成调用:

public class MyApplication {public static void main(String[] args) {ApplicationContext context =new ClassPathXmlApplicationContext("classpath*:application.xml");Person person = context.getBean("person", Person.class);person.findPerson();}
}

以上程序的执行结果为:

准备执行方法: findPerson
findPerson 被执行
findPerson 方法执行结束

可以看出 AOP 的拦截已经成功了。

② 基于 @AspectJ 注解的方式

首先需要在项目中添加 aspectjweaver 的 jar 包,配置如下:

<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.5</version>
</dependency>

此 jar 包来自于 AspectJ,因为 Spring 使用了 AspectJ 提供的一些注解,因此需要添加此 jar 包。之后,我们需要开启 @AspectJ 的注解,开启方式有两种。

可以在 application.xml 配置如下代码中开启 @AspectJ 的注解:

<aop:aspectj-autoproxy/>

也可以使用 @EnableAspectJAutoProxy 注解开启,代码如下:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

之后我们需要声明拦截器的类和拦截方法,以及配置相应的拦截规则,代码如下:

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class MyAspectJ {

   // 配置拦截类 Person
   @Pointcut(“execution( org.springframework.beans.Person.(…))”)
   public void pointCut() {
   }

   @Before(“pointCut()”)
   public void doBefore() {
      System.out.println(“执行 doBefore 方法”);
   }

   @After(“pointCut()”)
   public void doAfter() {
      System.out.println(“执行 doAfter 方法”);

然后我们只需要在 application.xml 配置中添加注解类,配置如下:

<bean class="org.springframework.advice.MyAspectJ"/>

紧接着,我们添加一个需要拦截的方法:

package org.springframework.beans;

// 需要拦截的 Bean
public class Person {
   public Person findPerson() {
      Person person = new Person(1“JDK”);
      System.out.println(“执行 findPerson 方法”);
      return person;
   }
    // 获取其他方法
}

最后,我们开启测试代码:

import org.springframework.beans.Person;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyApplication {
   public static void main(String[] args) {
      ApplicationContext context =
            new ClassPathXmlApplicationContext(“classpath*:application.xml”);
      Person person = context.getBean(“person”, Person.class);
      person.findPerson();
   }
}

以上程序的执行结果为:

执行 doBefore 方法
执行 findPerson 方法
执行 doAfter 方法

可以看出 AOP 拦截成功了。

③ 基于 XML <aop /> 标签的方式

基于 XML 的方式与基于注解的方式类似,只是无需使用注解,把相关信息配置到 application.xml 中即可,配置如下:

<!-- 拦截处理类 -->
<bean id="myPointcut" class="org.springframework.advice.MyPointcut"></bean>
<aop:config><!-- 拦截规则配置 --><aop:pointcut id="pointcutConfig"expression="execution(* org.springframework.beans.Person.*(..))"/><!-- 拦截方法配置 --><aop:aspect ref="myPointcut"><aop:before method="doBefore" pointcut-ref="pointcutConfig"/><aop:after method="doAfter" pointcut-ref="pointcutConfig"/></aop:aspect>
</aop:config>

之后,添加一个普通的类来进行拦截业务的处理,实现代码如下:

public class MyPointcut {public void doBefore() {System.out.println("执行 doBefore 方法");}public void doAfter() {System.out.println("执行 doAfter 方法");}
}

拦截的方法和测试代码与第二种注解的方式相同,这里就不在赘述。

最后执行程序,执行结果为:

执行 doBefore 方法
执行 findPerson 方法
执行 doAfter 方法

可以看出 AOP 拦截成功了。

Spring AOP 的原理其实很简单,它其实就是一个动态代理,我们在调用 getBean() 方法的时候返回的其实是代理类的实例,而这个代理类在 Spring 中使用的是 JDK Proxy 或 CgLib 实现的,它的核心代码在 DefaultAopProxyFactory#createAopProxy(...) 中,源码如下:

public class DefaultAopProxyFactory implements AopProxyFactorySerializable {
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span>&nbsp;AopProxy&nbsp;<span class="hljs-title">createAopProxy</span><span class="hljs-params">(AdvisedSupport&nbsp;config)</span>&nbsp;<span class="hljs-keyword">throws</span>&nbsp;AopConfigException&nbsp;</span>{<span class="hljs-keyword">if</span>&nbsp;(config.isOptimize()&nbsp;||&nbsp;config.isProxyTargetClass()&nbsp;||&nbsp;hasNoUserSuppliedProxyInterfaces(config))&nbsp;{Class&lt;?&gt;&nbsp;targetClass&nbsp;=&nbsp;config.getTargetClass();<span class="hljs-keyword">if</span>&nbsp;(targetClass&nbsp;==&nbsp;<span class="hljs-keyword">null</span>)&nbsp;{<span class="hljs-keyword">throw</span>&nbsp;<span class="hljs-keyword">new</span>&nbsp;AopConfigException(<span class="hljs-string">"TargetSource&nbsp;cannot&nbsp;determine&nbsp;target&nbsp;class:&nbsp;"</span>&nbsp;+<span class="hljs-string">"Either&nbsp;an&nbsp;interface&nbsp;or&nbsp;a&nbsp;target&nbsp;is&nbsp;required&nbsp;for&nbsp;proxy&nbsp;creation."</span>);}

            // 判断目标类是否为接口
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                // 是接口使用 jdk 的代理
return new JdkDynamicAopProxy(config);
}
            // 其他情况使用 CgLib 代理
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
    // 忽略其他代码
}

小结

本课时讲了 IoC 和 DI 概念,以及 IoC 的优势和 IoC 注入的三种方式:构造方法注入、Setter 注入和接口注入,最后讲了 Spring AOP 的概念与它的三种配置方式:基于 Java API 的方式、基于 Java 注解的方式和基于 XML 标签的方式。


第13讲:MyBati 使用了哪些设计模式?在源码中是如何体现的?

MyBatis 的前身是 IBatis,IBatis 是由 Internet 和 Abatis 组合而成,其目的是想当做互联网的篱笆墙,围绕着数据库提供持久化服务的一个框架,2010 年正式改名为 MyBatis。它是一款优秀的持久层框架,支持自定义 SQL、存储过程及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,还可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Ordinary Java Object,普通 Java 对象)为数据库中的记录。

关于 MyBatis 的介绍与使用,官方已经提供了比较详尽的中文参考文档,可点击这里查看,而本课时则以面试的角度出发,聊一聊不一样的知识点,它也是 MyBatis 比较热门的面试题之一,MyBatis 使用了哪些设计模式?在源码中是如何体现的?

注意:本课时使用的 MyBatis 源码为 3.5.5。

典型回答

1.工厂模式

工厂模式想必都比较熟悉,它是 Java 中最常用的设计模式之一。工厂模式就是提供一个工厂类,当有客户端需要调用的时候,只调用这个工厂类就可以得到自己想要的结果,从而无需关注某类的具体实现过程。这就好比你去餐馆吃饭,可以直接点菜,而不用考虑厨师是怎么做的。

工厂模式在 MyBatis 中的典型代表是 SqlSessionFactory。

SqlSession 是 MyBatis 中的重要 Java 接口,可以通过该接口来执行 SQL 命令、获取映射器示例和管理事务,而 SqlSessionFactory 正是用来产生 SqlSession 对象的,所以它在 MyBatis 中是比较核心的接口之一。

工厂模式应用解析:SqlSessionFactory 是一个接口类,它的子类 DefaultSqlSessionFactory 有一个 openSession(ExecutorType execType) 的方法,其中使用了工厂模式,源码如下:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx = null;try {final Environment environment = configuration.getEnvironment();final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);final Executor executor = configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}
}

从该方法我们可以看出它会 configuration.newExecutor(tx, execType) 读取对应的环境配置,而此方法的源码如下:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}if (cacheEnabled) {executor = new CachingExecutor(executor);}executor = (Executor) interceptorChain.pluginAll(executor);return executor;
}

可以看出 newExecutor() 方法为标准的工厂模式,它会根据传递 ExecutorType 值生成相应的对象然后进行返回。

2.建造者模式(Builder)

建造者模式指的是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。也就是说建造者模式是通过多个模块一步步实现了对象的构建,相同的构建过程可以创建不同的产品。

例如,组装电脑,最终的产品就是一台主机,然而不同的人对它的要求是不同的,比如设计人员需要显卡配置高的;而影片爱好者则需要硬盘足够大的(能把视频都保存起来),但对于显卡却没有太大的要求,我们的装机人员根据每个人不同的要求,组装相应电脑的过程就是建造者模式

建造者模式在 MyBatis 中的典型代表是 SqlSessionFactoryBuilder。

普通的对象都是通过 new 关键字直接创建的,但是如果创建对象需要的构造参数很多,且不能保证每个参数都是正确的或者不能一次性得到构建所需的所有参数,那么就需要将构建逻辑从对象本身抽离出来,让对象只关注功能,把构建交给构建类,这样可以简化对象的构建,也可以达到分步构建对象的目的,而 SqlSessionFactoryBuilder 的构建过程正是如此。

在 SqlSessionFactoryBuilder 中构建 SqlSessionFactory 对象的过程是这样的,首先需要通过 XMLConfigBuilder 对象读取并解析 XML 的配置文件,然后再将读取到的配置信息存入到 Configuration 类中,然后再通过 build 方法生成我们需要的 DefaultSqlSessionFactory 对象,实现源码如下(在 SqlSessionFactoryBuilder 类中):

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException("Error building SqlSession.", e);} finally {ErrorContext.instance().reset();try {inputStream.close();} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}
}
public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);
}

SqlSessionFactoryBuilder 类相当于一个建造工厂,先读取文件或者配置信息、再解析配置、然后通过反射生成对象,最后再把结果存入缓存,这样就一步步构建造出一个 SqlSessionFactory 对象。

3.单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,此模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类

单例模式也比较好理解,比如一个人一生当中只能有一个真实的身份证号,每个收费站的窗口都只能一辆车子一辆车子的经过,类似的场景都是属于单例模式

单例模式在 MyBatis 中的典型代表是 ErrorContext。

ErrorContext 是线程级别的的单例,每个线程中有一个此对象的单例,用于记录该线程的执行环境的错误信息。

ErrorContext 的实现源码如下:

public class ErrorContext {private static final String LINE_SEPARATOR = System.lineSeparator();// 每个线程存储的容器private static final ThreadLocal<ErrorContext> LOCAL = ThreadLocal.withInitial(ErrorContext::new);public static ErrorContext instance() {return LOCAL.get();}// 忽略其他
}

可以看出 ErrorContext 使用 private 修饰的 ThreadLocal 来保证每个线程拥有一个 ErrorContext 对象,在调用 instance() 方法时再从 ThreadLocal 中获取此单例对象。

4.适配器模式

适配器模式是指将一个不兼容的接口转换成另一个可以兼容的接口,这样就可以使那些不兼容的类可以一起工作。

例如,最早之前我们用的耳机都是圆形的,而现在大多数的耳机和电源都统一成了方形的 typec 接口,那之前的圆形耳机就不能使用了,只能买一个适配器把圆形接口转化成方形的,如下图所示:

面试1.png

而这个转换头就相当于程序中的适配器模式,适配器模式在 MyBatis 中的典型代表是 Log。

MyBatis 中的日志模块适配了以下多种日志类型:

  • SLF4J

  • Apache Commons Logging

  • Log4j 2

  • Log4j

  • JDK logging

首先 MyBatis 定义了一个 Log 的接口,用于统一和规范接口的行为,源码如下:

public interface Log {boolean isDebugEnabled();boolean isTraceEnabled();void error(String s, Throwable e);void error(String s);void debug(String s);void trace(String s);void warn(String s);
}

然后 MyBatis 定义了多个适配接口,例如 Log4j2 实现源码如下:

public class Log4j2Impl implements Log {private final Log log;public Log4j2Impl(String clazz) {Logger logger = LogManager.getLogger(clazz);if (logger instanceof AbstractLogger) {log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger);} else {log = new Log4j2LoggerImpl(logger);}}@Overridepublic boolean isDebugEnabled() {return log.isDebugEnabled();}@Overridepublic boolean isTraceEnabled() {return log.isTraceEnabled();}@Overridepublic void error(String s, Throwable e) {log.error(s, e);}@Overridepublic void error(String s) {log.error(s);}@Overridepublic void debug(String s) {log.debug(s);}@Overridepublic void trace(String s) {log.trace(s);}@Overridepublic void warn(String s) {log.warn(s);}
}

这样当你项目中添加了 Log4j2 时,MyBatis 就可以直接使用它打印 MyBatis 的日志信息了。Log 的所有子类如下图所示:

面试2.png

5.代理模式

代理模式指的是给某一个对象提供一个代理对象,并由代理对象控制原对象的调用。

代理模式在生活中也比较常见,比如我们常见的超市、小卖店其实都是一个个“代理”,他们的最上游是一个个生产厂家,他们这些代理负责把厂家生产出来的产品卖出去。

代理模式在 MyBatis 中的典型代表是 MapperProxyFactory。

MapperProxyFactory 的 newInstance() 方法就是生成一个具体的代理来实现功能的,源码如下:

public class MapperProxyFactory<T> {private final Class<T> mapperInterface;private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}public Class<T> getMapperInterface() {return mapperInterface;}public Map<Method, MapperMethodInvoker> getMethodCache() {return methodCache;}// 创建代理类@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}
}
6.模板方法模式

模板方法模式是最常用的设计模式之一,它是指定义一个操作算法的骨架,而将一些步骤的实现延迟到子类中去实现,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。此模式是基于继承的思想实现代码复用的。

例如,我们喝茶的一般步骤都是这样的:

  • 把热水烧开

  • 把茶叶放入壶中

  • 等待一分钟左右

  • 把茶倒入杯子中

  • 喝茶

整个过程都是固定的,唯一变的就是泡入茶叶种类的不同,比如今天喝的是绿茶,明天可能喝的是红茶,那么我们就可以把流程定义为一个模板,而把茶叶的种类延伸到子类中去实现,这就是模板方法的实现思路。

模板方法在 MyBatis 中的典型代表是 BaseExecutor。

在 MyBatis 中 BaseExecutor 实现了大部分 SQL 执行的逻辑,然后再把几个方法交给子类来实现,它的继承关系如下图所示:

面试3.png

比如 doUpdate() 就是交给子类自己去实现的,它在 BaseExecutor 中的定义如下:

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

在 SimpleExecutor 中的实现如下:

public class SimpleExecutor extends BaseExecutor {// 构造方法public SimpleExecutor(Configuration configuration, Transaction transaction) {super(configuration, transaction);}// 更新方法@Overridepublic int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);stmt = prepareStatement(handler, ms.getStatementLog());return handler.update(stmt);} finally {closeStatement(stmt);}}// 忽略其他代码...
}

可以看出 SimpleExecutor 每次使用完 Statement 对象之后,都会把它关闭掉,而 ReuseExecutor 中的实现源码如下:

public class ReuseExecutor extends BaseExecutor {private final Map<String, Statement> statementMap = new HashMap<>();public ReuseExecutor(Configuration configuration, Transaction transaction) {super(configuration, transaction);}// 更新方法@Overridepublic int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);Statement stmt = prepareStatement(handler, ms.getStatementLog());return handler.update(stmt);}// 忽略其他代码...
}

可以看出,ReuseExecutor 每次使用完 Statement 对象之后不会把它关闭掉。

7.装饰器模式

装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构,这种类型的设计模式属于结构型模式,它是作为现有类的一个包装。

装饰器模式在生活中很常见,比如装修房子,我们在不改变房子结构的同时,给房子添加了很多的点缀;比如安装了天然气报警器,增加了热水器等附加的功能都属于装饰器模式。

装饰器模式在 MyBatis 中的典型代表是 Cache。

Cache 除了有数据存储和缓存的基本功能外(由 PerpetualCache 永久缓存实现),还有其他附加的 Cache 类,比如先进先出的 FifoCache、最近最少使用的 LruCache、防止多线程并发访问的 SynchronizedCache 等众多附加功能的缓存类,Cache 所有实现子类如下图所示:

面试4.png

小结

本课时我们重点讲了 MyBatis 源码中的几个主要设计模式,即工厂模式、建造者模式、单例模式、适配器模式、代理模式、模板方法模式等,希望本课时的内容能起到抛砖引玉的作用,对你理解设计模式和 MyBatis 提供一些帮助,如果想要阅读全部的 MyBatis 源码可以访问:https://github.com/mybatis/mybatis-3 。


第14讲:SpringBoot 有哪些优点?它和 Spring 有什么区别?

Spring 和 Spring Boot 的区别很多新手容易搞混,从这道简单的面试题也可以很轻易试探出你的 Java 基础功底,如果连这个问题都答不上来的话,通常就没有什么下文了,因为这已经是用人单位对面试者的最低要求了,所以本课时我们就来看一下二者的区别,以及 Spring Boot 的特性。

我们本课时的面试题是,Spring 和 Spring Boot 有什么区别?Spring Boot 的优点有哪些?

典型回答

作为 Java 开发人员对 Spring 框架都很熟悉,Spring 为 Java 程序提供了全面的基础架构支持,包含了很多非常实用的功能,如 Spring JDBC、Spring AOP、Spring ORM、Spring Test 等,这些模块的出现,大大的缩短了应用程序的开发时间,同时提高了应用开发的效率。

Spring Boot 本质上是 Spring 框架的延伸和扩展,它的诞生是为了简化 Spring 框架初始搭建以及开发的过程,使用它可以不再依赖 Spring 应用程序中的 XML 配置,为更快、更高效的开发 Spring 提供更加有力的支持。Spring Boot 具体的特性如下。

Spring Boot 特性一:更快速的构建能力

Spring Boot 提供了更多的 Starters 用于快速构建业务框架,Starters 可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找依赖包。

例如在 Spring 中如果要创建 Web 应用程序的最小依赖项为:

<dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId><version>xxx</version>
</dependency>
<dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>xxx</version>
</dependency>

而 Spring Boot 只需要一个依赖项就可以来启动和运行 Web 应用程序,如下所示:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

当我们添加了 Starter 模块支持之后,在项目的构建期,它就会把所有其他依赖项将自动添加到项目中。

这样的例子还有很多,比如测试库,如果是 Spring 项目我们通常要添加 Spring Test、JUnit、Hamcrest 和 Mockito 库;而如果是 Spring Boot 项目的话,只需要添加 spring-boot-starter-test 即可,它会自动帮我们把其他的依赖项添加到项目中。

常见的 Starters 有以下几个:

  • spring-boot-starter-test
  • spring-boot-starter-web
  • spring-boot-starter-data-jpa
  • spring-boot-starter-thymeleaf

点击这里访问文档,查看完整的 Starters 列表。

Spring Boot 特性二:起步依赖

Spring Boot 提供了起步依赖,也就是在创建 Spring Boot 时可以直接勾选依赖模块,这样在项目初始化时就会把相关依赖直接添加到项目中,大大缩短了查询并添加依赖的时间,如下图所示:

image.png

Spring Boot 特性三:内嵌容器支持

Spring Boot 内嵌了 Tomcat、Jetty、Undertow 三种容器,其默认嵌入的容器是 Tomcat,这个在我们启动 Spring Boot 项目的时候,在控制台上就能看到,具体信息如下:

o.s.b.w.embedded.tomcat.TomcatWebServer :Tomcat started on port(s): 8080 (http) with context path ''

可以看出 Spring Boot 默认使用的是 Tomcat 容器启动的。

我们可以通过修改 pom.xml 来移除内嵌的 Tomcat 更换为其他的容器,比如更换为 Jetty 容器,配置如下:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><!-- 移处 Tomcat --><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions>
</dependency>
<!-- 移处 jetty 容器 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

当我们添加完成之后,再重新生成 pom.xml 文件,然后再启动 Spring Boot 项目容器信息就变了,如下所示:

o.e.jetty.server.AbstractConnector: Started ServerConnector@53f9009d{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
o.s.b.web.embedded.jetty.JettyWebServer

可以看出 Spring Boot 使用了我们指定的 Jetty 容器启动了。

Spring Boot 特性四:Actuator 监控

Spring Boot 自带了 Actuator 监控功能,主要用于提供对应用程序监控,以及控制的能力,比如监控应用程序的运行状况,或者内存、线程池、Http 请求统计等,同时还提供了关闭应用程序等功能。

Actuator 提供了 19 个接口,接口请求地址和代表含义如下表所示:

访问路径描述
/auditevents显示应用暴露的审计事件(比如认证进入)
/beans显示应用程序中所有 Spring Bean 的完整列表
/caches公开可用的缓存
/conditions显示在配置和自动配置类上评估的条件以及它们匹配或不匹配的原因
/configprops显示所有 @ConfigurationPropertie 的整理列表
/env获取全部环境属性
/flyway提供一份 Flyway 数据库迁移信息
/health显示应用程序运行状况信息
/httptrace显示 HTTP 跟踪信息(默认情况下,最近 100 个 HTTP 请求-响应交换)
/info获取应用程序的定制信息,这些信息由 info 开头的属性提供
/integrationgraph显示 Spring Integration 图,需要依赖于 spring-integration-core
/loggers显示和修改应用程序的配置
/liquibase显示已应用的所有 Liquibase 数据库迁移
/metrics/{name}报告指定名称的应用程序度量值
/mappings显示所有 @RequestMapping 路径的列表
/scheduledtasks显示应用程序中的计划任务
/sessions允许从 Spring Session 支持的会话存储中检索和删除用户会话,需要使用 Spring Session 基于 Servlet 的 Web 应用程序
/shutdown使应用程序正常关闭,默认禁用
/threaddump获取线程活动的快照

考点分析

很多人都知道 Spring Boot 是基于 Spring 的,使用它可以更加快速高效的构建 Spring,然而当面试官问到 Spring Boot 是如何高效构建 Spring 时,可能大部分人回答不上来了,上面讲解的 Spring Boot 四大特性基本涵盖了此问题的答案。如果面试官继续追问更深的细节的话,可能会问到关于 Spring Boot 执行的源码细节,比如 Spring Boot 的启动流程是怎么样的?

知识扩展

Spring Boot 启动源码分析

我们知道 Spring Boot 程序的入口是 SpringApplication.run(Application.class, args) 方法,那么就从 run() 方法开始分析吧,它的源码如下:

public ConfigurableApplicationContext run(String... args) {// 1.创建并启动计时监控类StopWatch stopWatch = new StopWatch();stopWatch.start();// 2.声明应用上下文对象和异常报告集合ConfigurableApplicationContext context = null;Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();// 3.设置系统属性 headless 的值this.configureHeadlessProperty();// 4.创建所有 Spring 运行监听器并发布应用启动事件SpringApplicationRunListeners listeners = this.getRunListeners(args);listeners.starting();Collection exceptionReporters;try {// 5.处理 args 参数ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 6.准备环境ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);this.configureIgnoreBeanInfo(environment);// 7.创建 Banner 的打印类Banner printedBanner = this.printBanner(environment);// 8.创建应用上下文context = this.createApplicationContext();// 9.实例化异常报告器exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);// 10.准备应用上下文this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);// 11.刷新应用上下文this.refreshContext(context);// 12.应用上下文刷新之后的事件的处理this.afterRefresh(context, applicationArguments);// 13.停止计时监控类stopWatch.stop();// 14.输出日志记录执行主类名、时间信息if (this.logStartupInfo) {(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);}// 15.发布应用上下文启动完成事件listeners.started(context);// 16.执行所有 Runner 运行器this.callRunners(context, applicationArguments);} catch (Throwable var10) {this.handleRunFailure(context, var10, exceptionReporters, listeners);throw new IllegalStateException(var10);}try {// 17.发布应用上下文就绪事件listeners.running(context);// 18.返回应用上下文对象return context;} catch (Throwable var9) {this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);throw new IllegalStateException(var9);}
}

从以上源码可以看出 Spring Boot 的启动总共分为以下 18 个步骤。

Spring Boot 的启动流程

1.创建并启动计时监控类

此计时器是为了监控并记录 Spring Boot 应用启动的时间的,它会记录当前任务的名称,然后开启计时器。

2.声明应用上下文对象和异常报告集合

此过程声明了应用上下文对象和一个异常报告的 ArrayList 集合。

3.设置系统属性 headless 的值

设置 Java.awt.headless = true,其中 awt(Abstract Window Toolkit)的含义是抽象窗口工具集。设置为 true 表示运行一个 headless 服务器,可以用它来作一些简单的图像处理。

4.创建所有 Spring 运行监听器并发布应用启动事件

此过程用于获取配置的监听器名称并实例化所有的类。

5.初始化默认应用的参数类

也就是说声明并创建一个应用参数对象。

6.准备环境

创建配置并且绑定环境(通过 property sources 和 profiles 等配置文件)。

7.创建 Banner 的打印类

Spring Boot 启动时会打印 Banner 图片,如下图所示:

image (1).png

此 banner 信息是在 SpringBootBanner 类中定义的,我们可以通过实现 Banner 接口来自定义 banner 信息,然后通过代码 setBanner() 方法设置 Spring Boot 项目使用自己自定义 Banner 信息,或者是在 resources 下添加一个 banner.txt,把 banner 信息添加到此文件中,就可以实现自定义 banner 的功能了。

8.创建应用上下文

根据不同的应用类型来创建不同的 ApplicationContext 上下文对象。

9.实例化异常报告器

它调用的是 getSpringFactoriesInstances() 方法来获取配置异常类的名称,并实例化所有的异常处理类。

10.准备应用上下文

此方法的主要作用是把上面已经创建好的对象,传递给 prepareContext 来准备上下文,例如将环境变量 environment 对象绑定到上下文中、配置 bean 生成器以及资源加载器、记录启动日志等操作。

11.刷新应用上下文

此方法用于解析配置文件,加载 bean 对象,并且启动内置的 web 容器等操作。

12.应用上下文刷新之后的事件处理

这个方法的源码是空的,可以做一些自定义的后置处理操作。

13.停止计时监控类

停止此过程第一步中的程序计时器,并统计任务的执行信息。

14.输出日志信息

把相关的记录信息,如类名、时间等信息进行控制台输出。

15.发布应用上下文启动完成事件

触发所有 SpringApplicationRunListener 监听器的 started 事件方法。

16.执行所有 Runner 运行器

执行所有的 ApplicationRunner 和 CommandLineRunner 运行器。

17.发布应用上下文就绪事件

触发所有的 SpringApplicationRunListener 监听器的 running 事件。

18.返回应用上下文对象

到此为止 Spring Boot 的启动程序就结束了,我们就可以正常来使用 Spring Boot 框架了。

小结

本课时首先讲了 Spring 和 Spring Boot 的区别,Spring Boot 本质上是 Spring 的延伸,它是基于 Spring 的,它为快速构建和开发 Spring 提供了有力的支撑;接着介绍了 Spring Boot 的四大特性:更快速的构建能力、起步依赖、内嵌容器支持、Actuator 监控支持等,最后 还介绍了 Spring Boot 启动的 18 个步骤。


这篇关于java大厂技术面试第四课 spring/mabatis/springboot相关的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/271973

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

sqlite3 相关知识

WAL 模式 VS 回滚模式 特性WAL 模式回滚模式(Rollback Journal)定义使用写前日志来记录变更。使用回滚日志来记录事务的所有修改。特点更高的并发性和性能;支持多读者和单写者。支持安全的事务回滚,但并发性较低。性能写入性能更好,尤其是读多写少的场景。写操作会造成较大的性能开销,尤其是在事务开始时。写入流程数据首先写入 WAL 文件,然后才从 WAL 刷新到主数据库。数据在开始