SpringBoot:拦截器从请求中读取特定字段并将其设置在响应中
SpringBoot: Interceptor to read particular field from request and set it in the response
我们的 Spring Rest Controller 处理的所有请求和响应都有一个具有特定值的公共部分:
{
"common": {
"requestId": "foo-bar-123",
"otherKey1": "value1",
"otherKey2": "value2",
"otherKey3": "value3"
},
...
}
目前我所有的控制器功能都在读取 common
并将其手动复制到响应中。我想把它移到某种拦截器中。
我尝试使用 ControllerAdvice
和 ThreadLocal
:
@ControllerAdvice
public class RequestResponseAdvice extends RequestBodyAdviceAdapter
implements ResponseBodyAdvice<MyGenericPojo> {
private ThreadLocal<Common> commonThreadLocal = new ThreadLocal<>();
/* Request */
@Override
public boolean supports(
MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return MyGenericPojo.class.isAssignableFrom(methodParameter.getParameterType());
}
@Override
public Object afterBodyRead(
Object body,
HttpInputMessage inputMessage,
MethodParameter parameter,
Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
var common = (MyGenericPojo)body.getCommon();
if (common.getRequestId() == null) {
common.setRequestId(generateNewRequestId());
}
commonThreadLocal(common);
return body;
}
/* Response */
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return MyGenericPojo.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public MyGenericPojo beforeBodyWrite(
MyGenericPojo body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
body.setCommon(commonThreadLocal.get());
commonThreadLocal.remove();
return body;
}
}
这在我测试一次发送一个请求时有效。但是,当多个请求到来时,是否保证 afterBodyRead
和 beforeBodyWrite
在同一个线程中被调用?
如果不是,或者甚至不是,最好的方法是什么?
如果只是从请求复制到响应的元数据,您可以执行以下操作之一:
1- 将元数据存储在 request/response header 中,然后使用过滤器进行复制:
@WebFilter(filterName="MetaDatatFilter", urlPatterns ={"/*"})
public class MyFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("metaData", httpServletRequest.getHeader("metaData"));
}
}
2- 将工作转移到服务层,在那里您可以通过可重用的通用方法来完成处理,或者通过 AOP 运行
public void copyMetaData(whatEverType request,whatEverType response) {
response.setMeta(request.getMeta);
}
快速回答:RequestBodyAdvice
和 ResponseBodyAdvice
在同一个线程中为一个请求调用。
您可以在以下位置调试实现:ServletInvocableHandlerMethod#invokeAndHandle
虽然您这样做并不安全:
ThreadLocal
应该定义为 static final
,否则它类似于任何其他 class 属性
- 正文中抛出的异常将跳过
ResponseBodyAdvice
的调用(因此不会删除线程本地数据)
"More safe way":使请求体支持任何class(不仅仅是MyGenericPojo
),在afterBodyRead
方法中:
- 第一次通话
ThreadLocal#remove
- 检查类型是否为
MyGenericPojo
然后将公共数据设置为 threadlocal
我觉得不需要你自己的ThreadLocal
你可以使用请求属性。
@Override
public Object afterBodyRead(
Object body,
HttpInputMessage inputMessage,
MethodParameter parameter,
Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
var common = ((MyGenericPojo) body).getCommon();
if (common.getRequestId() == null) {
common.setRequestId(generateNewRequestId());
}
Optional.ofNullable((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.map(ServletRequestAttributes::getRequest)
.ifPresent(request -> {request.setAttribute(Common.class.getName(), common);});
return body;
}
@Override
public MyGenericPojo beforeBodyWrite(
MyGenericPojo body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(rc -> rc.getAttribute(Common.class.getName(), RequestAttributes.SCOPE_REQUEST))
.ifPresent(o -> {
Common common = (Common) o;
body.setCommon(common);
});
return body;
}
编辑
Optional
可以换成
RequestContextHolder.getRequestAttributes().setAttribute(Common.class.getName(),common,RequestAttributes.SCOPE_REQUEST);
RequestContextHolder.getRequestAttributes().getAttribute(Common.class.getName(),RequestAttributes.SCOPE_REQUEST);
编辑 2
关于线程安全
1) 标准的基于 servlet 的 Spring Web 应用程序我们有一个线程每个请求的场景。请求由其中一个工作线程通过所有过滤器和例程进行处理。处理链将从头到尾由同一个线程执行。因此 afterBodyRead
和 beforeBodyWrite
保证由同一个线程针对给定请求执行。
2) 您的 RequestResponseAdvice 本身是无状态的。我们使用 RequestContextHolder.getRequestAttributes()
这是 ThreadLocal 并声明为
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
ThreadLocal javadoc 指出:
his class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via
its get or set method) has its own, independently initialized copy of
the variable.
所以我没有看到这个 sulotion 有任何线程安全问题。
我也已经回答了这个问题,但我更喜欢用另一种方式来解决这类问题
在这种情况下我会使用 Aspect-s。
我已将其包含在一个文件中,但您应该创建适当的单独文件 类。
@Aspect
@Component
public class CommonEnricher {
// annotation to mark methods that should be intercepted
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EnrichWithCommon {
}
@Configuration
@EnableAspectJAutoProxy
public static class CommonEnricherConfig {}
// Around query to select methods annotiated with @EnrichWithCommon
@Around("@annotation(com.example.CommonEnricher.EnrichWithCommon)")
public Object enrich(ProceedingJoinPoint joinPoint) throws Throwable {
MyGenericPojo myGenericPojo = (MyGenericPojo) joinPoint.getArgs()[0];
var common = myGenericPojo.getCommon();
if (common.getRequestId() == null) {
common.setRequestId(UUID.randomUUID().toString());
}
//actual rest controller method invocation
MyGenericPojo res = (MyGenericPojo) joinPoint.proceed();
//adding common to body
res.setCommon(common);
return res;
}
//example controller
@RestController
@RequestMapping("/")
public static class MyRestController {
@PostMapping("/test" )
@EnrichWithCommon // mark method to intercept
public MyGenericPojo test(@RequestBody MyGenericPojo myGenericPojo) {
return myGenericPojo;
}
}
}
我们这里有一个注释 @EnrichWithCommon
,它标记了应该发生富集的端点。
我们的 Spring Rest Controller 处理的所有请求和响应都有一个具有特定值的公共部分:
{
"common": {
"requestId": "foo-bar-123",
"otherKey1": "value1",
"otherKey2": "value2",
"otherKey3": "value3"
},
...
}
目前我所有的控制器功能都在读取 common
并将其手动复制到响应中。我想把它移到某种拦截器中。
我尝试使用 ControllerAdvice
和 ThreadLocal
:
@ControllerAdvice
public class RequestResponseAdvice extends RequestBodyAdviceAdapter
implements ResponseBodyAdvice<MyGenericPojo> {
private ThreadLocal<Common> commonThreadLocal = new ThreadLocal<>();
/* Request */
@Override
public boolean supports(
MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return MyGenericPojo.class.isAssignableFrom(methodParameter.getParameterType());
}
@Override
public Object afterBodyRead(
Object body,
HttpInputMessage inputMessage,
MethodParameter parameter,
Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
var common = (MyGenericPojo)body.getCommon();
if (common.getRequestId() == null) {
common.setRequestId(generateNewRequestId());
}
commonThreadLocal(common);
return body;
}
/* Response */
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return MyGenericPojo.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public MyGenericPojo beforeBodyWrite(
MyGenericPojo body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
body.setCommon(commonThreadLocal.get());
commonThreadLocal.remove();
return body;
}
}
这在我测试一次发送一个请求时有效。但是,当多个请求到来时,是否保证 afterBodyRead
和 beforeBodyWrite
在同一个线程中被调用?
如果不是,或者甚至不是,最好的方法是什么?
如果只是从请求复制到响应的元数据,您可以执行以下操作之一:
1- 将元数据存储在 request/response header 中,然后使用过滤器进行复制:
@WebFilter(filterName="MetaDatatFilter", urlPatterns ={"/*"})
public class MyFilter implements Filter{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("metaData", httpServletRequest.getHeader("metaData"));
}
}
2- 将工作转移到服务层,在那里您可以通过可重用的通用方法来完成处理,或者通过 AOP 运行
public void copyMetaData(whatEverType request,whatEverType response) {
response.setMeta(request.getMeta);
}
快速回答:RequestBodyAdvice
和 ResponseBodyAdvice
在同一个线程中为一个请求调用。
您可以在以下位置调试实现:ServletInvocableHandlerMethod#invokeAndHandle
虽然您这样做并不安全:
ThreadLocal
应该定义为static final
,否则它类似于任何其他 class 属性- 正文中抛出的异常将跳过
ResponseBodyAdvice
的调用(因此不会删除线程本地数据)
"More safe way":使请求体支持任何class(不仅仅是MyGenericPojo
),在afterBodyRead
方法中:
- 第一次通话
ThreadLocal#remove
- 检查类型是否为
MyGenericPojo
然后将公共数据设置为 threadlocal
我觉得不需要你自己的ThreadLocal
你可以使用请求属性。
@Override
public Object afterBodyRead(
Object body,
HttpInputMessage inputMessage,
MethodParameter parameter,
Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
var common = ((MyGenericPojo) body).getCommon();
if (common.getRequestId() == null) {
common.setRequestId(generateNewRequestId());
}
Optional.ofNullable((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.map(ServletRequestAttributes::getRequest)
.ifPresent(request -> {request.setAttribute(Common.class.getName(), common);});
return body;
}
@Override
public MyGenericPojo beforeBodyWrite(
MyGenericPojo body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(rc -> rc.getAttribute(Common.class.getName(), RequestAttributes.SCOPE_REQUEST))
.ifPresent(o -> {
Common common = (Common) o;
body.setCommon(common);
});
return body;
}
编辑
Optional
可以换成
RequestContextHolder.getRequestAttributes().setAttribute(Common.class.getName(),common,RequestAttributes.SCOPE_REQUEST);
RequestContextHolder.getRequestAttributes().getAttribute(Common.class.getName(),RequestAttributes.SCOPE_REQUEST);
编辑 2
关于线程安全
1) 标准的基于 servlet 的 Spring Web 应用程序我们有一个线程每个请求的场景。请求由其中一个工作线程通过所有过滤器和例程进行处理。处理链将从头到尾由同一个线程执行。因此 afterBodyRead
和 beforeBodyWrite
保证由同一个线程针对给定请求执行。
2) 您的 RequestResponseAdvice 本身是无状态的。我们使用 RequestContextHolder.getRequestAttributes()
这是 ThreadLocal 并声明为
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
ThreadLocal javadoc 指出:
his class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.
所以我没有看到这个 sulotion 有任何线程安全问题。
我也已经回答了这个问题,但我更喜欢用另一种方式来解决这类问题
在这种情况下我会使用 Aspect-s。
我已将其包含在一个文件中,但您应该创建适当的单独文件 类。
@Aspect
@Component
public class CommonEnricher {
// annotation to mark methods that should be intercepted
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface EnrichWithCommon {
}
@Configuration
@EnableAspectJAutoProxy
public static class CommonEnricherConfig {}
// Around query to select methods annotiated with @EnrichWithCommon
@Around("@annotation(com.example.CommonEnricher.EnrichWithCommon)")
public Object enrich(ProceedingJoinPoint joinPoint) throws Throwable {
MyGenericPojo myGenericPojo = (MyGenericPojo) joinPoint.getArgs()[0];
var common = myGenericPojo.getCommon();
if (common.getRequestId() == null) {
common.setRequestId(UUID.randomUUID().toString());
}
//actual rest controller method invocation
MyGenericPojo res = (MyGenericPojo) joinPoint.proceed();
//adding common to body
res.setCommon(common);
return res;
}
//example controller
@RestController
@RequestMapping("/")
public static class MyRestController {
@PostMapping("/test" )
@EnrichWithCommon // mark method to intercept
public MyGenericPojo test(@RequestBody MyGenericPojo myGenericPojo) {
return myGenericPojo;
}
}
}
我们这里有一个注释 @EnrichWithCommon
,它标记了应该发生富集的端点。