Controller interceptor that process endpoint annotation in WebFlux - spring

My team is in the middle of migrating our Spring MVC extensions to WebFlux.
We've got a feature that lets our clients customize metric of controller method. To do that we've created our annotation that is processed by HandlerInterceptorAdapter.
The problem is that I can't see any equivalent of this in Spring WebFlux. I can't use WebFilter because Spring does not know yet which endpoint will be called. How can I implement that?
The closest workaround I found is to use RequestMappingHandlerMapping and somehow build a map of Map<String(path), HandlerMethod>, but this is cumbersome and error prone in my opinion.
Is there any better way to solve this?
Edit:
It goes like this
public class MeteredHandlerInterceptor extends HandlerInterceptorAdapter {
public MeteredHandlerInterceptor() {
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// I save start time of method
return true;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// I read endpoint method from the HandlerMethod, I apply any customisation by our custom #MeteredEndpoint annotation (for example custom name) and I save it in MeterRegistry
}
}
I haven't coded workaround yet because I didn't want to invest time in it, but I see that I could obtain HandlerMethod for path, but I'm not sure I will receive same HandlerMethod as I normally would when the controller is called.

Maybe little bit late, but it can still be useful for someone...
I have not found an easy way to do that, the best I was able to create is a HandlerAdapter bean that intercepts handling in the following way:
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public HandlerAdapter handlerAdapter(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
return new HandlerAdapter() {
#Override
public boolean supports(Object handler) {
return handler instanceof HandlerMethod;
}
#Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
// your stuff here...
// e.g. ((HandlerMethod) handler).getMethod().getAnnotations()...
return requestMappingHandlerAdapter.handle(exchange, handler);
}
};
}
The idea is that this adapter is used for all HandlerMethod handlers (those are the ones created by collecting annotated methods from #Controllers) and delegates the handling to the RequestMappingHandlerAdapter (that would be used directly for HandlerMethod handlers in normal case, notice the #Order annotation here).
The point is you can put your code before/after the invocation of the handle method and you are aware of the method being invoked at this point.

Solution:
#Component
class AuditWebFilter(
private val requestMapping: RequestMappingHandlerMapping
): WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
// if not to call - then exchange.attributes will be empty
// so little early initialize exchange.attributes by calling next line
requestMapping.getHandler(exchange)
val handlerFunction = exchange.attributes.get(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE) as HandlerMethod
val annotationMethod = handlerFunction.method.getAnnotation(MyAnnotation::class.java)
// annotationMethod proccesing here
}
}

Related

MissingServletRequestParameterException intermittently being thrown even though request parameter is provided

I've got a Spring Boot 2.7.3 app with the following controller defined:
#RestController
#EnableAutoConfiguration
public class TrainController {
#CrossOrigin(origins = "http://localhost:3000")
#RequestMapping(value = "/trains/history", method = RequestMethod.GET)
public List<TrainStatus> getTrainStatusesForTimestamp(
#RequestParam long timestamp
) {
// do stuff
}
}
Invoking this API endpoint typically works just fine, certainly when I'm running the app locally, but in production under heavier load, e.g. repeated calls to this API endpoint in parallel with lots of calls to other API endpoints defined by my app across multiple controllers, I start to see messages like these in my logs:
2022-09-06 20:48:37.939 DEBUG 19282 --- [https-openssl-nio-443-exec-10] o.s.w.f.CommonsRequestLoggingFilter : Before request [GET /trains/history?timestamp=1662511707]
2022-09-06 20:48:37.945 WARN 19282 --- [https-openssl-nio-443-exec-10] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'timestamp' for method parameter type long is not present]
2022-09-06 20:48:37.945 DEBUG 19282 --- [https-openssl-nio-443-exec-10] o.s.w.f.CommonsRequestLoggingFilter : After request [GET /trains/history?timestamp=1662511707]
(The CommonsRequestLoggingFilter DEBUG log lines are coming from a bean I've defined in accordance with this doc; I was curious if the required timestamp parameter was actually being defined or not, which is why I added it.)
Furthermore, when these errant MissingServletRequestParameterException exceptions are thrown, the response is a 400 Bad Request. I've confirmed from the client side of things that timestamp is indeed being included as a request parameter, and the Spring Boot app logs seem to confirm this, yet I'm intermittently seeing these exceptions under heavy load.
What's going on? Am I hitting some kind of connection or thread limit defined by Tomcat or something? As far as I can tell the app has plenty of additional headroom with regards to memory.
Thanks in advance for any help anyone can provide!
For reference, here are some apparently similar issues I've found:
Is there any situation QueryString is present but HttpServletRequest.getParameterMap() is empty?
After reading this blog post, I believe I've just figured out what's going on: I've got another filter PublicApiFilter operating on a separate set of API endpoints that is asynchronously invoking a function where I pass the request object, i.e. the instance of HttpServletRequest, into it and invoke various methods offered by it. These asynchronous operations on these requests appear to be affecting subsequent requests, even ones to other API endpoints not covered by PublicApiFilter. I was able to simply make the invocation of this function synchronous instead of asynchronous by removing the #Async annotation I was using and now the issue appears to have been resolved!
Here are some snippets of my code in case it's useful to someone else someday:
#EnableScheduling
#SpringBootApplication // same as #Configuration #EnableAutoConfiguration #ComponentScan
#EnableAsync
public class Application implements WebMvcConfigurer, AsyncConfigurer {
// ...
#Override // AsyncConfigurer
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(1);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
#Override // AsyncConfigurer
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
#Component
public class PublicApiFilter extends GenericFilterBean {
private final PublicApiService publicApiService;
#Autowired
public PublicApiFilter(PublicApiService publicApiService) {
this.publicApiService = publicApiService;
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// ...
chain.doFilter(request, response);
this.publicApiService.logRequest(httpRequest);
}
}
#Service
public class PublicApiService {
// ...
#Async // <- simply removing this annotation appears to have done the trick!
public void logRequest(HttpServletRequest request) {
// invoke request.getRequestURI(), request.getHeader(...), request.getRemoteAddr, and request.getParameterMap() for logging purposes
}
}
Do not pass HttpServletRequest into any async method!
Must reads for solving above problem:
Never pass a request to an asynchronous thread! There are pits!
How to correctly use request in asynchronous threads in springboot
Occasional MissingServletRequestParameterException, who moved my parameters?

How to get request in MyBatis Interceptor

I want to measure time of sql execution which will be run by MyBatis (Spring Boot project) and bind that with other request parameters, so I can get full info about performance issues regarding specific requests. For that case I have used MyBatis Interceptor on following way:
#Intercepts({
#Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
#Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class QueryMetricsMybatisPlugin implements Interceptor {
#Override
public Object intercept(Invocation invocation) throws Throwable {
Stopwatch stopwatch = Stopwatch.createStarted();
Object result = invocation.proceed();
stopwatch.stop();
logExectionTime(stopwatch, (MappedStatement) invocation.getArgs()[0]);
return result;
}
}
Now when it come to binding with request, I want to store those metrics in request as attribute. I have tried this simple solution to get request, but that was not working since request was always null (I have read that this solution won't work in async methods, but with MyBatis Interceptor and its methods I think that's not the case):
#Autowired
private HttpServletRequest request;
So, the question is how properly get request within MyBatis interceptor?
One important note before I answer your question: it is a bad practice to access UI layer in the DAO layer. This creates dependency in the wrong direction. Outer layers of your application can access inner layers but in this case this is other way round. Instead of this you need to create a class that does not belong to any layer and will (or at least may) be used by all layers of the application. It can be named like MetricsHolder. Interceptor can store values to it, and in some other place where you planned to get metrics you can read from it (and use directly or store them into request if it is in UI layer and request is available there).
But now back to you question. Even if you create something like MetricsHolder you still will face the problem that you can't inject it into mybatis interceptor.
You can't just add a field with Autowired annotation to interceptor and expect it to be set. The reason for this is that interceptor is instantiated by mybatis and not by spring. So spring does not have chance to inject dependencies into interceptor.
One way to handle this is to delegate handling of the interception to a spring bean that will be part of the spring context and may access other beans there. The problem here is how to make that bean available in interceptor.
This can be done by storing a reference to such bean in the thread local variable. Here's example how to do that. First create a registry that will store the spring bean.
public class QueryInterceptorRegistry {
private static ThreadLocal<QueryInterceptor> queryInterceptor = new ThreadLocal<>();
public static QueryInterceptor getQueryInterceptor() {
return queryInterceptor.get();
}
public static void setQueryInterceptor(QueryInterceptor queryInterceptor) {
QueryInterceptorRegistry.queryInterceptor.set(queryInterceptor);
}
public static void clear() {
queryInterceptor.remove();
}
}
Query interceptor here is something like:
public interface QueryInterceptor {
Object interceptQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException;
}
Then you can create an interceptor that will delegate processing to spring bean:
#Intercepts({
#Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class }),
#Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) })
public class QueryInterceptorPlugin implements Interceptor {
#Override
public Object intercept(Invocation invocation) throws Throwable {
QueryInterceptor interceptor = QueryInterceptorRegistry.getQueryInterceptor();
if (interceptor == null) {
return invocation.proceed();
} else {
return interceptor.interceptQuery(invocation);
}
}
#Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
#Override
public void setProperties(Properties properties) {
}
}
You need to create an implementation of the QueryInterceptor that does what you need and make it a spring bean (that's where you can access other spring bean including request which is a no-no as I wrote above):
#Component
public class MyInterceptorDelegate implements QueryInterceptor {
#Autowired
private SomeSpringManagedBean someBean;
#Override
public Object interceptQuery(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
// do whatever you did in the mybatis interceptor here
// but with access to spring beans
}
}
Now the only problem is to set and cleanup the delegate in the registry.
I did this via aspect that was applied to my service layer methods (but you can do it manually or in spring mvc interceptor). My aspect looks like this:
#Aspect
public class SqlSessionCacheCleanerAspect {
#Autowired MyInterceptorDelegate myInterceptorDelegate;
#Around("some pointcut that describes service methods")
public Object applyInterceptorDelegate(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
QueryInterceptorRegistry.setQueryInterceptor(myInterceptorDelegate);
try {
return proceedingJoinPoint.proceed();
} finally {
QueryInterceptorRegistry.clear();
}
}
}

Control #RestController availability programmatically

Is it possible to control a #RestController programmatically to enable it or disable it? I don't want to just write code in each #RequestMapping method to do some kind of if (!enabled) { return 404Exception; }
I've seen this question but that works only at startup time. What I need is really something that would allow me to enable or disable the controller multiple times.
I've thought of different ways but don't know which are doable in spring.
Actually control the container (jetty in my case) so requests to that particular endpoint are disabled
Somehow control RequestMappingHandlerMapping since it seems to be that class that does the mapping between urls and controllers
control the lifecycle of the #RestController component so that i can create it and destroy it at will, but then i'm not sure how to trigger the mapping to the endpoint
If the end result is that you want to respond with a 404 when you decide that a specific endpoint should be disabled then you could write an interceptor which checks whether your enabled condition is false and, if so, sets the response accordingly.
For example:
#Component
public class ConditionalRejectionInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String requestUri = request.getRequestURI();
if (shouldReject(requestUri)) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return false;
}
return super.preHandle(request, response, handler);
}
private boolean shouldReject(String requestUri) {
// presumably you have some mechanism of inferring or discovering whether
// the endpoint represented by requestUri should be allowed or disallowed
return ...;
}
}
In Spring Boot, registering your own interceptor just involves implementing a WebMvcConfigurerAdapter. For example:
#Configuration
public class CustomWebMvcConfigurer extends WebMvcConfigurerAdapter {
#Autowired
private HandlerInterceptor conditionalRejectionInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
// you can use .addPathPatterns(...) here to limit this interceptor to specific endpoints
// this could be used to replace any 'conditional on the value of requestUri' code in the interceptor
registry.addInterceptor(conditionalRejectionInterceptor);
}
}

Spring MVC accessing Spring Security ConfigAttributes?

I want to produce HTTP Response Body with an error message referencing something like _"missing ... 'CUSTOM_AUTHORITY'"_ in addition to a 403 Forbidden HTTP Status code.
My application is Spring Boot with a Spring-Security-Secured #PreAuthorize method within a Spring-MVC-REST #Controller:
MyController
#Controller
#RequestMapping("/foo")
public FooController{
#PreAuthorize("hasAuthority('CUSTOM_AUTHORITY')")
public Object getSomething(){ ... }
}
GlobalExceptionHandlerResolver
#ControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(AccessDeniedException.class)
#ResponseStatus(HttpStatus.FORBIDDEN)
public Object forbidden(AccessDeniedException exception){ ... }
}
What I want is to expose/inject Collection<ConfigAttribute>. The Spring Security docs reference it.
There doesn't seem to be a straightforward way of accomplishing this. The AccessDecisionManager (which is AffirmativeBased) throws the AccessDeniedException with none of the information you want. So if you want to "expose/inject" the Collection<ConfigAttribute>, you'll want to provide your own AccessDecisionManager that throws a custom exception that holds the ConfigAttributes.
The easiest way to do this could be to wrap the default AccessDecisionManager with your own and delegate method calls to it:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled=true)
CustomMethodSecurityConfig extends GlobalMethodSecurityConfiguration
#Override
protected AccessDecisionManager accessDecisionManager() {
AccessDecisionManager default = super.accessDecisionManager();
MyCustomDecisionManager custom = new CustomDecisionManager(default);
}
}
You could define your custom AccessDecisionManager as follows:
public class MyCustomDecisionManager implements AccessDecisionManager {
private AccessDecisionManager default;
public MyCustomDecisionManager(AccessDecisionManager acm) {
this.default = acm;
}
#Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException{
try {
default.decide(authentication, object, configAttributes)
} catch(AccessDeniedException ex) {
throw new CustomAccessDeniedException(ex.getMessage(), configAttributes);
}
}
// other methods delegate to default
}
Now whenever access is denied, you will get an exception that holds the Collection<ConfigAttribute>.
Your custom exception could look like this:
public class CustomAccessDeniedException extends AccessDeniedException {
private Collection<ConfigAttribute> attributes;
public CustomAccessDeniedException(String message, Collection<ConfigAttribute> attr) {
super(message);
this.attributes = attr;
}
public Collection<ConfigAttribute> getAttributes() {
return this.attributes;
}
}
Now your #ExceptionHandler could handle your CustomAccessDeniedException and have access to the ConfigAttributes.
HOWEVER...
I am not sure that will provide you with the error message you wanted. The ConfigAttribute interface only has one method:
String getAttribute();
And the javadoc states:
If the ConfigAttribute cannot be expressed with sufficient precision as a String, null should be returned.
Since we can't rely on the interface method, how you deal with each ConfigAttribute will be heavily dependent on the type of the particular object you're dealing with.
For example, the ConfigAttribute that corresponds to #PreAuthorize("hasAuthority('CUSTOM_AUTHORITY')") is PreInvocationExpressionAttribute, and to print something that resembles what you want, you could do:
PreInvocationExpressionAttribute attr = (PreInvocationExpressionAttribute)configAttribute;
String expressionString = attr.getAuthorizeExpression().getExpressionString();
System.out.println(expressionString); // "hasAuthority('CUSTOM_AUTHORITY')"
That's the major drawback. Also, you would get ALL the ConfigAttributes, not necessarily the ones that failed.

How to checkin interceptor whether a controller triggered a redirect

In my Spring MVC project I added an interceptor class, to check, whether a redirect has been triggered.
Here is my controller-class:
#Controller
public class RedirectTesterController {
#RequestMapping (value="/page1")
public String showPage1(){
return "page1";
}
#RequestMapping (value="/submit1")
public String submitPage1(){
return "redirect:/page2";
}
#RequestMapping (value="/page2")
public String showPage2(){
return "page2";
}
}
So if I call e.g.
localhost:8080/MyContext/submit1
the method "submitPage1" is executed.
Now - the server tells the client, to call
localhost:8080/MyContext/page2
which is also working.
So - I want to step into that process, after method "submitPage1"has been executed.
In my mind there should be some order/command in the httpResponse, which I could ask.
To check that, I made a breakpoint in my interceptor class in the method: "postHandle" - bit since then, I have no idea how to continue.
I tried to read the outputStream - but doing so crashes my application. (leads to an exception --> outputStream has already been called..).
Isn't there an easy solution for that ?
Following example shows how to test if a view is a redirect:
#Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptorAdapter() {
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
if (modelAndView != null && StringUtils.startsWithIgnoreCase(modelAndView.getViewName(), "redirect:")) {
// handle redirect...
}
}
});
}
}
See: HandlerInterceptorAdapter, StringUtils
Spring MVC Documentation: Intercepting requests with a HandlerInterceptor

Resources