在我们进行项目编写时,有时相同的一段代码在不同的地方使用多次,对于这种情况,spring 帮我们实现了统一功能处理,下面介绍一些常用的统一功能处理。
一、拦截器
在有些项目中,是需要用户登录才能进行后续访问的。但如果这时有人拿到了后端接口的 url,这时就能跳过用户登录进而访问我们的网站,这就有可能对网站造成攻击,对于这种情况,我们可以将用户的部分信息存到 session 中,在每次访问接口前,都可以先获取到 session,再将用户信息取出,看看这个用户是否登录,若登录,就允许继续访问,若没有登陆,就强制登录。
但是,在我们的项目中,接口的数目可不小,若是在每个接口中都加上这段逻辑,就会显得我们的代码过于冗余,于是 Spring 就为我们构造了拦截器。
拦截器主要用来拦截用户的请求,在指定方法前后执行拦截器中预设的代码,也就是说,拦截器可以在用户的请求之前发挥作用,也可以在用户的请求之后发挥作用。
对于上述判断用户登录的业务,我们就可以在拦截器中写明判断用户是否登录的代码,这样在用户发送请求时,就会先执行这段代码,若用户没有登录,就会阻止用户进行访问,并跳转到登陆页面(这是前端干的事)。
下面介绍拦截器的具体用法。
在实现拦截器时,需要实现 HandlerInterceptor 类中的 preHandle,postHandle,afterCompletion 方法,preHandle 是在目标方法执行前执行的,postHandle 是在目标方法执行之后执行的,afterCompletion 是在试图渲染完成之后执行的,但现在后端开发基本上都不会涉及到试图,那么这个方法就不重点介绍了。
代码如下:
java
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
//在目标方法执行前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("在目标方法执行前执行");
return true;
}
//在目标方法之后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("在目方法执行后执行");
}
//试图渲染完成之后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("视图渲染完成后执行");
}
}
preHandle 有返回值,返回 true 表示不拦截请求,返回 false 表示拦截请求。
定义好拦截器的功能后,接下来就需要注册拦截器。注册拦截器需要实现 WebMvcConfigurer 中的 addInterceptors,代码如下:
java
@Slf4j
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test/**");
}
}
使用 addInterceptor 即表示需要注册的是哪个拦截器,使用 addPathPatterns 即表示需要拦截的路径,即拦截器对哪些请求生效,这里的 /test/** 表示对 test 路径下的所有方法均生效,还有其它的路径表示,如下:
/* : 表示一级路径,只能匹配 /test,不能匹配 /test/t1等;
/** : 表示任意级路径,能匹配 /test,/test/t1等;
/test/* : 表示 /test 下的一级路径,可以匹配 /test/t1,但不能匹配 /test/t1/t2等;
/test/** : 表示 /test 下的任意级路径,可以匹配 /test/t1,/test/t1/t2,但不能匹配 /user/login 等。
当我们配置完拦截器和注册完拦截器后,就可以开始运行代码,我们使用下面的代码来进行测试:
java
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public void t1() {
log.info("拦截器测试...");
}
}
代码运行结果如下:

在这里我们可以看到,当访问被拦截的方法时,首先执行的是 preHandle,由于 postHandle 返回值为 true,就不会拦截用户请求,于是就会先执行 t1 方法,之后会顺序执行 postHandle 和 afterCompletion。
若 preHandle 返回值为 false,代码的运行结果如下:

从结果中可以看出,程序只执行了 preHandle,由于 t1 被拦截,于是 t1、postHandle、afterCompletion 就不会执行。
在上述的拦截路径中,我们将 /test 下的所有路径都给拦截了,但是有的请求我们不需要拦截,那么我们就可以使用 excludPathPatterns 去去除掉不需要拦截的路径,改动后的代码如下:
java
@Slf4j
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test/**")
.excludePathPatterns("/test/t2");
}
}
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public void t1() {
log.info("t1拦截器测试...");
}
@RequestMapping("/t2")
public void t2() {
log.info("不拦截t2测试");
}
}
TestController 中新增了 t2 方法,只拦截 t1,不拦截 t2,代码运行结果如下:

从图中可以看出,t1被拦截,但 t2 没有被拦截。
当我们知道如何使用拦截器之后,就可以在项目中进行运用。
当用户在进行访问时,就可以对其访问进行拦截,在拦截器中判断用户是否登录,若登录就放行,若没有登录就跳转到登录页面进行登录,代码如下:
java
package com.gjm.demo.interceptor;
import com.gjm.demo.constant.Constants;
import com.gjm.demo.enums.ResultStatusEnums;
import com.gjm.demo.model.Result;
import com.gjm.demo.model.UserInfo;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
/**
* 登录拦截器
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法前执行, 拦截器拦截到后,就会将response返回给前端,前端的error中就能就受到response
* @param request
* @param response
* @param handler
* @return true: 继续执行, false: 中断后续操作
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("登录之前执行");
//用户未登录的处理
if (!checkUserInfo(request.getSession())) {
response.setContentType("text/html;character=utf-8");
response.setStatus(401);
String errorMessage = "用户未登录";
response.getOutputStream().write(errorMessage.getBytes("UTF-8"));
return false;
}
return true;
}
/**
* 对检查用户信息进行封装
* @param session
* @return
*/
public boolean checkUserInfo(HttpSession session) {
//用户未登录
if (session == null || session.getAttribute(Constants.SESSION_USER_KEY) == null) {
return false;
}
//获取用户信息
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
//用户信息不对
if (userInfo == null || userInfo.getId() <= 0) {
return false;
}
return true;
}
}
其中 check 的用处是检查用户是否登录。
二、统一数据返回格式
在进行项目开发时,我需要向前端返回一个统一的数据,这个数据是经过封装的,但是,在我们的接口中,有的返回的是 String,有的返回的是 boolean等,这就需要我们针对每一个接口都编写封装结果的代码,这样的代码就有点冗余了。于是,Spring 就封装了能够统一返回数据格式的方法。
现在有一个结果类 Result,对于接口中的每一种返回值,都需要将其封装为 Result 对象,实现的代码如下:
java
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
//false为不处理,true为处理
return true;
}
@SneakyThrows//异常处理
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
return objectMapper.writeValueAsString(Result.success(body));//将Result类型转化为String类型
}
//body已经是result类型,就不需要封装
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}
该类需实现 ResponseBodyAdvice 接口,实现 support 和 beforeBodyWrite 方法,而且需要加上 @ControllerAdvice 注解。
support 的返回值为 boolean,返回 true 表示对各类型的返回值都进行处理,返回 false 表示不处理。
beforeBodyWrite 即具体的实现类。在这里需要强调,若接口中的返回值为 String,那么就需要对其进行单独处理,不然就会报错。
对于本来就是 Result 类型的对象,就不要进行处理,直接返回即可。
三、统一异常处理
在我们编写代码的时候,有时候可能判断的不准确,不知道哪里会出现异常,那么如果我们不进行处理,就会影响程序的运行。对于这种情况,Spring 为我们封装了一个能统一处理异常的方法,代码如下:
java
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler
public Result handler(Exception e) {
log.error("发生异常, e: ", e);
return Result.fail(ResultStatusEnums.ERROR, "内部错误,请联系管理员");
}
@ExceptionHandler
public Result handler(NullPointerException e) {
log.error("发生异常, e: ", e);
return Result.fail(ResultStatusEnums.ERROR, "空指针异常,请联系管理员");
}
@ExceptionHandler
public Result handler(IndexOutOfBoundsException e) {
log.error("发生异常, e: ", e);
return Result.fail(ResultStatusEnums.ERROR, "数组越界异常,请联系管理员");
}
}
ExceptionAdvice 需要使用 @ControllerAdvice 和 @ResponseBody 注解。
在各个方法上需要使用 @ExceptionHandler 注解。
在这个类中,一共处理了三个异常,也可以继续增加异常的种类。若代码中抛出了异常并且没有手动捕获,就会抛给 ExceptionAdvice 类,在这个类中寻找与之距离最近的异常,若没有,就会直接使用 Exception 代替。
上述的代码还有另一种写法,代码如下:
java
@Slf4j
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public Result handler1(Exception e) {
log.error("发生异常, e: ", e);
return Result.fail(ResultStatusEnums.ERROR, "内部错误,请联系管理员");
}
@ExceptionHandler(NullPointerException.class)
public Result handler2(Exception e) {
log.error("发生异常, e: ", e);
return Result.fail(ResultStatusEnums.ERROR, "空指针异常,请联系管理员");
}
@ExceptionHandler(IndexOutOfBoundsException.class)
public Result handler3(Exception e) {
log.error("发生异常, e: ", e);
return Result.fail(ResultStatusEnums.ERROR, "数组越界异常,请联系管理员");
}
}
在每个 @ExceptionHandler 注解中标明捕获的是什么类型的异常,这样在每个方法的参数中就能直接使用 Exception 代替。