API 의 경우에는 각 오류 상황에 맞는 오류 응답 스펙을 정해서 JSON으로 내려줘야 오류를 대처할 수있다.
api 예외 컨트롤러 추가
@GetMapping("/api/members/{id}")
public MemberDto getMember(@PathVariable("id") String id){
if(id.equals("ex")){
throw new RuntimeException("잘못된 사용자");
}
return new MemberDto(id,"hello" + id);
}
@Data
@AllArgsConstructor
static class MemberDto{
private String memberId;
private String name;
}
ErrorPageController에 JSON 응답을 할 수있도록 추가
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500APi(HttpServletRequest request,
HttpServletResponse response) {
log.info("API ErrorPage 500");
Map<String, Object> result = new HashMap<>();
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
result.put("status", request.getAttribute(ERROR_STATUS_CODE));
result.put("message", ex.getMessage());
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}
MediaType.APPLICATION_JSON_VALUE
Json 형식이 key : value 로 이루어져 있기 때문에 Map을 통해서 값을 할당해준다.
IllegalArgumentException 을 처리하지 못해서 컨트롤러 밖으로 넘어가는 일이 발생하면 HTTP 상태코드를 400으로 처리하고 싶다.
@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
}
return new ModelAndView();
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
ExceptionResolver가 ModelAndView를 반환하는 이유
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
}
configureHandlerExceptionResolvers 사용시 스프링이 기본으로 등록하는 ExceptionResolver가 제거되기 때문에 extend를 사용해서 등록해줘야함.
사용자 정의 예외 추가
public class UserException extends RuntimeException{
public UserException() {
}
public UserException(String message) {
super(message);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
public UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
ApiExceptionController 에 사용자 오류 추가
if(id.equals("user-ex")){
throw new UserException("사용자 오류");
}
아직 예외처리를 등록하지 않았기 때문에 500오류가 발생
@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof UserException) {
log.info("UserException resolver to 400");
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex", ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else {
return new ModelAndView("error/500");
}
}
} catch (IOException e) {
log.error("resolver ex", e);
}
return null;
}
}
Accept 값이 application/json 인 경우 JSON으로 오류를 내주고 아닌경우 error/500 에 있는 html 오류 페이지를 보여준다.
ModelAndView를 반환해야하기 때문에 response에 contentype과 encoding등을 다 넣어줘야한다.
objectMapper.writeValueAsString(errorResult);
webConfig에 resolver를 추가해줌으로써 적용되게
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
resolvers.add(new MyHandlerExceptionResolver());
resolvers.add(new UserHandlerExceptionResolver());
}
기본적으로 3가지가 등록되어있다.
첫번째 처리가 안되면 두번째 가 처리 ,마찬가지로 두번째도 처리 못하면 마지막인 세번째가 처리
2가지 경우를 처리
@ResponseStatus 어노테이션 적용
@ResponseStatus(code = HttpStatus.BAD_REQUEST,reason = "잘못된 요청 오류")
public class BadRequestException extends RuntimeException{
}
-> BadRequsetException 예외가 컨트롤러 밖으로 넘어가게 되면 ResponseStatusExceptionResolver 예외가 해당 어노테이션을 발견하여 오류코드를 BAD_Request(400)으로 변경하고 메시지도 담아준다.
확인용 api 컨트롤러에 추가
@GetMapping("/api/response-status-ex1")
public String responseStatusEx1(){
throw new BadRequestException();
}
postMan을 통해서 제대로 작동하는 지 체크
server:
error:
include-message: always
include-exception: true
whitelabel:
enabled: false
include-exception : exception을 포함해서 보여줄 것인지
include-message : message를 포함해서 보여줄 것인지
whitelabel의 경우 오류 처리 화면을 못찾으면 스프링 whitelavel 페이지를 적용할것인지 체크하는 항목으로 현재는 false로 사용하지 않는 걸로 해놧다.
protected ModelAndView applyStatusAndReason(int statusCode, @Nullable String reason, HttpServletResponse response)
throws IOException {
if (!StringUtils.hasLength(reason)) {
response.sendError(statusCode);
}
else {
String resolvedReason = (this.messageSource != null ?
this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()) :
reason);
response.sendError(statusCode, resolvedReason);
}
return new ModelAndView();
}
Message.properties에 공통적으로 사용할 메시지 문구를 적어둔다.
error.bad=잘못된 요청 오류입니다.
여기서 한글 깨짐이 발생할 수있으니 Editor에 fileEncoding이 UTF-8 인지 꼭 확인하자.
@ResponseStatus 의 경우 개발자가 직접 변경할 수 없는 예외에는 적용 할 수 없다.
애노테이션을 직접 넣어야하기 때문에 내가 코드를 수정할 수없는 곳의 예외 코드 같은 곳은 사용 불가능.
애노테이션이기 때문에 동적으로 사용이 불가능하다.
이런 두가지 경우에는 ResponseStatusException 예외를 사용해야 한다.
ResponseStatusException 사용 코드
@GetMapping("/api/response-status-ex2")
public String responseStatusEx2(){
throw new ResponseStatusException(HttpStatus.NOT_FOUND,"error.bad",new IllegalArgumentException());
}
실험용 코드
@GetMapping("/api/default-handler-ex")
public String defaultException(@RequestParam("data") Integer data){
return "ok";
}