知识梳理:Rest&Filter&Interceptor的使用

    科技2026-10-01  11

    使用REST

    使用Spring MVC开发Web应用程序的主要工作就是编写Controller逻辑。在Web应用中,除了需要使用MVC给用户显示页面外,还有一类API接口,称之为REST,通常输入输出都是JSON,便于第三方调用或者使用页面JavaScript与之交互。

    Spring额外提供了一个@RestController注解,使用@RestController替代@Controller后,每个方法自动变成API接口方法。编写ApiController如下:

    @RestController @RequestMapping("/api") public class ApiController { @Autowired UserService userService; @GetMapping("/users") public List<User> users() { return userService.getUsers(); } @GetMapping("/users/{id}") public User user(@PathVariable("id") long id) { return userService.getUserById(id); } @PostMapping("/signin") public Map<String, Object> signin(@RequestBody SignInRequest signinRequest) { try { User user = userService.signin(signinRequest.email, signinRequest.password); return Map.of("user", user); } catch (Exception e) { return Map.of("error", "SIGNIN_FAILED", "message", e.getMessage()); } } public static class SignInRequest { public String email; public String password; } }

    User能被正确地序列化为JSON,但暴露了password属性。要避免输出password属性,可以把User复制到另一个UserBean对象,该对象只持有必要的属性,但这样做比较繁琐。另一种简单的方法是直接在User的password属性定义处加上@JsonIgnore表示完全忽略该属性:

    public class User { ... @JsonIgnore public String getPassword() { return password; } ... }

    但是这样如果写一个register(User user)方法,那么该方法的User对象也拿不到注册时用户传入的密码了。如果要允许输入password,但不允许输出password,即在JSON序列化和反序列化时,允许写属性,禁用读属性,可以更精细地控制如下:

    public class User { ... @JsonProperty(access = Access.WRITE_ONLY) public String getPassword() { return password; } ... }

    同样的,可以使用@JsonProperty(access = Access.READ_ONLY)允许输出,不允许输入。

    小结

    使用@RestController可以方便地编写REST服务,Spring默认使用JSON作为输入和输出。

    要控制序列化和反序列化,可以使用Jackson提供的@JsonIgnore和@JsonProperty注解。

    集成Filter

    在Spring MVC中,DispatcherServlet只需要固定配置到web.xml中,剩下的工作主要是专注于编写Controller。但是,在Servlet规范中,可以使用Filter。

    如果注册时输入中文会导致乱码,因为Servlet默认按非UTF-8编码读取参数。为了修复这一问题,我们可以简单地使用一个EncodingFilter,在全局范围类给HttpServletRequest和HttpServletResponse强制设置为UTF-8编码。

    可以自己编写一个EncodingFilter,也可以直接使用Spring MVC自带的一个CharacterEncodingFilter。配置Filter时,只需在web.xml中声明即可:

    <web-app> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ... </web-app>

    因为这种Filter和我们业务关系不大,注意到CharacterEncodingFilter其实和Spring的IoC容器没有任何关系,两者均互不知晓对方的存在,因此,配置这种Filter十分简单。

    我们再考虑这样一个问题:如果允许用户使用Basic模式进行用户验证,即在HTTP请求中添加头Authorization: Basic email:password,这个需求如何实现?

    编写一个AuthFilter是最简单的实现方式:

    @Component public class AuthFilter implements Filter { @Autowired UserService userService; public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; // 获取Authorization头: String authHeader = req.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Basic ")) { // 从Header中提取email和password: String email = prefixFrom(authHeader); String password = suffixFrom(authHeader); // 登录: User user = userService.signin(email, password); // 放入Session: req.getSession().setAttribute(UserController.KEY_USER, user); } // 继续处理请求: chain.doFilter(request, response); } }

    在Spring中创建的这个AuthFilter是一个普通Bean,Servlet容器并不知道,所以它不会起作用。

    如果直接在web.xml中声明这个AuthFilter,注意到AuthFilter的实例将由Servlet容器而不是Spring容器初始化,因此,@Autowire根本不生效,用于登录的UserService成员变量永远是null。

    所以,得通过一种方式,让Servlet容器实例化的Filter,间接引用Spring容器实例化的AuthFilter。Spring MVC提供了一个DelegatingFilterProxy,专门来干这个事情:

    <web-app> <filter> <filter-name>authFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>authFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ... </web-app>

    实现原理:

    Servlet容器从web.xml中读取配置,实例化DelegatingFilterProxy,注意命名是authFilter;Spring容器通过扫描@Component实例化AuthFilter。

    当DelegatingFilterProxy生效后,它会自动查找注册在ServletContext上的Spring容器,再试图从容器中查找名为authFilter的Bean,也就是我们用@Component声明的AuthFilter。

    DelegatingFilterProxy将请求代理给AuthFilter,核心代码如下:

    public class DelegatingFilterProxy implements Filter { private Filter delegate; public void doFilter(...) throws ... { if (delegate == null) { delegate = findBeanFromSpringContainer(); } delegate.doFilter(req, resp, chain); } }

    这就是一个代理模式的简单应用。如果在web.xml中配置的Filter名字和Spring容器的Bean的名字不一致,那么需要指定Bean的名字:

    <filter> <filter-name>basicAuthFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <!-- 指定Bean的名字 --> <init-param> <param-name>targetBeanName</param-name> <param-value>authFilter</param-value> </init-param> </filter>

    实际应用时,尽量保持名字一致,以减少不必要的配置。

    小结

    当一个Filter作为Spring容器管理的Bean存在时,可以通过DelegatingFilterProxy间接地引用它并使其生效。

    使用Interceptor

    在Web程序中,注意到使用Filter的时候,Filter由Servlet容器管理,它在Spring MVC的Web应用程序中作用范围如下:

    │ ▲ ▼ │ ┌───────┐ │Filter1│ └───────┘ │ ▲ ▼ │ ┌───────┐ ┌ ─ ─ ─│Filter2│─ ─ ─ ─ ─ ─ ─ ─ ┐ └───────┘ │ │ ▲ │ ▼ │ │ ┌─────────────────┐ │ │DispatcherServlet│<───┐ │ └─────────────────┘ │ │ │ ┌────────────┐ │ │ │ModelAndView││ │ └────────────┘ │ │ ▲ │ │ ┌───────────┐ │ │ ├───>│Controller1│────┤ │ │ └───────────┘ │ │ │ │ │ │ ┌───────────┐ │ │ └───>│Controller2│────┘ │ └───────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

    上图虚线框就是Filter2的拦截范围,Filter组件实际上并不知道后续内部处理是通过Spring MVC提供的DispatcherServlet还是其他Servlet组件,因为Filter是Servlet规范定义的标准组件,它可以应用在任何基于Servlet的程序中。

    如果只基于Spring MVC开发应用程序,还可以使用Spring MVC提供的一种功能类似Filter的拦截器:Interceptor。和Filter相比,Interceptor拦截范围不是后续整个处理流程,而是仅针对Controller拦截:

    │ ▲ ▼ │ ┌───────┐ │Filter1│ └───────┘ │ ▲ ▼ │ ┌───────┐ │Filter2│ └───────┘ │ ▲ ▼ │ ┌─────────────────┐ │DispatcherServlet│<───┐ └─────────────────┘ │ │ ┌────────────┐ │ │ModelAndView│ │ └────────────┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ▲ │ ┌───────────┐ │ ├─┼─>│Controller1│──┼─┤ │ └───────────┘ │ │ │ │ │ │ ┌───────────┐ │ └─┼─>│Controller2│──┼─┘ └───────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ┘

    上图虚线框就是Interceptor的拦截范围,注意到Controller的处理方法一般都类似这样:

    @Controller public class Controller1 { @GetMapping("/path/to/hello") ModelAndView hello() { ... } }

    所以,Interceptor的拦截范围其实就是Controller方法,它实际上就相当于基于AOP的方法拦截。因为Interceptor只拦截Controller方法,所以要注意,返回ModelAndView后,后续对View的渲染就脱离了Interceptor的拦截范围。

    使用Interceptor的好处是Interceptor本身是Spring管理的Bean,因此注入任意Bean都非常简单。此外,可以应用多个Interceptor,并通过简单的@Order指定顺序。`

    @Order(1) @Component public class LoggerInterceptor implements HandlerInterceptor { final Logger logger = LoggerFactory.getLogger(getClass()); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.info("preHandle {}...", request.getRequestURI()); if (request.getParameter("debug") != null) { PrintWriter pw = response.getWriter(); pw.write("<p>DEBUG MODE</p>"); pw.flush(); return false; } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { logger.info("postHandle {}.", request.getRequestURI()); if (modelAndView != null) { modelAndView.addObject("__time__", LocalDateTime.now()); } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { logger.info("afterCompletion {}: exception = {}", request.getRequestURI(), ex); } }

    一个Interceptor必须实现HandlerInterceptor接口,可以选择实现preHandle()、postHandle()和afterCompletion()方法。preHandle()是Controller方法调用前执行,postHandle()是Controller方法正常返回后执行,而afterCompletion()无论Controller方法是否抛异常都会执行,参数ex就是Controller方法抛出的异常(未抛出异常是null)。

    在preHandle()中,也可以直接处理响应,然后返回false表示无需调用Controller方法继续处理了,通常在认证或者安全检查失败时直接返回错误响应。在postHandle()中,因为捕获了Controller方法返回的ModelAndView,所以可以继续往ModelAndView里添加一些通用数据,很多页面需要的全局数据如Copyright信息等都可以放到这里,无需在每个Controller方法中重复添加。

    继续添加一个AuthInterceptor,用于替代使用AuthFilter进行Basic认证的功能:

    @Order(2) @Component public class AuthInterceptor implements HandlerInterceptor { final Logger logger = LoggerFactory.getLogger(getClass()); @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { logger.info("pre authenticate {}...", request.getRequestURI()); try { authenticateByHeader(request); } catch (RuntimeException e) { logger.warn("login by authorization header failed.", e); } return true; } private void authenticateByHeader(HttpServletRequest req) { String authHeader = req.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Basic ")) { logger.info("try authenticate by authorization header..."); String up = new String(Base64.getDecoder().decode(authHeader.substring(6)), StandardCharsets.UTF_8); int pos = up.indexOf(':'); if (pos > 0) { String email = URLDecoder.decode(up.substring(0, pos), StandardCharsets.UTF_8); String password = URLDecoder.decode(up.substring(pos + 1), StandardCharsets.UTF_8); User user = userService.signin(email, password); req.getSession().setAttribute(UserController.KEY_USER, user); logger.info("user {} login by authorization header ok.", email); } } } }

    这个AuthInterceptor是由Spring容器直接管理的,因此注入UserService非常方便。

    最后,要让拦截器生效,在WebMvcConfigurer中注册所有的Interceptor:

    @Bean WebMvcConfigurer createWebMvcConfigurer(@Autowired HandlerInterceptor[] interceptors) { return new WebMvcConfigurer() { public void addInterceptors(InterceptorRegistry registry) { for (var interceptor : interceptors) { registry.addInterceptor(interceptor); } } ... }; }

    如果拦截器没有生效,请检查是否忘了在WebMvcConfigurer中注册。

    处理异常

    在Controller中,Spring MVC还允许定义基于@ExceptionHandler注解的异常处理方法。我们来看具体的示例代码:

    @Controller public class UserController { @ExceptionHandler(RuntimeException.class) public ModelAndView handleUnknowException(Exception ex) { return new ModelAndView("500.html", Map.of("error", ex.getClass().getSimpleName(), "message", ex.getMessage())); } ... }

    异常处理方法没有固定的方法签名,可以传入Exception、HttpServletRequest等,返回值可以是void,也可以是ModelAndView,上述代码通过@ExceptionHandler(RuntimeException.class)表示当发生RuntimeException的时候,就自动调用此方法处理。

    注意到我们返回了一个新的ModelAndView,这样在应用程序内部如果发生了预料之外的异常,可以给用户显示一个出错页面,而不是简单的500 Internal Server Error或404 Not Found。

    可以编写多个错误处理方法,每个方法针对特定的异常。例如,处理LoginException使得页面可以自动跳转到登录页。

    使用ExceptionHandler时,要注意它仅作用于当前的Controller,即ControllerA中定义的一个ExceptionHandler方法对ControllerB不起作用。

    Processed: 0.011, SQL: 10