Spring MVC & SpringBoot & Spring 事务
😉 本文共4375字,阅读时间约15min
Spring MVC
说说自己对于 Spring MVC 了解?
如果前后端不分离的,将这三部分耦合到一起,开发代码想必很想必...
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。
Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。
Spring MVC的核心组件有哪些、工作原理?
核心组件
DispatcherServlet
:核心的中央处理器,负责接收请求、分发,并给予客户端响应。
HandlerMapping
:处理器映射器,根据 uri 去匹配查找能处理的 Handler
,并会将请求涉及到的拦截器和 Handler
一起封装。
HandlerAdapter
:处理器适配器,根据 HandlerMapping
找到的 Handler
,适配执行对应的 Handler
;
Handler
:请求处理器,处理实际请求的处理器。
ViewResolver
:视图解析器,根据 Handler
返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet
响应客户端
工作原理 / 流程说明
-
客户端(浏览器)发送请求,
DispatcherServlet
拦截请求。 -
DispatcherServlet
根据请求信息调用HandlerMapping
。HandlerMapping
根据 uri 去匹配查找能处理的Handler
(也就是我们平常说的Controller
控制器) ,并会将请求涉及到的拦截器和Handler
一起封装。 -
DispatcherServlet
调用HandlerAdapter
适配执行Handler
。 -
Handler
完成对用户请求的处理后,会返回一个ModelAndView
对象给DispatcherServlet
,ModelAndView
顾名思义,包含了数据模型以及相应的视图的信息。Model
是返回的数据对象,View
是个逻辑上的View
。 -
ViewResolver
会根据逻辑View
查找实际的View
。 -
DispaterServlet
把返回的Model
传给View
(视图渲染),把View
返回给请求者(浏览器)
统一异常处理怎么做?
这个我印象相当深刻,尤其是看到controller代码里一堆try-catch时,我会忍不住要改
推荐使用注解的方式统一异常处理,具体会使用到 @ControllerAdvice
+ @ExceptionHandler
这两个注解 。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(BaseException.class)
public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
//......
}
@ExceptionHandler(value = ResourceNotFoundException.class)
public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
//......
}
}
- AOP:这种异常处理方式下,会给所有或者指定的
Controller
织入异常处理的逻辑(AOP)。 - 当
Controller
中的方法抛出异常的时候,由被@ExceptionHandler
注解修饰的方法进行处理。ExceptionHandlerMethodResolver
中getMappedMethod
方法决定了抛出来的异常具体被哪个被@ExceptionHandler
注解修饰的方法处理异常。getMappedMethod()
会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。(源码就是这意思)
Spring事务
Spring 管理事务的方式有几种?
-
编程式事务 : 在代码中硬编码(不推荐使用) : 通过
TransactionTemplate
或者TransactionManager
手动管理事务,实际应用中很少使用。 -
声明式事务 : 在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于
@Transactional
的全注解方式使用最多)- @Transactional 注解使用详解:
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口:不推荐在接口上使用,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效
- @Transactional 注解使用详解:
事务传播行为
事务传播行为是为了解决业务层方法之间互相调用的事务问题。
这里就写用过的。
PROPAGATION_REQUIRED
@Transactional`默认使用就是这个事务传播行为。
- 如果外部方法没有开启事务的话,
Propagation.REQUIRED
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。 - 外部方法开启事务的情况下
Propagation.REQUIRED
修饰的内部方法会加入到外部方法的事务中,所有Propagation.REQUIRED
修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
PROPAGATION_REQUIRES_NEW
不管外部方法是否开启事务,Propagation.REQUIRES_NEW
修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
外部方法是Required,内部方法是Requires_New,内部方法抛出异常。
如果没catch,外部方法能感知到,就会回滚外部方法事务的内容;
如果catch住了,外部方法感知不到,也不会回滚。
事务属性
事务隔离级别
- 默认:使用后端数据库默认的隔离级别,MySQL 默认采用的
REPEATABLE_READ
- 读未提交、读已提交、可重复读
- 串行化: 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。
事务超时属性
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
事务只读属性
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
查询操作是否要启用事务支持
MySQL 默认对每一个新建立的连接都启用了
autocommit
模式。在该模式下,每一个发送到 MySQL 服务器的sql
语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
如果不加Transactional
,每条sql
会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。
- 总结:
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持
事务回滚规则
默认情况下,事务只有遇到运行期异常(
RuntimeException
的子类)时才会回滚,Error
也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。
@Transactional(rollbackFor = Exception.class)注解了解吗?
Exception
分为运行时异常 RuntimeException
和非运行时异常。
在 @Transactional
注解中如果不配置rollbackFor
属性,那么事务只会在遇到RuntimeException
的时候才会回滚,加上 rollbackFor=Exception.class
,可以让事务在遇到非运行时异常时也回滚。(比如IOException、SQLException)
为啥非运行时异常也需要呢?
- Exception衍生了两种子类:
- RuntimeException 非受检异常,不需要检查的异常
- CheckException类型, 如SqlException、IOException
- 受检异常,在编写程序时无法提前预料到的异常,如数据库异常、文件读写异常,这些异常无法提前预料到,所以在编写程序时必须被捕获,当发生时做相应处理;
@Transactional
事务注解原理
-
@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。 -
也就是说实际调用的是invoke方法,这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
@Transactional
的使用注意事项总结 / 失效场景
public方法、类内调用、回滚传播属性设置、必须被Spring管理、数据库引擎要支持事务
-
@Transactional
注解只有作用到 public 方法上事务才生效,不推荐在接口使用 -
避免同一个类中调用
@Transactional
注解的方法,这样会导致事务失效,- 因为调用的this,而不是代理类的方法
-
正确的设置
@Transactional
的rollbackFor
和propagation
属性,否则事务可能会回滚失败; -
被
@Transactional
注解的方法所在的类必须被 Spring 管理,否则不生效; -
数据库引擎支持事务
Spring事务传播实现原理
使用ThreadLocal来解决不同事务方法之间的数据库链接问题。
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
- Required,还是用同一个Connection对象
- Requires_NEW,用不同的connectiion对象
过程举例
最为核心的是:在执行某个方法时,判断当前是否已经存在一个事务,就是判断当前线程的ThreadLocal中是否存在一个数据库连接对象,如果存在则表示已经存在一个事务了。
场景是,a()在一个事务中执行,调用b()方法时需要新开一个事务执行:
-
首先,代理对象执行a()方法前,先利用事务管理器新建一个数据库连接a,将数据库连接a的autocommit改为false。
-
把数据库连接a设置到ThreadLocal中
-
执行a()方法中的sql
-
执行a()方法过程中,调用了b()方法(注意用代理对象调用b()方法)
-
代理对象执行b()方法前,判断出来了当前线程中已经存在一个数据库连接a了,表示当前线程其实已经拥有一个Spring事务了,则进行挂起
-
挂起就是把ThreadLocal中的数据库连接a从ThreadLocal中移除,并放入一个挂起资源对象中
-
挂起完成后,再次利用事务管理器新建一个数据库连接b,将数据库连接b的autocommit改为false
-
把数据库连接b设置到ThreadLocal中
-
执行b()方法中的sql
-
b()方法正常执行完,则从ThreadLocal中拿到数据库连接b进行提交
-
-
提交之后会恢复所挂起的数据库连接a,这里的恢复,其实只是把在挂起资源对象中所保存的数据库连接a再次设置到ThreadLocal中
-
a()方法正常执行完,则从ThreadLocal中拿到数据库连接a进行提交
SpringBoot
-
Spring 可以很方便的集成第三方,自己本身也很强大,IOC、AOP、Data、Web、MVC。
-
Spring 包含了多个功能模块,如下所示。Spring 提供的核心功能主要是 IoC 和 AOP。
- Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!
SpringBoot怎么简化Spring开发的?
Spring麻烦事:
spring的依赖设置很繁琐,原来你导入相关的依赖坐标,可能会产生版本冲突的问题,但是现在springboot已经将版本控制好了,你不用担心版本会产生冲突。
第二点就是spring的配置很繁琐,原来的spring配置bean,配置springmvc,配置web.xml,配置tomcat等等一系列的配置,现在springboot都把这些已经配置好了,都不需要你配置了。
- 起步依赖(简化依赖配置)
- 自动配置(简化工程相关的配置)
- 内置tomcat
起步依赖怎么简化?
parent标签和starter依赖,解决了用某项依赖存在的下级依赖的问题,并帮助我们将这些依赖坐标的版本号全部配置好,避免冲突
-
parent:在项目中pom.xml中继承了一个坐标
-
进去之后我们又发现它继承了一个坐标:
-
再点进去之后我们发现:
- 第一组是各式各样的依赖版本号属性
- 第二组是各式各样的的依赖坐标信息
starter定义了使用某种技术时对于依赖的固定搭配格式,比如springwebmvc就会使用spring-web,它可以减少依赖配置,此外你也不需要关心依赖版本的问题。一般的starter命名规则:spring-boot-starter-技术名称
自动配置 / SpringBoot如何完成自动装配
// SpringBoot的标准启动入口
@SpringBootApplication
public class GraduationProjectApplication {
public static void main(String[] args) {
SpringApplication.run(GraduationProjectApplication.class, args);
}
}
// @SpringBootApplication的源码包含:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
- 根据源码,可以看出@SpringBootApplication注解主要包含3个注解:@SpringBootConfiguration 、@EnableAutoConfiguration 、@ComponentScan。
@SpringBootConfiguration
包含@Configuration,标注在类上表示这是一个SpringBoot的配置类,允许在上下文中注册额外的bean或者导入其他配置项。相当于指明当前类是一个配置类,相当于把该类作为 spring 的 xml 配置文件中的 。
@ComponentScan
自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中 。
扫描被@Compent(@Service@Controller)注解的bean。
注解是会默认扫描启动类所在的包下的所有的类,也可以自定义不扫描一些bean。
@EnableAutoConfiguration 实现自动装配的核心注解
- 开启自动配置功能。主要包含两个注解:
- @AutoConfigurationPackage
- @Import({AutoConfigurationImportSelector.class})。这个是自动装配的核心。
@Import和@ComponentScan这两个注解都能实现对bean的加载,但是@Import导入类可以要求该类可以不是bean,导入时会自动将类配置成bean。like @Import(tiger.class),虽然tiger上啥注解也没有。
-
@AutoConfigurationPackage
-
在默认的情况下就是将:主配置类(
@SpringBootApplication
)的所在包及其子包里边的组件扫描到Spring容器中。 -
和@ComponentScan并不重复,两者扫描的对象不一样的。@ComponentScan只扫那四个注解
比如说,你用了Spring Data JPA,可能会在实体类上写
@Entity
注解。这个@Entity
注解由@AutoConfigurationPackage
扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository
这些注解是由ComponentScan
来扫描并加载的。
-
-
@Import({AutoConfigurationImportSelector.class})
-
META-INF/spring.factories配置文件中定义了大量的配置类,筛选出以
EnableAutoConfiguration
为key的数据,加载到IOC容器中,它会经过exclude和filter等操作,实现自动配置和按需加载功能。 -
啥是spring.factories?
springboot自动只扫自己模块下的,如果想要被Spring容器管理的Bean的路径不再Spring Boot 的包扫描路径下,也就是如何去加载第三方的Bean 呢?
- 方案一:在启动类上把想import的import一下,比如@Import(SwaggerConfig.class),可想而知要import超多。
- 方案二:使用spring.factories机制,把想自动加载的类写进去,配置下EnableAutoConfiguration,然后利用@Import注解就行。
-
spring.factories
中这么多配置,每次启动都要全部加载么?-
不会,加载进IOC前还会有一轮筛选,
@ConditionalOnXXX
中的所有条件都满足,该类才会生效。用于Bean的按需加载。@Configuration // 检查相关的类:RabbitTemplate 和 Channel是否存在 // 存在才会加载 @ConditionalOnClass({ RabbitTemplate.class, Channel.class }) @EnableConfigurationProperties(RabbitProperties.class) @Import(RabbitAnnotationDrivenConfiguration.class) public class RabbitAutoConfiguration { }
-
比如@ConditionalOnMissingBean:是当容器中丢失(缺少)这个Bean对象的时候,会启用,防止在不同的配置类中配置多个相同的bean。@ConditionalOnMissingClass、@ConditionalOnClass 的作用与 @ConditionalOnMissingBean、@ConditionalOnBean() 的作用相同
-
又比如@ConditionalOnProperty,可以在yaml里配置
-
-
内置tomcat
而且已经配置好了