问题
Considering that we are using Kotlin
, Spring Boot
, annotations and other related libraries.
If we have a situation in which our code throws an exception, how could we automatically retrieve the method parameters values in the moment of that exception?
Can we do this using AOP, Spring Interceptors or other techniques?
We would like to have this to enrich our error messages so we could replicate the errors from where they occurred.
Please note that we are searching for a solution that we don't need to annotate all possible methods but something that would handle the code when an exception occurs. We can use the Java stacktrace elements to retrieve some useful information like the method, line and file where the exception occurred but we don't have the parameters values there.
In Spring we have the Controller Advice feature that we can use to handle all of our exceptions, so we would like to put something there for this purpose, for example.
Edit
Adding some example code:
fun exceptionHandler(throwable: Throwable) {
logger.severe("""
Error ${throwable.message}
File: ${throwable.stackTrace[2].fileName}
Class: ${throwable.stackTrace[2].className}
Method: ${throwable.stackTrace[2].methodName}
Line: ${throwable.stackTrace[2].lineNumber}
Parameters: ## Somehow get the parameters values here, in this case "Hello, 1, false"
""".trimIndent())
}
fun myController() {
myMethodWithErrors("Hello", 1, false)
}
fun myMethodWithErrors(param1: String, param2: Int, param3: Boolean) {
throw RuntimeException("Some bad thing happened here when executing this code.")
}
回答1:
I assume that you were talking about rest API parameters and not every single java method parameter. You can implement controller advice that captures all exceptions in your rest API calls.
@ControllerAdvice
public class ExceptionHandler {
@ExceptionHandler(value = [Exception::class])
@ResponseBody
fun onException(exception: Exception, request: WebRequest): ResponseEntity<ErrorDetailsClass> {
log.error("error when request with parameters ${request.parameterMap} ")
return buildDetails(request)
}
}
In this way, you can do both retrieve a proper error message and also log something internally for error tracking purposes.
回答2:
With Spring AOP this requirement can be met with @AfterThrowing advice.
Following example Aspect will intercept all method calls under package org.aop.bean.impl
that exits with an exception . We can further filter to the specific exception type with throwing
attribute. The given example filters out the methods exiting with IllegalArgumentException
.
The arguments during the method call can be obtained with joinpoint.getArgs()
method.
@Aspect
@Component
public class ExceptionLoggerAspect {
@Pointcut("execution(* org.aop.bean.impl..*(..))")
public void allExceptions() {
}
@AfterThrowing(pointcut = "allExceptions()",throwing="ex")
public void logException(JoinPoint jp , IllegalArgumentException ex) {
Object[] args= jp.getArgs();
for(Object obj:args) {
System.out.println(obj);
}
}
}
From the docs
Often, you want the advice to run only when exceptions of a given type are thrown, and you also often need access to the thrown exception in the advice body. You can use the throwing attribute to both restrict matching (if desired — use Throwable as the exception type otherwise) and bind the thrown exception to an advice parameter
回答3:
The example I'm writing is in spring-boot using org.springframework.web.bind.annotation.ExceptionHandler annotation
It works perfectly fine for me
Suppose I made a Get request to https://example.com/user-api/users/a535c777-c906-45e2-b1c3-940965a507f2q , then our api validates if that user-id exists or not and if not throws a proper message including which parameters are invalid or has errors.
Response ex 1:
{
"apierror": {
"dateTime": "2020-02-13T06:24:14.985",
"timestamp": "1581603854985",
"status": 404,
"error": "Not Found",
"message": "User not found",
"debugMessage": null,
"errors": [
{
"field": "userId",
"rejectedValue": "a535c777-c906-45e2-b1c3-940965a507f2q",
"message": "User not found with userId:a535c777-c906-45e2-b1c3-940965a507f2q"
}
]
}
}
Response ex2:
{
"apierror": {
"dateTime": "2020-02-13T06:43:23.377",
"timestamp": "1581605003377",
"status": 400,
"error": "Bad Request",
"message": "Validation error",
"debugMessage": null,
"errors": [
{
"field": "userName",
"rejectedValue": "Ash",
"message": "Username should have at least 6 characters"
},
{
"field": "userName",
"rejectedValue": "Ash",
"message": "Invalid username"
},
{
"field": "password",
"rejectedValue": "shutosh@",
"message": "Invalid password"
}
]
}
}
Exception message "User not found with userId:a535c777-c906-45e2-b1c3-940965a507f2q" is as per the api. Below is the use-case.
Controller:
@PrivilegeMapper.HasPlaceUserPrivilege
@GetMapping(value = "/{userId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<?> getUserProfile(@NotBlank @PathVariable String userId) {
return myService.buildUserProfile(userId);
}
Service:
@Override
public ResponseEntity<?> buildUserProfile(final String userId) {
ApiUser apiUser = userRepository.findById(userId)
.orElseThrow(() -> new ApiUserNotFoundException("userId",userId));
return ResponseEntity.ok(sirfUser);
}
Exception Classes:
@Getter
@Setter
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ApiUserNotFoundException extends NotFoundException {
public ApiUserNotFoundException(String msg, Throwable t) {
super(msg, t);
}
public ApiUserNotFoundException(String msg) {
super(msg);
}
public ApiUserNotFoundException(String key, String value) {
super(key, value);
}
public ApiUserNotFoundException(String key, String value, List<Error> errors) {
super(key, value, errors);
}
}
@Getter
@Setter
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
private String key;
private String value;
private List<Error> errors;
public NotFoundException(String msg, Throwable t) {
super(msg, t);
}
public NotFoundException(String msg) {
super(msg);
}
public NotFoundException(String key, String value) {
this.key = key;
this.value = value;
}
public NotFoundException(String key, String value, List<Error> errors) {
this.key = key;
this.value = value;
this.errors = errors;
}
}
Exception Handler:
@ExceptionHandler(ApiUserNotFoundException.class)
protected ResponseEntity<Object> handleSirfUserNotFound(ApiUserNotFoundException ex) {
log.error(String.format("User not found with %s:%s",ex.getKey(),ex.getValue()));
ApiError apiError = new ApiError(NOT_FOUND);
apiError.setMessage("User not found");
List<Error> errors = new ArrayList<>();
Error error = new ApiValidationError(SirfUser.class.getSimpleName());
((ApiValidationError) error).setMessage(String.format("User not found with %s:%s",ex.getKey(),ex.getValue()));
((ApiValidationError) error).setField(ex.getKey());
((ApiValidationError) error).setRejectedValue(ex.getValue());
errors.add(error);
apiError.setErrors(errors);
return buildResponseEntity(apiError);
}
And this is it. You are done. such type of handling is always useful both for logging and ui perspective.
来源:https://stackoverflow.com/questions/60209758/java-kotlin-spring-boot-how-can-we-automatically-retrieve-parameter-values