반응형

Controller 기본

get

@GetMapping("/api/get")
public Map<String, Object> get(
    @RequestHeader(required = false) Map<String, Object> headers,
    @RequestParam(required = false) Map<String, Object> params
) {
    System.out.println(headers);
    System.out.println(params);
    return params;
}

post

@PostMapping("/api/post")
public Map<String, Object> post(
    @RequestHeader(required = false) Map<String, Object> headers,
    @RequestParam(required = false) Map<String, Object> params,
    @RequestBody(required = false) String body
) {
    System.out.println(headers);
    System.out.println(params);
    System.out.println(body);
    return params;
}

multipart - 기본

  • application.yml
    spring:
      servlet:
        multipart:
          file-size-threshold: 1MB
          location: C:/temp
          max-file-size: 100MB
          max-request-size: 100MB
    
  • Controller
    @PostMapping("/api/multipart")
    public Map<String, Object> multipart(
        @RequestHeader(required = false) Map<String, Object> headers,
        @RequestParam(required = false) Map<String, Object> params,
        @RequestPart(required = false) List<MultipartFile> files
    ) throws IOException {
        System.out.println(headers);
        System.out.println(params);
        System.out.println(files);
    
        for (MultipartFile file : files) {
            File desc = new File("C:/download/" + file.getOriginalFilename());
            file.transferTo(desc);
        }
    
        return params;
    }

multipart - DTO

test.http

### test
POST http://localhost:8080/api/multipart
Content-Type: multipart/form-data; boundary=WebAppBoundary

--WebAppBoundary
Content-Disposition: form-data; name="files[0].file"; filename="HELP.md"
Content-Type: text/plain

< ./HELP.md
--WebAppBoundary
Content-Disposition: form-data; name="files[0].url";

/help.md
--WebAppBoundary
Content-Disposition: form-data; name="files[1].file"; filename="build.gradle.kts"
Content-Type: text/plain

< ./build.gradle.kts
--WebAppBoundary
Content-Disposition: form-data; name="files[1].url";

/build.gradle.kts
--WebAppBoundary
Content-Disposition: form-data; name="message";

Hello World
--WebAppBoundary

java controller

@PostMapping("/api/multipart")
public void multipart(Request request) {
    log.info("message: {}", request.message);
    request.files.forEach(it -> log.info("url: {}, filename: {}", it.url, it.file.getOriginalFilename()));
}

@Data
public static class Request {
    private String message;
    private List<Item> files;
}

@Data
public static class Item {
    private String url;
    private MultipartFile file;
}

kotlin controller

@PostMapping("/api/multipart")
fun multipart(request: Request) {
    log.info("message: {}", request.message)
    request.files.forEach { log.info("url: {}, filename: {}", it.url, it.file?.originalFilename) }
}

data class Request( // 주의) 기본 생성자를 가질 수 있도록 멤버 필드들 모두 디폴트값 세팅해주어야함. 그렇지 않으면 오류 발생.
    var message: String = "",
    var files: List<Item> = mutableListOf(), // 주의) mutableListOf()가 아닌 listOf()일 경우 오류 발생.
)

data class Item(
    var url: String = "", // 주의) var가 아닌 val을 사용하면 요청값이 매핑되지 않음.
    var file: MultipartFile? = null,
)

RequestMappingHandlerMapping를 활용한 Api Versioning

설명

  • RequestMappingHandlerMapping란 요청 URL과 매칭되는 컨트롤러를 검색하고, 어떤 컨트롤러가 처리할지를 결정하는 클래스를 말한다.
  • 아래에는 ApiVersion 어노테이션을 컨트롤러에 설정하여 어노테이션에 지정된 버전값에 적절하게 매칭되는 컨트롤러로 서비스할 수 있도록 RequestMappingHandlerMapping를 커스터마이징하는 코드를 제시한다.

ApiVersion

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestCondition

@RequiredArgsConstructor
public class ApiVersionRequestCondition implements RequestCondition<ApiVersionRequestCondition> {
    private final int[] versions;

    @Override
    public ApiVersionRequestCondition getMatchingCondition(HttpServletRequest request) {
        Matcher matcher = Pattern.compile("/api/v(\\d+)/(.*)").matcher(request.getRequestURI());

        if (matcher.find()) {
            int version = Integer.parseInt(matcher.group(1));

            if (versions.length == 1) {
                if (versions[0] <= version) {
                    return this;
                }
            } else {
                if (versions[0] <= version && versions[1] >= version) {
                    return this;
                }
            }
        }

        return null;
    }

    @Override
    public ApiVersionRequestCondition combine(ApiVersionRequestCondition other) {
        throw new IllegalStateException("Not Supported");
    }

    @Override
    public int compareTo(ApiVersionRequestCondition other, HttpServletRequest request) {
        throw new IllegalStateException("Not Supported");
    }
}

ApiVersionHandlerMapping

public class ApiVersionHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return createCondition(AnnotationUtils.findAnnotation(handlerType, ApiVersion.class));
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return createCondition(AnnotationUtils.findAnnotation(method, ApiVersion.class));
    }

    private RequestCondition<?> createCondition(ApiVersion apiVersion) {
        return Optional.ofNullable(apiVersion)
            .map(item -> new ApiVersionRequestCondition(item.value()))
            .orElse(null);
    }
}

WebMvcConfig

@Configuration
public class WebMvcConfig {
    @Bean
    public WebMvcRegistrations webMvcRegistrations() {
        return new WebMvcRegistrations() {
            @Override
            public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
                return new ApiVersionHandlerMapping();
            }
        };
    }
}

UserController

@RestController
public class UserController {
    @GetMapping("/api/v*/users")
    @ApiVersion({1, 2}) // v1 ~ v2까지는 여기
    public String getUserV1() {
        return "johnV1";
    }

    @GetMapping("/api/v*/users")
    @ApiVersion(3) // v3 이상부터는 여기
    public String getUserV3() {
        return "johnV3";
    }
}

참고

  • https://seungwoo0429.tistory.com/37
  • https://programmer.group/customized-request-mapping-handler-mapping-for-spring-mvc.html
반응형

'Development > Spring' 카테고리의 다른 글

[Spring] BeanFactory & ApplicationContext  (0) 2020.12.27
[Spring] Paging  (0) 2020.12.27
[Spring] FileDownload  (0) 2020.12.27
[Spring] Spock Test  (0) 2020.12.27
[Spring] Spring Boot 프로젝트 세팅  (0) 2020.12.27

+ Recent posts