package hello.springmvc.basic.requestmapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
// RestController Annotation을 사용할 경우 String을 통해 넘겼을 때 뷰의 논리이름이 아니라 HTTP Body에 그대로 문자열이 들어감
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/hello-basic", method = RequestMethod.GET) // 얘로 들어오면 밑의 메서드 실행됨.
// method parameter를 넘김으로써, HTTP 호출 방식 제한 가능
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
@GetMapping(value = "/mapping-get-v2")
// 들어가면 request mapping의 메서드 존재
// GetMapping은 RequestMapping에 method parameter를 추가한 것과 동일
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
// @PathVariable Annotation을 이용하여 HTTP로 넘어오는 Parameter를 간단하게 바인딩 할 수 있음
// 변수 이름과 Parameter 이름이 같은 경우, "("userID")"는 생략 가능하다
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String userId){
log.info("mappingPath userId={}", userId);
return "ok";
}
}
@RestController - @Controller의 경우 기본적으로 반환값이 String인 경우 뷰의 논리 이름으로 인식된다.
뷰를 찾고 뷰가 렌더링되는 과정(이전에 forward())가 일어난다고 보면 되는데, @RestController는 반환값으로 뷰를 찾는것이 아니라 HTTP 메세지 바디에 바로 반환 String이 입력된다.
@ResponseBody와도 연관이 있다 (추후 작성)
@RequestMapping("/hello-basic")
RequestMapping의 parameter로 전달된 URI가 호출되면 그 애노테이션과 바인딩 된 메소드를 호출한다.
대부분의 속성을 배열로 제공하기 때문에, 다중 설정이 가능하다.
스프링 부트 3.0 이전까지는 다음 두 URL을 같은 요청으로 취급하지만, 3.0 이후부터는 이 둘은 다른 요청으로 인식된다
(이전에 3.0 이전까지는 동일한 타입의 스프링 빈을 가지는 경우, 수동 등록된 빈이 우선권을 가졌는데 3.0 이후부터는 수동 자동 둘이 있으면 NoUniqueBeanDefinitionException이 발생한다는 사실도 기억하면 좋을 듯 하다)
RequestMapping 메서드에 별도의 속성 GET, POST, PUT, DELETE, PATCH를 등록하지 않을 경우
HTTP 메서드와 무관하게 호출된다(전부 허용 - 별로 좋지는 않다)
다만 속성을 지정했는데 해당 속성과 무관한 요청이 들어오게 되는 경우에는 HTTP 405 Method Not Allowed를 반환한다.
@RequestMapping의 인자로 Method를 지정하는 것 보다,
@GetMapping, @PostMapping, @PatchMapping, @DeleteMapping, @PutMapping 등
메서드 축약 애노테이션을 이용하는게 더 직관적이다. -> 해당 코드 내부는 다음과 같다
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* Annotation for mapping HTTP {@code GET} requests onto specific handler
* methods.
*
* <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}.
*
* @author Sam Brannen
* @since 4.3
* @see PostMapping
* @see PutMapping
* @see DeleteMapping
* @see PatchMapping
* @see RequestMapping
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
* @since 4.3.5
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}
@GetMapping의 내부를 보면 @RequestMapping의 인자로 method를 주는 것을 알 수 있다.
또, 편리한 @PathVariable 애노테이션을 통해 URI로 들어오는 Parameter와 쉽게 바인딩 할 수 있다.
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String userId){
log.info("mappingPath userId={}", userId);
return "ok";
}
이전에는 HttpServletRequest의 .getParameter를 통해 조회했어야 했던 값이, 이제는 Spring MVC에 의해 애노테이션 하나만으로 간단하게 정리되는 모습을 볼 수 있다.
@PathVariable 애노테이션 내부를 들여다보자
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.beans.PropertyEditor;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.method.support.UriComponentsContributor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.View;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Resolves method arguments annotated with an @{@link PathVariable}.
*
* <p>An @{@link PathVariable} is a named value that gets resolved from a URI template variable.
* It is always required and does not have a default value to fall back on. See the base class
* {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver}
* for more information on how named values are processed.
*
* <p>If the method parameter type is {@link Map}, the name specified in the annotation is used
* to resolve the URI variable String value. The value is then converted to a {@link Map} via
* type conversion, assuming a suitable {@link Converter} or {@link PropertyEditor} has been
* registered.
*
* <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable
* values that don't yet match the method parameter type.
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 3.1
*/
public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
implements UriComponentsContributor {
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
return false;
}
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
}
return true;
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
Assert.state(ann != null, "No PathVariable annotation");
return new PathVariableNamedValueInfo(ann);
}
@Override
@SuppressWarnings("unchecked")
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
}
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
throw new MissingPathVariableException(name, parameter);
}
@Override
protected void handleMissingValueAfterConversion(
String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
throw new MissingPathVariableException(name, parameter, true);
}
@Override
@SuppressWarnings("unchecked")
protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) {
String key = View.PATH_VARIABLES;
int scope = RequestAttributes.SCOPE_REQUEST;
Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
if (pathVars == null) {
pathVars = new HashMap<>();
request.setAttribute(key, pathVars, scope);
}
pathVars.put(name, arg);
}
@Override
public void contributeMethodArgument(MethodParameter parameter, Object value,
UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) {
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
return;
}
PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
String name = (ann != null && StringUtils.hasLength(ann.value()) ? ann.value() : parameter.getParameterName());
String formatted = formatUriValue(conversionService, new TypeDescriptor(parameter.nestedIfOptional()), value);
uriVariables.put(name, formatted);
}
@Nullable
protected String formatUriValue(@Nullable ConversionService cs, @Nullable TypeDescriptor sourceType, Object value) {
if (value instanceof String) {
return (String) value;
}
else if (cs != null) {
return (String) cs.convert(value, sourceType, STRING_TYPE_DESCRIPTOR);
}
else {
return value.toString();
}
}
private static class PathVariableNamedValueInfo extends NamedValueInfo {
public PathVariableNamedValueInfo(PathVariable annotation) {
super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE);
}
}
}
지금까지 봐왔던 리졸버 형식으로 모든 iteration으로 모든 Parameter Type을 support하는지 조회한 후,
이전의 HttpServletRequest와는 다르게 NativeWebRequest request를 통해 .getParameter()를 하는 것을 알 수 있다.
다중 사용도 가능하다 해당 코드 구조를 보면 Map으로 모든 Parameter를 긁어다가 Map 형식의 변수에 집어넣는 것을 볼 수 있다.
특정 헤더 조건 매핑
- 파라미터 매핑과 비슷하나, HTTP 헤더를 사용한다.
미디어 타입 조건 매핑
컨트롤러 기준에서 소비하는 (받아들일 수 있는 값) - consumes
컨트롤러 기준에서 만들어 내야하는 (클라이언트가 받아들일 수 있는 값) - produces
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
/**
* Accept 헤더 기반 Media Type * produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
해당 consume parameter의 value와 HTTP Header의 content-type 값이 맞지 않는 경우 Unsupported Media Type를 반환함.
밑의 produces의 미디어 타입이 제대로 매핑되지 않을 경우 HTTP 406 상태코드를 반환함.
요청 매핑 - API 예시
package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MappingClassController {
@GetMapping("/mapping/users")
public String user(){
return "get users";
}
@PostMapping("/mapping/users")
public String addUser(){
return "add user";
}
@PostMapping("mapping/users/{userId}")
public String findUser(@PathVariable String userId){
return "get userId=" + userId;
}
@PatchMapping("/mapping/users/{userId}")
public String updateUser(@PathVariable String userId){
return "patch userId" + userId;
}
@DeleteMapping("/mapping/users/{userId}")
public String deleteUser(@PathVariable String userId){
return "delete userId = " + userId;
}
}
@RequestMapping("/mapping/users")와 같이 클래스단에 매핑정보를 애노테이션으로 배치하면,
메서드 레벨에서 해당 정보를 조합해서 사용함 (공통으로 묶을 수 있음)
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 1편' 카테고리의 다른 글
[스프링 웹 MVC 1편] 22. HTTP 요청 파라미터 (@ModelAttribute, 단순 텍스트) (0) | 2023.06.03 |
---|---|
[스프링 웹 MVC 1편] 21. 스프링 MVC - 기본기능 (기본/헤더/쿼리파라미터/ HTML Form 조회) (1) | 2023.06.03 |
[스프링 웹 MVC 1편] 19. 스프링 MVC - 기본 기능 (로깅) (0) | 2023.06.01 |
[스프링 웹 MVC 1편] 18. 스프링 MVC - 시작 (0) | 2023.05.30 |
[스프링 웹 MVC 1편] 17. 스프링 MVC - 핸들러 매핑과 어댑터 / 뷰 리졸버 (0) | 2023.05.29 |