package sample.controller; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; import javax.persistence.EntityNotFoundException; import javax.validation.ConstraintViolationException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.validation.BindException; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.*; import sample.ValidationException; import sample.ValidationException.*; import sample.context.actor.ActorSession; /** * REST用の例外Map変換サポート。 * <p>AOPアドバイスで全てのRestControllerに対して例外処理を当て込みます。 */ @ControllerAdvice(annotations = RestController.class) public class RestErrorAdvice { protected Log log = LogFactory.getLog(getClass()); @Autowired private MessageSource msg; @Autowired private ActorSession session; /** Servlet例外 */ @ExceptionHandler(ServletRequestBindingException.class) public ResponseEntity<Map<String, String[]>> handleServletRequestBinding(ServletRequestBindingException e) { log.warn(e.getMessage()); return new ErrorHolder(msg, locale(), "error.ServletRequestBinding").result(HttpStatus.BAD_REQUEST); } private Locale locale() { return session.actor().getLocale(); } /** メディアタイプのミスマッチ例外 */ @ExceptionHandler(HttpMediaTypeNotAcceptableException.class) public ResponseEntity<Map<String, String[]>> handleHttpMediaTypeNotAcceptable( HttpMediaTypeNotAcceptableException e) { log.warn(e.getMessage()); return new ErrorHolder(msg, locale(), "error.HttpMediaTypeNotAcceptable").result(HttpStatus.BAD_REQUEST); } /** 楽観的排他(Hibernateのバージョンチェック)の例外 */ @ExceptionHandler(OptimisticLockingFailureException.class) public ResponseEntity<Map<String, String[]>> handleOptimisticLockingFailureException( OptimisticLockingFailureException e) { log.warn(e.getMessage(), e); return new ErrorHolder(msg, locale(), "error.OptimisticLockingFailure").result(HttpStatus.BAD_REQUEST); } /** 権限例外 */ @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<Map<String, String[]>> handleAccessDeniedException(AccessDeniedException e) { log.warn(e.getMessage()); return new ErrorHolder(msg, locale(), ErrorKeys.AccessDenied).result(HttpStatus.UNAUTHORIZED); } /** 指定した情報が存在しない例外 */ @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity<Map<String, String[]>> handleEntityNotFoundException(EntityNotFoundException e) { log.warn(e.getMessage(), e); return new ErrorHolder(msg, locale(), ErrorKeys.EntityNotFound).result(HttpStatus.BAD_REQUEST); } /** BeanValidation(JSR303)の制約例外 */ @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity<Map<String, String[]>> handleConstraintViolation(ConstraintViolationException e) { log.warn(e.getMessage()); Warns warns = Warns.init(); e.getConstraintViolations().forEach((v) -> warns.add(v.getPropertyPath().toString(), v.getMessage())); return new ErrorHolder(msg, locale(), warns.list()).result(HttpStatus.BAD_REQUEST); } /** Controllerへのリクエスト紐付け例外 */ @ExceptionHandler(BindException.class) public ResponseEntity<Map<String, String[]>> handleBind(BindException e) { log.warn(e.getMessage()); Warns warns = Warns.init(); e.getAllErrors().forEach((oe) -> { String field = ""; if (1 == oe.getCodes().length) { field = bindField(oe.getCodes()[0]); } else if (1 < oe.getCodes().length) { // low: プリフィックスは冗長なので外してます field = bindField(oe.getCodes()[1]); } List<String> args = Arrays.stream(oe.getArguments()) .filter((arg) -> !(arg instanceof MessageSourceResolvable)) .map(Object::toString) .collect(Collectors.toList()); String message = oe.getDefaultMessage(); if (0 <= oe.getCodes()[0].indexOf("typeMismatch")) { message = oe.getCodes()[2]; } warns.add(field, message, args.toArray(new String[0])); }); return new ErrorHolder(msg, locale(), warns.list()).result(HttpStatus.BAD_REQUEST); } protected String bindField(String field) { return Optional.ofNullable(field).map((v) -> v.substring(v.indexOf('.') + 1)).orElse(""); } /** アプリケーション例外 */ @ExceptionHandler(ValidationException.class) public ResponseEntity<Map<String, String[]>> handleValidation(ValidationException e) { log.warn(e.getMessage()); return new ErrorHolder(msg, locale(), e).result(HttpStatus.BAD_REQUEST); } /** IO例外(Tomcatの Broken pipe はサーバー側の責務ではないので除外しています) */ @ExceptionHandler(IOException.class) public ResponseEntity<Map<String, String[]>> handleIOException(IOException e) { if (e.getMessage() != null && e.getMessage().contains("Broken pipe")) { log.info("クライアント事由で処理が打ち切られました。"); return new ResponseEntity<>(HttpStatus.OK); } else { return handleException(e); } } /** 汎用例外 */ @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String[]>> handleException(Exception e) { log.error("予期せぬ例外が発生しました。", e); return new ErrorHolder(msg, locale(), ErrorKeys.Exception, "サーバー側で問題が発生した可能性があります。") .result(HttpStatus.INTERNAL_SERVER_ERROR); } /** * 例外情報のスタックを表現します。 * <p>スタックした例外情報は{@link #result(HttpStatus)}を呼び出す事でMapを持つResponseEntityへ変換可能です。 * Mapのkeyはfiled指定値、valueはメッセージキーの変換値(messages-validation.properties)が入ります。 * <p>{@link #errorGlobal}で登録した場合のキーは空文字となります。 * <p>クライアント側は戻り値を [{"fieldA": "messageA"}, {"fieldB": "messageB"}]で受け取ります。 */ public static class ErrorHolder { private Map<String, List<String>> errors = new HashMap<>(); private MessageSource msg; private Locale locale; public ErrorHolder(final MessageSource msg, final Locale locale) { this.msg = msg; this.locale = locale; } public ErrorHolder(final MessageSource msg, final Locale locale, final ValidationException e) { this(msg, locale, e.list()); } public ErrorHolder(final MessageSource msg, final Locale locale, final List<Warn> warns) { this.msg = msg; this.locale = locale; warns.forEach((warn) -> { if (warn.global()) errorGlobal(warn.getMessage()); else error(warn.getField(), warn.getMessage()); }); } public ErrorHolder(final MessageSource msg, final Locale locale, String globalMsgKey, String... msgArgs) { this.msg = msg; this.locale = locale; errorGlobal(globalMsgKey, msgArgs); } /** グローバルな例外(フィールドキーが空)を追加します。 */ public ErrorHolder errorGlobal(String msgKey, String defaultMsg, String... msgArgs) { if (!errors.containsKey("")) errors.put("", new ArrayList<>()); errors.get("").add(msg.getMessage(msgKey, msgArgs, defaultMsg, locale)); return this; } /** グローバルな例外(フィールドキーが空)を追加します。 */ public ErrorHolder errorGlobal(String msgKey, String... msgArgs) { return errorGlobal(msgKey, msgKey, msgArgs); } /** フィールド単位の例外を追加します。 */ public ErrorHolder error(String field, String msgKey, String... msgArgs) { if (!errors.containsKey(field)) errors.put(field, new ArrayList<>()); errors.get(field).add(msg.getMessage(msgKey, msgArgs, msgKey, locale)); return this; } /** 保有する例外情報をResponseEntityへ変換します。 */ public ResponseEntity<Map<String, String[]>> result(HttpStatus status) { return new ResponseEntity<Map<String, String[]>>( errors.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, (entry) -> entry.getValue().toArray(new String[0]))), status); } } }