框架篇,现在企业项目开发几乎没有不使用框架的,所以框架相关的知识在实际开发过程中一定是非常重要的,而Java框架生态中最著名的就是Spring系列的框架了,所以本章主要讲解Spring、Spring MVC、Spring boot的相关知识,另外还会对目前主流的持久层框架mybatis进行讲解。

Spring 的单例 bean是线程安全的吗

首先需要明确的是,绝大多数bean都是单例的,只有我们在类声明上显示添加@Scope("prototype")注解,该bean才会变成多例bean,如果不添加该注解或者使用@Scope("singleton"),那么默认该bean就是单例的

spring 的单例bean是否线程安全,取决于其内部是否包含可变的成员变量。

  • 如果是bean中包含可变的成员变量,即bean是有状态的,那么是线程不安全的,存在并发修改问题,可以通过加锁或多例bean解决。

  • 如果bean中不包含可变的成员变量,即这个bean是无状态的,那么则是线程安全的。

Spring AOP原理

AOP,全称面向切面编程,用于将那些与业务无关,但却对多个公共对象产生影响的公共逻辑和行为,抽取并封装为一个可重用的模块,这个模块被称为切面(Aspect),主要作用是减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

AOP常见的使用场景:

  • 记录操作日志:日志记录并不是某个模块特有的,很多业务都需要记录日志,AOP使得我们不需要在每个方法中都编写日志记录相关代码,而是定义一个切面,将日志记录逻辑封装到切面中即可。

  • 缓存处理:缓存也是很多个service中都需要使用的,可以将缓存处理的逻辑也封装到切面中,降低耦合

  • Spring 内置的事务处理。

Spring中的事务是如何实现的

有时候我们需要保证一段代码的执行具有原子性,即将多行代码封装在一个事务中,Spring支持编程式事务和声明式事务两种方式

  • 编程式事务控制:需要使用TransactionTemplate来进行实现,对业务代码有侵入性,一般不在项目中使用。

  • 声明式事务控制:声明式事务通过Spring AOP实现。其本质是通过AOP,对方法进行拦截,在目标方法开始前开启事务,在方法执行结束后根据执行情况选择提交或回滚事务。具体方法是在需要保证原子性的方法上添加@Transactional注解,即可使该方法原子性地执行。

Spring中事务失效的场景

在某些场景下编码不当可能会导致Spring的事务失效,而事务一旦失效,意味着数据的安全性就没有保证了,所以我们需要了解spring中常见的事务失效的场景。

@Transactional的原理是通过Spring AOP动态代理实现的,只有经过代理对象调用方法,事务才会生效,很多事务失效的场景都是因为没有走代理对象。

下面介绍几种常见的事务失效的原因:

  • 异常捕获处理。Spring实现事务的切面逻辑是通过try-catch捕获异常,如果捕获到异常才会回滚事务,如果我们在方法内部使用try-catch捕获到了异常从而导致异常没有向上抛出,那么就不会触发事务回滚逻辑,从而导致事务失效。

    • 解决方法:在方法内部的catch块中的逻辑处理完之后再向上throw一个RuntimeException。

  • 对象没有被Spring 容器管理。对象通过new关键字手动创造,没有进入Spring容器管理,无法创建代理对象,事务失效。

  • 方法内部使用this调用其他方法。方法内部如果使用this.method()调用其他方法,调用的是原始对象的方法而不是代理对象的对应方法,如果该方法执行出错,则无法回滚。

    • 解决方法:调用代理对象的对应方法,使用AopContext.currentProxy()获取到代理对象,调用代理对象的方法。

  • 方法不是public权限。Spring AOP只能拦截public方法,非public无法生成代理增强。

  • 抛出受检异常。Spring事务的增强逻辑中,默认catch运行时异常和error,不会捕获受检异常,因此如果方法抛出受检异常,也会导致事务失效。

    • 解决方法:配置@Transactional的rollbackFor属性,指定回滚的异常类型。例如@Transactional(rollbackFor = Exception.class)表示抛出所有类型的异常都会回滚。

Spring bean的生命周期

Spring容器在进行实例化时,会根xml配置文件或@Component注解将类信息封装成BeanDefinition对象,Spring容器会根据BeanDefinition来创建bean对象,BeanDefinition中有很多用来描述bean的属性。

有了BeanDefinition之后我们就可以通过其中的方法获取到bean的构造方法并执行创建bean。bean创建完成之后执行依赖注入,在使用@Autowired或@Resource注解的地方将依赖注入到对应的字段上。

当依赖注入完成之后,就要执行初始化方法了,如果类实现了InitializingBean接口,或者定义了init方法,则会在这个阶段执行这些初始化方法。

到此为止bean的创建和初始化就结束了,可以看到bean的创建和初始化是分开的,因此需要注意它们之间的先后顺序,在开发中我们可能遇到使用的bean在该阶段还没有初始化完成的问题,因此需要注意bean的初始化顺序。

bean使用完成之后,在销毁之前,如果我们定义了preDestroy方法,那么该方法会在bean销毁之前执行,一般会做一些资源释放的工作,执行完之后bean就会被彻底销毁了。

下面是一张bean的详细生命周期示意图,包含了很多刚才没有提到的阶段。

因此Spring bean的整个生命周期可以总结为:

  1. 通过BeanDefinition获取bean的定义信息。

  2. 调用构造函数实例化bean。

  3. 对bean中的实例字段进行依赖注入。

  4. 处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware)。

  5. bean的后置处理器BeanPostProcessor-前置。

  6. 初始化方法(InitializingBean、init-method)。

  7. bean的后置处理器BeanPostProcessor-后置。Spring AOP的动态代理增强就是在这个阶段进行的,会生成代理对象对方法进行环绕增强。

  8. 销毁bean。

Spring循环依赖

假设我们在类A中引用了B对象,而在B类中又引用了A对象,现在AB两个类都被注册为了单例bean,那么此时便会出现循环依赖问题。假设A的bean执行完了构造方法,需要进行依赖注入,此时在容器中查找B的bean对象,发现还没有创建,于是开始创建B的bean,显然B的bean在依赖注入的时候也会因为A的bean没有初始化完成而阻塞,这就导致了A和B的bean都无法创建,这就是循环依赖。

Spring通过三级缓存解决循环依赖问题,在Spring中有一个单实例对象注册器,其简化代码如下:

 //单实例对象注册器
 public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry{
     private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
     private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);        //一级缓存
     private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);    //二级缓存
     private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);       //三级缓存
 }
  • 一级缓存:singletonObjects,单例池,缓存已经经历了完整的声明周期,已经完成初始化的bean对象。

  • 二级缓存:earlySingletonObjects,缓存早期的bean对象,生命周期还没有执行完,通常是已经执行完构造函数,但之后的初始化逻辑还没有执行完毕。

  • 三级缓存:singletonFactories,缓存ObjectFactory,表示对象工厂,用来创建对象的。

为什么需要三级缓存

一级缓存的作用是保证bean在容器中只存在一个实例,即实现singleton scope,解决不了循环依赖。

那么一级和二级缓存能否解决循环依赖问题呢?假设引入了二级缓存,那么在我们之前描述的情况中,当A对象需要查找B对象时,A对象会先进入二级缓存,同样B对象也会进入二级缓存,此时Spring会将还未初始化完成的A对象的引用注入到B对象中,这样B对象就已经初始化完成了,便可以进入一级缓存,并且注入到A对象的B字段中,然后A对象也可以进入一级缓存了。看样子一级缓存和二级缓存确实解决了循环依赖问题,事实上,一级缓存和二级缓存可以解决一般对象的循环依赖问题,但是当对象是代理对象的时候,二级缓存就不够用了,需要引入第三级缓存。

在前面的基础上我们再假设A对象需要代理增强,如果只有一级和二级缓存,A对象执行完构造函数之后放入二级缓存的是原始A对象,当B对象在二级缓存中查找A对象注入后,注入的实际上是A的原始对象,而不是代理增强对象。B填充完属性之后放入一级缓存,此时A才能注入B,并且继续走完生命周期,生成代理增强后的A。最终结果就是容器中一级缓存存放的是代理A,而B对象引用的是原始A的bean。

二级缓存生成bean流程图。

那么引入三级缓存是如何解决这个问题的呢?加入了三级缓存之后,当A对象执行完构造函数之后,会被存入一个对象工厂(ObjectFactory),该对象工厂会被存入三级缓存,当B对象需要注入A时,在一级和二级缓存中都没有查找到A,这时会去三级缓存中查找,查找到A的对象工厂便会去尝试获取A对象。对象工厂的作用就是,对外提供获取A对象的接口,在内部判断A是否需要AOP代理增强,如果需要就提前创建AOP代理对象并返回,如果不需要则直接返回原始A对象,这样一来B对象拿到了正确的A对象便可以正常完成依赖注入进入一级缓存。并且对象工厂产生对象之后会将这个半成品bean放入二级缓存,这样下次再查找A的bean时候就会先在二级缓存查找,不需要再次让对象工厂产生了,保证了bean的单例性。

Spring的三级缓存帮助我们解决了循环依赖问题,那么是否意味着开发中一定不会出现循环依赖问题呢?答案是否定的,因为我们刚才所有假设的前提都是bean已经创建出来,那么如果bean在构造函数中就循环依赖了,Spring的三级缓存便束手无策了,因为构造函数在bean的声明周期中是最先执行的,此时bean甚至还没有创建出来,三级缓存也就没用了,此时解决办法便是在构造方法的对应bean参数前添加一个@Lazy懒加载注解,意思是当前先跳过该bean的依赖注入,什么时候需要这个bean了再进行创建或在缓存中寻找并注入。

Spring MVC的执行流程

Spring MVC在早期 JSP流行的时候和现代前后端分离开发的执行流程是不同的,因为现在JSP基本已经淘汰,所以这里主要讲解基于前后端分离的Spring MVC执行流程。

  1. 用户发出的请求到达前端控制器DispatcherServlet。

  2. DispatcherServlet收到请求调用HandlerMapping(处理器映射器,作用:以键值对的形式存储所有URL及其对应的方法,根据请求的URL找到对应的方法)。

  3. HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。

  4. DispatcherServlet调用HandlerAdapter(处理器适配器,作用:对不同类型的Handler进统一处理,并将结果返回,体现了适配器模式)。

  5. HandlerAdapter根据不同类型的Handler执行不同的逻辑,调用不同的处理器(Handler/Controller)并将执行结果返回。

  6. 如果方法上添加了@ResponseBody,则通过HttpMessageConverter将响应结果转换为JSON并作为响应体响应。

Spring Boot自动配置原理

Spring Boot的最大亮点和优势就是自动配置,提到Spring Boot就不得不提到它的自动配置原理了。

Spring Boot的自动配置原理如下:

  1. 在Spring Boot项目的引导类上有一个注解@SpringBootApplication,该注解对三个注解进行了封装,分别是:

    • @SpringBootConfiguration:标记当前类是一个配置类

    • @EnableAutoConfiguration:开启Spring Boot自动配置。

    • @ComponentScan:组件扫描,自动发现并注册bean,默认扫描范围是启动类所在包及其子包。

  2. 其中@EnableAutoConfiguration注解是实现自动配置的核心注解,该注解通过@Import注解导入对应的配置选择器。其内部会读取该项目和项目引用的jar包的classpath路径下的META-INF/spring.factories文件中所配置的类的全类名,在这些配置类中所定义的bean会根据条件注解所指定的条件来判断是否需要将其注入到spring容器中。

  3. 条件判断会有类似@ConditionalOnClass这样的注解,如果存在对应类的class文件则会加载该配置类,并且将配置类中所有的bean放入spring容器中使用。

Spring框架的常见注解有哪些

Spring框架中的注解分为三类:Spring Framework中的注解、Spring MVC中的注解、Spring Boot中的注解。

Spring Framework常见注解。

注解

说明

@Component、@Controller、@Service、@Repository

使用在类上用于实例化bean

@Autowired

使用在字段上用于根据数据类型进行依赖注入

@Qualifier

结合@Autowired用于根据名称进行依赖注入

@Scope

标注bean的作用范围,singleton表示bean是单例的,prototype表示bean是多例的。

@Configuration

指定当前类是一个Spring配置类,当创建容器时会从该类上加载注解。

@ComponentScan

用于指定Spring在初始化容器时要扫描的包

@Bean

添加在方法上,标注该方法的返回值会被注入到Spring容器中

@Import

使用该注解标注的类会被加入到Spring容器中

@Aspect、@Before、@After、@Around、@Pointcut

使用于切面编程(AOP)。

Spring MVC常见注解。

注解

说明

@RequestMapping

用于映射请求路径,可以定义在类或方法上,如果添加在类上,表示类中所有方法都是以该地址为父路径。

@RequestBody

注解实现接收http请求的json数据,将json数据转换为Java对象。

@RequestParam

指定请求参数的名称,用于后端接收参数名称和前端传入参数名称不一致 的情况。

@PathVariable

从请求路径中获取请求参数,传递给方法的形式参数。

@ResponseBody

注解实现将controller方法返回对象转化为json数据响应给客户端。

@RequestHeader

获取指定的请求头数据

@RestController

@RestController = @Controller + @ResponseBody

Spring Boot常见注解

注解

说明

@SpringBootConfiguration

组合了@Configuration注解,实现了配置文件的功能

@EnableAutoConfiguration

开启自动配置功能

@ComponentScan

Spring组件扫描

这部分主要还是会在实际开发中使用,一般面试不太作为考察内容。

Mybatis执行流程

mybatis是现在Java开发中最流行的持久层框架,所以学习该框架的底层原理是很有必要的,这里先介绍一下mybatis的执行流程。

  1. mybatis在启动该之后会先读取mybatis-config.xml配置文件,这份配置文件包含了数据库的连接信息以及表映射文件的存储位置。根据这份配置文件便可以配置mybatis的运行环境。

  2. 创建会话工厂SessionFactory,主要作用是创建SqlSession(包含了执行SQL语句的所有方法)。

  3. 操作数据库的接口,Executor执行器,同时负责查询缓存的维护。

  4. Executor的执行方法中有一个MapperStatement类型的参数,这个参数封装了这次数据库操作的信息,包括执行的SQL语句,映射文件的标签,标签信息等。

  5. 将输入参数映射为MySQL对应的参数类型。

  6. 操作完成数据库之后,将数据库返回结果映射成Java对象。

Mybatis的延迟加载

mybatis的延迟加载是指在关联查询时,先不查询关联表,只有在需要获取关联表的数据时才执行查询关联表的SQL。

例如现在有一张用户表,用户表关联了订单表, 即一个用户可以有多个订单,当我们在业务中查询用户信息时,因为用户信息中包含订单列表。如果不开启延迟加载,那么调用mapper接口时会执行两条SQL语句,同时查询用户表和订单表;如果开启了延迟加载,那么查询订单表的SQL暂时不会执行,只有当我们调用user.getOrderList()时获取用户订单列表时才会执行查询订单的SQL。

在xml映射文件中的对应标签上添加fetchType="lazy"属性可以开启该字段的延迟加载。

示例xml映射文件

 <!DOCTYPE mapper
         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.itheima.mapper.UserMapper">
 ​
     <resultMap id="userResultMap" type="user" autoMapping="true">
         <id property="id" column="id"></id>
         <result property="username" column="username"></result>
         <result property="password" column="password"></result>
         <result property="gender" column="gender"></result>
         <result property="addr" column="addr"></result>
         <collection property="orderList" ofType="order"
                     select="com.itheima.mapper.OrderMapper.findByUid"
                     column="id" fetchType="lazy">
         </collection>
     </resultMap>
 ​
     <select id="selectById" resultMap="userResultMap">
         select *
         from tb_user
         where id = #{id}
     </select>
 ​
 </mapper>

延迟加载的原理:

  1. 当执行SQL查询时,mybatis不会返回原始对象,而是使用CGLIB创建代理对象返回,包裹真实对象。

  2. 当调用目标方法时吗,进入拦截器的invoke方法,发现目标值为null,执行SQL查询。

  3. 获取数据以后调用setter方法设置值,之后再次调用getter方法就可以正常获取值了。

Mybatis一级缓存和二级缓存

Mybatis为了提高查询同一信息的效率,设计了多级缓存,如果持续查询重复数据的话,直接返回缓存中的结果就可以了。

  • 一级缓存:基于PerpetualCache的HashMap本地缓存,其作用域为SqlSession,当Session进行flush或者close之后,该Session中的Cache就将清空,一级缓存默认是开启的。

  • 二级缓存:namepsace和mapper级别,不依赖于SqlSession,也就是每个mapper映射文件(每个mapper类)拥有一个独立的缓存,默认也是采用PerpetualCache的HashMap缓存。默认关闭,需要手动开启。

使用注意事项:

  1. 对于缓存数据更新机制,当某一级缓存中进行了增删改操作后,默认该缓存中的所有select的缓存都会被清空。

  2. 二级缓存需要缓存的数据需要实现Serializable接口。

  3. 只有会话提交或者关闭之后,一级缓存中的数据才会转移到二级缓存中。