我刚刚使用 AspectJ 完成了这项工作。
只需将此类复制粘贴到您的项目中即可。
Spring 应该会自动拾取它。
能力:
- 这应该将路径变量从您的控制器和方法复制到您的请求 DTO。
- 就我而言,我还需要将任何 HTTP 标头映射到请求上。随意禁用此功能。
- 这还会设置您的请求 DTO 可能扩展的任何超类的属性。
- 这应该适用于 POST、PUT、PATCH DELETE 和 GET 方法。
- 使用您在请求属性中定义的注释执行验证。
非常小的警告:
- 请注意,您注册的任何
WebDataBinders 都不适用于这种情况。我还没想好怎么捡。这就是我创建 coerceValue() 方法的原因,该方法将字符串从路径转换为 DTO 上声明的所需数据类型。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Validator;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* This class extracts values from the following places:
* - {@link PathVariable}s from the controller request path
* - {@link PathVariable}s from the method request path
* - HTTP headers
* and attempts to set those values onto controller method arguments.
* It also performs validation
*/
@Aspect
@Component
public class RequestDtoMapper {
private final HttpServletRequest request;
private final Validator validator;
public RequestDtoMapper(HttpServletRequest request, Validator validator) {
this.request = request;
this.validator = validator;
}
@Around("execution(public * *(..)) && (@annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping) || @annotation(org.springframework.web.bind.annotation.PatchMapping) || @annotation(org.springframework.web.bind.annotation.DeleteMapping) || @annotation(org.springframework.web.bind.annotation.GetMapping))")
public Object process(ProceedingJoinPoint call) throws Throwable {
MethodSignature signature = (MethodSignature) call.getSignature();
Method method = signature.getMethod();
// Extract path from controller annotation
Annotation requestMappingAnnotation = Arrays.stream(call.getTarget().getClass().getDeclaredAnnotations())
.filter(ann -> ann.annotationType() == RequestMapping.class)
.findFirst()
.orElseThrow();
String controllerPath = ((RequestMapping) requestMappingAnnotation).value()[0];
// Extract path from method annotation
List<Class<?>> classes = Arrays.asList(PostMapping.class, PutMapping.class, PatchMapping.class, DeleteMapping.class, GetMapping.class);
Annotation methodMappingAnnotation = Arrays.stream(method.getDeclaredAnnotations())
.filter(ann -> classes.contains(ann.annotationType()))
.findFirst()
.orElseThrow();
String methodPath = methodMappingAnnotation.annotationType().equals(PostMapping.class)
? ((PostMapping) methodMappingAnnotation).value()[0]
: methodMappingAnnotation.annotationType().equals(PutMapping.class)
? ((PutMapping) methodMappingAnnotation).value()[0]
: methodMappingAnnotation.annotationType().equals(PatchMapping.class)
? ((PatchMapping) methodMappingAnnotation).value()[0]
: methodMappingAnnotation.annotationType().equals(DeleteMapping.class)
? ((DeleteMapping) methodMappingAnnotation).value()[0]
: methodMappingAnnotation.annotationType().equals(GetMapping.class)
? ((GetMapping) methodMappingAnnotation).value()[0]
: null;
// Extract parameters from request URI
Map<String, String> paramsMap = extractParamsMapFromUri(controllerPath + "/" + methodPath);
// Add HTTP headers to params map
Map<String, String> headers =
Collections.list(request.getHeaderNames())
.stream()
.collect(Collectors.toMap(h -> h, request::getHeader));
paramsMap.putAll(headers);
// Set properties onto request object
List<Class<?>> requestBodyClasses = Arrays.asList(PostMapping.class, PutMapping.class, PatchMapping.class, DeleteMapping.class);
Arrays.stream(call.getArgs()).filter(arg ->
(requestBodyClasses.contains(methodMappingAnnotation.annotationType()) && arg.getClass().isAnnotationPresent(RequestBody.class))
|| methodMappingAnnotation.annotationType().equals(GetMapping.class))
.forEach(methodArg -> getMapOfClassesToFields(methodArg.getClass())
.forEach((key, value1) -> value1.stream().filter(field -> paramsMap.containsKey(field.getName())).forEach(field -> {
field.setAccessible(true);
try {
String value = paramsMap.get(field.getName());
Object valueCoerced = coerceValue(field.getType(), value);
field.set(methodArg, valueCoerced);
} catch (Exception e) {
throw new RuntimeException(e);
}
})));
// Perform validation
for (int i = 0; i < call.getArgs().length; i++) {
Object arg = call.getArgs();
BeanPropertyBindingResult result = new BeanPropertyBindingResult(arg, arg.getClass().getName());
SpringValidatorAdapter adapter = new SpringValidatorAdapter(this.validator);
adapter.validate(arg, result);
if (result.hasErrors()) {
MethodParameter methodParameter = new MethodParameter(method, i);
throw new MethodArgumentNotValidException(methodParameter, result);
}
}
// Execute remainder of method
return call.proceed();
}
private Map<String, String> extractParamsMapFromUri(String path) {
List<String> paramNames = Arrays.stream(path.split("/"))
.collect(Collectors.toList());
Map<String, String> result = new HashMap<>();
List<String> pathValues = Arrays.asList(request.getRequestURI().split("/"));
for (int i = 0; i < paramNames.size(); i++) {
String seg = paramNames.get(i);
if (seg.startsWith("{") && seg.endsWith("}")) {
result.put(seg.substring(1, seg.length() - 1), pathValues.get(i));
}
}
return result;
}
/**
* Convert provided String value to provided class so that it can ultimately be set onto the request DTO property.
* Ideally it would be better to hook into any registered WebDataBinders however we are manually casting here.
* Add your own conditions as required
*/
private Object coerceValue(Class<?> valueType, String value) {
if (valueType == Integer.class || valueType == int.class) {
return Integer.parseInt(value);
} else if (valueType == Boolean.class || valueType == boolean.class) {
return Integer.parseInt(value);
} else if (valueType == UUID.class) {
return UUID.fromString(value);
} else if (valueType != String.class) {
throw new RuntimeException(String.format("Cannot convert '%s' to type of '%s'. Add another condition to `%s.coerceValue()` to resolve this error", value, valueType, RequestDtoMapper.class.getSimpleName()));
}
return value;
}
/**
* Recurse up the class hierarchy and gather a map of classes to fields
*/
private Map<Class<?>, List<Field>> getMapOfClassesToFields(Class<?> t) {
Map<Class<?>, List<Field>> fields = new HashMap<>();
Class<?> clazz = t;
while (clazz != Object.class) {
if (!fields.containsKey(clazz)) {
fields.put(clazz, new ArrayList<>());
}
fields.get(clazz).addAll(Arrays.asList(clazz.getDeclaredFields()));
clazz = clazz.getSuperclass();
}
return fields;
}
}