Skip to content

Commit 1cf4a2f

Browse files
committed
Add ExceptionHandlerSupport class
The new class is functionally equivalent to the DefaultHandlerExceptionResolver (i.e. it translates Spring MVC exceptions to various status codes) but uses an @ExceptionHandler returning a ResponseEntity<Object>, which means it can be customized to write error content to the body of the response. Issue: SPR-9290
1 parent 58daeea commit 1cf4a2f

File tree

4 files changed

+751
-74
lines changed

4 files changed

+751
-74
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.servlet.mvc.method.annotation;
17+
18+
import java.util.HashSet;
19+
import java.util.List;
20+
import java.util.Set;
21+
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.springframework.beans.ConversionNotSupportedException;
25+
import org.springframework.beans.TypeMismatchException;
26+
import org.springframework.http.HttpHeaders;
27+
import org.springframework.http.HttpMethod;
28+
import org.springframework.http.HttpStatus;
29+
import org.springframework.http.MediaType;
30+
import org.springframework.http.ResponseEntity;
31+
import org.springframework.http.converter.HttpMessageNotReadableException;
32+
import org.springframework.http.converter.HttpMessageNotWritableException;
33+
import org.springframework.util.CollectionUtils;
34+
import org.springframework.validation.BindException;
35+
import org.springframework.web.HttpMediaTypeNotAcceptableException;
36+
import org.springframework.web.HttpMediaTypeNotSupportedException;
37+
import org.springframework.web.HttpRequestMethodNotSupportedException;
38+
import org.springframework.web.bind.MethodArgumentNotValidException;
39+
import org.springframework.web.bind.MissingServletRequestParameterException;
40+
import org.springframework.web.bind.ServletRequestBindingException;
41+
import org.springframework.web.bind.annotation.ControllerAdvice;
42+
import org.springframework.web.bind.annotation.ExceptionHandler;
43+
import org.springframework.web.bind.annotation.RequestMapping;
44+
import org.springframework.web.context.request.WebRequest;
45+
import org.springframework.web.multipart.support.MissingServletRequestPartException;
46+
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
47+
48+
/**
49+
* A convenient base for classes with {@link ExceptionHandler} methods providing
50+
* infrastructure to handle standard Spring MVC exceptions. The functionality is
51+
* equivalent to that of the {@link DefaultHandlerExceptionResolver} except it
52+
* can be customized to write error content to the body of the response. If there
53+
* is no need to write error content, use {@code DefaultHandlerExceptionResolver}
54+
* instead.
55+
*
56+
* <p>It is expected the sub-classes will be annotated with
57+
* {@link ControllerAdvice @ControllerAdvice} and that
58+
* {@link ExceptionHandlerExceptionResolver} is configured to ensure this class
59+
* applies to exceptions from any {@link RequestMapping @RequestMapping} method.
60+
*
61+
* @author Rossen Stoyanchev
62+
* @since 3.2
63+
*
64+
* @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
65+
*/
66+
public abstract class ExceptionHandlerSupport {
67+
68+
protected final Log logger = LogFactory.getLog(getClass());
69+
70+
/**
71+
* Log category to use when no mapped handler is found for a request.
72+
* @see #pageNotFoundLogger
73+
*/
74+
public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";
75+
76+
/**
77+
* Additional logger to use when no mapped handler is found for a request.
78+
* @see #PAGE_NOT_FOUND_LOG_CATEGORY
79+
*/
80+
protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);
81+
82+
83+
/**
84+
* Provides handling for standard Spring MVC exceptions.
85+
* @param ex the target exception
86+
* @param request the current request
87+
*/
88+
@ExceptionHandler(value={
89+
NoSuchRequestHandlingMethodException.class,
90+
HttpRequestMethodNotSupportedException.class,
91+
HttpMediaTypeNotSupportedException.class,
92+
HttpMediaTypeNotAcceptableException.class,
93+
MissingServletRequestParameterException.class,
94+
ServletRequestBindingException.class,
95+
ConversionNotSupportedException.class,
96+
TypeMismatchException.class,
97+
HttpMessageNotReadableException.class,
98+
HttpMessageNotWritableException.class,
99+
MethodArgumentNotValidException.class,
100+
MissingServletRequestPartException.class,
101+
BindException.class
102+
})
103+
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) {
104+
105+
HttpHeaders headers = new HttpHeaders();
106+
107+
HttpStatus status;
108+
Object body;
109+
110+
if (ex instanceof NoSuchRequestHandlingMethodException) {
111+
status = HttpStatus.NOT_FOUND;
112+
body = handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, headers, status, request);
113+
}
114+
else if (ex instanceof HttpRequestMethodNotSupportedException) {
115+
status = HttpStatus.METHOD_NOT_ALLOWED;
116+
body = handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, headers, status, request);
117+
}
118+
else if (ex instanceof HttpMediaTypeNotSupportedException) {
119+
status = HttpStatus.UNSUPPORTED_MEDIA_TYPE;
120+
body = handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, headers, status, request);
121+
}
122+
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
123+
status = HttpStatus.NOT_ACCEPTABLE;
124+
body = handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request);
125+
}
126+
else if (ex instanceof MissingServletRequestParameterException) {
127+
status = HttpStatus.BAD_REQUEST;
128+
body = handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request);
129+
}
130+
else if (ex instanceof ServletRequestBindingException) {
131+
status = HttpStatus.BAD_REQUEST;
132+
body = handleServletRequestBindingException((ServletRequestBindingException) ex, headers, status, request);
133+
}
134+
else if (ex instanceof ConversionNotSupportedException) {
135+
status = HttpStatus.INTERNAL_SERVER_ERROR;
136+
body = handleConversionNotSupported((ConversionNotSupportedException) ex, headers, status, request);
137+
}
138+
else if (ex instanceof TypeMismatchException) {
139+
status = HttpStatus.BAD_REQUEST;
140+
body = handleTypeMismatch((TypeMismatchException) ex, headers, status, request);
141+
}
142+
else if (ex instanceof HttpMessageNotReadableException) {
143+
status = HttpStatus.BAD_REQUEST;
144+
body = handleHttpMessageNotReadable((HttpMessageNotReadableException) ex, headers, status, request);
145+
}
146+
else if (ex instanceof HttpMessageNotWritableException) {
147+
status = HttpStatus.INTERNAL_SERVER_ERROR;
148+
body = handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, headers, status, request);
149+
}
150+
else if (ex instanceof MethodArgumentNotValidException) {
151+
status = HttpStatus.BAD_REQUEST;
152+
body = handleMethodArgumentNotValid((MethodArgumentNotValidException) ex, headers, status, request);
153+
}
154+
else if (ex instanceof MissingServletRequestPartException) {
155+
status = HttpStatus.BAD_REQUEST;
156+
body = handleMissingServletRequestPart((MissingServletRequestPartException) ex, headers, status, request);
157+
}
158+
else if (ex instanceof BindException) {
159+
status = HttpStatus.BAD_REQUEST;
160+
body = handleBindException((BindException) ex, headers, status, request);
161+
}
162+
else {
163+
logger.warn("Unknown exception type: " + ex.getClass().getName());
164+
status = HttpStatus.INTERNAL_SERVER_ERROR;
165+
body = handleExceptionInternal(ex, headers, status, request);
166+
}
167+
168+
return new ResponseEntity<Object>(body, headers, status);
169+
}
170+
171+
/**
172+
* A single place to customize the response body of all Exception types.
173+
* This method returns {@code null} by default.
174+
* @param ex the exception
175+
* @param headers the headers to be written to the response
176+
* @param status the selected response status
177+
* @param request the current request
178+
*/
179+
protected Object handleExceptionInternal(Exception ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
180+
return null;
181+
}
182+
183+
/**
184+
* Customize the response for NoSuchRequestHandlingMethodException.
185+
* This method logs a warning and delegates to
186+
* {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
187+
* @param ex the exception
188+
* @param headers the headers to be written to the response
189+
* @param status the selected response status
190+
* @param request the current request
191+
* @return an Object or {@code null}
192+
*/
193+
protected Object handleNoSuchRequestHandlingMethod(NoSuchRequestHandlingMethodException ex,
194+
HttpHeaders headers, HttpStatus status, WebRequest request) {
195+
196+
pageNotFoundLogger.warn(ex.getMessage());
197+
198+
return handleExceptionInternal(ex, headers, status, request);
199+
}
200+
201+
/**
202+
* Customize the response for HttpRequestMethodNotSupportedException.
203+
* This method logs a warning, sets the "Allow" header, and delegates to
204+
* {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
205+
* @param ex the exception
206+
* @param headers the headers to be written to the response
207+
* @param status the selected response status
208+
* @param request the current request
209+
* @return an Object or {@code null}
210+
*/
211+
protected Object handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
212+
HttpHeaders headers, HttpStatus status, WebRequest request) {
213+
214+
pageNotFoundLogger.warn(ex.getMessage());
215+
216+
Set<HttpMethod> mediaTypes = new HashSet<HttpMethod>();
217+
for (String value : ex.getSupportedMethods()) {
218+
mediaTypes.add(HttpMethod.valueOf(value));
219+
}
220+
if (!mediaTypes.isEmpty()) {
221+
headers.setAllow(mediaTypes);
222+
}
223+
224+
return handleExceptionInternal(ex, headers, status, request);
225+
}
226+
227+
/**
228+
* Customize the response for HttpMediaTypeNotSupportedException.
229+
* This method sets the "Accept" header and delegates to
230+
* {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
231+
* @param ex the exception
232+
* @param headers the headers to be written to the response
233+
* @param status the selected response status
234+
* @param request the current request
235+
* @return an Object or {@code null}
236+
*/
237+
protected Object handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex,
238+
HttpHeaders headers, HttpStatus status, WebRequest request) {
239+
240+
List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
241+
if (!CollectionUtils.isEmpty(mediaTypes)) {
242+
headers.setAccept(mediaTypes);
243+
}
244+
return handleExceptionInternal(ex, headers, status, request);
245+
}
246+
247+
/**
248+
* Customize the response for HttpMediaTypeNotAcceptableException.
249+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
250+
* @param ex the exception
251+
* @param headers the headers to be written to the response
252+
* @param status the selected response status
253+
* @param request the current request
254+
* @return an Object or {@code null}
255+
*/
256+
protected Object handleHttpMediaTypeNotAcceptable(HttpMediaTypeNotAcceptableException ex,
257+
HttpHeaders headers, HttpStatus status, WebRequest request) {
258+
259+
return handleExceptionInternal(ex, headers, status, request);
260+
}
261+
262+
/**
263+
* Customize the response for MissingServletRequestParameterException.
264+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
265+
* @param ex the exception
266+
* @param headers the headers to be written to the response
267+
* @param status the selected response status
268+
* @param request the current request
269+
* @return an Object or {@code null}
270+
*/
271+
protected Object handleMissingServletRequestParameter(MissingServletRequestParameterException ex,
272+
HttpHeaders headers, HttpStatus status, WebRequest request) {
273+
274+
return handleExceptionInternal(ex, headers, status, request);
275+
}
276+
277+
/**
278+
* Customize the response for ServletRequestBindingException.
279+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
280+
* @param ex the exception
281+
* @param headers the headers to be written to the response
282+
* @param status the selected response status
283+
* @param request the current request
284+
* @return an Object or {@code null}
285+
*/
286+
protected Object handleServletRequestBindingException(ServletRequestBindingException ex,
287+
HttpHeaders headers, HttpStatus status, WebRequest request) {
288+
289+
return handleExceptionInternal(ex, headers, status, request);
290+
}
291+
292+
/**
293+
* Customize the response for ConversionNotSupportedException.
294+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
295+
* @param ex the exception
296+
* @param headers the headers to be written to the response
297+
* @param status the selected response status
298+
* @param request the current request
299+
* @return an Object or {@code null}
300+
*/
301+
protected Object handleConversionNotSupported(ConversionNotSupportedException ex,
302+
HttpHeaders headers, HttpStatus status, WebRequest request) {
303+
304+
return handleExceptionInternal(ex, headers, status, request);
305+
}
306+
307+
/**
308+
* Customize the response for TypeMismatchException.
309+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
310+
* @param ex the exception
311+
* @param headers the headers to be written to the response
312+
* @param status the selected response status
313+
* @param request the current request
314+
* @return an Object or {@code null}
315+
*/
316+
protected Object handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers,
317+
HttpStatus status, WebRequest request) {
318+
319+
return handleExceptionInternal(ex, headers, status, request);
320+
}
321+
322+
/**
323+
* Customize the response for HttpMessageNotReadableException.
324+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
325+
* @param ex the exception
326+
* @param headers the headers to be written to the response
327+
* @param status the selected response status
328+
* @param request the current request
329+
* @return an Object or {@code null}
330+
*/
331+
protected Object handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
332+
HttpHeaders headers, HttpStatus status, WebRequest request) {
333+
334+
return handleExceptionInternal(ex, headers, status, request);
335+
}
336+
337+
/**
338+
* Customize the response for HttpMessageNotWritableException.
339+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
340+
* @param ex the exception
341+
* @param headers the headers to be written to the response
342+
* @param status the selected response status
343+
* @param request the current request
344+
* @return an Object or {@code null}
345+
*/
346+
protected Object handleHttpMessageNotWritable(HttpMessageNotWritableException ex,
347+
HttpHeaders headers, HttpStatus status, WebRequest request) {
348+
349+
return handleExceptionInternal(ex, headers, status, request);
350+
}
351+
352+
/**
353+
* Customize the response for MethodArgumentNotValidException.
354+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
355+
* @param ex the exception
356+
* @param headers the headers to be written to the response
357+
* @param status the selected response status
358+
* @param request the current request
359+
* @return an Object or {@code null}
360+
*/
361+
protected Object handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
362+
HttpHeaders headers, HttpStatus status, WebRequest request) {
363+
364+
return handleExceptionInternal(ex, headers, status, request);
365+
}
366+
367+
/**
368+
* Customize the response for MissingServletRequestPartException.
369+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
370+
* @param ex the exception
371+
* @param headers the headers to be written to the response
372+
* @param status the selected response status
373+
* @param request the current request
374+
* @return an Object or {@code null}
375+
*/
376+
protected Object handleMissingServletRequestPart(MissingServletRequestPartException ex,
377+
HttpHeaders headers, HttpStatus status, WebRequest request) {
378+
379+
return handleExceptionInternal(ex, headers, status, request);
380+
}
381+
382+
/**
383+
* Customize the response for BindException.
384+
* This method delegates to {@link #handleExceptionInternal(Exception, HttpHeaders, HttpStatus, WebRequest)}.
385+
* @param ex the exception
386+
* @param headers the headers to be written to the response
387+
* @param status the selected response status
388+
* @param request the current request
389+
* @return an Object or {@code null}
390+
*/
391+
protected Object handleBindException(BindException ex, HttpHeaders headers,
392+
HttpStatus status, WebRequest request) {
393+
394+
return handleExceptionInternal(ex, headers, status, request);
395+
}
396+
397+
}

0 commit comments

Comments
 (0)