package com.estate.expert.common.config;
|
|
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.parser.ParserConfig;
|
import com.alibaba.fastjson.parser.deserializer.Jdk8DateCodec;
|
import com.alibaba.fastjson.serializer.SerializeConfig;
|
import com.alibaba.fastjson.support.config.FastJsonConfig;
|
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
|
import com.estate.expert.common.AppRuntime;
|
import com.estate.expert.common.auth.AuthCallerResolver;
|
import com.estate.expert.common.auth.OriginalCallerResolver;
|
import com.estate.expert.common.auth.SessionResolver;
|
import com.estate.expert.common.context.HttpRequestContext;
|
import com.estate.expert.common.context.RequestContextInterceptor;
|
import com.estate.expert.common.exception.AbstractExceptionHandler;
|
import com.estate.expert.common.log.TraceInterceptor;
|
import com.estate.expert.common.utils.DateConvertUtils;
|
import com.estate.expert.common.utils.FastjsonUtils;
|
import com.google.common.base.Strings;
|
import com.google.common.collect.Lists;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.lang3.StringUtils;
|
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Import;
|
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.format.FormatterRegistry;
|
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpInputMessage;
|
import org.springframework.http.HttpOutputMessage;
|
import org.springframework.http.MediaType;
|
import org.springframework.http.converter.*;
|
import org.springframework.validation.Validator;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
import org.springframework.web.servlet.DispatcherServlet;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.view.InternalResourceViewResolver;
|
import org.yaml.snakeyaml.Yaml;
|
|
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletResponse;
|
import java.io.IOException;
|
import java.time.Instant;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.ZoneId;
|
import java.util.*;
|
import java.util.concurrent.TimeUnit;
|
|
/**
|
* 默认WebMvc配置
|
*/
|
@Slf4j
|
@Import({DefaultWebMvcConfig.GlobalExceptionHandler.class})
|
public class DefaultWebMvcConfig extends DelegatingWebMvcConfiguration implements InitializingBean {
|
private static final String ACCESS_CONTROL_MAX_AGE = String.valueOf(TimeUnit.DAYS.toSeconds(1));
|
@Autowired
|
private DispatcherServlet servlet;
|
|
@Autowired
|
private AppRuntime appRuntime;
|
|
// XXX:handlerInterceptors可以为空,不做非空约束
|
@Autowired(required = false)
|
private HandlerInterceptor[] handlerInterceptors;
|
|
@Autowired(required = false)
|
private HandlerMethodArgumentResolver[] handlerMethodArgumentResolvers;
|
|
//该validator由ValidationAutoConfiguration提供.
|
@Autowired(required = false)
|
@Qualifier(value = "defaultValidator")
|
Validator validator;
|
|
/** 系统使用的时区 (默认为 CTT 中国上海时区)*/
|
@Value("${shortZoneId.system:CTT}")
|
private String systemShortZoneId;
|
|
/** 需要转换的时区 (默认转换为 PST美国洛杉矶时区)*/
|
@Value("${shortZoneId.convert:PST}")
|
private String convertShortZoneId;
|
|
/**
|
* 配置对http request,response的converter
|
* 1. 使用fastjson来序列化/反序列化json string
|
* 2. 全局配置fastjson对LocalDateTime, LocalDate的序列化/反序列化
|
*/
|
// @Override
|
// public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
// FastJsonConfig fastJsonConfig = new FastJsonConfig();
|
// fastJsonConfig.setSerializerFeatures(FastjsonUtils.SERIALIZER_FEATURES);
|
//
|
// // 支持Actuator. Actuator返回的数据content-type是application/vnd.spring-boot.actuator.v2+json
|
// MediaType application = new MediaType("application", "*+json");
|
//
|
// FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
|
// fastJsonHttpMessageConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8, application));
|
// fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
|
// converters.add(0, fastJsonHttpMessageConverter);
|
//
|
// // 适用于直接原文返回数据的response @RequestMapping(value = "/xxx", produces = "text/plain;charset=utf-8")
|
// StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
|
// stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN));
|
// converters.add(1, stringHttpMessageConverter);
|
//
|
// // support yml
|
// YamlHttpMessageConverter yamlHttpMessageConverter = new YamlHttpMessageConverter();
|
// yamlHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(YamlHttpMessageConverter.MEDIA_TYPE_YAML, YamlHttpMessageConverter.MEDIA_TYPE_YML));
|
// converters.add(2, yamlHttpMessageConverter);
|
//
|
//// initFastJsonGlobalConfig();
|
// }
|
|
// private void initFastJsonGlobalConfig() {
|
// // 全局配置, 为LocalDateTime添加专用的serializer
|
// SerializeConfig.getGlobalInstance().put(LocalDateTime.class, (serializer, object, fieldName, fieldType, features) -> {
|
// if (object == null) {
|
// serializer.out.writeNull();
|
// return;
|
// }
|
// long value = ((LocalDateTime) object).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
// serializer.out.writeLong(value);
|
// });
|
//
|
// // 全局配置, 为LocalDate添加专用的serializer
|
// SerializeConfig.getGlobalInstance().put(LocalDate.class, (serializer, object, fieldName, fieldType, features) -> {
|
// if (object == null) {
|
// serializer.out.writeNull();
|
// return;
|
// }
|
// long value = ((LocalDate) object).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
// serializer.out.writeLong(value);
|
// });
|
//
|
// // 初始状态的ParserConfig.deserializers中没有LocalDateTime.class对应的deserializer
|
// // 会造成直接从timestamp反序列化到LocalDateTime失败;需要预设成默认的Jdk8DateCodec
|
// ParserConfig.getGlobalInstance().putDeserializer(LocalDateTime.class, Jdk8DateCodec.instance);
|
// }
|
|
// /**
|
// * 配置formatter,将API入参中的timestamp转化为LocalDateTime或者LocalDate
|
// *
|
// * @param registry
|
// */
|
// @Override
|
// public void addFormatters(FormatterRegistry registry) {
|
// super.addFormatters(registry);
|
// //XXX: 不能使用lambda
|
// registry.addConverter(new Converter<String, LocalDateTime>() {
|
// @Override
|
// public LocalDateTime convert(String source) {
|
// if (Strings.isNullOrEmpty(source)) {
|
// return null;
|
// }
|
// return LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(source)), ZoneId.systemDefault());
|
// }
|
// });
|
//
|
// registry.addConverter(new Converter<String, LocalDate>() {
|
// @Override
|
// public LocalDate convert(String source) {
|
// if (Strings.isNullOrEmpty(source)) {
|
// return null;
|
// }
|
// return Instant.ofEpochMilli(Long.parseLong(source)).atZone(ZoneId.systemDefault()).toLocalDate();
|
// }
|
// });
|
// }
|
|
/**
|
* 添加拦截器
|
*
|
* @param registry
|
*/
|
@Override
|
protected void addInterceptors(InterceptorRegistry registry) {
|
registry.addInterceptor(new TraceInterceptor(appRuntime));
|
// 跨域的拦截器, 并设置最高优先级. 避免抛出异常时跨域未生效
|
// 不建议使用DelegatingWebMvcConfiguration.addCorsMappings. 使用该方法配置之后再使用自定义拦截器时跨域相关配置就会失效
|
registry.addInterceptor(new HandlerInterceptor() {
|
@Override
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
//优先使用请求传入的ORIGIN, 参考org.springframework.web.cors.DefaultCorsProcessor#handleInternal
|
String origin = StringUtils.firstNonBlank(request.getHeader(HttpHeaders.ORIGIN), CorsConfiguration.ALL);
|
if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).isEmpty()) {
|
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
|
}
|
// 前端手动设置了 headers: {content-type: 'application/json'},在 OPTIONS 预请求的时候需要指定允许
|
// 反向代理后,需要设置auth-tk
|
if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS).isEmpty()) {
|
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "content-type,*");
|
// response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "content-type,auth-tk,*");
|
}
|
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, CorsConfiguration.ALL);
|
if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS).isEmpty()) {
|
response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString());
|
}
|
response.addHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_MAX_AGE);
|
return true;
|
}
|
});
|
|
super.addInterceptors(registry);
|
|
if (null != handlerInterceptors) {
|
Arrays.stream(handlerInterceptors).forEach(e -> {
|
registry.addInterceptor(e);
|
});
|
}
|
registry.addInterceptor(new RequestContextInterceptor());
|
}
|
|
/**
|
* 添加ArgumentResolver,以便在Controller中直接获取argument
|
*
|
* @param argumentResolvers
|
*/
|
@Override
|
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
super.addArgumentResolvers(argumentResolvers);
|
if (null != handlerMethodArgumentResolvers) {
|
Arrays.stream(handlerMethodArgumentResolvers).forEach(e -> {
|
argumentResolvers.add(e);
|
});
|
}
|
//XXX 用于controller方法参数列表直接获取AuthCaller
|
argumentResolvers.add(new AuthCallerResolver());
|
argumentResolvers.add(new OriginalCallerResolver());
|
argumentResolvers.add(new SessionResolver());
|
}
|
|
/**
|
* 显示指定spring mvc使用的validator, 避免在方法签名中使用@Validated不生效
|
*
|
* @return
|
*/
|
@Override
|
protected Validator getValidator() {
|
return Optional.ofNullable(validator).orElse(super.getValidator());
|
}
|
|
@Override
|
public void afterPropertiesSet() throws Exception {
|
//设置找不到mapping时抛出异常, 避免直接返回错误页面
|
servlet.setThrowExceptionIfNoHandlerFound(true);
|
|
//初始化数据到时间转换工具类中
|
DateConvertUtils.systemShortZoneId = systemShortZoneId;
|
DateConvertUtils.convertShortZoneId = convertShortZoneId;
|
}
|
|
/**
|
* 全局的异常处理,当API调用抛出异常后,由该GlobalExceptionHandler处理
|
*/
|
@ControllerAdvice
|
static class GlobalExceptionHandler extends AbstractExceptionHandler {
|
public GlobalExceptionHandler(ExceptionHandlerHook hook) {
|
super(hook);
|
}
|
}
|
|
/**
|
* RequestContext保存当前Http Request
|
*
|
* @return RequestContext
|
*/
|
@Bean
|
public HttpRequestContext requestContext() {
|
return new HttpRequestContext();
|
}
|
|
/**
|
* 获取当前服务AppId的Hook
|
*
|
* @param appRuntime
|
* @return
|
*/
|
@Bean
|
@ConditionalOnMissingBean
|
AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) {
|
return () -> appRuntime.getAppId();
|
}
|
@Bean
|
public InternalResourceViewResolver defaultViewResolver() {
|
return new InternalResourceViewResolver();
|
}
|
|
/**
|
* copy from https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/yaml-msg-converter.html
|
* @param <T>
|
*/
|
private static class YamlHttpMessageConverter<T> extends AbstractHttpMessageConverter<T> {
|
static final MediaType MEDIA_TYPE_YAML = MediaType.valueOf("text/yaml");
|
static final MediaType MEDIA_TYPE_YML = MediaType.valueOf("text/yml");
|
|
@Override
|
protected boolean supports (Class<?> clazz) {
|
return true;
|
}
|
|
@Override
|
protected T readInternal (Class<? extends T> clazz, HttpInputMessage inputMessage)
|
throws IOException, HttpMessageNotReadableException {
|
Yaml yaml = new Yaml();
|
Iterable<Object> itr = yaml.loadAll(inputMessage.getBody());
|
|
Object itrNext = itr.iterator().next();
|
if (itrNext instanceof Collection) {
|
return (T) new JSONArray().fluentAddAll((Collection) itrNext).toJavaList(JSONObject.class);
|
}
|
|
// find the root which is a map
|
return new JSONObject((Map)itrNext).toJavaObject(clazz);
|
}
|
|
@Override
|
protected void writeInternal (T t, HttpOutputMessage outputMessage)
|
throws HttpMessageNotWritableException {
|
throw new UnsupportedOperationException("not convert object to yml");
|
}
|
}
|
|
|
/**
|
* 发现如果继承了WebMvcConfigurationSupport,则在yml中配置的相关内容会失效。 需要重新指定静态资源
|
*
|
* @param registry
|
*/
|
@Override
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
// swagger 资源
|
registry.addResourceHandler("/**").addResourceLocations(
|
"classpath:/static/");
|
registry.addResourceHandler("swagger-ui.html").addResourceLocations(
|
"classpath:/META-INF/resources/");
|
registry.addResourceHandler("/swagger-ui/**").addResourceLocations(
|
"classpath:/META-INF/resources/");
|
registry.addResourceHandler("/swagger-ui/index.html").addResourceLocations(
|
"classpath:/META-INF/resources/");
|
registry.addResourceHandler("/swagger-ui/**")
|
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
|
registry.addResourceHandler("/webjars/**").addResourceLocations(
|
"classpath:/META-INF/resources/webjars/");
|
// 有什么需要自测的网页,可以放在这个目录下
|
registry.addResourceHandler("/**").addResourceLocations(
|
"classpath:/templates/");
|
// registry.addResourceHandler("/**")
|
// .addResourceLocations("classpath:/META-INF/resources/").resourceChain(false);
|
super.addResourceHandlers(registry);
|
}
|
}
|