实现Spring拦截器

6

我希望实现Spring拦截器,以便打印每个接收和发送的API XML请求。我尝试了这个测试代码:

@SpringBootApplication
@EntityScan(".....")
public class Application extends SpringBootServletInitializer implements WebMvcConfigurer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();

    List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();

    // check if restTeamplate doesn't already have other interceptors
    if (CollectionUtils.isEmpty(interceptors)) { 
        interceptors = new ArrayList<>();
    }

    interceptors.add(new RestTemplateHeaderModifierInterceptor());
    restTemplate.setInterceptors(interceptors);
    return restTemplate;
}

@Bean
public WebMvcConfigurer webMvcConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new RestTemplateHeaderModifierInterceptor());
        }
    };
} 
}

记录日志的组件:

import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.servlet.HandlerInterceptor;

import io.micrometer.core.instrument.util.IOUtils;

@Component
public class RestTemplateHeaderModifierInterceptor implements ClientHttpRequestInterceptor, HandlerInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(HomeController.class);

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {

        StringBuilder sb = new StringBuilder();
        sb.append("[ ");
        for (byte b : body) {
            sb.append(String.format("0x%02X ", b));
        }
        sb.append("]");

        LOGGER.debug("!!!!!!!!!!!!!!! Input " + sb.toString());

        System.out.println("!!!!!!!!!!!!!!!");

        ClientHttpResponse response = execution.execute(request, body);    
        InputStream inputStream = response.getBody();    
        String result = IOUtils.toString(inputStream, StandardCharsets.UTF_8);

        LOGGER.debug("!!!!!!!!!!!!!!! result " + result);

        System.out.println("!!!!!!!!!!!!!!!");

        return response;
    }

}

但在DEBUG模式下控制台没有输出任何内容。我错在哪里了?可能是这个组件没有注册或者我缺少一些重要的配置吗?


你可以应用AOP来解决你的问题。 - Jonathan JOhx
你能否把完整的答案粘贴在这里吗? - Peter Penzov
@PeterPenzov 完成 https://dev59.com/LLDla4cB1Zd3GeqP2Aya#54258906 - Jonathan JOhx
@JonathanJohx 很不幸,我被这个问题阻塞了:https://dev59.com/s7Hma4cB1Zd3GeqPIDd9,现在无法进行测试。你能否给些建议,如何解决它? - Peter Penzov
@PeterPenzov 这是我的答案 https://stackoverflow.com/a/53863603/10426557 - Jonathan JOhx
2个回答

3

应用AOP(面向切面编程)来进行日志记录,您可以这样做:

  1. 创建一个Aspect来在进入Controller之前拦截。
  2. 为before和after创建切入点表达式。

用于日志记录的Aspect:

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Enumeration;

@Aspect
@Component
public class LoggingAspect {

    private static final String CONTROLLER_EXPRESION = "within(@org.springframework.stereotype.Controller *) && execution(* *.*(..))";
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    /**
     * Before -> Any resource annotated with @Controller annotation and all method
     * and function taking HttpServletRequest as first parameter.
     *
     * @param joinPoint
     * @param request
     */
    @Before(CONTROLLER_EXPRESION)
    public void logBefore(JoinPoint joinPoint, HttpServletRequest request) {

        log.debug("Entering in Method : {}", joinPoint.getSignature().getName());
        log.debug("Class Name :  {}", joinPoint.getSignature().getDeclaringTypeName());
        log.debug("Arguments :  {}", Arrays.toString(joinPoint.getArgs()));
        log.debug("Target class : {}", joinPoint.getTarget().getClass().getName());

        if (null != request) {
            log.debug("Start Header Section of request ");
            log.debug("Method Type : {}", request.getMethod());
            Enumeration headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement().toString();
                String headerValue = request.getHeader(headerName);
                log.debug("Header Name: {} Header Value : {}", headerName, headerValue);
            }
            log.debug("Request Path info : {}", request.getServletPath());
            log.debug("End Header Section of request ");
        }
    }

    /**
     * After -> All method within resource annotated with @Controller annotation.
     *
     * @param joinPoint
     * @param result
     */
    @AfterReturning(pointcut = CONTROLLER_EXPRESION, returning = "result")
    public void logAfter(JoinPoint joinPoint, Object result) {
        String returnValue = this.getValue(result);
        log.debug("Method Return value : {}", returnValue);
    }

    /**
     * After -> Any method within resource annotated with @Controller annotation and throws an exception ...Log it 
     * @param joinPoint
     * @param exception
     */
    @AfterThrowing(pointcut = CONTROLLER_EXPRESION, throwing = "exception")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable exception) {
        log.error("An exception has been thrown in {} {}", joinPoint.getSignature().getName(), " ()");
        log.error("Cause : {}", exception.getCause());
    }

    /**
     * Around -> Any method within resource annotated with @Controller annotation. 
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(CONTROLLER_EXPRESION)
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {

        long start = System.currentTimeMillis();
        try {
            String className = joinPoint.getSignature().getDeclaringTypeName();
            String methodName = joinPoint.getSignature().getName();
            Object result = joinPoint.proceed();
            long elapsedTime = System.currentTimeMillis() - start;
            log.debug("Method {}.{} () execution time :  {} ms", className, methodName, elapsedTime);

            return result;
        } catch (IllegalArgumentException e) {
            log.error("Illegal argument {} in {}()", Arrays.toString(joinPoint.getArgs()), joinPoint.getSignature().getName());
            throw e;
        }
    }

    private String getValue(Object result) {
        String returnValue = null;
        if (null != result) {
            if (result.toString().endsWith("@" + Integer.toHexString(result.hashCode()))) {
                returnValue = ReflectionToStringBuilder.toString(result);
            } else {
                returnValue = result.toString();
            }
        }
        return returnValue;
    }
}

@PeterPenzov 你试过了吗?如果这对你有帮助,请标记它。 - Jonathan JOhx

3
根据您的代码,在RestTemplate中注册了一个空拦截器列表。尝试按以下方式更改您的代码:
@Bean
public RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();

    List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();

    // check if restTeamplate doesn't already have other interceptors
    if (CollectionUtils.isEmpty(interceptors)) { 
        interceptors = new ArrayList<>();
    }

    interceptors.add(new RestTemplateHeaderModifierInterceptor());
    restTemplate.setInterceptors(interceptors);
    return restTemplate;
}

更多信息请点击这里

该拦截器将用于处理发出的请求。

对于接收的请求,您需要从HandlerInterceptorAdapter继承自己的拦截器:

public class MyIncomeRequestInterceptor extends HandlerInterceptorAdapter {
    //...
}

然后,例如以下方式,在WebMvcConfigurer中注册:

@Bean
public WebMvcConfigurer webMvcConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new MyIncomeRequestInterceptor());
        }
    };
}

更多信息请点击这里

在这两种情况下,不需要从您的拦截器中创建bean(可以删除注释@Component)。

更新

一个可行的示例:

@Slf4j
@RestController
@ControllerAdvice
@SpringBootApplication
public class Application implements WebMvcConfigurer, ResponseBodyAdvice<Object> {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @GetMapping("/hello")
    public ResponseEntity<?> hello() {
        return ResponseEntity.ok(Map.of("message", "hello"));
    }

    @EventListener
    public void onReady(final ApplicationReadyEvent e) {
        Map result = restTemplate().getForObject("http://localhost:8080/hello", Map.class);
        if (result != null) {
            log.info("[i] Request result: '{}'", result.get("message"));
        }
    }

    @Bean
    public RestTemplate restTemplate() {

        ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory());
        RestTemplate restTemplate = new RestTemplate(factory);

        var interceptors = restTemplate.getInterceptors();
        if (CollectionUtils.isEmpty(interceptors)) interceptors = new ArrayList<>();

        interceptors.add(new OutgoingInterceptor());
        restTemplate.setInterceptors(interceptors);
        return restTemplate;
    }

    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        registry.addInterceptor(new IncomingInterceptor());
    }

    @Override
    public boolean supports(final MethodParameter returnType, final Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(final Object body, final MethodParameter returnType, final MediaType selectedContentType, final Class<? extends HttpMessageConverter<?>> selectedConverterType, final ServerHttpRequest request, final ServerHttpResponse response) {
        log.info("[i] ResponseBodyAdvice: response body {}", body);
        return body;
    }

    class OutgoingInterceptor implements ClientHttpRequestInterceptor {
        @Override
        public ClientHttpResponse intercept(final HttpRequest request, final byte[] bytes, final ClientHttpRequestExecution execution) throws IOException {
            log.info("[i] Outgoing interceptor: requested URL is '{}'", request.getURI());
            ClientHttpResponse response = execution.execute(request, bytes);
            String body = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset());
            log.info("[i] Outgoing interceptor: response body is '{}'", body);
            return response;
        }
    }

    class IncomingInterceptor implements HandlerInterceptor {
        @Override
        public void postHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final ModelAndView mw) throws Exception {
            log.info("[i] Incoming interceptor: requested URL is '{}'", request.getRequestURL().toString());
        }
    }
}

为了记录控制器每个方法的响应体,我认为最好使用带有@ControllerAdvice注解的ResponseBodyAdvice实现(请参见上面的代码)。

结果:

2019-01-16 14:05:07.260  : [i] Outgoing interceptor: requested URL is 'http://localhost:8080/hello'
2019-01-16 14:05:07.366  : [i] ResponseBodyAdvice: response body {message=hello}
2019-01-16 14:05:07.383  : [i] Incoming interceptor: requested URL is 'http://localhost:8080/hello'
2019-01-16 14:05:07.387  : [i] Outgoing interceptor: response body is '{"message":"hello"}'
2019-01-16 14:05:07.402  : [i] Request result: 'hello'

Repo: sb-web-interceptors-demo


由于某些原因它无法工作,我找不到原因。有什么办法可以找到问题吗? - Peter Penzov
没有 @Component,我会得到 java.lang.NoClassDefFoundError: Failed to link org/springframework/boot/autoconfigure/data/rest/SpringBootRepositoryRestConfigurer。 - Peter Penzov
@PeterPenzov 只需克隆我的 repo,看看它是如何工作的。 - Cepr0
我会尝试,但是在我的情况中使用“var”会出错。我该如何替换它? - Peter Penzov
好的,我做到了。还有一个问题,当我尝试打印正文时,会出现Caused by: java.io.IOException: UT010029: Stream is closed。有没有解决方法? - Peter Penzov
显示剩余13条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接