Loading

Spring MVC & SpringBoot & Spring 事务

😉 本文共4375字,阅读时间约15min

Spring MVC

说说自己对于 Spring MVC 了解?

如果前后端不分离的,将这三部分耦合到一起,开发代码想必很想必...

MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码

img

Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。

Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。

Spring MVC的核心组件有哪些、工作原理?

img

核心组件

DispatcherServlet核心的中央处理器,负责接收请求、分发,并给予客户端响应。

HandlerMapping处理器映射器,根据 uri 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。

HandlerAdapter处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler

Handler请求处理器,处理实际请求的处理器。

ViewResolver视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端

工作原理 / 流程说明

  1. 客户端(浏览器)发送请求, DispatcherServlet拦截请求。

  2. DispatcherServlet 根据请求信息调用 HandlerMappingHandlerMapping 根据 uri 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。

  3. DispatcherServlet 调用 HandlerAdapter适配执行 Handler

  4. Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServletModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View

  5. ViewResolver 会根据逻辑 View 查找实际的 View

  6. 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 注解修饰的方法进行处理。ExceptionHandlerMethodResolvergetMappedMethod 方法决定了抛出来的异常具体被哪个被 @ExceptionHandler 注解修饰的方法处理异常。
    • getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。(源码就是这意思)

Spring事务

Spring 管理事务的方式有几种?

  • 编程式事务 : 在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用。

  • 声明式事务 : 在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)

    • @Transactional 注解使用详解:
      • 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
      • 类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
      • 接口:不推荐在接口上使用,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效

事务传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题

这里就写用过的。

PROPAGATION_REQUIRED

@Transactional`默认使用就是这个事务传播行为。

  1. 如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  2. 外部方法开启事务的情况下Propagation.REQUIRED修饰的内部方法会加入到外部方法的事务中,所有Propagation.REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。

PROPAGATION_REQUIRES_NEW

不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

外部方法是Required,内部方法是Requires_New,内部方法抛出异常。

如果没catch,外部方法能感知到,就会回滚外部方法事务的内容;

如果catch住了,外部方法感知不到,也不会回滚。

事务属性

事务隔离级别

  1. 默认:使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ
  2. 读未提交、读已提交、可重复读
  3. 串行化: 最高的隔离级别,完全服从 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,而不是代理类的方法
  • 正确的设置 @TransactionalrollbackForpropagation 属性,否则事务可能会回滚失败;

  • @Transactional 注解的方法所在的类必须被 Spring 管理,否则不生效

  • 数据库引擎支持事务

Spring事务传播实现原理

使用ThreadLocal来解决不同事务方法之间的数据库链接问题。

private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");
  • Required,还是用同一个Connection对象
  • Requires_NEW,用不同的connectiion对象

过程举例

最为核心的是:在执行某个方法时,判断当前是否已经存在一个事务,就是判断当前线程的ThreadLocal中是否存在一个数据库连接对象,如果存在则表示已经存在一个事务了。

场景是,a()在一个事务中执行,调用b()方法时需要新开一个事务执行:

  1. 首先,代理对象执行a()方法前,先利用事务管理器新建一个数据库连接a,将数据库连接a的autocommit改为false。

  2. 把数据库连接a设置到ThreadLocal中

  3. 执行a()方法中的sql

  4. 执行a()方法过程中,调用了b()方法(注意用代理对象调用b()方法)

    1. 代理对象执行b()方法前,判断出来了当前线程中已经存在一个数据库连接a了,表示当前线程其实已经拥有一个Spring事务了,则进行挂起

    2. 挂起就是把ThreadLocal中的数据库连接a从ThreadLocal中移除,并放入一个挂起资源对象中

    3. 挂起完成后,再次利用事务管理器新建一个数据库连接b,将数据库连接b的autocommit改为false

    4. 把数据库连接b设置到ThreadLocal中

    5. 执行b()方法中的sql

    6. b()方法正常执行完,则从ThreadLocal中拿到数据库连接b进行提交

  5. 提交之后会恢复所挂起的数据库连接a,这里的恢复,其实只是把在挂起资源对象中所保存的数据库连接a再次设置到ThreadLocal中

  6. 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都把这些已经配置好了,都不需要你配置了。

  1. 起步依赖(简化依赖配置)
  2. 自动配置(简化工程相关的配置)
  3. 内置tomcat

起步依赖怎么简化?

parent标签和starter依赖,解决了用某项依赖存在的下级依赖的问题,并帮助我们将这些依赖坐标的版本号全部配置好,避免冲突

  • parent:在项目中pom.xml中继承了一个坐标

    img

  • 进去之后我们又发现它继承了一个坐标:

img

  • 再点进去之后我们发现:

    • 第一组是各式各样的依赖版本号属性
    • 第二组是各式各样的的依赖坐标信息

    img

    img

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上啥注解也没有。

  1. @AutoConfigurationPackage

    1. 默认的情况下就是将:主配置类(@SpringBootApplication)的所在包及其子包里边的组件扫描到Spring容器中。

    2. 和@ComponentScan并不重复,两者扫描的对象不一样的。@ComponentScan只扫那四个注解

      比如说,你用了Spring Data JPA,可能会在实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。

  2. @Import({AutoConfigurationImportSelector.class})

    1. META-INF/spring.factories配置文件中定义了大量的配置类,筛选出以EnableAutoConfiguration为key的数据,加载到IOC容器中,它会经过exclude和filter等操作,实现自动配置和按需加载功能。

    2. 啥是spring.factories?

      springboot自动只扫自己模块下的,如果想要被Spring容器管理的Bean的路径不再Spring Boot 的包扫描路径下,也就是如何去加载第三方的Bean 呢?

      1. 方案一:在启动类上把想import的import一下,比如@Import(SwaggerConfig.class),可想而知要import超多。
      2. 方案二:使用spring.factories机制,把想自动加载的类写进去,配置下EnableAutoConfiguration,然后利用@Import注解就行。
    3. spring.factories中这么多配置,每次启动都要全部加载么?

      1. 不会,加载进IOC前还会有一轮筛选,@ConditionalOnXXX 中的所有条件都满足,该类才会生效。用于Bean的按需加载。

        @Configuration
        // 检查相关的类:RabbitTemplate 和 Channel是否存在
        // 存在才会加载
        @ConditionalOnClass({ RabbitTemplate.class, Channel.class })
        @EnableConfigurationProperties(RabbitProperties.class)
        @Import(RabbitAnnotationDrivenConfiguration.class)
        public class RabbitAutoConfiguration {
        }
        
      2. 比如@ConditionalOnMissingBean:是当容器中丢失(缺少)这个Bean对象的时候,会启用,防止在不同的配置类中配置多个相同的bean。@ConditionalOnMissingClass、@ConditionalOnClass 的作用与 @ConditionalOnMissingBean、@ConditionalOnBean() 的作用相同

      3. 又比如@ConditionalOnProperty,可以在yaml里配置

        image-20230120194941369

内置tomcat

image-20230120190009505

而且已经配置好了

参考自Spring MVC & IOC

posted @ 2023-02-06 12:33  iterationjia  阅读(203)  评论(0编辑  收藏  举报