Loading...
墨滴

jrh

2021/10/27  阅读:37  主题:橙心

Java 面试八股文之框架篇(二)

前言

这是系列文章【 Java 面试八股文】框架篇的第二期。

【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,带你理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过下方给出的链接进行查看~

按照惯例——首先要做几点说明:

  1. 【 Java 面试八股文】中的面试题来源于社区论坛,书籍等资源;感谢使我读到这些宝贵面经的作者们。
  2. 对于【 Java 面试八股文】中的每个问题,我都会尽可能地写出我自己认为的“完美解答”。但是毕竟我的身份不是一个“真理持有者”,只是一个秉承着开源分享精神的 “knowledge transmitter” & 菜鸡,所以,如果这些答案出现了错误,可以留言写出你认为更好的解答,并指正我。非常感谢您的分享。
  3. 知识在于“融释贯通”,而非“死记硬背”;现在市面上固然有很多类似于“Java 面试必考 300 题” 这类的文章,但是普遍上都是糟粕,仅讲述其果,而不追其源;希望我的【 Java 面试八股文】可以让你知其然,且知其所以然~

那么,废话不多说,我们正式开始吧!

往期文章

框架篇(二)

1、说一下 Spring Bean 的生命周期?


Spring Bean 的生命周期很复杂,不过可以总体概括为四个阶段:

  1. 实例化(Instantiation)
  2. 属性赋值(populate)
  3. 初始化(Initialization)
  4. 销毁(Destruction)

在我的上一篇文章中,第二个问题:【 Spring IoC 如何解决循环依赖 】已经简要介绍过实例化,属性赋值以及初始化了,它们都发生在 AbstractAutowireCapableBeanFactory#doCreateBean 方法中。从宏观的角度来看,以上四个阶段构成了一个完整的 Bean 的生命周期;不过落实到微观的代码细节,我们会发现在这些阶段的前后,有很多 Spring 自身以及对外暴露的扩展点。接下来,我们就先介绍下几个重要的扩展点的功能,然后再完整地将一个 Spring Bean 的生命周期串联起来。

1. BeanFactoryPostProcessor

当一个 Spring IoC 容器启动时,会扫描在 XML 文件中定义的,或注解标注的这些 Bean 的信息,然后将这些 Bean 的信息封装为 BeanDefinition 对象作为元数据加载起来。

Spring 允许 BeanFactoryPostProcessor 的 postProcessBeanFactory 方法在 IoC 容器加载了这些 BeanDefinition 之后,去获元数据,修改元数据。我们可以自定义一个 BeanFactoryPostProcessor 接口的实现类:

MyBeanFactoryPostProcessor

import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition bd = beanFactory.getBeanDefinition("myTestBean");
        MutablePropertyValues propertyValues = bd.getPropertyValues();

        if (propertyValues.contains("field"))
            propertyValues.addPropertyValue("field""update");
        bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
    }
}

MyTestBean

public class MyTestBean {

    private String field;

    public String getField() {
        return field;
    }

    public void setField(String field) {
        this.field = field;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("MyTestBean{");
        sb.append("field='").append(field).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="myBeanFactoryPostProcessor" class="com.github.test.MyBeanFactoryPostProcessor" />

    <bean id="myTestBean" class="com.github.test1.MyTestBean" scope="singleton">
        <property name="field" value="test"></property>
    </bean>
</beans>

在 beans.xml 文件中,我定义了 myTestBean 这个 Bean,指定了它的 scope 为 singleton,并且其属性 “field” 的值为 “test”。

而在 MyBeanFactoryPostProcessor 中,我修改了这个 Bean 的原始信息,将它的 scope 设置为 prototype,并将属性 “field” 的值设置为 “update”。

2. BeanPostProcessor

BeanPostProcessor 叫做后置处理器,这个接口中定义了两个方法:

  • postProcessBeforeInitialization
  • postProcessAfterInitialization

通过这两个方法名,我们可以直观地看出,后置处理器的功能是在一个 Bean 调用初始化方法的前后去做一些逻辑处理。

Spring 当中有一些内置的 BeanPostProcessor,同时它也支持我们自定义的 BeanPostProcessor。大家可以想一下,这些 BeanPostProcessor 也会注册为一个 Bean,所以 Spring 必然要将这些 BeanPostProcessor 于我们的业务 Bean 之前初始化完成。

我们来看一下 AbstractApplicationContext#refresh 的代码:

@Override
public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      // Prepare this context for refreshing.
      prepareRefresh();

      // Tell the subclass to refresh the internal bean factory.
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      // Prepare the bean factory for use in this context.
      prepareBeanFactory(beanFactory);

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

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

         // Register bean processors that intercept bean creation.
         registerBeanPostProcessors(beanFactory);

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

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

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

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

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

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

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }

         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         // Reset 'active' flag.
         cancelRefresh(ex);

         // Propagate exception to caller.
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

其中,registerBeanPostProcessors 就是将所有的 BeanPostProcessor 进行注册,它是 BeanPostProcessor 初始化的调用点;而 finishBeanFactoryInitialization 则是所有单例且非懒加载的 Bean 的实例化调用点。通过代码的执行顺序,我们就知道,所有 BeanPostProcessor 的初始化要先于业务 Bean 的实例化。

3. InstantiationAwareBeanPostProcessor

InstantiationAwareBeanPostProcessor 继承了 BeanPostProcessor。它额外扩展了 3 个方法:

  1. postProcessBeforeInstantiation
  2. postProcessAfterInstantiation
  3. postProcessPropertyValues

postProcessBeforeInstantiation 会在一个 Bean 实例化之前进行回调,该方法返回类型为 Object,如果需要为这个 Bean 提前生成一个代理,那么该方法便会返回一个代理实例;postProcessAfterInstantiation 则调用于 Bean 实例化之后,属性赋值之前,它的返回值为布尔类型,如果返回 fasle,就是告诉 Spring 该 Bean 不需要进行属性填充,在 postProcessAfterInstantiation 中会将 postProcessBeforeInstantiation 返回的代理替换原本的 Bean;postProcessPropertyValues 方法的作用是对 Bean 的属性值进行修改,它会在属性赋值(AbstractAutowireCapableBeanFactory#populateBean)之前调用,来修改原本该设置的属性值,如果 postProcessAfterInstantiation 返回 false,则该方法不会执行。

我们看到,InstantiationAwareBeanPostProcessor 作用于 Bean 的实例化前后,而 BeanPostProcessor 则作用于 Bean 的初始化前后,我们可以通过一张图来加深下理解:

4. XXXAware

Aware 类型的接口可以使我们获取到 Spring 容器中的一些资源。当我们的 Bean 实现了对应的 Aware 接口,Spring 容器就会根据 Aware 接口为 Bean 注入相应的属性。

常用的 Aware 接口有:

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware

这些 Aware 接口的回调时机为属性赋值后,初始化之前。

5. InitializingBean 与 DisposableBean

InitializingBean 与 DisposableBean 为两个生命周期接口,对应了初始化与销毁阶段。

我们的 Bean 如果实现了 InitializingBean 接口,那么在方法 afterPropertiesSet 中就可以指定属性赋值之后, Bean 初始化时执行的逻辑,除此之外,我们也可以通过设置 Bean 的 init-method 来指定初始化 Bean 时调用的方法;我们的 Bean 如果实现了 DisposableBean 接口,那么在方法 destroy 中就可以编写销毁 Bean 时执行的逻辑,同理,我们也可以通过设置 destroy-method 指定 Bean 销毁时调用的方法。

在 Spring 中,关闭容器对应的钩子函数为:ConfigurableApplicationContext#close,该方法的实现便是通过循环取所有实现了 DisposableBean 接口的 Bean,然后再调用其 destroy 方法。

接下来,我们就使用一个示例程序,将整个 Spring Bean 的生命周期串联起来。

TestBean

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;

public class TestBean implements BeanFactoryAwareBeanNameAwareBeanClassLoaderAware,InitializingBeanDisposableBean {

    private String field;

    public String getField() {
        return field;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("TestBean{");
        sb.append("field='").append(field).append('\'');
        sb.append('}');
        return sb.toString();
    }

    public void setField(String value){
        System.out.println("------populate------");
        this.field = value;
    }

    public TestBean(){
        System.out.println("------Instantiation------");
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("call BeanNameAware#setBeanName");
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        System.out.println("call BeanClassLoaderAware#setBeanClassLoader");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("call BeanFactoryAware#setBeanFactory");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("call DisposableBean#destroy");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("call InitializingBean#afterPropertiesSet ");
    }

    public void myInit(){
        System.out.println("call myInit");
    }

    public void myDestroy(){
        System.out.println("call myDestroy");
    }

}

MyBeanFactoryPostProcessor

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    public MyBeanFactoryPostProcessor(){
        super();
        System.out.println("Initialize BeanFactoryPostProcessor");
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("call BeanFactoryPostProcessor#postProcessBeanFactory");
    }
}

MyBeanPostProcessor

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class MyBeanPostProcessor implements BeanPostProcessor {

    public MyBeanPostProcessor(){
        super();
        System.out.println("Initialize BeanPostProcessor");
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("call BeanPostProcessor#postProcessBeforeInitialization");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("call BeanPostProcessor#postProcessAfterInitialization");
        return bean;
    }
}

MyInstantiationAwareBeanPostProcessor

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;

public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {

    @Override
    public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
        System.out.println("InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation");
        return null;
    }

    @Override
    public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
        System.out.println("InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation");
        return true;
    }
}

BeanLifeCycleTest

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

public class BeanLifeCycleTest {

    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("/beans.xml");
        // getBean
        TestBean testBean = ac.getBean("testBean", TestBean.class);
        System.out.println(testBean);
        System.out.println("----- Destruction -----");
        ((ClassPathXmlApplicationContext) ac).registerShutdownHook();
    }
}

beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="beanPostProcessor" class="com.github.test.MyBeanPostProcessor">
    </bean>

    <bean id="instantiationAwareBeanPostProcessor" class="com.github.test.MyInstantiationAwareBeanPostProcessor">
    </bean>

    <bean id="beanFactoryPostProcessor" class="com.github.test.MyBeanFactoryPostProcessor">
    </bean>

    <bean id="testBean" class="com.github.test.TestBean" init-method="myInit"
          destroy-method="myDestroy" scope="singleton">

        <property name="field" value="test"></property>
    </bean>

</beans>

该程序的运行结果如下(Spring 版本号:5.2.9.RELEASE):

DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@544a2ea6
DEBUG org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loaded 4 bean definitions from class path resource [beans.xml]
DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'beanFactoryPostProcessor'
Initialize BeanFactoryPostProcessor
call BeanFactoryPostProcessor#postProcessBeanFactory
DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'beanPostProcessor'
Initialize BeanPostProcessor
DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'instantiationAwareBeanPostProcessor'
DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'testBean'
InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation
------Instantiation------
InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation
------populate------
call BeanNameAware#setBeanName
call BeanClassLoaderAware#setBeanClassLoader
call BeanFactoryAware#setBeanFactory
call BeanPostProcessor#postProcessBeforeInitialization
call InitializingBean#afterPropertiesSet 
call myInit
call BeanPostProcessor#postProcessAfterInitialization
TestBean{field='test'}
----- Destruction -----
DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@544a2ea6, started on Tue Oct 26 22:55:29 CST 2021
call DisposableBean#destroy
call myDestroy
总结

对于 Spring Bean 的生命周期,我们只需要记住总体的四个阶段即可:

  1. 实例化(Instantiation)
  2. 属性赋值(populate)
  3. 初始化(Initialization)
  4. 销毁(Destruction)

BeanFactoryPostProcessor 可以在 Bean 实例化之前,就修改 BeanDefinition 元数据;BeanPostProcessor 主要作用于 Bean 初始化前后;InstantiationAwareBeanPostProcessor 主要作用于 Bean 实例化的前后。

我们还可以自定义 Aware 接口,来获取设置 Spring 容器的资源,这些 Aware 接口的回调时机为属性赋值后,初始化之前。

除此之外,我们还需要记住两个生命周期接口:InitializingBean 与 DisposableBean。

它们的执行顺序可以参考上面的程序输出,背下来这个顺序并不重要,重要的是在于理解。

面试官可能会问,BeanFactoryPostProcessor 与 BeanPostProcessor 接口有什么区别?InstantiationAwareBeanPostProcessor 与 BeanPostProcessor 接口有什么区别?等等,只要你理解了上面的内容,回答这些问题并不难~

2、你用过哪些 Spring 的注解?它们的功能是什么?


接下来,我给大家列举一些工作中常用的 Spring 注解(不包括 web 模块下的注解)。

这些注解大概可以分为三类:

  1. Java Config 相关注解
  2. 定义相关注解
  3. 注入相关注解

首先是常用的 Java Config 相关注解:

  1. @Configuration
  2. @ComponentScan
  3. @Bean

@Configuration 用于定义配置类,可替换 XML 配置文件,被注解的类内部包含有一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类进行扫描,并用于构建 Bean 定义,初始化 Spring 容器。

@ComponentScan 告诉 Spring 哪个 package 里面用注解标识的类会被 Spring 自动扫描并且装入容器。

@Bean 是 Spring 当中最基础的注解。@Bean 注解用于方法上,该方法会返回一个 Bean 对象,然后将这个 Bean 对象交给 Spring 管理。产生这个 Bean 对象的方法 Spring 只会调用一次,随后 Spring 会将这个 Bean 对象放在自己的 IoC 容器中。

常用的定义相关的注解有:

  1. @Component
  2. @Repository
  3. @Service
  4. @Controller

@Component 是一个泛化的概念,仅仅表示一个组件(Bean),可以作用在任何层次。

@Repository 注解用于将数据访问层(DAO 层)的类标识为 Spring Bean;@Service 一般作用在业务层;而 @Controller 一般作用在控制层。后三种注解本身和 @Component 注解的功能是完全相同的,不过它们以更好的方式指定了意图,区分了层次。

面试题:@Component@Bean 注解有什么区别?

首先,二者的目的是一样的,都是注册 Bean 到 Spring 容器中。

@Component 注解作用于类,表明一个类会作为组件类,并告知 Spring 要为这个类创建 Bean,它是通过类路径扫描来自动侦测以及自动注册到 Spring 容器中的。

@Bean 注解则作用于方法,返回值是一个 Bean 对象,通常是我们在标有该注解的方法中定义产生这个 Bean 的逻辑。@Bean 注解告诉 Spring 这个方法将会返回一个对象,这个对象要注册为 Spring 应用上下文中的 Bean。

@Bean@Component 的“自定义性”更强,比如我们如果想将第三方的类库变成组件,而我们又没有源代码,也就没有办法使用 @Component ,这个时候就可以使用 @Bean 注解。

常用的注入相关注解有:

  1. @Autowired
  2. @Qualifier
  3. @Resource
  4. @Value
  5. @ConfigurationProperties

@Autowired 注解是 Spring 提供的一种注入 Bean 的方法;该注解可以对成员变量,方法和构造器进行标注,实现快速的自动装配。

@Qualifier 注解限定了哪一个 Bean 应该被自动注入,该注解有助于消除歧义 Bean 的自动注入。

来举一个例子🌰:

MyService

public interface MyService{
    // ...
}

MyServiceImpl1

@Service
public class MyServiceImpl1 implements MyService {
 // ...
}

MyServiceImpl2

@Service
public class MyServiceImpl2 implements MyService {
 // ...
}

当我们要在 Controller 层中将 MyService 服务注入进来的时候,因为其有两个实现类,所以,我们应该使用 @Qualifier 注解指定哪个实现类应该被注入:

@Controller
public class MyController {
    @Autowired
    @Qualifier("MyServiceImpl1"// 指定 MyServiceImpl1 应该被注入
    private MyService myService;
    
    // ...
}

@Resource 注解和 @Autowired 注解的功能实际上差不多,二者的功能都是注入 Bean,接下来我们就看一下这两个注解具体有什么相同与不同之处:

二者的相同点为:@Autowired@Resource 注解都可以用来装配 Bean;且,二者都可以写在字段上,也可以写在 setter 方法上。

二者的不同点为:

@Autowired 注解是由 Spring 框架提供,而 @Resource 注解则是 javax.annotation.Resource 提供,也就是 J2EE 的标准,但是 Spring 支持该注解注入 Bean。

@Autowired 注解默认按照 Bean 类型 (byType) 装配依赖的 Bean,如果想按照 Bean 名称(byName) 来装配,可以结合 @Qualifier 注解一起使用。默认的情况下,它要求依赖的对象必须存在,默认是不允许 null 值的,如果想要设置允许 null 值,可以设置它的 required 属性为 false。最常见的问题就是 IDEA 中, Mybatis 的 mapper 接口使用 @Autowired 引入会出现错误提示(但不会影响正常使用):

这种情况下,我们可以使用 @Autowired(required = false)@Resource 注解即可解决问题。

@Resource 注解默认按照 Bean 名字 (byName) 来装配依赖的 Bean。@Resource 有两个属性比较重要,分别是 name 与 type 。 @Resource 注入 Bean 的原则如下:

  • 如果没有指定 name 与 type 属性,即,默认情况按照 byName 进行装配
  • 如果同时指定了 name 与 type 属性,则从 Spring 上下文中找到名称与类型都唯一匹配的 Bean 进行装配,找不到则抛出异常
  • 如果仅指定了 name 属性,则从 Spring 上下文中查找名称匹配的 Bean 进行装配,找不到则抛出异常
  • 如果仅指定了 type 属性,则从 Spring 上下文中查找类型匹配的,唯一的 Bean 进行装配,找不到或找到多个都会抛出异常

@Value 的作用是从配置文件中读取值。

@ConfigurationProperties 是 Spring Boot 中的注解。在 Spring Boot 中,当我们想要获取到配置文件的数据时,除了可以用 Spring 的 @Value 注解外,还可以使用 Spring Boot 提供的更方便的 @ConfigurationProperties 注解,该注解支持批量注入配置文件的属性,松散绑定等,比 @Value 更加强大。

3、Spring 事务相关


面试题一:Spring 支持的事务管理类型有哪些?

首先,要说明的是 Spring 事务的本质就是数据库对事务的支持。Spring 并非提供了完整的事务操作 API,而是提供了多种事务管理器,将事务职责托管给了 JDBC、Hibernate、JTA 等持久化平台来实现。

Spring 支持两种类型的事务管理:

  1. 编程式事务
  2. 声明式事务

编程式事务顾名思义,需要我们通过编程的方式来管理事务,这种方式的优点在于灵活,缺点为难以维护,其主要是使用事务管理器 TransactionTemplate 来实现,如示例程序:

schema.sql

CREATE TABLE FOO (
  ID INT IDENTITY ,
  BAR VARCHAR(64)
);

ProgrammaticTransactionDemoApplication

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

@SpringBootApplication
@Slf4j
public class ProgrammaticTransactionDemoApplication implements CommandLineRunner {

    @Autowired
    private TransactionTemplate transactionTemplate;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public static void main(String[] args) {
        SpringApplication.run(ProgrammaticTransactionDemoApplication.classargs);
    }

    @Override
    public void run(String... args) throws Exception {
        log.info("COUNT BEFORE TRANSACTION:{}", getCount());
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                jdbcTemplate.execute("INSERT INTO FOO(ID,BAR) VALUES(1,'aaa')");
                log.info("COUNT IN TRANSACTION:{}", getCount());
                // 回滚
                transactionStatus.setRollbackOnly();
            }
        });
        log.info("COUNT AFTER TRANSACTION:{}", getCount());
    }

    private long getCount() {
        return (long) jdbcTemplate.queryForList("SELECT COUNT(*) AS CNT FROM FOO")
                .get(0).get("CNT");
    }
}

我们向空表中插入了一条数据,然后使用编程式事务执行了回滚操作。该程序运行后,输出结果如下:

COUNT BEFORE TRANSACTION:0
COUNT IN TRANSACTION:1
COUNT AFTER TRANSACTION:0

那什么是声明式事务呢?

Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,我们只需在配置文件中做相关的事务规则声明(或通过等价的基于注解的方式),便可以将事务规则应用到业务逻辑中。因为事务管理本身就是一个典型的横切逻辑,正是 AOP 的用武之地。Spring 开发团队也意识到了这一点,为声明式事务提供了简单而强大的支持。

声明式事务开启的方式是通过 @EnableTranscationManagement 注解,该注解有以下几个重要的属性:

  1. proxyTargetClass
  2. mode

proxyTargetClass 属性值为 true 时,对于目标对象是否实现了接口都会使用 CGLIB 代理机制;当 proxyTargetClass 为 false 时,目标对象如果实现了接口则会使用 JDK 动态代理机制,否则当代理对象没有接口只有实现类时,则还是会使用 CGLIB 代理。

mode 的属性值可以设置为 mode = AdviceMode.PROXYmode = AdviceMode.ASPECTJ。前者为底层实现使用 JDK 动态代理的方式,后者则是使用 AspectJ。

我们来看一个示例程序:

schema.sql

CREATE TABLE FOO(
  ID INT IDENTITY,
  BAR VARCHAR(64)
);

RollbackException

public class RollbackException extends Exception {
}

FooService

public interface FooService {

    void insertRecord();

    void insertThenRollback() throws RollbackException;

    void invokeInsertThenRollback() throws RollbackException;
    
}

FooServiceImpl

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class FooServiceImpl implements FooService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private FooService fooService;

    @Override
    public void insertRecord() {
        jdbcTemplate.execute("INSERT INTO FOO(BAR) VALUES('AAA')");
    }

    @Override
    @Transactional(rollbackFor = RollbackException.class)
    public void insertThenRollback() throws RollbackException 
{
        jdbcTemplate.execute("INSERT INTO FOO(BAR) VALUES('BBB')");
        throw new RollbackException();
    }

    @Override
    public void invokeInsertThenRollback() throws RollbackException {
        insertThenRollback();
    }
}

DeclarativeTransactionDemoApplication

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.EnableTransactionManagement;


@SpringBootApplication
@EnableTransactionManagement(mode = AdviceMode.PROXY)
@Slf4j
public class DeclarativeTransactionDemoApplication implements CommandLineRunner {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private FooService fooService;

    public static void main(String[] args) {
        SpringApplication.run(DeclarativeTransactionDemoApplication.classargs);
    }

    private void queryRecordsCount() {
        log.info("COUNT : {}", jdbcTemplate.queryForObject("SELECT COUNT(*) FROM FOO ", Long.class));
    }

    @Override
    public void run(String... args) throws Exception {
        // insertRecord
        fooService.insertRecord();
        queryRecordsCount();

        // insertThenRollback
        try {
            fooService.insertThenRollback();
        } catch (RollbackException e) {
            queryRecordsCount();
        }

        // invokeInsertThenRollback
        try {
            fooService.invokeInsertThenRollback();
        } catch (RollbackException e) {
            queryRecordsCount();
        }
    }
}

程序执行后,输出结果为:

COUNT : 1
COUNT : 1
COUNT : 2

我们发现,invokeInsertThenRollback 方法并没有发生回滚。

其原因在于,声明式事务是通过 AOP 动态代理实现的,这样会产生一个代理类来做事务管理,而目标类(service)本身是不能感知到代理类的存在的。

对于加了 @Transactional 注解的方法来说,在调用代理类的方法时,会先通过拦截器 TransactionInterceptor 开启事务,然后再调用目标类的方法,最后调用结束后,TransactionInterceptor 会提交或回滚事务。如果我们用 invokeInsertThenRollback 方法去调用使用了 @Transactional 注解标识的 insertThenRollback 方法时,这里面实际上是隐式地通过 this 调用,此时的类是未被增强的类,所以也就不存在执行事务的代理,也就没有发生回滚。我们只有真正地用到被代理的类,才会起作用。

所以,我们可以将 FooServiceImpl 的 invokeInsertThenRollback 方法修正为:

@Override
public void invokeInsertThenRollback() throws RollbackException {
    fooService.insertThenRollback();
}

再次运行程序,执行结果为:

COUNT : 1
COUNT : 1
COUNT : 1

此时的 invokeInsertThenRollback 也起了作用,发生了回滚。

面试题二:Spring 事务的传播行为有哪些?

Spring 事务的传播行为指定了当有多个事务存在时,Spring 该如何处理这些事务的行为,我们可以在 @Transactional 的属性 propagation 中进行设置,共有七个值:

Spring 默认的事务传播级别为 PROPAGATION_REQUIRED,也就是当前如果有事务就使用当前的事务,没有就开启一个新的事务执行,这个级别可以满足大多数的业务场景。

对于 PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED 这三种事务传播级别可能初学者并不是很好理解,接下来我们就通过一个示例来进行说明:

假设我们定义了一个 seviceA.methodA() 方法,该方法上使用了 @Transactional(propagation = Propagation.REQUIRED) 注解,设置事务传播级别为 REQUIRED;同时有一个 serviceB.methodB() 方法,methodA 中调用了 methodB 方法,当我们分别对 methodB 方法设置事务传播级别为 PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED 时,在不同的异常状态下,A 与 B 发生回滚与提交情况如下表所示:

大家可以结合表格,理解这些传播行为的不同。

面试题三:Spring 事务的隔离级别有哪些?

在数据库篇,我已经介绍过脏读,不可重复读,幻读的概念,并且也十分详细地介绍了 MySQL 事务中的隔离级别。而 Spring 对事务的支持实际上就是数据库对事务的支持。

在 Spring 中,我们可以在 @Transactional 注解的 isolation 属性里设置以下四个值:

  1. ISOLATION_READ_UNCOMMITTED
  2. ISOLATION_READ_COMMITTED
  3. ISOLATION_REPEATABLE_READ
  4. ISOLATION_SERIALIZABLE

它们分别对应 RU,RC,RR 以及 Serializable。大家可以看下面的那张图好好回顾复习一下,这里我就不赘述重复的内容了。

4、AOP 相关


面试题一:什么是 AOP ?

AOP(Aspect Oriented Programming)即面向切面编程。它和 OOP 一样都只是一种编程范式。

我们拿日志模块来举例。当我们想在业务代码里实现一个打印日志的功能,你就会发现,如果使用面向对象设计,那就需要在每一个类的每一个方法中都加入日志的内容,即便它们可能是重复的逻辑,我们也无法简化代码。最重要的是,日志功能本来就和主业务是无关的,在软件设计的思想中,我们应该尽可能地将无关的逻辑剥离。

而 AOP 思想就可以解决这个问题,AOP 可以动态地将代码切入到一个类的指定方法,指定位置上。将我们需要实现的逻辑抽取到一个切片中,然后等到需要的时候再切入到目标中去,从而改变其原有的行为。

大家可以看个图理解一下:

AOP 中有如下几个术语:

  • Aspect(切面)
  • Joinpoint(连接点)
  • Advice(通知)
  • Target(目标对象)
  • Pointcut(切点)
  • Weaving(织入)

Aspect 为切面,切面是通知和切点的结合。

Joinpoint 为连接点,是应用执行过程中,能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,甚至是修改一个字段时(Spring 只支持方法级别的连接点)。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

Advice 为通知,是切面功能的实现,它会通知程序新的行为。Spring AOP 共有五种类型的通知:

  1. 前置通知(Before):在连接点之前执行
  2. 正常返回通知(After-returning):在连接点执行成功后调用通知,如果连接点抛出异常,则不会执行通知
  3. 异常通知(After-throwing):在连接点抛异常时,调用通知
  4. 返回通知(After):无论连接点是正确执行还是抛异常都会调用通知
  5. 环绕通知(Around):在连接点前后都可以执行的通知

Target 为目标对象,即被代理的对象,或被通知的对象。

Pointcut 为切点,切点其实就是筛选出的连接点。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或者是正则表达式指定这些切点。

Weaving 即织入,织入就是将切面应用到目标对象并创建新的代理对象的过程。共有三种织入切面的方式:

  1. 编译时织入
  2. 装载时织入
  3. 运行时织入

编译时织入是指在代码编译阶段,将切面代码融合进来生成完整功能的 Java 字节码,需要使用特殊的编译器;类加载时织入就是在 Java 字节码加载阶段,将切面的字节码融合进来,这种方式需要特殊的类加载器;而运行时织入就是在程序运行时,使用动态代理的方式来实现增强功能,而 Spring AOP 就是采用的这种方式。

我们再来看一张图加深一下对这些概念的理解:

面试题二:JDK 动态代理和 CGLIB 动态代理的区别 ?

首先,我们先来看一下什么是代理模式?

代理模式是指 “为对象提供一种代理以控制对这个对象的访问”。

简单来说就是:之前 A 自己做一件事儿,在使用代理之后,A 不直接去做了,而是由 A 的代理 B 来做(参考性地理解一下中介这个概念)。

我们来看一下代理模式的关系示意图:

代理模式中的角色:

  • Subject
  • RealSubject
  • Proxy

Subject 为目标对象和代理对象共同实现的接口;RealSubject 为被代理的角色;Proxy 则是代理类,代理对象将持有目标对象的引用,从而可以在任何时候操作目标对象。

关于静态代理,JDK 动态代理,CGLIB 动态代理的示例程序可以参考我的代码,代码仓地址为:

链接:https://github.com/jinrunheng/proxy-pattern-demo

那么 CGLIB 动态代理和 JDK 动态代理二者有什么区别呢?

  1. JDK 动态代理只能对实现了接口的类生成代理,而不能针对没有接口的目标类生成代理。这里面往往面试官会接着问,为什么 JDK 动态代理是基于接口实现的?其原因是:Java 是单继承的,我们生成的动态代理类在内部已经继承了 Proxy 类,就不能再继承其他的类了,所以只能靠实现被代理类的接口这种形式来实现代理。
  2. CGLIB 是针对类实现的代理,主要是对目标类生成一个子类,并覆盖其方法实现字节码增强,但是因为采用的是继承,所以该类或想要实现增强的方法如果声明了 final 关键字则无法成功。
  3. CGLIB 底层采用 ASM 字节码生成框架,使用字节码生成代理类,在 JDK6 之前效率要高于 JDK 动态代理,不过随着 JDK 版本不断升级与改善,目前 JDK 代理的效率得到了大幅度提升,从 JDK8 之后,其效率是要略高于 CGLIB 的
  4. Spring AOP 提供了 JDK 动态代理以及 CGLIB 动态代理的支持,当 Bean 实现了接口时,Spring 会使用 JDK 动态代理,当 Bean 没有实现接口时,Spring 使用的是 CGLIB 的实现

总结

关于 Spring 框架的部分,暂时就写这么多内容了,如果我发现了比较好的知识点或面试题,还会补充的哦~

不过框架篇还没有结束,我们会继续深入 Spring Boot,Spring Cloud 的相关面试题,敬请期待~

好啦,至此为止,这篇文章就到这里了,感谢您的阅读与支持~~欢迎大家关注我的公众号,在这里希望你可以收获更多的知识,我们下一期再见!

jrh

2021/10/27  阅读:37  主题:橙心

作者介绍

jrh