View Javadoc
1   package cn.home1.oss.lib.errorhandle.api;
2   
3   import static cn.home1.oss.lib.errorhandle.api.ResolvedErrorException.isResolvedError;
4   import static cn.home1.oss.lib.errorhandle.api.ResolvedErrorException.isResolvedErrorWrapByOther;
5   import static com.google.common.base.Throwables.getStackTraceAsString;
6   import static com.google.common.collect.Lists.newArrayList;
7   import static cn.home1.oss.lib.common.CurlUtils.curl;
8   import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST;
9   import static org.springframework.web.servlet.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
10  
11  import com.google.common.collect.ImmutableList;
12  
13  import cn.home1.oss.lib.errorhandle.api.ExceptionTranslator.Location;
14  
15  import cn.home1.oss.lib.common.Defaults;
16  
17  import lombok.extern.slf4j.Slf4j;
18  
19  import org.joda.time.DateTime;
20  import org.slf4j.Marker;
21  import org.slf4j.MarkerFactory;
22  import org.springframework.core.GenericTypeResolver;
23  import org.springframework.core.convert.ConversionService;
24  import org.springframework.http.HttpHeaders;
25  import org.springframework.http.HttpStatus;
26  import org.springframework.util.Assert;
27  import org.springframework.web.context.request.RequestAttributes;
28  import org.springframework.web.context.request.ServletRequestAttributes;
29  
30  import java.util.List;
31  import java.util.Optional;
32  
33  import javax.servlet.ServletException;
34  import javax.servlet.http.HttpServletRequest;
35  
36  /**
37   * AbstractConcreteExceptionResolver.
38   *
39   * @param <T> exception type
40   * @author zhanghaolun
41   */
42  @Slf4j
43  public abstract class AbstractConcreteExceptionResolver<T extends Throwable> implements ConcreteExceptionResolver<T> {
44  
45    private static final String TRACE_OFF = "trace off";
46  
47    protected final Class<T> exceptionClass;
48  
49    protected ConversionService conversionService;
50    protected ExceptionTranslator exceptionTranslator;
51    protected StackTraceIndicator stackTraceIndicator;
52  
53    /**
54     * This constructor determines the exception class from the generic class parameter {@code T}.
55     */
56    protected AbstractConcreteExceptionResolver() {
57      this.exceptionClass = determineTargetType();
58    }
59  
60    protected AbstractConcreteExceptionResolver(final Class<T> exceptionClass) {
61      this.exceptionClass = exceptionClass;
62    }
63  
64    @Deprecated
65    static HttpStatus parseHttpStatus(final Object value) {
66      Assert.notNull(value, "Values of the resolverMap map must not be null");
67  
68      final HttpStatus result;
69      if (value instanceof HttpStatus) {
70        result = (HttpStatus) value;
71      } else if (value instanceof Integer) {
72        result = HttpStatus.valueOf((int) value);
73      } else if (value instanceof String) {
74        result = HttpStatus.valueOf(Integer.parseInt((String) value));
75      } else {
76        throw new IllegalArgumentException(String.format( //
77          "Values of the resolverMap maps must be instance of " //
78            + "ErrorResponseFactory, HttpStatus, String, or int, " //
79            + "but %s given",
80          value.getClass()));
81      }
82      return result;
83    }
84  
85    @Deprecated
86    @SuppressWarnings("unused")
87    private static <T extends Throwable> Throwable cause(final T exception) {
88      Throwable cause = null;
89      if (exception != null) {
90        cause = exception;
91        while (cause instanceof ServletException && cause.getCause() != null) {
92          final ServletException servletException = ((ServletException) cause);
93          cause = servletException.getCause();
94        }
95      }
96      return cause;
97    }
98  
99    private static String error(final Optional<Integer> statusOptional) {
100     // TODO Optional<Integer>' used as type for parameter
101     String result;
102     if (statusOptional.isPresent()) {
103       final Integer status = statusOptional.get();
104       try {
105         final HttpStatus httpStatus = HttpStatus.valueOf(status);
106         result = httpStatus.getReasonPhrase();
107       } catch (final IllegalArgumentException ignored) { // Unable to obtain a reason
108         if (log.isDebugEnabled()) {
109           log.debug("Unable to obtain a reason, status {}", status, ignored);
110         }
111         result = "Http Status " + status;
112       }
113     } else {
114       result = "None";
115     }
116     return result;
117   }
118 
119   @Override
120   public final ResolvedError resolve( //
121     final HttpServletRequest request, //
122     final T throwable //
123   ) {
124     final DateTime now = Defaults.now();
125 
126     // See http://stackoverflow.com/a/12979543/2217862
127     // This attribute is never set in MockMvc, so it's not covered in integration it.
128     request.removeAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
129 
130     final RequestAttributes requestAttributes = new ServletRequestAttributes(request);
131 
132     final Boolean stackTrace = this.stackTraceIndicator.stackTrace(request, null);
133     final String path = request.getQueryString() != null ? //
134       request.getRequestURI() + request.getQueryString() : //
135       request.getRequestURI();
136     final String track = stackTrace ? curl(request) : TRACE_OFF;
137 
138     return this.resolve(requestAttributes, throwable, now, stackTrace, path, track);
139   }
140 
141   /*
142    * (non-Javadoc)
143    * 
144    * @see ExceptionResolver#resolve(RequestAttributes, Throwable)
145    */
146   @Override
147   public final ResolvedError resolve( //
148     final RequestAttributes requestAttributes, //
149     final T throwable //
150   ) {
151     final DateTime now = Defaults.now();
152 
153     final Object requestUri = getAttribute( //
154       requestAttributes, //
155       "javax.servlet.error.request_uri" //
156     );
157     final Boolean stackTrace = this.stackTraceIndicator.stackTrace(null, null);
158     final String path = requestUri != null ? requestUri.toString() : null;
159     // TODO final String track = stackTrace ? curl(request) : TRACE_OFF; ? 忘记为什么这里简单使用requestUri
160     final String track = stackTrace ? path : TRACE_OFF;
161 
162     return this.resolve(requestAttributes, throwable, now, stackTrace, path, track);
163   }
164 
165   protected ResolvedError resolve( //
166     final RequestAttributes requestAttributes, //
167     final T throwable, //
168     final DateTime now, //
169     final Boolean stackTrace, //
170     final String path, //
171     final String track //
172   ) {
173     final ResolvedError resolvedError;
174     if (isResolvedError(throwable)) {
175       resolvedError = ((ResolvedErrorException) throwable).getError();
176       resolvedError.trackPrepend(track);
177     } else if (isResolvedErrorWrapByOther(throwable)) {
178       // 注意顺序
179       // 1.判断是不是ResolvedErrorException
180       // 2.判断是不是被封装了的ResolvedErrorException(如HystrixException)
181       log.warn("这里将被封装过ResolvedErrorException提取出来,原异常信息为:", throwable);
182       resolvedError = ((ResolvedErrorException) throwable.getCause()).getError();
183       resolvedError.trackPrepend(track);
184     } else {
185       final Location location = find(throwable).orElse(null);
186       // basic
187       final Optional<List<ValidationError>> validationErrorsOptional = this.validationErrors(throwable);
188       final Optional<Integer> statusOptional = this.status(requestAttributes, location, throwable);
189       final String error = error(statusOptional);
190       final List<ValidationError> validationErrors = validationErrorsOptional.orElse(null);
191       final String exception = throwable != null ? throwable.getClass().getName() : null;
192       final String message = message(requestAttributes, throwable, validationErrorsOptional);
193       final Integer status = statusOptional.orElse(500);
194       final Long timestamp = now.getMillis();
195       final String trace = stackTrace && throwable != null ? getStackTraceAsString(throwable) : null;
196       // extended
197       final String datetime = now.toString(Defaults.ISO8601);
198       final String localizedMessage = this.localizedMessage(requestAttributes, location, throwable).orElse("null");
199       final HttpHeaders headers = this.createHeaders(requestAttributes, throwable).orElse(null);
200       final List<String> tracks = ImmutableList.of(track);
201 
202       resolvedError = ResolvedError.resolvedErrorBuilder() //
203         // basic
204         .error(error) //
205         .validationErrors( //
206           validationErrors != null ? validationErrors.toArray(new ValidationError[validationErrors.size()]) : null //
207         ) //
208         .exception(exception) //
209         .message(message) //
210         .path(path) //
211         .status(status) //
212         .timestamp(timestamp) //
213         .trace(trace) //
214         // extended
215         .datetime(datetime) //
216         .localizedMessage(localizedMessage) //
217         .headers(HttpHeader.fromHttpHeaders(headers)) //
218         .tracks(tracks.toArray(new String[tracks.size()])) //
219         .build();
220     }
221 
222     logError(requestAttributes, throwable, resolvedError);
223 
224     return resolvedError;
225   }
226 
227   /**
228    * Logs the exception; on ERROR level when status is 5xx, otherwise on INFO level without stack trace, or DEBUG level
229    * with stack trace. The logger name is {@code ExceptionResolver}.
230    *
231    * @param requestAttributes requestAttributes
232    * @param throwable         throwable
233    * @param resolvedError     The exception to log.
234    */
235   protected void logError( //
236     final RequestAttributes requestAttributes, //
237     final T throwable, //
238     final ResolvedError resolvedError //
239   ) {
240     if (resolvedError.getStatus() >= HttpStatus.INTERNAL_SERVER_ERROR.value()) {
241       final Marker marker = MarkerFactory.getMarker("error");
242       final String msg = String.format( //
243         "%s ~> %d", //
244         resolvedError.getPath(), //
245         resolvedError.getStatus() //
246       );
247 
248       if (log.isTraceEnabled()) {
249         log.trace("attributes in request scope: {}", //
250           newArrayList(requestAttributes.getAttributeNames(SCOPE_REQUEST)));
251       }
252       log.warn(marker, msg, new Object[]{throwable});
253     }
254   }
255 
256   @SuppressWarnings("unchecked")
257   private Class<T> determineTargetType() {
258     return (Class<T>) GenericTypeResolver.resolveTypeArguments( //
259       this.getClass(), //
260       AbstractConcreteExceptionResolver.class //
261     )[0];
262   }
263 
264   @Override
265   public Class<T> getExceptionClass() {
266     return this.exceptionClass;
267   }
268 
269   @Override
270   public void setConversionService(final ConversionService conversionService) {
271     this.conversionService = conversionService;
272   }
273 
274   @Override
275   public ExceptionTranslator getExceptionTranslator() {
276     return this.exceptionTranslator;
277   }
278 
279   @Override
280   public void setExceptionTranslator(final ExceptionTranslator exceptionTranslator) {
281     this.exceptionTranslator = exceptionTranslator;
282   }
283 
284   @Override
285   public void setStackTraceIndicator(final StackTraceIndicator stackTraceIndicator) {
286     this.stackTraceIndicator = stackTraceIndicator;
287   }
288 }