说下对 Spring MVC 的理解?

Spring MVC 是基于 Servlet API 构建的,可以说核心就是 DispatcherServlet,即一个前端控制器。

还有几个重要的组件:处理器映射、控制器、视图解析器等。

由这几个组件让我们与 Servlet 解耦,不需要写一个个 Servlet ,基于 Spring 的管理就可以很好的实现 web 应用,简单,方便。

可以简单解释下 MVC 的 Model,View,Controller。

Model:模型表示应用程序的数据状态。它负责处理应用程序的数据,并将数据传递给视图层。

View:视图负责呈现数据给用户。它主要是用户界面的表示层,可以是 JSP、Thymeleaf、FreeMarker 等。视图是从模型中获取数据并进行展示。

Controller:控制器处理用户请求,负责将请求分发到相应的服务层或业务逻辑层,并将处理结果传递给视图层。它是连接模型和视图的桥梁。

Spring MVC 具体的工作原理?

当一个请求过来的时候,由 DispatcherServlet 接待,它会根据处理器映射(HandlerMapping)找到对应的 HandlerExecutionChain(这里面包含了很多定义的 HandlerInterceptor,拦截器)。

然后通过 HandlerAdapter 适配器的适配(适配器模式了解一下)后,执行 handler,即通过 controller 的调用,返回 ModelAndView。

然后 DispatcherServlet 解析得到 ViewName,将其传给 ViewResoler 视图解析器,解析后获得 View 视图。

然后 DispatcherServlet 将 model 数据填充到 view ,得到最终的 Responose 返回给用户。

能简单说说请求是如何找到对应的 controller 吗?

当请求打到 Spring MVC 应用程序时,请求首先会到达一个叫做 DispatcherServlet 的核心控制器,它是所有请求的入口点。

然后请求会利用 HandlerMapping 找到具体的 handle,实际就是通过请求的 URL 查找对应的映射配置,得到对应的 controller 。

不过再具体一些,HandlerMapping 找到的其实是 HandlerExecutionChain ,它包含了拦截器和 handler,请求需要先进过拦截器的处理才会到最后的handler(也就是controller内的方法)。

介绍下 Spring MVC 的核心组件?

Spring MVC 的核心组件主要包括以下几个:

  1. DispatcherServlet:前端控制器,是 Spring MVC 的核心。负责接收 HTTP 请求,将其分派到相应的处理器(Controller)进行处理。
  2. HandlerMapping:根据请求 URL、请求方法、请求参数等,映射请求到具体的处理器(Controller)方法。
  3. Controller:处理用户请求,执行业务逻辑,返回视图名称和模型数据
  4. ModelAndView:将数据传递给视图层,渲染响应。
  5. ViewResolver:将逻辑视图名称解析为实际视图对象,找到具体的视图文件,如 JSP、Thymeleaf 模板。
  6. View:负责渲染模型数据的视图对象。
  7. Model:用于在 Controller 和 View 之间传递数据,包含需要在视图中展示的属性和对象。

什么是 Restful 风格的接口?

实际上 RESTful 就是要求我们不要在 URL 上表现出动作,而是用 HTTP 动词代表动作,URL 上只做资源。

比如获取一个 user。

非 RESTful :GET /getUserById?userId=1

RESTful :GET /users/1

对应到 SpringMVC 代码就是 url 不写什么 get 之类的动词,而是通过 HTTP 请求(如 GET、POST、PUT、DELETE)访问服务端资源。

扩展 REST

REST 不是一个单词,是 Representational State Transfer 的缩写。

直译过来就是表述性状态转移,其实它还有个主语 Resource,所以是资源的表述性状态转移

知道 REST 之后 RESTful 就不难解释了,加 ful 就是变形容词了。

核心就是资源,用 URL 定位资源,用 HTTP 动词来描述所要做的操作,这就是 REST 的核心

HTTP的提供了很多动词:GET、PUT、POST、DELETE…

这些动词都是有含义的。

比如 GET 就是获取资源,是查询请求。

PUT 指的是修改资源,是幂等的。

POST 也是修改(新增也是一种修改),指的是不幂等的操作。

所以根据这些规范我们都能得知这次交互的一些动作,所以 RESTful 风格正确的使用姿势如下:

比如获取一个 user。

错误姿势:GET /getUserById?userId=1

正确姿势:GET /users/1

再比如新增 user。

错误姿势:POST /addUser (省略body)

正确姿势:POST /users (省略body)

可以看到 HTTP 的动词其实就能指代你要对资源做的操作,所以不需要在 URL 上做一些东西,就把 URL 表明的东西看作一个资源即可。

这里注意要用对 HTTP 动词,比如一个获取资源的请求用 PUT,用了也能获取资源但是这不合适。

其实更深一步的理解是 HTTP 是一个协议

协议其实就是约定好的一个东西,协议就规定 GET 是获取资源,那你非得在 URL 上再重复一遍或者所有请求不论增删改都用 GET 这个动作,这其实就是没有完全遵循这个协议。

可以说只是把 HTTP 当成一个传输管道,而不是约定好的协议。

这其实是对 HTTP 更深一层的认识,我认为也是 RESTful 被推出的原因。

当然理想很丰满,现实很骨感,还是有很多人就 getUserById。

不过我个人觉得问题不大,公司统一就行。

Spring MVC中的Controller是什么?如何定义一个Controller?

在 Spring MVC 中,Controller 是一个用于处理用户请求和返回响应的组件。它作为模型-视图-控制器(MVC)设计模式中的控制器部分,负责接收用户的输入,调用业务逻辑,并返回响应结果。

@Controller 用于标记传统 MVC 控制器,它们返回视图和模型数据。

@RestController 是 @Controller 和 @ResponseBody 的组合,用于标记 RESTful 控制器,它们直接返回响应体。

参数接受

使用 @RequestParam 注解来接收查询参数或表单数据。

使用 @PathVariable 注解来提取 URL 路径中的变量。

使用 @RequestBody 注解来接收请求正文中的 JSON 或 XML 数据。

Spring MVC 中的国际化支持是如何实现的?

国际化(i18n)是指在应用程序中提供多种语言和区域设置的支持,使得不同语言的用户都能使用该应用程序。(例如一些航空公司的网页或软件,都需要提供国际化的支持)

Spring 中国际化的主要组件:

  • MessageSource:用于加载和管理国际化消息资源。
  • LocaleResolver:用于解析和确定用户的区域设置(Locale)。

具体实现步骤:

1)配置 MessageSource:

定义消息资源文件(如 messages.properties、messages_en.properties)。

2)注册 MessageSource bean,用于加载这些消息资源文件。

4)配置 LocaleResolver:

配置 LocaleResolver bean,用于确定用户的区域设置或通过 HandlerInterceptor 拦截器获取前端传的 lang 参数。

4)控制器中的使用:

在控制器中使用 Locale 和 MessageSource 获取国际化消息。

扩展知识

1)定义消息资源文件:

messages.properties:

greeting=你好

messages_en.properties:

greeting=Hello

2)注入配置 MessageSource bean:

@Configuration
public class AppConfig {

@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}

3)配置 LocaleResolver

@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.ENGLISH);
return localeResolver;
}

4)控制器中的使用

通过 MessageSource 和 Locale 获取国际化信息

java复制代码@Controller
public class GreetingController {

@Autowired
private MessageSource messageSource;

@GetMapping("/greet")
public String greet(Locale locale, Model model) {
String greeting = messageSource.getMessage("greeting", null, locale);
model.addAttribute("message", greeting);
return "greet";
}
}

Spring MVC 父子容器是什么知道吗?

可以看到,services 和 repositories 是属于父容器的,而 Controllers 等是属于子容器的。

那为什么会有父子之分?

其实 Spring 容器在启动的时候,不会有 Spring MVC 这个概念,只会扫描文件然后创建一个 context ,此时就是父容器。

然后发现是 web 服务需要生成 DispatcherServlet ,此时就会调用 DispatcherServlet#init,这个方法里面最会生成一个新的 context,并把之前的 context 置为自己的 Parent。

这样就有了父子之分,这样指责就更加清晰,子容器就负责 web 部分,父容器则是通用的一些 bean。

也正是有了父子之分,如果有些人没把 controller 扫包的配置写在 spring-servlet.xml ,而写到了 service.xml 里,那就会把 controller 添加到父容器里,这样子容器里面就找不到了,请求就 404 了

当然,如果你把 services 和 repositories 添加到子容器是没影响的,不过没必要,分层还是比较好的方式。

对了,子容器可以用父容器的 Bean,父容器不能用子容器的 Bean。

你了解的 Spring 都用到哪些设计模式?

  • 工厂模式,从名字就看出来了 BeanFacotry,整个 Spring IOC 就是一个工厂。
  • 模板方法,例如 JdbcTemplate、RestTemplate 名字是 xxxTemplate 的都是模板。
  • 代理模式,AOP 整的都是代理。
  • 单例,默认 bean 都是单例的。
  • 责任链模式,比如 Spring MVC 中的拦截器,多个拦截器串联起来就是责任链。
  • 观察者模式,Spring 里的监听器。
  • 适配器模式,SpringMVC 提到的 handlerApdaper 其实就是适配器。

Spring 事务有几个隔离级别?

从源码定义我们可以看到,一共有 5 种隔离级别,而 DEFAULT 就是使用数据库定义的隔离级别。

其他几种分别是:读未提交、读已提交、可重复读、序列化。

1)读未提交,READ UNCOMMITED,简称 RU,最宽松的限制,即一个事务的修改还未提交,另一个事务就能看到修改的结果,会产生脏读现象。

2)读已提交,READ COMMITED,简称 RC,即一个事务只能读到另一个事务已经提交的修改,所以在一个事务里面的多次查询可能会得到不同的结果,因为第一次查询的时候,另一个事务还未提交,所以看不到修改的结果,第二次查询的时候,另一个事务提交了,因此读到了修改后的结果,所以两次查询结果不一致,称之为:不可重复读。

3)可重复读,REPEATABLE READ,简称 RR,它比 RC 更严格,即一个事务开始的时候读不到,那之后也读不到,也就是一个事务内的多次读结果是一致的,但是有幻读情况,即第一次读拿到了四行数据,第二次读拿到了五行数据,因为有新插入的行,不过 InnoDB 利用 MVCC 解决了大部分幻读的情况,利用 update当前读再 select 的幻读无法解决,之前文章已经提到,这里不再赘述。

4)串行化,SERIALIZABLE,最严格的模式,即这个隔离级别的读写访问会把需要遍历到的记录上锁,这样另一个事务要访问对应的记录时候就被阻塞了,需要等待上一个事务提交之后,才能继续执行,所以叫串行。在 InnoDB 中,非自动提交时,会隐式地将所有普通的 SELECT 语句转换为 SELECT…LOCK IN SHARE MODE来满足这个隔离级别。

Spring 有哪几种事务传播行为?

从源码来看,一共有 7 种事务传播行为:

  • PROPAGATION_REQUIRED(默认):如果当前存在事务,则用当前事务,如果没有事务则新起一个事务
  • PROPAGATION_SUPPORTS:支持当前事务,如果不存在,则以非事务方式执行
  • PROPAGATION_MANDATORY:支持当前事务,如果不存在,则抛出异常
  • PROPAGATION_REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起当前事务
  • PROPAGATION_NOT_SUPPORTED:不支持当前事务,始终以非事务方式执行
  • PROPAGATION_NEVER:不支持当前事务,如果当前存在事务,则抛出异常
  • PROPAGATION_NESTED:如果当前事务存在,则在嵌套事务中执行,内层事务依赖外层事务,如果外层失败,则会回滚内层,内层失败不影响外层。

Spring 事务传播行为有什么用?

其实答案就几个字:控制事务的边界

拿 PROPAGATION_NESTED 来举例。如果外层事务失败,则会回滚内层事务,内层事务失败不影响外层事务

可以发现,外层事务失败会影响到内层事务,这说明从事务角度来看,外层到内层之间是没有边界的,因为外层会影响到内层的事务。

而内层失败则不影响外层,说明内层往外层之间事务是有边界的,使得影响无法传播出去。

再比如 PROPAGATION_REQUIRED(默认)。如果当前存在事务,则用当前事务,如果没有事务则新起一个事务。

说明这个配置下,多个方法之间不想要有边界,它们想在一个事务中,这样但凡有一个方法出错都会回滚,就能保证它们是“一体”的,它们想要相互影响。

因此,每种事务传播行为都有其独特的使用场景和作用,能够灵活控制事务的边界,确保事务的一致性和隔离性。

Spring 事务在什么情况下会失效?

一般而言失效的情况都是用了声明式事务即 @Transactional 注解,如果使用了这个注解那么在以下几个情况下会导致事务失效:

1)rollbackFor 没设置对,比如默认没有任何设置(RuntimeException 或者 Error 才能捕获),则方法内抛出 IOException 则不会回滚,需要配置 @Transactional(rollbackFor = Exception.class)

2)异常被捕获了,比如代码抛错,但是被 catch 了,仅打了 log 没有抛出异常,这样事务无法正常获取到错误,因此不会回滚。

3)同一个类中方法调用,因此事务是基于动态代理实现的,同类的方法调用不会走代理方法,因此事务自然就失效了。

4)@Transactional 应用在非 public 修饰的方法上,Spring 事务管理器判断非公共方法则不应用事务。

5)@Transactional 应用在 final 和 static 方法上,因为 aop (Spring Boot2.x版本默认是 cglib,Spring 自身默认是 jdk,一般现在用的都是 SpringBoot)默认是 cglib 代理,无法对 final 方法子类化。static 是静态方法,属于类,不属于实例对象,无法被代理!

6)propagation 传播机制配置错误,例如以下的代码

java复制代码@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void addUserAndAddress(User user,Address address) throws Exception {
userMapper.save(user);
addAddress(address);
}

@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void addAddress(Address address) {
addressMapper.save(address);
}

因为配置了 Propagation.REQUIRES_NEW,是新起了一个事务,即 addAddress 的事务和 addUserAndAddress 其实不是一个事务,因此两个事务之间当然就无法保证数据的一致性了。

7)多线程环境,因为 @Transactional 是基于 ThreadLocal 存储上下文的,多线程情况下每个线程都有自己的上下文,那么之间如何保持事务同步?保持不了,因此事务失效。

8)用的是 MyISAM,这个引擎本身不支持事务!