View Javadoc
1   package cn.home1.oss.lib.errorhandle.starter;
2   
3   import static cn.home1.oss.boot.autoconfigure.AppErrorProperties.SearchStrategy.HIERARCHY_FIRST;
4   import static cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location.HIERARCHY_FIRST_COMPARATOR;
5   import static cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location.ORDER_FIRST_COMPARATOR;
6   import static cn.home1.oss.lib.errorhandle.api.ResolvedError.RESOLVED_ERROR_COOKIE;
7   import static com.google.common.base.Preconditions.checkNotNull;
8   import static com.google.common.collect.Lists.newArrayList;
9   import static cn.home1.oss.lib.common.Jackson2Utils.getJackson2Present;
10  import static java.util.concurrent.TimeUnit.DAYS;
11  
12  import cn.home1.oss.boot.autoconfigure.AppErrorProperties;
13  import cn.home1.oss.boot.autoconfigure.AppProperties;
14  import cn.home1.oss.boot.autoconfigure.ConditionalOnNotEnvProduction;
15  import cn.home1.oss.lib.errorhandle.api.ExceptionTranslator;
16  import cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location;
17  import cn.home1.oss.lib.errorhandle.api.ResolvedError;
18  import cn.home1.oss.lib.errorhandle.internal.BaseErrorController;
19  import cn.home1.oss.lib.errorhandle.internal.ContentCachingRequestFilter;
20  import cn.home1.oss.lib.errorhandle.internal.DefaultStackTraceIndicator;
21  import cn.home1.oss.lib.errorhandle.internal.ExtendedErrorAttributes;
22  import cn.home1.oss.lib.errorhandle.internal.rpc.FeignErrorDecoderConfiguration;
23  import cn.home1.oss.lib.errorhandle.internal.translator.DefaultExceptionTranslator;
24  import cn.home1.oss.lib.webmvc.internal.DefaultHttpEntityMethodProcessor;
25  
26  import com.fasterxml.jackson.databind.ObjectMapper;
27  import cn.home1.oss.lib.common.msginterpolate.SpelMessageInterpolator;
28  import cn.home1.oss.lib.webmvc.api.DomainResolver;
29  import cn.home1.oss.lib.webmvc.api.JsonToken;
30  import cn.home1.oss.lib.webmvc.api.TokenBasedCookie;
31  import cn.home1.oss.lib.webmvc.api.TypeSafeCookie;
32  import cn.home1.oss.lib.webmvc.api.UrlEncodedToken;
33  import cn.home1.oss.lib.webmvc.starter.WebApplicationAutoConfiguration;
34  
35  import lombok.extern.slf4j.Slf4j;
36  
37  import org.apache.commons.lang3.StringUtils;
38  import org.springframework.beans.factory.annotation.Autowired;
39  import org.springframework.boot.autoconfigure.AutoConfigureAfter;
40  import org.springframework.boot.autoconfigure.AutoConfigureBefore;
41  import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
42  import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
43  import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
44  import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
45  import org.springframework.boot.autoconfigure.condition.SearchStrategy;
46  import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
47  import org.springframework.boot.autoconfigure.web.ErrorAttributes;
48  import org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration;
49  import org.springframework.boot.autoconfigure.web.ServerProperties;
50  import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
51  import org.springframework.boot.context.properties.EnableConfigurationProperties;
52  import org.springframework.boot.web.servlet.ServletComponentScan;
53  import org.springframework.context.ApplicationContext;
54  import org.springframework.context.ApplicationContextAware;
55  import org.springframework.context.MessageSource;
56  import org.springframework.context.annotation.Bean;
57  import org.springframework.context.annotation.ComponentScan;
58  import org.springframework.context.annotation.Configuration;
59  import org.springframework.context.annotation.Import;
60  import org.springframework.context.support.ReloadableResourceBundleMessageSource;
61  import org.springframework.web.servlet.DispatcherServlet;
62  import org.springframework.web.servlet.HandlerExceptionResolver;
63  import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
64  import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
65  import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
66  import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
67  import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice;
68  import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
69  
70  import java.util.Comparator;
71  import java.util.List;
72  import java.util.Optional;
73  
74  import javax.servlet.Servlet;
75  
76  /**
77   * A replacement of {@link org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration}.
78   * see: {@link org.springframework.boot.autoconfigure.web.DefaultErrorAttributes}
79   * see: {@link org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration}
80   * Extends WebMvcConfigurerAdapter make this visable to MockMvc tests,
81   * does not override it's method.
82   */
83  @AutoConfigureBefore({ErrorMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, SecurityAutoConfiguration.class})
84  @AutoConfigureAfter({WebApplicationAutoConfiguration.class})
85  @ComponentScan(basePackages = {"cn.home1.oss.lib.errorhandle.starter"})
86  @ConditionalOnClass({Servlet.class, DispatcherServlet.class})
87  @ConditionalOnWebApplication
88  @Configuration
89  @EnableConfigurationProperties(value = {AppProperties.class, DefaultStackTraceIndicator.class})
90  @Import({ //
91    ExceptionResolverConfiguration.class, //
92    FeignErrorDecoderConfiguration.class, //
93    ExceptionHandlerConfiguration.class})
94  @ServletComponentScan(basePackages = {"cn.home1.oss.lib.errorhandle.filter"})
95  @Slf4j
96  public class ErrorHandleAutoConfiguration extends WebMvcConfigurerAdapter implements ApplicationContextAware {
97  
98    private ApplicationContext applicationContext;
99  
100   @Autowired
101   private AppProperties appProperties;
102 
103   @Autowired
104   private DefaultHttpEntityMethodProcessor defaultHttpEntityMethodProcessor;
105 
106   @Autowired
107   private DomainResolver domainResolver;
108   @Autowired
109   private ObjectMapper objectMapper;
110   @Autowired
111   private ServerProperties serverProperties;
112 
113   @Bean
114   @ConditionalOnNotEnvProduction
115   public ContentCachingRequestFilter contentCachingRequestFilter() {
116     return new ContentCachingRequestFilter();
117   }
118 
119   @Bean(name = RESOLVED_ERROR_COOKIE)
120   @ConditionalOnMissingBean(name = RESOLVED_ERROR_COOKIE)
121   public TypeSafeCookie<ResolvedError> resolvedErrorCookie() {
122     return buildResolvedErrorCookie(this.domainResolver, this.objectMapper);
123   }
124 
125   @Bean
126   @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
127   public ExtendedErrorAttributes errorAttributes() {
128     return new ExtendedErrorAttributes();
129   }
130 
131   @Bean
132   //@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
133   @ConditionalOnProperty(prefix = "app", name = "type", havingValue = "RESTFUL", matchIfMissing = false)
134   public BaseErrorController.RestfulErrorController restfulErrorController( //
135     final ExtendedErrorAttributes errorAttributes) {
136     return new BaseErrorController.RestfulErrorController(errorAttributes, this.serverProperties.getError());
137   }
138 
139   @Bean
140   //@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
141   @ConditionalOnProperty(prefix = "app", name = "type", havingValue = "TEMPLATE", matchIfMissing = false)
142   public BaseErrorController.TemplateErrorController templateErrorController( //
143     final ExtendedErrorAttributes errorAttributes) {
144     return new BaseErrorController.TemplateErrorController(errorAttributes, this.serverProperties.getError());
145   }
146 
147   @Bean
148   //@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
149   @ConditionalOnProperty(prefix = "app", name = "type", havingValue = "MIXED", matchIfMissing = true)
150   public BaseErrorController.MixedErrorController mixedErrorController( //
151     final ExtendedErrorAttributes errorAttributes) {
152     return new BaseErrorController.MixedErrorController(errorAttributes, this.serverProperties.getError());
153   }
154 
155   @Bean
156   public ExceptionTranslator exceptionTranslator() {
157     return buildExceptionTranslator(this.appProperties.getError().getSearchStrategy());
158   }
159 
160   @Override
161   public void setApplicationContext(final ApplicationContext applicationContext) {
162     this.applicationContext = applicationContext;
163   }
164 
165   // ------------------------------ experimental WebMvcConfigurerAdapter ------------------------------
166 
167   /**
168    * see: {@link WebMvcConfigurationSupport#handlerExceptionResolver()}.
169    *
170    * @param resolvers resolvers
171    */
172   @Override
173   public void configureHandlerExceptionResolvers(final List<HandlerExceptionResolver> resolvers) {
174     // call this addDefaultHandlerExceptionResolvers ?
175   }
176 
177   /**
178    * @param resolvers exceptionResolvers
179    * @deprecated see: {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
180    * #addDefaultHandlerExceptionResolvers(List) addDefaultHandlerExceptionResolvers}
181    */
182   @Deprecated
183   protected final void addDefaultHandlerExceptionResolvers( //
184     final List<HandlerExceptionResolver> resolvers //
185   ) {
186     final List<ResponseBodyAdvice<?>> interceptors = newArrayList();
187     if (getJackson2Present()) {
188       interceptors.add(new JsonViewResponseBodyAdvice());
189     }
190     final ExceptionHandlerExceptionResolver handlerExceptionResolver = new ExceptionHandlerExceptionResolver();
191     handlerExceptionResolver.setMessageConverters(this.defaultHttpEntityMethodProcessor.getMessageConverters());
192     handlerExceptionResolver.setResponseBodyAdvice(interceptors);
193     handlerExceptionResolver.setApplicationContext(this.applicationContext);
194     handlerExceptionResolver.afterPropertiesSet();
195 
196     final ResponseStatusExceptionResolver responseStatusExceptionResolver = new ResponseStatusExceptionResolver();
197     responseStatusExceptionResolver.setMessageSource(this.applicationContext);
198 
199     resolvers.add(handlerExceptionResolver);
200     // resolvers add responseStatusExceptionResolver ?
201     // resolvers add new DefaultHandlerExceptionResolver ?
202   }
203 
204   public static ExceptionTranslator buildExceptionTranslator(final AppErrorProperties.SearchStrategy searchStrategy) {
205     final Optional<MessageSource> messageSourceOptional = messageSource("classpath:/errorhandle/default");
206     final MessageSource defaultMessageSource = messageSourceOptional.isPresent() ? messageSourceOptional.get() : null;
207     checkNotNull(defaultMessageSource, "message source classpath:/errorhandle/default not present");
208 
209     final Optional<MessageSource> applicationMessageSource = messageSource("classpath:/errorhandle/application");
210     final List<MessageSource> messageSources = applicationMessageSource.isPresent() ? //
211       newArrayList(applicationMessageSource.get(), defaultMessageSource) : //
212       newArrayList(defaultMessageSource);
213 
214     final Comparator<Location> locationComparator = HIERARCHY_FIRST == searchStrategy ? //
215       HIERARCHY_FIRST_COMPARATOR : ORDER_FIRST_COMPARATOR;
216 
217     final DefaultExceptionTranslator defaultExceptionTranslator = new DefaultExceptionTranslator();
218     defaultExceptionTranslator.setLocationComparator(locationComparator);
219     defaultExceptionTranslator.setMessageSources(messageSources);
220     defaultExceptionTranslator.setMessageInterpolator(new SpelMessageInterpolator());
221     return defaultExceptionTranslator;
222   }
223 
224   public static TypeSafeCookie<ResolvedError> buildResolvedErrorCookie( //
225     final DomainResolver domainResolver, final ObjectMapper objectMapper) {
226     final JsonToken<ResolvedError> jsonToken = new JsonToken<>(ResolvedError.class, objectMapper);
227     final UrlEncodedToken<ResolvedError> urlEncodedToken = new UrlEncodedToken<>(jsonToken);
228     final int maxAge = (int) DAYS.toSeconds(1L);
229     return new TokenBasedCookie<>(domainResolver, false, maxAge, "resolved_error", false, urlEncodedToken);
230   }
231 
232   static Optional<MessageSource> messageSource(final String location) {
233     final Optional<MessageSource> result;
234     if (StringUtils.isNotBlank(location)) {
235       final ReloadableResourceBundleMessageSource messages = new ReloadableResourceBundleMessageSource();
236       messages.setBasename(location);
237       messages.setDefaultEncoding("UTF-8");
238       messages.setFallbackToSystemLocale(false);
239       result = Optional.of(messages);
240     } else {
241       log.info("errorhandle messageSource not found at {}", location);
242       result = Optional.empty();
243     }
244     return result;
245   }
246 }