I have a DTO
:
public class UserDto {
private Long id;
private String name;
}
and Controller
:
@RestController
@RequestMapping("user")
public Class UserController {
@PostMapping(value = "{id}")
public String update(@PathVariable String id, @RequestBody UserDto userDto){
userDto.setId(id);
service.update(userDto);
}
}
What I don't like is manually putting ID
from @PathVariable
to DTO
: userDto.setId(id);
.
For POST request /user/5
with body: { name: "test" }
, how could I automatically set ID
in DTO
, so that you'd get DTO
like below?
{
id: 5,
name: "test"
}
Basically, I'd like to have something like:
@RestController
@RequestMapping("user")
public Class UserController {
@PostMapping(value = "{id}")
public String update(@RequestBody UserDto userDto){
service.update(userDto);
}
}
Is there a way to accomplish this?
Thank you! :)
EDIT: this is an old question, still unanswered, so I'd like to add new perspective to this question.
Another problem we had is validation, to specific - defining custom constraint that does validation based on some field and id
.
if we remove id
from request body, then how can we access it from custom constraint? :)
It seems this endpoint is performing an update operation, so let's do two steps back.
PUT
requests are used to update a single resource, and it is best practice to prefer POST
over PUT
for creation of (at least top-level) resources. Instead, PATCH
requests are used to update parts of single resources, i.e. where only a specific subset of resource fields should be replaced.
In PUT
requests, the primary resource ID is passed as a URL path segment and the associated resource is replaced (in case of success) with the representation passed in the payload.
For the payload, you can extract another model domain class that contains all the fields of UserDto
except the ID.
According to this, I suggest to design your controller in this way:
@RestController
@RequestMapping("/api/{api}/users")
public class UserController {
@PutMapping("/{id}")
String update(@PathVariable String id, @RequestBody UpdateUserRequest request){
service.update(id, request);
}
}
I just got this working by using AspectJ.
Just copy-paste this class into your project.
Spring should pick it up automatically.
Capabilities:
Very minor caveat:
WebDataBinder
s you've registered will not apply in this situation. I haven't figured out how to pick that up yet. This is why I have created the coerceValue()
method that converts strings from your path into the desired data type as declared on your 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;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With