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 根据请求信息调用 HandlerMapping 。HandlerMapping 根据 uri 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。

  3. DispatcherServlet 调用 HandlerAdapter适配执行 Handler 。

  4. Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServlet,ModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。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 注解修饰的方法进行处理。ExceptionHandlerMethodResolver 中 getMappedMethod 方法决定了抛出来的异常具体被哪个被 @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,而不是代理类的方法
  • 正确的设置 @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()方法时需要新开一个事务执行:

  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  阅读(290)  评论(0)    收藏  举报