ErrorHandleAutoConfiguration.java
package cn.home1.oss.lib.errorhandle.starter;
import static cn.home1.oss.boot.autoconfigure.AppErrorProperties.SearchStrategy.HIERARCHY_FIRST;
import static cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location.HIERARCHY_FIRST_COMPARATOR;
import static cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location.ORDER_FIRST_COMPARATOR;
import static cn.home1.oss.lib.errorhandle.api.ResolvedError.RESOLVED_ERROR_COOKIE;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayList;
import static cn.home1.oss.lib.common.Jackson2Utils.getJackson2Present;
import static java.util.concurrent.TimeUnit.DAYS;
import cn.home1.oss.boot.autoconfigure.AppErrorProperties;
import cn.home1.oss.boot.autoconfigure.AppProperties;
import cn.home1.oss.boot.autoconfigure.ConditionalOnNotEnvProduction;
import cn.home1.oss.lib.errorhandle.api.ExceptionTranslator;
import cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location;
import cn.home1.oss.lib.errorhandle.api.ResolvedError;
import cn.home1.oss.lib.errorhandle.internal.BaseErrorController;
import cn.home1.oss.lib.errorhandle.internal.ContentCachingRequestFilter;
import cn.home1.oss.lib.errorhandle.internal.DefaultStackTraceIndicator;
import cn.home1.oss.lib.errorhandle.internal.ExtendedErrorAttributes;
import cn.home1.oss.lib.errorhandle.internal.rpc.FeignErrorDecoderConfiguration;
import cn.home1.oss.lib.errorhandle.internal.translator.DefaultExceptionTranslator;
import cn.home1.oss.lib.webmvc.internal.DefaultHttpEntityMethodProcessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.home1.oss.lib.common.msginterpolate.SpelMessageInterpolator;
import cn.home1.oss.lib.webmvc.api.DomainResolver;
import cn.home1.oss.lib.webmvc.api.JsonToken;
import cn.home1.oss.lib.webmvc.api.TokenBasedCookie;
import cn.home1.oss.lib.webmvc.api.TypeSafeCookie;
import cn.home1.oss.lib.webmvc.api.UrlEncodedToken;
import cn.home1.oss.lib.webmvc.starter.WebApplicationAutoConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import javax.servlet.Servlet;
/**
* A replacement of {@link org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration}.
* see: {@link org.springframework.boot.autoconfigure.web.DefaultErrorAttributes}
* see: {@link org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration}
* Extends WebMvcConfigurerAdapter make this visable to MockMvc tests,
* does not override it's method.
*/
@AutoConfigureBefore({ErrorMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, SecurityAutoConfiguration.class})
@AutoConfigureAfter({WebApplicationAutoConfiguration.class})
@ComponentScan(basePackages = {"cn.home1.oss.lib.errorhandle.starter"})
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@ConditionalOnWebApplication
@Configuration
@EnableConfigurationProperties(value = {AppProperties.class, DefaultStackTraceIndicator.class})
@Import({ //
ExceptionResolverConfiguration.class, //
FeignErrorDecoderConfiguration.class, //
ExceptionHandlerConfiguration.class})
@ServletComponentScan(basePackages = {"cn.home1.oss.lib.errorhandle.filter"})
@Slf4j
public class ErrorHandleAutoConfiguration extends WebMvcConfigurerAdapter implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Autowired
private AppProperties appProperties;
@Autowired
private DefaultHttpEntityMethodProcessor defaultHttpEntityMethodProcessor;
@Autowired
private DomainResolver domainResolver;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ServerProperties serverProperties;
@Bean
@ConditionalOnNotEnvProduction
public ContentCachingRequestFilter contentCachingRequestFilter() {
return new ContentCachingRequestFilter();
}
@Bean(name = RESOLVED_ERROR_COOKIE)
@ConditionalOnMissingBean(name = RESOLVED_ERROR_COOKIE)
public TypeSafeCookie<ResolvedError> resolvedErrorCookie() {
return buildResolvedErrorCookie(this.domainResolver, this.objectMapper);
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public ExtendedErrorAttributes errorAttributes() {
return new ExtendedErrorAttributes();
}
@Bean
//@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
@ConditionalOnProperty(prefix = "app", name = "type", havingValue = "RESTFUL", matchIfMissing = false)
public BaseErrorController.RestfulErrorController restfulErrorController( //
final ExtendedErrorAttributes errorAttributes) {
return new BaseErrorController.RestfulErrorController(errorAttributes, this.serverProperties.getError());
}
@Bean
//@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
@ConditionalOnProperty(prefix = "app", name = "type", havingValue = "TEMPLATE", matchIfMissing = false)
public BaseErrorController.TemplateErrorController templateErrorController( //
final ExtendedErrorAttributes errorAttributes) {
return new BaseErrorController.TemplateErrorController(errorAttributes, this.serverProperties.getError());
}
@Bean
//@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
@ConditionalOnProperty(prefix = "app", name = "type", havingValue = "MIXED", matchIfMissing = true)
public BaseErrorController.MixedErrorController mixedErrorController( //
final ExtendedErrorAttributes errorAttributes) {
return new BaseErrorController.MixedErrorController(errorAttributes, this.serverProperties.getError());
}
@Bean
public ExceptionTranslator exceptionTranslator() {
return buildExceptionTranslator(this.appProperties.getError().getSearchStrategy());
}
@Override
public void setApplicationContext(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
// ------------------------------ experimental WebMvcConfigurerAdapter ------------------------------
/**
* see: {@link WebMvcConfigurationSupport#handlerExceptionResolver()}.
*
* @param resolvers resolvers
*/
@Override
public void configureHandlerExceptionResolvers(final List<HandlerExceptionResolver> resolvers) {
// call this addDefaultHandlerExceptionResolvers ?
}
/**
* @param resolvers exceptionResolvers
* @deprecated see: {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
* #addDefaultHandlerExceptionResolvers(List) addDefaultHandlerExceptionResolvers}
*/
@Deprecated
protected final void addDefaultHandlerExceptionResolvers( //
final List<HandlerExceptionResolver> resolvers //
) {
final List<ResponseBodyAdvice<?>> interceptors = newArrayList();
if (getJackson2Present()) {
interceptors.add(new JsonViewResponseBodyAdvice());
}
final ExceptionHandlerExceptionResolver handlerExceptionResolver = new ExceptionHandlerExceptionResolver();
handlerExceptionResolver.setMessageConverters(this.defaultHttpEntityMethodProcessor.getMessageConverters());
handlerExceptionResolver.setResponseBodyAdvice(interceptors);
handlerExceptionResolver.setApplicationContext(this.applicationContext);
handlerExceptionResolver.afterPropertiesSet();
final ResponseStatusExceptionResolver responseStatusExceptionResolver = new ResponseStatusExceptionResolver();
responseStatusExceptionResolver.setMessageSource(this.applicationContext);
resolvers.add(handlerExceptionResolver);
// resolvers add responseStatusExceptionResolver ?
// resolvers add new DefaultHandlerExceptionResolver ?
}
public static ExceptionTranslator buildExceptionTranslator(final AppErrorProperties.SearchStrategy searchStrategy) {
final Optional<MessageSource> messageSourceOptional = messageSource("classpath:/errorhandle/default");
final MessageSource defaultMessageSource = messageSourceOptional.isPresent() ? messageSourceOptional.get() : null;
checkNotNull(defaultMessageSource, "message source classpath:/errorhandle/default not present");
final Optional<MessageSource> applicationMessageSource = messageSource("classpath:/errorhandle/application");
final List<MessageSource> messageSources = applicationMessageSource.isPresent() ? //
newArrayList(applicationMessageSource.get(), defaultMessageSource) : //
newArrayList(defaultMessageSource);
final Comparator<Location> locationComparator = HIERARCHY_FIRST == searchStrategy ? //
HIERARCHY_FIRST_COMPARATOR : ORDER_FIRST_COMPARATOR;
final DefaultExceptionTranslator defaultExceptionTranslator = new DefaultExceptionTranslator();
defaultExceptionTranslator.setLocationComparator(locationComparator);
defaultExceptionTranslator.setMessageSources(messageSources);
defaultExceptionTranslator.setMessageInterpolator(new SpelMessageInterpolator());
return defaultExceptionTranslator;
}
public static TypeSafeCookie<ResolvedError> buildResolvedErrorCookie( //
final DomainResolver domainResolver, final ObjectMapper objectMapper) {
final JsonToken<ResolvedError> jsonToken = new JsonToken<>(ResolvedError.class, objectMapper);
final UrlEncodedToken<ResolvedError> urlEncodedToken = new UrlEncodedToken<>(jsonToken);
final int maxAge = (int) DAYS.toSeconds(1L);
return new TokenBasedCookie<>(domainResolver, false, maxAge, "resolved_error", false, urlEncodedToken);
}
static Optional<MessageSource> messageSource(final String location) {
final Optional<MessageSource> result;
if (StringUtils.isNotBlank(location)) {
final ReloadableResourceBundleMessageSource messages = new ReloadableResourceBundleMessageSource();
messages.setBasename(location);
messages.setDefaultEncoding("UTF-8");
messages.setFallbackToSystemLocale(false);
result = Optional.of(messages);
} else {
log.info("errorhandle messageSource not found at {}", location);
result = Optional.empty();
}
return result;
}
}