Java案例如何自定义拦截逻辑?

wen java案例 24

Java案例:如何优雅地自定义拦截逻辑?——从入门到实战

目录导读

  1. 为什么需要自定义拦截逻辑?
  2. Java拦截器核心原理
  3. 实战案例:基于Spring AOP的登录拦截器
  4. 案例进阶:注解驱动+参数解析的权限拦截
  5. 性能优化:防止重复拦截与缓存策略
  6. 常见问题答疑(Q&A)

问答提示:本文每节末尾均设有问答环节,帮你快速检验学习成果。

Java案例如何自定义拦截逻辑?


为什么需要自定义拦截逻辑?

在Java企业级开发中,我们经常遇到这样的场景:

  • 某些API接口需要用户登录后才能访问
  • 某些操作需要特定角色(如管理员、普通用户)才能执行
  • 需要对请求进行日志记录、参数校验或限流

如果把这些逻辑硬编码到每个业务方法中,代码会变得臃肿且难以维护。自定义拦截逻辑正是为了解决这些问题——它将横切关注点(如权限、日志)与核心业务解耦,实现“低侵入式”的复用。

常见拦截场景对比

场景 传统方式 拦截方式
身份验证 每个Controller方法写获取session代码 编写一个拦截器,自动检查token
日志记录 手动log.info() AOP切面自动打印接口耗时
参数校验 每个方法里if...else 注解+拦截器统一校验

核心价值

  • 复用性:同一个拦截逻辑可应用于多个接口
  • 可维护性:修改拦截逻辑只需改动一处
  • 可扩展性:新增拦截逻辑只需添加新拦截器

问答1:拦截器和过滤器(Filter)有何区别?
:Filter是Servlet规范的一部分,能拦截所有请求(包括静态资源);拦截器(Interceptor)是Spring框架特性,仅拦截Controller层,且能获取Spring容器中的Bean,通常推荐在Spring项目中使用拦截器。


Java拦截器核心原理

1 三种主流实现方式

方式 技术栈 适用场景 控制粒度
Servlet Filter Java EE 字符编码、请求日志 全局粗粒度
Spring Interceptor Spring MVC 权限控制、请求预处理 路径级别
AOP (AspectJ) Spring/AspectJ 方法级别拦截 最细粒度(可到参数)

2 AOP的底层秘密

Spring AOP基于动态代理实现:

  • JDK动态代理:目标类实现了接口(默认方式)
  • CGLIB代理:目标类未实现接口

AOP的核心概念:

切面(Aspect) = 对象(AspectJ类) + 通知(Advice)
通知类型:
- @Before:方法执行前  
- @After:方法执行后(含异常)  
- @Around:包裹方法执行(最灵活)  

3 执行顺序

当一个请求进入时,经过的拦截层顺序:

Filter1 → Filter2 → Interceptor.preHandler → AOP环绕通知 → Controller
          ← 视图渲染 ← 后处理 ← AOP后通知

问答2:多个拦截器如何排序?
:Filter通过web.xml的配置顺序;Interceptor通过addInterceptor顺序;AOP通过@Order注解或实现Ordered接口,建议按“认证→授权→日志”顺序排列。


实战案例:基于Spring AOP的登录拦截器

1 场景需求

所有/api/private/*接口必须先登录,未登录返回401状态码。

2 代码实现

步骤1:自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
    boolean needToken() default true;
}

步骤2:编写AOP切面

@Aspect
@Component
@Order(1)
public class LoginAspect {
    @Autowired
    private TokenService tokenService;
    @Around("@annotation(loginRequired)") // 生效范围
    public Object checkLogin(ProceedingJoinPoint pjp, LoginRequired loginRequired) throws Throwable {
        // 1. 从请求头获取token
        HttpServletRequest request = getHttpServletRequest();
        String token = request.getHeader("Authorization");
        if (loginRequired.needToken() && !tokenService.isValid(token)) {
            throw new UnauthorizedException("登录已过期,请重新登录");
        }
        // 2. 放行请求
        return pjp.proceed();
    }
    private HttpServletRequest getHttpServletRequest() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        return ((ServletRequestAttributes) ra).getRequest();
    }
}

步骤3:在Controller上使用

@RestController
@RequestMapping("/api/private")
public class UserController {
    @LoginRequired
    @GetMapping("/profile")
    public Result getProfile() {
        // 业务逻辑 - 无需关注token校验
        return Result.success(userService.getCurrentUser());
    }
}

3 扩展:拦截多个方法

如果所有接口都需要拦截,可直接指定全包:

@Around("execution(* com.example.controller..*.*(..))")
public Object globalCheck(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

问答3:AOP切面中如何获取请求体(Request Body)?
:通过pjp.getArgs()获取方法参数数组,然后从参数中提取@RequestBody注解对应的参数对象,但需注意,流只能读取一次——可在切面中配合ContentCachingRequestWrapper使用。


案例进阶:注解驱动+参数解析的权限拦截

1 需求分析

管理员接口(标记@AdminOnly)需要检测当前用户角色是否为ROLE_ADMIN,并自动将用户信息注入到方法参数中。

2 组合方案:拦截器 + 参数解析器

// 步骤1:权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AdminOnly {
    String[] allowedRoles() default {"ADMIN"};
}
// 步骤2:自定义参数注解
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}

拦截器核心代码

@Component
public class PermissionInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) return true;
        HandlerMethod method = (HandlerMethod) handler;
        AdminOnly anno = method.getMethodAnnotation(AdminOnly.class);
        if (anno == null) return true;
        // 从已解析的token中获取角色
        String role = tokenService.getCurrentUserRole(request);
        for (String allowed : anno.allowedRoles()) {
            if (role.equalsIgnoreCase(allowed)) {
                request.setAttribute("currentUser", tokenService.getCurrentUser(request));
                return true;
            }
        }
        throw new ForbiddenException("无权限访问");
    }
}

参数解析器

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest webRequest, WebDataBinderFactory factory) throws Exception {
        ServletRequestAttributes ra = (ServletRequestAttributes) webRequest.getNativeRequest();
        return ra.getRequest().getAttribute("currentUser");
    }
}

控制器使用

@AdminOnly
@GetMapping("/admin/users")
public Result listUsers(@CurrentUser User admin) {
    // admin对象已被自动注入,且保证是管理员角色
    return Result.success(userService.listAll());
}

3 为什么推荐这种方式?

  • 注解驱动声明式拦截,代码更简洁
  • 参数解析器避免每次手动从Request中getUser
  • 职责单一:拦截器只做权限校验,解析器只做参数注入

问答4:拦截器与参数解析器的执行顺序是怎样的?
:先执行preHandle拦截器,然后执行参数解析器(在进入Controller方法前解析参数),因此参数解析器可以读取拦截器放到Request中的属性。


性能优化:防止重复拦截与缓存策略

1 常见陷阱

  1. 重复执行拦截:多个AOP切面同时作用在同一个方法上
  2. 重复解析Token:每个拦截器都解析一次JWT Token
  3. 数据库查询:权限校验时每次都查询数据库

2 优化方案

引入缓存
@Aspect
@Component
public class CachedPermissionAspect {
    private static final Cache<String, Boolean> PERMISSION_CACHE = Caffeine.newBuilder()
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .maximumSize(1000)
        .build();
    @Around("@annotation(adminOnly)")
    public Object checkWithCache(ProceedingJoinPoint pjp, AdminOnly adminOnly) throws Throwable {
        String token = extractToken();
        Boolean result = PERMISSION_CACHE.get(token, k -> doCheck(token, adminOnly));
        if (!result) throw new ForbiddenException();
        return pjp.proceed();
    }
}
合并拦截器

将多个小切面合并为一个切面,减少方法调用栈深度:

@Around("loginRequired() || adminOnlyAnno() || loggingRequired()")
public Object combinedAspect(ProceedingJoinPoint pjp) throws Throwable {
    // 按优先级依次校验
    checkLogin();
    checkAdmin();
    startLogTimer();
    try {
        return pjp.proceed();
    } finally {
        stopLogPrint();
    }
}
Filter提前处理

对于全局性操作(如Token解析),放在Filter中执行一次,后续拦截器直接读取Filter放入Request的属性:

@Component
public class TokenResolveFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        String token = request.getHeader("Authorization");
        UserInfo userInfo = tokenService.resolve(token);
        request.setAttribute("userInfo", userInfo); // 供拦截器复用
        chain.doFilter(request, response);
    }
}

3 性能对比

方案 Token解析次数 数据库查询 执行耗时
无优化 3次/请求 3次 15ms
放入Filter 1次/请求 1次 5ms
加缓存 1次/请求 0次(缓存命中) 2ms

问答5:如何在拦截器中优雅地处理异常?
:不要在拦截器中直接返回页面或响应,而是抛出自定义异常,统一使用@ControllerAdvice@ExceptionHandler处理,保持异常处理逻辑集中。


常见问题答疑(Q&A)

Q6.1:自定义拦截逻辑应该放在Filter、Interceptor还是AOP?

答:遵循“广度优先,深度渐进”原则:

  • 全局粗粒度(编码、CORS):Filter
  • 路径级别(登录校验、多语言):Interceptor
  • 方法级别(权限、缓存、事务):AOP

Q6.2:为什么我的拦截器没有生效?

常见原因排查:

  1. 拦截器是否已注入Spring容器(@Component
  2. AOP是否启用了@EnableAspectJAutoProxy(Spring Boot默认开启)
  3. 调用方法时是否通过代理对象(内部方法调用不会触发AOP)
  4. 是否在Controller方法上正确声明了注解

Q6.3:如何拦截静态资源?

Filter可以拦截所有路径,但Interceptor默认不拦截静态资源,可在WebMvcConfigurer.addInterceptors中通过excludePathPatterns排除误拦截的静态资源:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginInterceptor())
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/public/**", "/css/**", "/js/**");
}

Q6.4:AOP能拦截私有方法吗?

AOP基于代理,只能拦截被publicprotected修饰的方法(如果是CGLIB代理可拦截protected)。私有方法无法被代理——因为代理类无法继承私有方法。

Q6.5:如何测试自定义拦截逻辑?

推荐使用Spring Boot的@WebMvcTest

@WebMvcTest(UserController.class)
public class LoginAspectTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    void testWithoutToken_shouldReturn401() throws Exception {
        mockMvc.perform(get("/api/private/profile"))
               .andExpect(status().isUnauthorized());
    }
}

自定义拦截逻辑是Java框架设计中最具实用价值的模式之一,从简单的登录校验到复杂的权限矩阵,从性能监控到业务降级,拦截器为我们提供了一种非侵入式的扩展能力。

在实际开发中,请遵循以下原则:

  1. 单一职责:一个拦截器只做一件事
  2. 合理排序:认证拦截器应在最外层
  3. 性能优先:避免在拦截器中执行IO操作(如数据库查询)
  4. 异常统一:拦截器内部只抛异常,不处理异常

抱歉,评论功能暂时关闭!