programing

@PathVariable과 @RequestBody 결합

iphone6s 2023. 7. 21. 21:27
반응형

@PathVariable과 @RequestBody 결합

나는 있습니다DTO:

public class UserDto {
  private Long id;
  private String name;
}

그리고.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);
  }
}

제가 싫어하는 것은 수동으로 넣는 것입니다.ID부터@PathVariable로.DTO:userDto.setId(id);.

POST 요청의 경우/user/5본문 포함:{ name: "test" }자동으로 설정하려면 어떻게 해야 합니까?IDDTO당신이 얻을 수 있도록.DTO아래와 같이?

{
  id: 5,
  name: "test"
}

기본적으로 다음과 같은 것을 원합니다.

@RestController
@RequestMapping("user")
public Class UserController {
  @PostMapping(value = "{id}")
  public String update(@RequestBody UserDto userDto){
    service.update(userDto);
  }
}

이것을 달성할 수 있는 방법이 있습니까?

감사합니다! :)

편집: 이 질문은 오래된 질문으로, 여전히 답이 없기 때문에, 저는 이 질문에 새로운 관점을 추가하고 싶습니다.

우리가 가진 또 다른 문제는 특정 필드를 기반으로 유효성 검사를 수행하는 특정 사용자 정의 제약 조건에 대한 유효성 검사입니다.id.

제거하면id요청 본문에서, 그러면 어떻게 사용자 정의 제약 조건에서 액세스할 수 있습니까? :)

편집 2: Github에서 발생한 바람 문제에 대해 설명합니다. https://github.com/spring-projects/spring-framework/issues/28637

이 끝점이 업데이트 작업을 수행하고 있는 것 같습니다. 두 단계 뒤로 돌아가 보겠습니다.

PUT요청은 단일 리소스를 업데이트하는 데 사용되며 선호하는 것이 가장 좋습니다.POST위에PUT(최소한 최상위 수준의) 리소스를 생성할 수 있습니다.대신,PATCH요청은 단일 리소스의 일부를 업데이트하는 데 사용됩니다. 즉, 리소스 필드의 특정 하위 집합만 교체해야 합니다.

PUT요청하면 기본 리소스 ID가 URL 경로 세그먼트로 전달되고 성공한 경우 관련 리소스가 페이로드에 전달된 표현으로 대체됩니다.

페이로드의 경우 모든 필드를 포함하는 다른 모델 도메인 클래스를 추출할 수 있습니다.UserDto신분증을 제외하고요

이에 따라 다음과 같은 방식으로 컨트롤러를 설계할 것을 제안됩니다.

@RestController
@RequestMapping("/api/{api}/users")
public class UserController {

  @PutMapping("/{id}")
  String update(@PathVariable String id, @RequestBody UpdateUserRequest request){
      service.update(id, request);
  }
}

Aspect J를 사용하여 이 작업을 수행했습니다.
이 클래스를 프로젝트에 복사하여 붙여넣기만 하면 됩니다.
봄이 되면 자동으로 그것을 줍게 될 것입니다.

기능:

  • 이렇게 하면 경로 변수가 컨트롤러 및 메서드에서 요청 DTO로 복사됩니다.
  • 저의 경우 요청에 HTTP 헤더를 매핑해야 했습니다.사용하지 않도록 설정하십시오.
  • 또한 요청 DTO가 확장할 수 있는 모든 슈퍼 클래스의 속성을 설정합니다.
  • 이는 POST, PUT, PATCH DELETE 및 GET 메서드와 함께 작동합니다.
  • 요청 속성에 정의한 주석을 사용하여 유효성 검사를 수행합니다.

매우 사소한 주의 사항:

  • 참고: 임의WebDataBinder등록한 항목은 이 경우 적용되지 않습니다.저는 아직 그것을 어떻게 주워야 할지 모르겠어요.가 이이제만이다니유입든을 이유입니다.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;
    }

}

언급URL : https://stackoverflow.com/questions/57905483/combining-pathvariable-and-requestbody

반응형