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> 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() { // @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() { // @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 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 */ private static class YamlHttpMessageConverter extends AbstractHttpMessageConverter { 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 clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { Yaml yaml = new Yaml(); Iterable 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); } }