All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.yizzuide.milkomeda.hydrogen.uniform.UniformHandler Maven / Gradle / Ivy

/*
 * Copyright (c) 2021 yizzuide All rights Reserved.
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.github.yizzuide.milkomeda.hydrogen.uniform;

import com.github.yizzuide.milkomeda.universe.context.ApplicationContextHolder;
import com.github.yizzuide.milkomeda.universe.context.WebContext;
import com.github.yizzuide.milkomeda.universe.lang.Tuple;
import com.github.yizzuide.milkomeda.universe.parser.yml.YmlParser;
import com.github.yizzuide.milkomeda.universe.parser.yml.YmlResponseOutput;
import com.github.yizzuide.milkomeda.util.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * UniformHandler
 *
 * @author yizzuide
 * @since 3.0.0
 * @version 3.14.1
 * @see org.springframework.boot.SpringApplication#run(java.lang.String...)
 * #see org.springframework.boot.SpringApplication#registerLoggedException(java.lang.Throwable)
 * #see org.springframework.boot.SpringBootExceptionHandler.LoggedExceptionHandlerThreadLocal#initialValue()
 * @see org.springframework.boot.SpringApplication#setRegisterShutdownHook(boolean)
 * @see org.springframework.context.support.AbstractApplicationContext#registerShutdownHook()
 * 
* Create at 2020/03/25 22:47 */ @Slf4j // 可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute, 并应用到所有@RequestMapping中 //@ControllerAdvice // 这种方式默认就会扫描并加载到Ioc,不好动态控制是否加载,但好处是外部API对未来版本的兼容性强 public class UniformHandler extends ResponseEntityExceptionHandler { @Autowired private UniformProperties props; /** * 自定义异常列表 */ private List> customExpClazzList; @SuppressWarnings("unchecked") @PostConstruct public void init() { // 初始化自定义异常 Object customs = props.getResponse().get(YmlResponseOutput.CUSTOMS); if (customs == null) { return; } this.customExpClazzList = new ArrayList<>(); Map> customsMap = (Map>) customs; for (String k : customsMap.keySet()) { Map configNodeMap = customsMap.get(k); Object clazzComposite = configNodeMap.get(YmlResponseOutput.CLAZZ); // clazz -> clazz list ArrayList> expClazzList = new ArrayList<>(); if (clazzComposite instanceof Map) { Map clazzCompositeIndexMap = (Map) clazzComposite; for (String index : clazzCompositeIndexMap.keySet()) { Class expClazz = createExceptionClass(clazzCompositeIndexMap.get(index)); expClazzList.add(expClazz); } } else { Class expClazz = createExceptionClass(clazzComposite); if (expClazz != null) { expClazzList.add(expClazz); } } configNodeMap.put(YmlResponseOutput.CLAZZ, expClazzList); this.customExpClazzList.add(configNodeMap); } } private Class createExceptionClass(Object clazz) { if (!(clazz instanceof String)) return null; Class expClazz = null; try { expClazz = Class.forName(clazz.toString()); } catch (Exception ex) { log.error("Hydrogen load class error with msg: {}", ex.getMessage(), ex); } return expClazz; } // 4xx异常处理 @Override protected @NonNull ResponseEntity handleExceptionInternal(@NonNull Exception ex, @Nullable Object body, @NonNull HttpHeaders headers, HttpStatus status, @NonNull WebRequest request) { ResponseEntity responseEntity = handleExceptionResponse(ex, status.value(), ex.getMessage()); if (responseEntity == null) { return super.handleExceptionInternal(ex, body, headers, status, request); } return responseEntity; } // 方法上单个普通类型(如:String、Long等)参数校验异常(校验注解直接写在参数前面的方式) @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity constraintViolationException(ConstraintViolationException e) { ConstraintViolation constraintViolation = e.getConstraintViolations().iterator().next(); String value = String.valueOf(constraintViolation.getInvalidValue()); String message = WebContext.getRequestNonNull().getRequestURI() + " [" + constraintViolation.getPropertyPath() + "=" + value + "] " + constraintViolation.getMessage(); log.warn("Hydrogen uniform valid response exception with msg: {} ", message); ResponseEntity responseEntity = handleExceptionResponse(e, HttpStatus.BAD_REQUEST.value(), message); return responseEntity == null ? ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(null) : responseEntity; } // 对方法上@RequestBody的Bean参数校验的处理 @Override protected @NonNull ResponseEntity handleMethodArgumentNotValid(@NonNull MethodArgumentNotValidException ex, @NonNull HttpHeaders headers, @NonNull HttpStatus status, @NonNull WebRequest request) { ResponseEntity responseEntity = handleValidBeanExceptionResponse(ex, ex.getBindingResult()); return responseEntity == null ? super.handleMethodArgumentNotValid(ex, headers, status, request) : responseEntity; } // 对方法的Form提交参数绑定校验的处理 @Override protected @NonNull ResponseEntity handleBindException(@NonNull BindException ex, @NonNull HttpHeaders headers, @NonNull HttpStatus status, @NonNull WebRequest request) { ResponseEntity responseEntity = handleValidBeanExceptionResponse(ex, ex.getBindingResult()); return responseEntity == null ? super.handleBindException(ex, headers, status, request) : responseEntity; } // 其它内部异常处理 @SuppressWarnings("unchecked") @ExceptionHandler(Throwable.class) public ResponseEntity handleException(Throwable e) { Map response = props.getResponse(); Object status = response.get(YmlResponseOutput.STATUS); status = status == null ? 500 : status; Map result = new HashMap<>(); // 查找自定义异常处理 if (this.customExpClazzList != null) { for (Map map : this.customExpClazzList) { List> expClazzList = (List>) map.get(YmlResponseOutput.CLAZZ); if (expClazzList.stream().anyMatch(expClazz -> expClazz.isInstance(e))) { YmlResponseOutput.output(map, result, null, (Exception) e, true); return ResponseEntity.status(Integer.parseInt(status.toString())).body(result); } } } // 500异常 return handleInnerErrorExceptionResponse((Exception) e, response, status.toString()); } /** * 处理Bean校验异常 * @param ex 异常 * @param bindingResult 错误绑定数据 * @return ResponseEntity */ private ResponseEntity handleValidBeanExceptionResponse(Exception ex, BindingResult bindingResult) { ObjectError objectError = bindingResult.getAllErrors().get(0); String message = objectError.getDefaultMessage(); if (objectError.getArguments() != null && objectError.getArguments().length > 0) { FieldError fieldError = (FieldError) objectError; message = WebContext.getRequestNonNull().getRequestURI() + " [" + fieldError.getField() + "=" + fieldError.getRejectedValue() + "] " + message; } log.warn("Hydrogen uniform valid response exception with msg: {} ", message); return handleExceptionResponse(ex, HttpStatus.BAD_REQUEST.value(), message); } /** * 处理非5xx异常响应 * @param ex 异常 * @param presetStatusCode 预设响应码 * @param presetMessage 预设错误消息 * @return ResponseEntity */ @SuppressWarnings("unchecked") private @Nullable ResponseEntity handleExceptionResponse(Exception ex, Object presetStatusCode, String presetMessage) { Map response = props.getResponse(); Map result = new HashMap<>(); String code = presetStatusCode.toString(); // 返回参数类型错误,这里会走500 if (Objects.equals(code, "500")) { return handleInnerErrorExceptionResponse(ex, response, code); } Object exp4xx = response.get(code); if (!(exp4xx instanceof Map)) { log.warn("Hydrogen uniform can't find {} code response.", presetStatusCode); // 调用方判断,按框架默认处理 return null; } Map exp4xxResponse = (Map) exp4xx; Object statusCode4xx = exp4xxResponse.get(YmlResponseOutput.STATUS); if (statusCode4xx == null || presetStatusCode.equals(statusCode4xx)) { return ResponseEntity.status(Integer.parseInt(presetStatusCode.toString())).body(null); } Map defValMap = new HashMap<>(); defValMap.put(YmlResponseOutput.CODE, presetStatusCode); defValMap.put(YmlResponseOutput.MESSAGE, presetMessage); YmlResponseOutput.output(exp4xxResponse, result, defValMap, null, false); return ResponseEntity.status(Integer.parseInt(statusCode4xx.toString())).body(result); } // 500异常 private ResponseEntity handleInnerErrorExceptionResponse(Exception ex, Map response, String presetStatusCode) { Map result = new HashMap<>(); log.error("Hydrogen uniform response exception with msg: {}", ex.getMessage(), ex); YmlResponseOutput.output(response, result, null, ex, false); return ResponseEntity.status(Integer.parseInt(presetStatusCode)).body(result); } /** * Used for external match with status code to get response result. * @param response response object * @param source replace data * @return tuple(yml node map, response content) * @since 3.14.0 */ @SuppressWarnings("unchecked") public static Tuple, Map> matchStatusResult(HttpServletResponse response, Map source) { UniformProperties props = Binder.get(ApplicationContextHolder.get().getEnvironment()).bind(UniformProperties.PREFIX, UniformProperties.class).get(); Map resolveMap; if (props == null) { resolveMap = createInitResolveMap(); } else { resolveMap = (Map) props.getResponse().get(String.valueOf(response.getStatus())); if (resolveMap == null) { resolveMap = createInitResolveMap(); } } Map result = new HashMap<>(); // status == 200? if (response.getStatus() == HttpStatus.OK.value()) { YmlParser.parseAliasMapPath(resolveMap, result, YmlResponseOutput.CODE, null, source); YmlParser.parseAliasMapPath(resolveMap, result, YmlResponseOutput.MESSAGE, null, source); YmlParser.parseAliasMapPath(resolveMap, result, YmlResponseOutput.DATA, null, source); resultFilter(result); } else { // status != 200 YmlResponseOutput.output(resolveMap, result, source, null, false); } return Tuple.build(resolveMap, result); } @NotNull private static Map createInitResolveMap() { Map resolveMap = new HashMap<>(7); resolveMap.put(YmlResponseOutput.STATUS, HttpStatus.OK.value()); resolveMap.put(YmlResponseOutput.CODE, "0"); resolveMap.put(YmlResponseOutput.MESSAGE, ""); resolveMap.put(YmlResponseOutput.DATA, Collections.emptyMap()); return resolveMap; } /** * Used for external match with status code to write. * @param response response object * @param source replace data * @throws IOException if an input or output exception occurred * @since 3.14.0 */ public static void matchStatusToWrite(HttpServletResponse response, Map source) throws IOException { Tuple, Map> mapTuple = matchStatusResult(response, source); Map resolveMap = mapTuple.getT1(); Map result = mapTuple.getT2(); if (mapTuple.getT1() == null || mapTuple.getT1().size() == 0) { return; } String body = JSONUtil.serialize(result); Object statusCode = resolveMap.get(YmlResponseOutput.STATUS); if (statusCode != null) { response.setStatus(Integer.parseInt(statusCode.toString())); } response.setCharacterEncoding(StandardCharsets.UTF_8.toString()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentLength(body.length()); PrintWriter writer = response.getWriter(); writer.println(body); writer.flush(); writer.close(); } /** * Force code type with config milkomeda.hydrogen.uniform.code-type * @param result response result map * @since 3.14.0 */ public static void resultFilter(Map result) { ResultVO.CodeType codeType = UniformHolder.getProps().getCodeType(); Object code = result.get(YmlResponseOutput.CODE); if (code != null) { if (codeType == ResultVO.CodeType.INT) { code = Integer.parseInt(code.toString()); } else { code = code.toString(); } result.put(YmlResponseOutput.CODE, code); } } }