MiguelSlv
MiguelSlv

Reputation: 15103

How to Access a Request-Scoped Bean in a Spring Exception Handler?

I need to log trace information using a Tracer bean, which is request-scoped, in both my REST method and exception handler. The Tracer bean is consumed in the REST method to log the trace information at the start of processing the request, and I want to use the same trace message in the exception handler when an exception occurs.

However, I'm facing difficulties accessing the request-scoped Tracer bean in my exception handler. I have tried both rest controller @ExceptionHhandler annotation in the REST controller and @ControllerAdvice, but neither seams to support access to a scope bean.

Here's my setup:

@RestController
public class MyController {

    @Bean
    @RequestScope
    Tracer tracer(){
        return new Tracer();
    }
    @GetMapping(path = "list")
    public String list(Tracer trace){
        return  trace.getCorrelationId();
    }

    @GetMapping(path = "throw")
    public String throwEx(Tracer trace,HttpServletRequest req){
         throw new RuntimeException("Ops");
    }


    @ExceptionHandler({Exception.class})
    ResponseEntity<String> defaultExceptionHandler(Tracer tracer, HttpServletRequest req, Exception ex) {
    
        return ResponseEntity
                .internalServerError()
                .body("ERROR: " + tracer.getCorrelationId());

    }
}

The tracer (for completeness):

@Getter
public class Tracer {
    private final String requestId;
    private final String correlationId;


    public Tracer() {
        requestId = null;
        correlationId = UUID.randomUUID().toString();
    }
    public Tracer(String requestId, String correlationId) {
        this.requestId = requestId;
        this.correlationId = correlationId;
    }
}

Here the test:

@SpringBootTest
@AutoConfigureMockMvc
class MyControllerTest {
    @Autowired
    protected MockMvc mockMvc;

    @Test
    void testExceptionHandler() throws Exception {
  
        mockMvc
                .perform(MockMvcRequestBuilders.get("/throw")
                )
                .andDo(MockMvcResultHandlers.print())
                .andReturn()
                .getResponse()
                .getContentAsString();            
    }
}

And here is the problem:

Could not resolve parameter [0] in org.springframework.http.ResponseEntity<java.lang.String> com.baeldung.scopes.MyController.defaultExceptionHandler(com.baeldung.scopes.Tracer,jakarta.servlet.http.HttpServletRequest,java.lang.Exception): No suitable resolver

It seems that I cannot directly inject the Tracer bean into the exception handler method.

I'm I missing something here? Is there another approach to do this problem?

Any help or suggestions would be greatly appreciated!

Upvotes: 0

Views: 97

Answers (2)

SoT
SoT

Reputation: 1203

UPDATE: Most correct way is this

Maybe "dirty", but at least it works:

@Getter
public class YourException extends RuntimeException {

  private final Tracer tracer;
  
  //Your constructors
}

@GetMapping(path = "throw")
public String throwEx(Tracer trace,HttpServletRequest req){
  throw new YourException("Ops", tracer);  -- pass your tracer to the constructor
}


@ExceptionHandler({YourException.class})
ResponseEntity<String> defaultExceptionHandler(HttpServletRequest req, YourException ex) {
    
  return ResponseEntity
        .internalServerError()
        .body("ERROR: " + ex.getTracer().getCorrelationId());

}

Other solution is ThreadLocal (spring web-mvc uses thread-per-request model)

public class TracerInfo {
  private static final ThreadLocal<Tracer> TRACER = new ThreadLocal<>();

  public static void set(Tracer tracer) {
    TRACER.set(tracer);
  }

  public static Tracer get() {
    return TRACER.get();
  }
}

@GetMapping(path = "throw")
public String throwEx(Tracer trace,HttpServletRequest req) {
  TracerInfo.set(tracer);
  throw new YourException("Ops");
}

@ExceptionHandler({Exception.class})
ResponseEntity<String> defaultExceptionHandler(HttpServletRequest req, Exception ex) {
    
  return ResponseEntity
        .internalServerError()
        .body("ERROR: " + TracerInfo.get().getCorrelationId());

}

Ps: fix compile error if any since I do not have IDE right now

Upvotes: 0

Erik
Erik

Reputation: 61

Another approach. Add annotations @RequestScope and @Component to Tracer:

@Getter
@Component
@RequestScope
public class Tracer {
    private final String requestId;
    private final String correlationId;

    public Tracer() {
        requestId = null;
        correlationId = UUID.randomUUID().toString();
    }

    public Tracer(String requestId, String correlationId) {
        this.requestId = requestId;
        this.correlationId = correlationId;
    }
}

Inject it in your controller, and remove the trace method parameters.

@RestController
@Log4j2
public class MyController {

    private Tracer tracer; // use instance variable

    public MyController(Tracer tracer) {
        this.tracer = tracer;
    }

    @GetMapping(path = "list")
    public String list() {
        return tracer.getCorrelationId();
    }

    @GetMapping(path = "throw")
    public String throwEx(HttpServletRequest req) {
        log.info(tracer.getCorrelationId());
        throw new RuntimeException("Ops");
    }

    @ExceptionHandler({Exception.class})
    ResponseEntity<String> defaultExceptionHandler(HttpServletRequest req, Exception ex) {

        return ResponseEntity
                .internalServerError()
                .body("ERROR: " + tracer.getCorrelationId()); // use the instance variable

    }
}

Upvotes: 1

Related Questions