June's Studio.

SpringBoot常见面试题2

字数统计: 4k阅读时长: 14 min
2023/03/27

问题:如何实现拦截器?

参考答案:在 Spring Boot 中拦截器的实现分为两步:

  1. 创建一个普通的拦截器,实现 HandlerInterceptor 接口,并重写接口中的相关方法;
  2. 将上一步创建的拦截器加入到 Spring Boot 的配置文件中,并配置拦截规则。

具体实现如下。

① 实现自定义拦截器

import org.springframework.stereotype.Component;

import org.springframework.web.servlet.HandlerInterceptor;

import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

@Component

public class TestInterceptor implements HandlerInterceptor {

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

System.out.println(“拦截器:执行 preHandle 方法。”);

return true;

}

@Override

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

System.out.println(“拦截器:执行 postHandle 方法。”);

}

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

System.out.println(“拦截器:执行 afterCompletion 方法。”);

}

}

其中:

  1. boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handle):在请求方法执行前被调用,也就是调用目标方法之前被调用。比如我们在操作数据之前先要验证用户的登录信息,就可以在此方法中实现,如果验证成功则返回 true,继续执行数据操作业务;否则就返回 false,后续操作数据的业务就不会被执行了。
  2. void postHandle(HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView):调用请求方法之后执行,但它会在 DispatcherServlet 进行渲染视图之前被执行。
  3. void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex):会在整个请求结束之后再执行,也就是在 DispatcherServlet 渲染了对应的视图之后再执行。

② 配置拦截规则

最后,我们再将上面的拦截器注入到项目配置文件中,并设置相应拦截规则,具体实现代码如下:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration

public class AppConfig implements WebMvcConfigurer {

@Autowired

private TestInterceptor testInterceptor;

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(testInterceptor)

.addPathPatterns(“/**“);

.excludePathPatterns(“/login”);

}

}

问题:如何实现过滤器?

参考答案:过滤器可以使用 Servlet 3.0 提供的 @WebFilter 注解,配置过滤的 URL 规则,然后再实现 Filter 接口,重写接口中的 doFilter 方法,具体实现代码如下:

import org.springframework.stereotype.Component;

import javax.servlet.*;

import javax.servlet.annotation.WebFilter;

import java.io.IOException;

@Component

@WebFilter(urlPatterns = “/*“)

public class TestFilter implements Filter {

@Override

public void init(FilterConfig filterConfig) throws ServletException {

System.out.println(“过滤器:执行 init 方法。”);

}

@Override

public void doFilter(ServletRequest servletRequest,

ServletResponse servletResponse,

FilterChain filterChain) throws IOException, ServletException {

System.out.println(“过滤器:开始执行 doFilter 方法。”);

filterChain.doFilter(servletRequest, servletResponse);

System.out.println(“过滤器:结束执行 doFilter 方法。”);

}

@Override

public void destroy() {

System.out.println(“过滤器:执行 destroy 方法。”);

}

}

其中:

  1. void init(FilterConfig filterConfig):容器启动(初始化 Filter)时会被调用,整个程序运行期只会被调用一次。用于实现 Filter 对象的初始化。
  2. void doFilter(ServletRequest request, ServletResponse response,FilterChain chain):具体的过滤功能实现代码,通过此方法对请求进行过滤处理,其中 FilterChain 参数是用来调用下一个过滤器或执行下一个流程
  3. void destroy():用于 Filter 销毁前完成相关资源的回收工作。

问题:拦截器和过滤器有什么区别?

参考答案:拦截器和过滤器的区别主要体现在以下 5 点:

  1. 出身不同:过滤器来自于 Servlet,而拦截器来自于 Spring 框架;
  2. 触发时机不同:请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller),所以过滤器和拦截器的执行时机,是过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法;
  3. 底层实现不同:过滤器是基于方法回调实现的,拦截器是基于动态代理(底层是反射)实现的。;
  4. 支持的项目类型不同:过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;而拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中;
  5. 使用的场景不同:因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务;而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能。

问题:如何操作事务?

参考答案:在 Spring Boot 中操作事务有两种方式:编程式事务或声明式事务。

① 编程式事务有两种实现方法

  1. 使用 TransactionTemplate 对象实现编程式事务;
  2. 使用更加底层的 TransactionManager 对象实现编程式事务。

a) TransactionTemplate 实现

要使用 TransactionTemplate 对象需要先将 TransactionTemplate 注入到当前类中 ,然后再使用它提供的 execute 方法执行事务并返回相应的执行结果,如果程序在执行途中出现了异常,那么就可以使用代码手动回滚事务,具体实现代码如下:

b) TransactionManager 实现

TransactionManager 实现编程式事务相对麻烦一点,它需要使用两个对象:TransactionManager 的子类,加上 TransactionDefinition 事务定义对象,再通过调用 TransactionManager 的 getTransaction 获取并开启事务,然后调用 TransactionManager 提供的 commit 方法提交事务,或使用它的另一个方法 rollback 回滚事务,它的具体实现代码如下:

② 声明式事务

声明式事务的实现比较简单,只需要在方法上或类上添加 @Transactional 注解即可,当加入了 @Transactional 注解就可以实现在方法执行前,自动开启事务;在方法成功执行完,自动提交事务;如果方法在执行期间,出现了异常,那么它会自动回滚事务。

它的具体使用如下:

当然,@Transactional 支持很多参数的设置,它的参数设置列表如下:

参数的设置方法如下:

问题:导致 @Transactional 失效的场景有哪些?

参考答案:导致 @Transactional 失效的常见场景有以下 5 个:

  1. 非 public 修饰的方法;
  2. timeout 超时时间设置过小:如果事务的超时时间设置过小,而方法执行时间过长(超过 timeout),那么事务也会失效;
  3. 代码中使用 try/catch 处理异常;
  4. 调用类内部的 @Transactional 方法;
  5. 数据库不支持事务。

问题:为什么非 public 方法 @Transactional 会失效?

参考答案因为 @Transactional 使用的是 Spring AOP 实现的,而 Spring AOP 是通过动态代理实现的,而 @Transactional 在生成代理时会判断,如果方法为非 public 修饰的方法,则不生成代理对象,这样也就没办法自动执行事务了,它的部分实现源码如下:

protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {

if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {

return null;

}

}

问题:为什么 try-catch 之后,事务不能自动回滚了?

参考答案:造成这个问题的原因是和 @Transactional 注解的实现有关的,因为 @Transactional 在实现中,会捕捉异常,如果有异常了才会回滚事务,而程序中如果添加了 try-catch 之后,@Transactional 就感知不到异常了,从而也就不会回滚事务了,@Transactional 的部分实现源码如下:

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)

throws Throwable {

final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);

final PlatformTransactionManager tm = determineTransactionManager(txAttr);

final String joinpointIdentification = methodIdentification(method, targetClass);

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

Object retVal = null;

try {

retVal = invocation.proceedWithInvocation();

}

catch (Throwable ex) {

completeTransactionAfterThrowing(txInfo, ex);

throw ex;

}

finally {

cleanupTransactionInfo(txInfo);

}

commitTransactionAfterReturning(txInfo);

return retVal;

}

else {

}

}

问题:如何处理事务不自动回滚的问题?

参考答案:当 @Transactional 感知不到异常时,也就是程序中加入了 try-catch 之后事务就不自动回滚了,此时的解决方案有两种:

  1. 在 catch 中重新将异常抛出(这样 @Transactional 就能感知到异常了);
  2. 使用代码手动回滚事务。

具体实现如下。

① 重新抛出异常

@RequestMapping(“/save”)

@Transactional(isolation = Isolation.SERIALIZABLE)

public Object save(User user) {

try {

int i = 10 / 0;

} catch (Exception e) {

throw e;

}

return result;

}

② 手动回滚事务

在方法中使用 TransactionAspectSupport.currentTransactionStatus() 可以得到当前的事务,然后设置回滚方法 setRollbackOnly 就可以实现回滚了,具体实现代码如下:

@RequestMapping(“/save”)

@Transactional

public Object save(User user) {

int result = userService.save(user);

try {

int i = 10 / 0;

} catch (Exception e) {

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

}

return result;

}

问题:为什么调用类内部 @Transactional 事务会失效?

参考答案:因为 @Transactional 是基于 Spring AOP 实现的,而 Spring AOP 又是基于动态代理实现的,而当调用类内部的方法时,不是通过代理对象完成的,而是通过 this 对象实现的,这样就绕过了代理对象,从而事务就失效了。

问题:说一下 @Transactional 工作原理?

参考答案:@Transactional 是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。@Transactional 在开始执行业务之前,通过代理先开启事务,在执行成功之后再提交事务。如果中途遇到的异常,则回滚事务。

@Transactional 实现思路预览:

@Transactional 具体执行细节如下图所示:

问题:Spring 有几种事务隔离级别?

参考答案:Spring 中事务隔离级别包含以下 5 种:

  1. DEFAULT:Spring 默认的事务隔离级别,以连接的数据库的事务隔离级别为准;
  2. READ_UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读;
  3. READ_COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读;
  4. REPEATABLE_READ:可重复读,它能确保同一事务多次查询的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读 (Phantom Read);
  5. SERIALIZABLE:串行化,最高的事务隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。

问题:说一下 Spring 事务传播机制?

参考答案:Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。

Spring 事务传播机制可使用 @Transactional(propagation=Propagation.REQUIRED) 来定义,Spring 事务传播机制的级别包含以下 7 种:

  1. Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  3. Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  4. Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  5. Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  6. Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  7. Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。

问题:加入事务和嵌套事务有什么区别?

参考答案:加入事务(REQUIRED)和嵌套事务(NESTED)都是事务传播机制中的两种传播级别,如果当前不存在事务,那么二者的行为是一致的;但如果当前存在事务,那么加入事务的事务传播级别当遇到异常时会回滚全部事务,而嵌套事务则是回滚部分事务。嵌套事务之所以能回滚部分事务,是因为数据库中存在一个保存点的概念,嵌套事务相对于新建了一个保存点,如果出现异常了,那么只需要回滚到保存点即可,这样就实现了部分事务的回滚。

问题:说一下嵌套事务的实现原理?

参考答案:嵌套事务之所以能实现部分事务的回滚,是因为在数据库中存在一个保存点(savepoint)的概念,以 MySQL 为例,嵌套事务相当于新建了一个保存点,而滚回时只回滚到当前保存点,因此之前的事务是不受影响的,这一点可以在 MySQL 的官方文档汇总找到相应的资料:https://dev.mysql.com/doc/refman/5.7/en/savepoint.html

而 REQUIRED 是加入到当前事务中,并没有创建事务的保存点,因此出现了回滚就是整个事务回滚,这就是嵌套事务和加入事务的区别。

保存点就像玩通关游戏时的“游戏存档”一样,如果设置了游戏存档,那么即使当前关卡失败了,也能继续上一个存档点继续玩,而不是从头开始玩游戏。

问题:事务隔离级别和传播机制有什么区别?

参考答案:事务隔离级别描述的是多个事务同时执行时的某种行为;而事务传播机制是描述,包含了多个事务的方法在相互调用时事务的传播行为。所以事务隔离级别描述的是纵向事务并发调用时的行为模式,而事务传播机制描述的是横向事务传递时的行为模式,如下图所示:

问题:如何进行统一异常处理?

参考答案:在 Spring Boot 中,统一异常处理可以使用 @ControllerAdvice + @ExceptionHandler 来实现,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执行某个通知,也就是执行某个方法事件,具体实现代码如下:

import java.util.HashMap;

@ControllerAdvice

public class ErrorAdive {

@ExceptionHandler(Exception.class)

@ResponseBody

public Object handler(Exception e) {

HashMap<String, Object> map = new HashMap<>();

map.put(“code”, 0);

map.put(“data”, null);

map.put(“msg”, e.getMessage());

return map;

}

}

这样写完之后,当项目中有异常时就会触发此方法,返回固定格式的异常信息。

参考 & 鸣谢

《Offer来了》

CATALOG
  1. 1. 问题:如何实现拦截器?
  2. 2. 问题:如何实现过滤器?
  3. 3. 问题:拦截器和过滤器有什么区别?
  4. 4. 问题:如何操作事务?
  5. 5. 问题:导致 @Transactional 失效的场景有哪些?
  6. 6. 问题:为什么非 public 方法 @Transactional 会失效?
  7. 7. 问题:为什么 try-catch 之后,事务不能自动回滚了?
  8. 8. 问题:如何处理事务不自动回滚的问题?
  9. 9. 问题:为什么调用类内部 @Transactional 事务会失效?
  10. 10. 问题:说一下 @Transactional 工作原理?
  11. 11. 问题:Spring 有几种事务隔离级别?
  12. 12. 问题:说一下 Spring 事务传播机制?
  13. 13. 问题:加入事务和嵌套事务有什么区别?
  14. 14. 问题:说一下嵌套事务的实现原理?
  15. 15. 问题:事务隔离级别和传播机制有什么区别?
  16. 16. 问题:如何进行统一异常处理?
  17. 17. 参考 & 鸣谢