1

Is there a built in Spring way to say give me the RestController + method for "/customers/a" (/customers/{id})? I.e. do the standard spring routing, but know which class/method is going to be called?

I'm trying to, in a central location, figure out based on the route if that method has a specific annotation. For centralized request / response logging.

I can iterate through the app for rest controllers and build the map myself, but wondering if there is anything exposed already to know where a request would go to?

Thanks.

SledgeHammer
  • 7,338
  • 6
  • 41
  • 86
  • Do you want to intercept all endpoint calls to logging it? did I get it right? – Roberto Manfreda Nov 23 '19 at 00:36
  • @RobertoManfreda I am using Logbook to do the logging... the REAL issue that I'm trying to resolve is that validation annotations happen BEFORE OAuth, so I get log entries from unauthorized requests... but I need to know if the particular method allows anonymous. – SledgeHammer Nov 23 '19 at 00:54
  • As @MarkBramnik said you can use the Spring AOP module. Defining an advice with `@RestController` and allMethods pointcuts. Using the `@Before` annotation you can proxy the calls to your endpoints before they occurs. I think that this is the clean way to do something before a method occurs. You can inject an `@Autowired` HttpServletRequest in your endpoints and do some validation logic from your AOP advice – Roberto Manfreda Nov 23 '19 at 01:30
  • @RobertoManfreda you can ready the request body from a pointcut and it won't break for the pass through to the real method? From the link, it looks like you just call pointcut.proceed()? wouldn't the request stream be disposed after I read it? – SledgeHammer Nov 23 '19 at 03:19
  • Yes you are right. Calling the proceed() method you are saying to the proxy class to forward the request to the original class! But you can, for example: intercept the request using your advice, validate the request, if the validation is ok you call the prooceed() method on join point else you can return a new HttpResponse directly from the advice (tipically a customized ResponseEntity) and set from there error messages and all the necessary! https://stackoverflow.com/questions/31075594/aop-around-return-bad-request-response – Roberto Manfreda Nov 23 '19 at 03:47
  • @RobertoManfreda Been playing around with this for a few hours now -- not much cleaner tbh, actually dirtier -- only different parts are dirty lol -- in this solution, I have to use Around so I can get duration and stuff (or come up with some correlation scheme) and with Around, I have to catch the exception and translate them to status codes, etc. I can get the body from the args though, just need to serialize it and then get that all to work with exception handling, etc. – SledgeHammer Nov 23 '19 at 06:33
  • You can mix the Around advice and the AfterThrowing advice. Do your validation logic and throw a new exception when something is broken. – Roberto Manfreda Nov 23 '19 at 16:01

2 Answers2

2

You can use the Spring AOP module. And i think that a clean way (managing response without break the RestController endpoints flow) can be the following.

First of all add the spring-boot-starter-aop dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Let's see the code.

This is our ExampleRequest

import lombok.Data;

@Data
public class ExampleRequest {
    String name;
}

We assume that we want to validate our request -> if name == "anonymous" we want to return an HTTP status BAD_REQUEST.


This is our TestController

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
    @Anonymous
    @GetMapping("")
    public ResponseEntity<String> test(ExampleRequest exampleRequest, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        else return new ResponseEntity<>(HttpStatus.OK);
    }    
}

We pass as arguments the ExampleRequest that is our request and BindingResult that we will use later. Anyway if bindingResult has errors our validation logic is broken so we need to return the error we want!

The @Anonymous is our custom annotation that we will use to tell to our Advice(s) which methods, or better - which classes (an Aspect is an entire class "proxied"), we want to proxy

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Anonymous {

}  

And our TestAspect

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;

@Aspect
@Component
public class TestAspect {

    @Before(value = "@annotation(Anonymous) && args(exampleRequest, bindingResult,..)", argNames = "joinPoint,exampleRequest,bindingResult")
    public Object logExecutionTime(JoinPoint joinPoint, ExampleRequest exampleRequest, BindingResult bindingResult) throws Throwable {
        if (exampleRequest.getName().equals("anonymous")) bindingResult.rejectValue("name", "incorrect");
        return joinPoint;
    }

}   

where we are saying: BEFORE the execution of EVERY METHODS annotated with our custom ANONYMOUS annotation with these ARGUMENTS execute this logic!

Obviously you will have your customized validation logic so implement it.


As I was saying in comments above i think you could return errors directly from the proxied class but at the same time I think that this example is a cleaner way to work with AOP (using BindingResult the flow of endpoints is respected and this technique does't break other validations of your model classes. You can use tha javax validation for example mixing in the advices and all will work perfectly!), personally I love this Spring AOP module but don't abuse it!

For reference: https://docs.spring.io/spring/docs/2.5.x/reference/aop.html

Roberto Manfreda
  • 2,345
  • 3
  • 25
  • 39
1

You might want to try an another approach:

Use Aspect Oriented programming supported by spring. Create an aspect that will act as an interceptor for all your controllers. The Advice will be called right before (or "instead of", depending on the type of advice) the actual rest method so that you'll be able to get the access and then will forward the execution to the real controller. In the code of aspect you could introspect the method to be called on a real controller and figure out whether it has annotations.

I've found an example of doing exactly this Here (You'll have to slightly change the point-cut definition to make the aspect applicable to the rest controller of your choice).

Mark Bramnik
  • 39,963
  • 4
  • 57
  • 97
  • Hmm... I am using Logbook with a sink right now, but I ran into an issue where it seems like validation annotations are triggering a call to the sink before oauth is happening so I wanted to filter out an anonymous request by checking if the method it was going to go to requires authorization. Will take a look at this AOP stuff though. The logbook library is intended for logging request / response, so it has some stuff done already for that. – SledgeHammer Nov 23 '19 at 01:00
  • One of the things I spotted right away in the link is that they don't appear to have a way to get the request body on a POST. The request stream is read-once. Logbook solves that by capturing it some other way. – SledgeHammer Nov 23 '19 at 01:03
  • Well, i cant comment on logbook, never used it, but in general request body cant be read twice. It possoble to read it once and provide a fake request object that will cache the body that has been read, but again it happens only once – Mark Bramnik Nov 23 '19 at 01:06
  • Yeah, I think that's what logbook does. They aren't AOP though, they're filter based. I've got logbook almost working. I was just trying to see if there is a magic getMethodInfoFromRoute() type method so I can see if it has a PreAuthorize annotation on it... pretty easy to enum the controllers... had to do it for my swagger setup, so I can just refactor that part to do this too and build a cache of routes that require authorization. – SledgeHammer Nov 23 '19 at 01:09
  • @SledgeHammer In a sense, filters _are_ AOP: They're `around` advice applied to every request. – chrylis -cautiouslyoptimistic- Nov 23 '19 at 02:28