├── .gitignore ├── .springBeans ├── README.md ├── blog.md ├── build.gradle ├── manifest.yml ├── pom-war.xml ├── pom.xml ├── settings.gradle ├── src └── main │ ├── java │ ├── demo │ │ ├── config │ │ │ ├── ExceptionConfiguration.java │ │ │ ├── ResponseDataControllerAdvice.java │ │ │ └── package-info.java │ │ ├── example │ │ │ ├── ExampleExceptionHandlerExceptionResolver.java │ │ │ ├── ExampleSimpleMappingExceptionResolver.java │ │ │ └── package-info.java │ │ ├── exceptions │ │ │ ├── CustomException.java │ │ │ ├── DatabaseException.java │ │ │ ├── InvalidCreditCardException.java │ │ │ ├── OrderNotFoundException.java │ │ │ ├── SupportInfoException.java │ │ │ ├── UnhandledException.java │ │ │ └── package-info.java │ │ ├── filter │ │ │ └── BrokenFilter.java │ │ ├── main │ │ │ ├── Main.java │ │ │ ├── Profiles.java │ │ │ └── package-info.java │ │ ├── utils │ │ │ └── BeanLogger.java │ │ └── web │ │ │ ├── ViewMappingController.java │ │ │ └── package-info.java │ ├── demo1 │ │ └── web │ │ │ ├── ExceptionHandlingController.java │ │ │ └── package-info.java │ ├── demo2 │ │ └── web │ │ │ ├── ControllerWithoutExceptionHandlers.java │ │ │ ├── GlobalExceptionHandlingControllerAdvice.java │ │ │ └── package-info.java │ ├── demo3 │ │ ├── config │ │ │ ├── DemoExceptionConfiguration.java │ │ │ └── package-info.java │ │ └── web │ │ │ ├── ExceptionThrowingController.java │ │ │ ├── SwitchController.java │ │ │ ├── SwitchableSimpleMappingExceptionResolver.java │ │ │ └── package-info.java │ ├── demo5 │ │ └── web │ │ │ ├── ReturnOrRedirectController.java │ │ │ └── package-info.java │ ├── mvc-configuration.xml │ └── org │ │ └── springframework │ │ └── dao │ │ ├── DataAccessException.java │ │ └── DataIntegrityViolationException.java │ └── resources │ ├── application.properties │ ├── mvc-configuration.xml │ ├── public │ ├── extlink.png │ ├── logo-vmware-tanzu.png │ ├── pws-header-logo_new.png │ ├── spring-trans.png │ └── styles.css │ └── templates │ ├── creditCardError.html │ ├── databaseError.html │ ├── databaseException.html │ ├── defaultErrorPage.html │ ├── demo5.html │ ├── error.html │ ├── footer.html │ ├── global.html │ ├── header.html │ ├── index.html │ ├── link.html │ ├── local.html │ ├── no-handler.html │ ├── support.html │ └── unannotated.html └── support.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target/* 3 | build/ 4 | .gradle/ 5 | .project 6 | .classpath 7 | .settings/ 8 | bin/ 9 | Thumbs.db 10 | .DS_Store 11 | 12 | # Package Files # 13 | *.jar 14 | *.war 15 | *.ear 16 | /target 17 | -------------------------------------------------------------------------------- /.springBeans: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1 4 | 5 | 6 | 7 | 8 | 9 | 10 | src/main/resources/mvc-configuration.xml 11 | java:org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration 12 | java:org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration 13 | java:demo.config.ExceptionConfiguration 14 | java:demo.main.Main 15 | java:demo.utils.BeanLogger 16 | java:demo3.config.DemoExceptionConfiguration 17 | 18 | 19 | 20 | 21 | 22 | 23 | true 24 | false 25 | 26 | src/main/resources/mvc-configuration.xml 27 | java:org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration 28 | java:org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration 29 | java:demo.config.ExceptionConfiguration 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mvc-exceptions 2 | 3 | ## Project Overview 4 | 5 | * Last updated July 2024 6 | * This project is built using release 2.7.18 of Spring Boot, Java 8 and Spring 5.3.31 - see pom.xml. 7 | * Web pages use Thymeleaf 3.0.15 and Bootstrap 4.5.3. 8 | * The POM builds a JAR file, not a WAR, so you must run it as a Java application. Use `mvn exec:java` or `mvn spring-boot:run` to run it, then goto ```http://localhost:8080```. 9 | * If you wish to build a WAR, see `pom-war.xml` 10 | 11 | This application demos most of the points covered on my MVC Exceptions blog: 12 | 13 | * Local copy is here: [blog.md](blog.md). 14 | * Original blog: https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc. 15 | 16 | ## Release History 17 | 18 | * November 2013: V1 19 | * October 2014: V2 20 | * April 2018: V2.0.1 21 | * January 2021: V2.1.0 22 | * January 2021: V2.1.1 23 | 24 | ## Application Overview 25 | 26 | The most significant files are: 27 | 28 | ### Demo 1 29 | 30 | Controller with @ExceptionHandler methods 31 | 32 | * `src/main/java/demo1/web/ExceptionHandlingController.java` 33 | * A controller that raises exceptions and provides its own handlers to catch and process them. 34 | 35 | ### Demo 2 36 | 37 | Controller relying on a ControllerAdvice to handle its exceptions. 38 | 39 | * `src/main/java/demo2/web/ControllerWithoutExceptionHandlers.java` 40 | * A controller with all the same handlers as `ExceptionHandlingController` (so they all throw exceptions) but no handler methods. 41 | 42 | * `src/main/java/demo2/web/GlobalControllerExceptionHandler.java` 43 | * `@ControllerAdvice` class with all the same handlers as `ExceptionHandlingController`, but they would apply to all controllers. 44 | 45 | ### Demo 3 and 4 46 | 47 | Exception handling using a `SimpleMappingExceptionResolver`. When running in demo mode (profile is set to `demo-config`, which is setup by default), it defines a `SimpleMappingExceptionResolver` subclass that can be enabled (Demo 3) or disabled (Demo 4) to show the difference. 48 | 49 | * `src/main/java/demo3/config/DemoExceptionConfiguration.java` 50 | * Java configuration to setup the beans for this demo. 51 | * `src/main/java/demo3/web/ExceptionThrowingController.java` 52 | * Controller used by the demo. 53 | * `src/main/java/demo3/web/SwitchableSimpleMappingExceptionResolver.java` 54 | * The resolver subclass described above. 55 | * `src/main/java/demo3/web/ExceptionThrowingController.java` 56 | * Controller that provides `/simpleMappingExceptionResolver/on` and 57 | `/simpleMappingExceptionResolver/off` for switching the resolver on/off. 58 | 59 | ### Demo 5 60 | 61 | * `ReturnOrRedirectController` 62 | * Controller highlighting how Spring Boot implements its error-page mechanism. 63 | 64 | ### Exceptions 65 | 66 | * `src/main/java/demo/exceptions/CustomException.java` 67 | * `src/main/java/demo/exceptions/DatabaseException.java` 68 | * `src/main/java/demo/exceptions/InvalidCreditCardException.java` 69 | * `src/main/java/demo/exceptions/OrderNotFoundException.java` 70 | * `src/main/java/demo/exceptions/UnhandledException.java` 71 | * Custom exceptions - see blog for usage. 72 | * `src/main/java/org/springframework/dao/DataAccessException.java` 73 | * Example of a predefined annotation, copied from Spring. 74 | * `src/main/java/org/springframework/dao/DataIntegrityViolationException.java` 75 | * Example of a predefined annotation, copied from Spring. 76 | 77 | ### Application Setup 78 | 79 | The Demo configuration profile is useful for this demo application, but not typical. So two other profiles are provided to configure a `SimpleMappingExceptionResolver` in a more typical way using either Java configuration or an XML bean file. 80 | 81 | * `src/main/java/demo/main/Main.java` 82 | * Main entry point for the application. Can run as a Java application (using an embedded Tomcat container) or as a WAR inside a container. Sets a few initialization properties and Spring Bean profile to use. Available profiles are `demo-config` (default), `java-config`, `xml-config`. 83 | * `src/main/java/demo/main/Profiles.java` 84 | * The Spring Bean profiles used in the application. 85 | * `src/main/java/demo/config/ExceptionConfiguration.java` 86 | * Java configuration class to setup a `SimpleMappingExceptionResolver`. Only used if the `java-config` profile is active. 87 | * `src/main/resources/mvc-configuration.xml` 88 | * XML alternative to `ExceptionConfiguration`. Also sets up a `SimpleMappingExceptionResolver`. Only used if the `xml-config` profile is active. 89 | * `src/main/java/demo/config/ResponseDataControllerAdvice` 90 | * Controller advice that puts useful data into the model for every request. 91 | 92 | ### Utility Classes 93 | 94 | * `src/main/java/demo/utils/BeanLogger.java` 95 | * Simple BeanPostProcessor to log all beans created. Not required by the demo, but as Spring Boot does so much, it allows all the beans created to be easily logged. And a lot less output than enabling `debug=true`. 96 | 97 | ### Templates 98 | 99 | All the views used, generated via Thymeleaf. 100 | 101 | * Error views 102 | * `src/main/resources/templates/creditCardError.html` 103 | * `src/main/resources/templates/databaseError.html` 104 | * `src/main/resources/templates/databaseException.html` 105 | * `src/main/resources/templates/error.html` 106 | * `src/main/resources/templates/exceptionPage.html` 107 | * `src/main/resources/templates/support.html` 108 | 109 | * Web Pages 110 | * `src/main/resources/templates/index.html` - Home page 111 | * `src/main/resources/templates/local.html` - Demo 1 112 | * `src/main/resources/templates/global.html` - Demo 2 113 | * `src/main/resources/templates/unannotated.html` - Demo 3 114 | * `src/main/resources/templates/nohandler.html` - Demo 4 115 | * `src/main/resources/templates/demo5.html` - Demo 5 116 | 117 | ### Build 118 | 119 | * `pom.xml` 120 | * Maven POM - notice how short it is - Spring Boot does most of the work. However heed the comments in the file. 121 | * Build in the usual way: `mvn package` to create an executable JAR with embedded Tomcat. 122 | * You can also run the demo using `java -jar target/mvc-exceptions-2.0.1-RELEASE.jar` 123 | 124 | * `pom-war.xml` - If you prefer to build a traditional WAR file instead of an executable JAR. 125 | * Build using `mvn -f pom-war.xml package` 126 | 127 | ### Examples 128 | 129 | Not used by the application, but provided as sample code. 130 | 131 | * `src/main/java/demo/example/ExampleExceptionHandlerExceptionResolver.java` 132 | * Unused in the demo (to keep it simple), but implements the example discussed in the blog. 133 | * `src/main/java/demo/example/ExampleSimpleMappingExceptionResolver.java` 134 | * Unused in the demo (to keep it simple), but implements the example shown in the blog (called `MySimpleMappingExceptionResolver` in the blog article). 135 | 136 | 137 | -------------------------------------------------------------------------------- /blog.md: -------------------------------------------------------------------------------- 1 | # Exception Handling in Spring MVC 2 | 3 | This article is also on the Spring Blog. 4 | 5 | Spring MVC provides several complimentary approaches to exception handling but, when teaching Spring MVC, I often find that my students are confused or not comfortable with them. 6 | 7 | Today I'm going to show you the 8 | various options available. Our goal is to not handle exceptions explicitly in Controller methods 9 | where possible. They are a cross-cutting concern better handled separately in dedicated code. 10 | 11 | There are three options: per exception, per controller or globally. 12 | 13 | _A demonstration application that shows the points discussed here can be found at 14 | http://github.com/paulc4/mvc-exceptions. 15 | See Sample Application below for details._ 16 | 17 | __NOTE:__ _The demo applications has been revamped and updated (October 2014) to use Spring Boot 1.1.8 and is (hopefully) easier to use and understand._ 18 | 19 | ## Using HTTP Status Codes 20 | 21 | Normally any unhandled exception thrown when processing a web-request causes the server to return an 22 | HTTP 500 response. However, any exception that you write yourself can be annotated with the 23 | `@ResponseStatus` annotation (which supports all the HTTP status codes defined by the HTTP 24 | specification). When an _annotated_ exception is thrown from a controller method, and not handled elsewhere, 25 | it will automatically cause the appropriate HTTP response to be returned with the specified status-code. 26 | 27 | For example, here is an exception for a missing order. 28 | 29 | ```java 30 | @ResponseStatus(value=HttpStatus.NOT_FOUND, reason="No such Order") // 404 31 | public class OrderNotFoundException extends RuntimeException { 32 | // ... 33 | } 34 | ``` 35 | 36 | And here is a controller method using it: 37 | 38 | ```java 39 | @RequestMapping(value="/orders/{id}", method=GET) 40 | public String showOrder(@PathVariable("id") long id, Model model) { 41 | Order order = orderRepository.findOrderById(id); 42 | if (order == null) throw new OrderNotFoundException(id); 43 | model.addAttribute(order); 44 | return "orderDetail"; 45 | } 46 | ``` 47 | 48 | A familiar HTTP 404 response will be returned if the URL handled by this method includes an unknown order id. 49 | 50 | ## Controller Based Exception Handling 51 | ### Using @ExceptionHandler 52 | 53 | You can add extra (`@ExceptionHandler`) methods to any controller to specifically handle exceptions 54 | thrown by request handling (`@RequestMapping`) methods in the same controller. Such methods can: 55 | 56 | 1. Handle exceptions without the `@ResponseStatus` annotation (typically predefined exceptions 57 | that you didn't write) 58 | 2. Redirect the user to a dedicated error view 59 | 3. Build a totally custom error response 60 | 61 | The following controller demonstrates these three options: 62 | 63 | ```java 64 | @Controller 65 | public class ExceptionHandlingController { 66 | 67 | // @RequestHandler methods 68 | ... 69 | 70 | // Exception handling methods 71 | 72 | // Convert a predefined exception to an HTTP Status code 73 | @ResponseStatus(value=HttpStatus.CONFLICT, reason="Data integrity violation") // 409 74 | @ExceptionHandler(DataIntegrityViolationException.class) 75 | public void conflict() { 76 | // Nothing to do 77 | } 78 | 79 | // Specify the name of a specific view that will be used to display the error: 80 | @ExceptionHandler({SQLException.class,DataAccessException.class}) 81 | public String databaseError() { 82 | // Nothing to do. Returns the logical view name of an error page, passed to 83 | // the view-resolver(s) in usual way. 84 | // Note that the exception is _not_ available to this view (it is not added to 85 | // the model) but see "Extending ExceptionHandlerExceptionResolver" below. 86 | return "databaseError"; 87 | } 88 | 89 | // Total control - setup a model and return the view name yourself. Or consider 90 | // subclassing ExceptionHandlerExceptionResolver (see below). 91 | @ExceptionHandler(Exception.class) 92 | public ModelAndView handleError(HttpServletRequest req, Exception exception) { 93 | logger.error("Request: " + req.getRequestURL() + " raised " + exception); 94 | 95 | ModelAndView mav = new ModelAndView(); 96 | mav.addObject("exception", exception); 97 | mav.addObject("url", req.getRequestURL()); 98 | mav.setViewName("error"); 99 | return mav; 100 | } 101 | } 102 | ``` 103 | 104 | In any of these methods you might choose to do additional processing - the most common example is to log the 105 | exception. 106 | 107 | Handler methods have flexible signatures so you can pass in obvious servlet-related objects such 108 | as `HttpServletRequest`, `HttpServletResponse`, `HttpSession` and/or `Principle`. __Important Note:__ the 109 | `Model` may __not__ be a parameter of any `@ExceptionHandler` method. Instead, setup a model inside the method 110 | using a `ModelAndView` as shown by `handleError()` above. 111 | 112 | ### Exceptions and Views 113 | 114 | Be careful when adding exceptions to the model. Your users do not want to see 115 | web-pages containing Java exception details and stack-traces. However, it can be useful to put exception 116 | details in the page source as a comment, to assist your support people. If using JSP, you could 117 | do something like this to output the exception and the corresponding stack-trace (using a hidden 118 | `
` is another option). 119 | 120 | ```html 121 |

Error Page

122 |

Application has encountered an error. Please contact support on ...

123 | 124 | 130 | ``` 131 | 132 | For the Thymeleaf equivalent see 133 | support.html 134 | in the demo application. The result looks like this. 135 | 136 | ![Example of an error page with a hidden exception for support](http://assets.spring.io/wp/wp-content/uploads/2013/10/support-page-example.png "Error Page with Hidden Exception") 137 | 138 | ## Global Exception Handling 139 | ### Using @ControllerAdvice Classes 140 | 141 | A controller advice allows you to use exactly the same exception handling techniques but apply them 142 | across the whole application, not just to an individual controller. You can think of them as an annotation 143 | driven interceptor. 144 | 145 | Any class annotated with `@ControllerAdvice` becomes a controller-advice and three types of method 146 | are supported: 147 | 148 | * Exception handling methods annotated with `@ExceptionHandler`. 149 | * Model enhancement methods (for adding additional data to the model) annotated with 150 | `@ModelAttribute`. Note that these attributes are _not_ available to the exception handling views. 151 | * Binder initialization methods (used for configuring form-handling) annotated with 152 | `@InitBinder`. 153 | 154 | We are only going to look at exception handling - see the online manual for more on 155 | `@ControllerAdvice` methods. 156 | 157 | Any of the exception handlers you saw above can be defined on a controller-advice class - but now they 158 | apply to exceptions thrown from any controller. Here is a simple example: 159 | 160 | ```java 161 | @ControllerAdvice 162 | class GlobalControllerExceptionHandler { 163 | @ResponseStatus(HttpStatus.CONFLICT) // 409 164 | @ExceptionHandler(DataIntegrityViolationException.class) 165 | public void handleConflict() { 166 | // Nothing to do 167 | } 168 | } 169 | ``` 170 | 171 | If you want to have a default handler for any exception, there is a slight wrinkle. You need to ensure 172 | annotated exceptions are handled by the framework. The code looks like this: 173 | 174 | ```java 175 | @ControllerAdvice 176 | class GlobalDefaultExceptionHandler { 177 | public static final String DEFAULT_ERROR_VIEW = "error"; 178 | 179 | @ExceptionHandler(value = Exception.class) 180 | public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { 181 | // If the exception is annotated with @ResponseStatus rethrow it and let 182 | // the framework handle it - like the OrderNotFoundException example 183 | // at the start of this post. 184 | // AnnotationUtils is a Spring Framework utility class. 185 | if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) 186 | throw e; 187 | 188 | // Otherwise setup and send the user to a default error-view. 189 | ModelAndView mav = new ModelAndView(); 190 | mav.addObject("exception", e); 191 | mav.addObject("url", req.getRequestURL()); 192 | mav.setViewName(DEFAULT_ERROR_VIEW); 193 | return mav; 194 | } 195 | } 196 | ``` 197 | 198 | ## Going Deeper 199 | 200 | ### HandlerExceptionResolver 201 | 202 | Any Spring bean declared in the `DispatcherServlet`'s application context that implements 203 | `HandlerExceptionResolver` will be used to intercept and process any exception raised 204 | in the MVC system and not handled by a Controller. The interface looks like this: 205 | 206 | ```java 207 | public interface HandlerExceptionResolver { 208 | ModelAndView resolveException(HttpServletRequest request, 209 | HttpServletResponse response, Object handler, Exception ex); 210 | } 211 | ``` 212 | 213 | The `handler` refers to the controller that generated the exception (remember that 214 | `@Controller` instances are only one type of handler supported by Spring MVC. 215 | For example: `HttpInvokerExporter` and the WebFlow Executor are also types of handler). 216 | 217 | Behind the scenes, MVC creates three such resolvers by default. It is these resolvers that implement the 218 | behaviours discussed above: 219 | 220 | * `ExceptionHandlerExceptionResolver` matches uncaught exceptions against for 221 | suitable `@ExceptionHandler` methods on both the handler (controller) and on any controller-advices. 222 | * `ResponseStatusExceptionResolver` looks for uncaught exceptions 223 | annotated by `@ResponseStatus` (as described in Section 1) 224 | * `DefaultHandlerExceptionResolver` converts standard Spring exceptions and converts them 225 | to HTTP Status Codes (I have not mentioned this above as it is internal to Spring MVC). 226 | 227 | These are chained and processed in the order listed (internally Spring creates a dedicated bean - the 228 | HandlerExceptionResolverComposite to do this). 229 | 230 | Notice that the method signature of ```resolveException``` does not include the ```Model```. This is why 231 | ```@ExceptionHandler``` methods cannot be injected with the model. 232 | 233 | You can, if you wish, implement your own `HandlerExceptionResolver` to setup your own custom 234 | exception handling system. Handlers typically implement Spring's `Ordered` interface so you can define the 235 | order that the handlers run in. 236 | 237 | ### SimpleMappingExceptionResolver 238 | 239 | Spring has long provided a simple but convenient implementation of `HandlerExceptionResolver` 240 | that you may well find being used in your appication already - the `SimpleMappingExceptionResolver`. 241 | It provides options to: 242 | 243 | * Map exception class names to view names - just specify the classname, no package needed. 244 | * Specify a default (fallback) error page for any exception not handled anywhere else 245 | * Log a message (this is not enabled by default). 246 | * Set the name of the `exception` attribute to add to the Model so it can be used inside a View 247 | (such as a JSP). By default this attribute is named ```exception```. Set to ```null``` to disable. Remember 248 | that views returned from `@ExceptionHandler` methods _do not_ have access to the exception but views 249 | defined to `SimpleMappingExceptionResolver` _do_. 250 | 251 | Here is a typical configuration using XML: 252 | 253 | ```xml 254 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ``` 270 | 271 | Or using Java Configuration: 272 | 273 | ```java 274 | @Configuration 275 | @EnableWebMvc // Optionally setup Spring MVC defaults if you aren't doing so elsewhere 276 | public class MvcConfiguration extends WebMvcConfigurerAdapter { 277 | @Bean(name="simpleMappingExceptionResolver") 278 | public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { 279 | SimpleMappingExceptionResolver r = 280 | new SimpleMappingExceptionResolver(); 281 | 282 | Properties mappings = new Properties(); 283 | mappings.setProperty("DatabaseException", "databaseError"); 284 | mappings.setProperty("InvalidCreditCardException", "creditCardError"); 285 | 286 | r.setExceptionMappings(mappings); // None by default 287 | r.setDefaultErrorView("error"); // No default 288 | r.setExceptionAttribute("ex"); // Default is "exception" 289 | r.setWarnLogCategory("example.MvcLogger"); // No default 290 | return r; 291 | } 292 | ... 293 | } 294 | ``` 295 | 296 | The _defaultErrorView_ property is especially useful as it ensures any uncaught exception generates 297 | a suitable application defined error page. (The default for most application servers is to display a Java 298 | stack-trace - something your users should _never_ see). 299 | 300 | ### Extending SimpleMappingExceptionResolver 301 | 302 | It is quite common to extend `SimpleMappingExceptionResolver` for several reasons: 303 | 304 | * Use the constructor to set properties directly - for example to enable exception logging and set the 305 | logger to use 306 | * Override the default log message by overriding `buildLogMessage`. The default implementation 307 | always returns this fixed text: 308 | * To make additional information available to the error view by overriding `doResolveException` 309 | 310 | For example: 311 | 312 | ```java 313 | public class MyMappingExceptionResolver extends SimpleMappingExceptionResolver { 314 | public MyMappingExceptionResolver() { 315 | // Enable logging by providing the name of the logger to use 316 | setWarnLogCategory(MyMappingExceptionResolver.class.getName()); 317 | } 318 | 319 | @Override 320 | public String buildLogMessage(Exception e, HttpServletRequest req) { 321 | return "MVC exception: " + e.getLocalizedMessage(); 322 | } 323 | 324 | @Override 325 | protected ModelAndView doResolveException(HttpServletRequest request, 326 | HttpServletResponse response, Object handler, Exception exception) { 327 | // Call super method to get the ModelAndView 328 | ModelAndView mav = super.doResolveException(request, response, handler, exception); 329 | 330 | // Make the full URL available to the view - note ModelAndView uses addObject() 331 | // but Model uses addAttribute(). They work the same. 332 | mav.addObject("url", request.getRequestURL()); 333 | return mav; 334 | } 335 | } 336 | ``` 337 | 338 | This code is in the demo application as 339 | ExampleSimpleMappingExceptionResolver 340 | 341 | ### Extending ExceptionHandlerExceptionResolver 342 | 343 | It is also possible to extend `ExceptionHandlerExceptionResolver` and override its 344 | `doResolveHandlerMethodException` method in the same way. It has almost the same signature 345 | (it just takes the new `HandlerMethod` instead of a `Handler`). 346 | 347 | To make sure it gets used, also set the inherited order property (for example in the constructor of 348 | your new class) to a value less than `MAX_INT` so it runs _before_ the default 349 | ExceptionHandlerExceptionResolver instance (it is easier to create your own handler instance than try to 350 | modify/replace the one created by Spring). See 351 | ExampleExceptionHandlerExceptionResolver 352 | in the demo app for more. 353 | 354 | ### Errors and REST 355 | 356 | RESTful GET requests may also generate exceptions and we have already seen how we can return standard HTTP 357 | Error response codes. However, what if you want to return information about the error? This is very easy to do. 358 | Firstly define an error class: 359 | 360 | ```java 361 | public class ErrorInfo { 362 | public final String url; 363 | public final String ex; 364 | 365 | public ErrorInfo(String url, Exception ex) { 366 | this.url = url; 367 | this.ex = ex.getLocalizedMessage(); 368 | } 369 | } 370 | ``` 371 | 372 | Now we can return an instance from a handler as the ```@ResponseBody``` like this: 373 | 374 | ```java 375 | @ResponseStatus(HttpStatus.BAD_REQUEST) 376 | @ExceptionHandler(MyBadDataException.class) 377 | @ResponseBody ErrorInfo handleBadRequest(HttpServletRequest req, Exception ex) { 378 | return new ErrorInfo(req.getRequestURL(), ex); 379 | } 380 | ``` 381 | 382 | ## What to Use When? 383 | 384 | As usual, Spring likes to offer you choice, so what should you do? Here are some rules of thumb. 385 | However if you have a preference for XML configuration or Annotations, that's fine too. 386 | 387 | 401 | 402 | ## Sample Application 403 | 404 | A demonstration application can be found at github. 405 | It uses Spring Boot and Thymeleaf to build a simple web application. 406 | 407 | The application was revised (Oct 2014) and is (hopefully) better and easier to understand. The fundamentals stay the same. It uses Spring Boot V1.1.8 and Spring 4.1 but the code is applicable to Spring 3.x also. 408 | 409 | ### About the Demo 410 | The application leads the user through 5 demo pages, highlighting different exception handling techniques: 411 | 412 | 1. A controller with `@ExceptionHandler` methods to handle its own exceptions 413 | 1. A contoller that throws exceptions for a global ControllerAdvice to handle 414 | 1. Using a `SimpleMappingExceptionResolver` to handle exceptions 415 | 1. Same as demo 3 but with the `SimpleMappingExceptionResolver` disabled for comparison 416 | 1. Shows how Spring Boot generates its error page 417 | 418 | A description of the most important files in the application and how they relate to each demo can be found in the project's 419 | README.md. 420 | 421 | The home web-page is 422 | index.html 423 | which: 424 | 425 | * Links to each demo page 426 | * Links (bottom of the page) to Spring Boot endpoints for those interested in Spring Boot. 427 | 428 | Each demo page contains several links, all of which deliberately raise exceptions. You will need to use the back-button on your browser each time to return to the demo page. 429 | 430 | Thanks to Spring Boot, you can run this demo as a Java application (it runs an embedded Tomcat container). To run the application, you can use one of the following (the second is thanks to the Spring Boot maven plugin): 431 | 432 | * `mvn exec:java` 433 | * `mvn spring-boot:run` 434 | 435 | Your choice. The home page URL will be http://localhost:8080. 436 | 437 | ### Spring Boot and Error Handling 438 | Spring Boot allows a Spring project to be setup with 439 | minimal configuration. Spring Boot creates sensible defaults automatically when it detects 440 | certain key classes and packages on the classpath. For example if it sees that you are using a Servlet 441 | environment, it sets up Spring MVC with the most commonly used view-resolvers, hander mappings and so forth. 442 | If it sees JSP and/or Thymeleaf, it sets up these view-technologies. 443 | 444 | Spring MVC offers no default (fall-back) error page out-of-the-box. The most common way to set a default error 445 | page has always been the `SimpleMappingExceptionResolver` (since Spring V1 in fact). However 446 | Spring Boot also provides for a fallback error-handling page. 447 | 448 | At start-up, Spring Boot tries to find a mapping for `/error`. By convention, a URL ending in `/error` maps to 449 | a logical view of the same name: `error`. In the demo application this view maps in turn to the `error.html` 450 | Thymeleaf template. (If using JSP, it would map to `error.jsp` according to the setup of your 451 | `InternalResourceViewResolver`). 452 | 453 | If no mapping from `/error` to a View can be found, Spring Boot defines its own fall-back error page - the so-called "Whitelabel Error Page" (a minimal page with just the HTTP status information and any error details, such as the message from an uncaught exception). If you rename the `error.html` template to, say, `error2.html` 454 | then restart, you will see it being used. 455 | 456 | By defining a Java configuration `@Bean` method called `defaultErrorView()` you can return your own error `View` instance. (see Spring Boot's `ErrorMvcAutoConfiguration` class for more information). 457 | 458 | What if you are already using `SimpleMappingExceptionResolver` to setup a default 459 | error view? Simple, make sure the `defaultErrorView` defines the same view that Spring Boot uses: `error`. Or you can disable Spring boot's error page by setting the property 460 | `error.whitelabel.enabled` to `false`. Your container's default error page is used instead. 461 | There are examples of setting Spring Boot properties in the constructor of 462 | Main. 463 | 464 | Note that in the demo, the `defaultErrorView` property of the `SimpleMappingExceptionResolver` is 465 | deliberately set not to `error` but to `defaultErrorPage` so you can see when the handler is generating the error page and when 466 | Spring Boot is responsible. Normally _both_ would be set to `error`. 467 | 468 | Also in the demo application I show how to create a support-ready error page with a stack-trace hidden in the HTML source (as a comment). Ideally support should get this information from the logs, but life isn't always ideal. Regardless, what this page _does_ show is how the underlying error-handling method `handleError` creates its own `ModelAndView` to provide extra information in the error page. See: 469 | * `ExceptionHandlingController.handleError()` on github 470 | * `GlobalControllerExceptionHandler.handleError()` on github 471 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'maven' 3 | 4 | group = 'io.spring.demo' 5 | version = '2.1.0-RELEASE' 6 | 7 | description = """Demo application for Spring Exception Handling Blog""" 8 | 9 | sourceCompatibility = 1.5 10 | targetCompatibility = 1.5 11 | 12 | repositories { 13 | //maven { url "https://repo.spring.io/libs-milestone" } 14 | maven { url "http://repo.maven.apache.org/maven2" } 15 | } 16 | 17 | dependencies { 18 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version:'2.0.1.RELEASE' 19 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version:'2.0.1.RELEASE' 20 | compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version:'2.0.1.RELEASE' 21 | compile group: 'org.springframework.boot', name: 'spring-boot-devtools', version:'2.0.1.RELEASE' 22 | } 23 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | # Deployment manifest (blueprint) for deploying to Cloud Foundry 2 | # 3 | --- 4 | applications: 5 | - name: mvc-exceptions 6 | memory: 1G 7 | instances: 1 8 | host: mvc-exceptions-v2 9 | path: target/mvc-exceptions-2.1.0-RELEASE.jar 10 | -------------------------------------------------------------------------------- /pom-war.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | io.spring.demo 7 | 2.1.0 8 | mvc-exceptions-war 9 | Demo WAR for Spring Exception Handling Blog 10 | 11 | 12 | war 13 | 14 | 15 | 16 | UTF-8 17 | demo.main.Main 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-parent 25 | 2.7.18 26 | 27 | 28 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-tomcat 41 | provided 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-thymeleaf 48 | 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-actuator 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | io.spring.demo 7 | 2.1.0 8 | 4.0.0 9 | mvc-exceptions 10 | Demo application for Spring Exception Handling Blog 11 | 12 | 13 | jar 14 | 15 | 16 | 17 | UTF-8 18 | demo.main.Main 19 | 1.8 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-parent 26 | 2.7.18 27 | 28 | 29 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-thymeleaf 42 | 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-starter-actuator 48 | 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-devtools 54 | true 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.springframework.boot 63 | spring-boot-maven-plugin 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mvc-exceptions' 2 | -------------------------------------------------------------------------------- /src/main/java/demo/config/ExceptionConfiguration.java: -------------------------------------------------------------------------------- 1 | package demo.config; 2 | 3 | import java.util.Properties; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; 11 | 12 | import demo.main.Main; 13 | import demo.main.Profiles; 14 | 15 | /** 16 | * Setup for exception handling using a {@link SimpleMappingExceptionResolver} 17 | * bean. 18 | *

19 | * If you prefer to do this in XML set the {@link Main#activeProfile} property 20 | * to XML_CONFIG_PROFILE (see mvc-configuration.xml). 21 | *

22 | * The use of the JAVA_CONFIG profile here is for demonstration only. A real 23 | * application wouldn't normally need to switch between XML or Java 24 | * Configuration. You would use one, or other, or both, all the time. 25 | * 26 | * @author Paul Chapman 27 | */ 28 | @Configuration 29 | @Profile(Profiles.JAVA_CONFIG_PROFILE) 30 | public class ExceptionConfiguration { 31 | 32 | protected Logger logger; 33 | 34 | public ExceptionConfiguration() { 35 | logger = LoggerFactory.getLogger(getClass()); 36 | logger.info("Creating ExceptionConfiguration"); 37 | } 38 | 39 | /** 40 | * Setup the classic SimpleMappingExceptionResolver. This provides useful 41 | * defaults for logging and handling exceptions. It has been part of Spring 42 | * MVC since Spring V2 and you will probably find most existing Spring MVC 43 | * applications are using it. 44 | *

45 | * Only invoked if the "global" profile is active. 46 | * 47 | * @return The new resolver 48 | */ 49 | @Bean(name = "simpleMappingExceptionResolver") 50 | public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { 51 | logger.info("Creating SimpleMappingExceptionResolver"); 52 | SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); 53 | 54 | Properties mappings = new Properties(); 55 | mappings.setProperty("DatabaseException", "databaseException"); 56 | mappings.setProperty("InvalidCreditCardException", "creditCardError"); 57 | 58 | r.setExceptionMappings(mappings); // None by default 59 | r.setExceptionAttribute("ex"); // Default is "exception" 60 | r.setWarnLogCategory("demo1.ExceptionLogger"); // No default 61 | 62 | /* 63 | * Normally Spring MVC has no default error view and this class is the 64 | * only way to define one. A nice feature of Spring Boot is the ability 65 | * to provide a very basic default error view (otherwise the application 66 | * server typically returns a Java stack trace which is not acceptable 67 | * in production). See Blog for more details. 68 | * 69 | * To stick with the Spring Boot approach, DO NOT set this property of 70 | * SimpleMappingExceptionResolver. 71 | * 72 | * Here we are choosing to use SimpleMappingExceptionResolver since many 73 | * Spring applications have used the approach since Spring V1. Normally 74 | * we would specify the view as "error" to match Spring Boot, however so 75 | * you can see what is happening, we are using a different page. 76 | */ 77 | r.setDefaultErrorView("defaultErrorPage"); 78 | return r; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/demo/config/ResponseDataControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package demo.config; 2 | 3 | import java.util.Date; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ModelAttribute; 8 | import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; 9 | 10 | import demo.main.Main; 11 | import demo3.web.SwitchableSimpleMappingExceptionResolver; 12 | 13 | /** 14 | * Adds useful data into to the model for every request. This is another use of 15 | * a Controller Advice (besides exception handling). 16 | * 17 | * @author Paul Chapman 18 | * 19 | */ 20 | @ControllerAdvice 21 | public class ResponseDataControllerAdvice { 22 | 23 | public static final String SOURCE_ON_GITHUB = // 24 | "https://github.com/paulc4/mvc-exceptions/blob/master/src/main"; 25 | 26 | public static final String BLOG_URL = // 27 | "https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc"; 28 | 29 | private SimpleMappingExceptionResolver resolver; 30 | 31 | /** 32 | * Need to see if the {@link SwitchableSimpleMappingExceptionResolver} is 33 | * being used and if so, is it enabled? 34 | * 35 | * @param resolver 36 | */ 37 | @Autowired(required = false) 38 | public void setSimpleMappingExceptionResolver( 39 | SimpleMappingExceptionResolver resolver) { 40 | this.resolver = resolver; 41 | } 42 | 43 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 44 | /* . . . . . . . . . . . . . . MODEL ATTRIBUTES . . . . . . . . . . . . .. */ 45 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 46 | 47 | /** 48 | * What profile are we currently using? 49 | *

50 | * Note that error views do not have automatically have access to the model, 51 | * so they do not have access to model-attributes either. 52 | * 53 | * @return Always includes "CONTROLLER". 54 | */ 55 | @ModelAttribute("profiles") 56 | public String getProfiles() { 57 | return Main.getProfiles(); 58 | } 59 | 60 | /** 61 | * Required for compatibility with Spring Boot. 62 | * 63 | * @return Date and time of current request. 64 | */ 65 | @ModelAttribute("timestamp") 66 | public String getTimestamp() { 67 | return new Date().toString(); 68 | } 69 | 70 | /** 71 | * Do we have a {@link SwitchableSimpleMappingExceptionResolver} and if so, 72 | * what state is it in? 73 | * 74 | * @return Date and time of current request. 75 | */ 76 | @ModelAttribute("switchState") 77 | public String getSwitchState() { 78 | // Check if the SwitchableSimpleMappingExceptionResolver is in use and 79 | // enabled, or not. 80 | if (resolver == null) 81 | return "off"; 82 | else if (resolver instanceof SwitchableSimpleMappingExceptionResolver) { 83 | return ((SwitchableSimpleMappingExceptionResolver) resolver) 84 | .isEnabled() ? "on" : "off"; 85 | } else 86 | return "on"; 87 | } 88 | 89 | /** 90 | * URL of this source on github. 91 | * 92 | * @return 93 | */ 94 | @ModelAttribute("gitHubSrc") 95 | public String getGitHubSrcURL() { 96 | return SOURCE_ON_GITHUB; 97 | } 98 | 99 | /** 100 | * Add Blog URL to model for use in any web-page. 101 | * 102 | * @return URL of the Spring IO Blog article this demo relates to. 103 | */ 104 | @ModelAttribute("blogUrl") 105 | public String getBlogUrl() { 106 | return BLOG_URL; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/demo/config/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Spring Configuration files 3 | * 4 | * @author Paul Chapman 5 | */ 6 | package demo.config; -------------------------------------------------------------------------------- /src/main/java/demo/example/ExampleExceptionHandlerExceptionResolver.java: -------------------------------------------------------------------------------- 1 | package demo.example; 2 | 3 | import java.util.Date; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | import org.springframework.web.method.HandlerMethod; 9 | import org.springframework.web.servlet.ModelAndView; 10 | import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; 11 | 12 | import demo.config.ExceptionConfiguration; 13 | 14 | /** 15 | * Not used in this application, but an example of how to extend the 16 | * ExceptionHandlerExceptionResolver to provide extra information in the 17 | * model for the view. To use, add a @Bean method to 18 | * {@link ExceptionConfiguration} to return an instance. 19 | * 20 | * @author Paul Chapman 21 | */ 22 | public class ExampleExceptionHandlerExceptionResolver extends 23 | ExceptionHandlerExceptionResolver { 24 | 25 | /** 26 | * The default ExceptionHandlerExceptionResolver has order MAX_INT 27 | * (lowest priority - see {@link Ordered#LOWEST_PRECEDENCE). The constructor 28 | * gves this slightly higher precedence so it runs first. Also enable 29 | * logging to this classe's logger by default. 30 | */ 31 | public ExampleExceptionHandlerExceptionResolver() { 32 | // Turn logging on by default 33 | setWarnLogCategory(getClass().getName()); 34 | 35 | // Make sure this handler runs before the default 36 | // ExceptionHandlerExceptionResolver 37 | setOrder(LOWEST_PRECEDENCE - 1); 38 | } 39 | 40 | /** 41 | * Override the default to generate a log message with dynamic content. 42 | */ 43 | @Override 44 | public String buildLogMessage(Exception e, HttpServletRequest req) { 45 | return "MVC exception: " + e.getLocalizedMessage(); 46 | } 47 | 48 | /** 49 | * This method uses the newee API and gets passed the handler-method 50 | * (typically the method on the @Controller) that generated the 51 | * exception. 52 | */ 53 | @Override 54 | protected ModelAndView doResolveHandlerMethodException( 55 | HttpServletRequest request, HttpServletResponse response, 56 | HandlerMethod handlerMethod, Exception exception) { 57 | 58 | // Get the ModelAndView to use 59 | ModelAndView mav = super.doResolveHandlerMethodException(request, 60 | response, handlerMethod, exception); 61 | 62 | // Make more information available to the view 63 | mav.addObject("exception", exception); 64 | mav.addObject("url", request.getRequestURL()); 65 | mav.addObject("timestamp", new Date()); 66 | mav.addObject("status", 500); 67 | return mav; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/demo/example/ExampleSimpleMappingExceptionResolver.java: -------------------------------------------------------------------------------- 1 | package demo.example; 2 | 3 | import java.util.Date; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | 8 | import org.springframework.web.servlet.ModelAndView; 9 | import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; 10 | 11 | import demo.config.ExceptionConfiguration; 12 | 13 | /** 14 | * Not used in this application, but an example of how to extend the 15 | * SimpleMappingExceptionResolver to provide extra information in the 16 | * log messages it generates and in the model for the view. To use, change 17 | * {@link ExceptionConfiguration#createSimpleMappingExceptionResolver()} to 18 | * return an instance of this class instead. 19 | * 20 | * @author Paul Chapman 21 | */ 22 | public class ExampleSimpleMappingExceptionResolver extends 23 | SimpleMappingExceptionResolver { 24 | 25 | /** 26 | * Also enable logging to this classe's logger by default. 27 | */ 28 | public ExampleSimpleMappingExceptionResolver() { 29 | // Turn logging on by default 30 | setWarnLogCategory(getClass().getName()); 31 | } 32 | 33 | /** 34 | * Override the default to generate a log message with dynamic content. 35 | */ 36 | @Override 37 | public String buildLogMessage(Exception e, HttpServletRequest req) { 38 | return "MVC exception: " + e.getLocalizedMessage(); 39 | } 40 | 41 | /** 42 | * This method uses the older API and gets passed the handler (typically the 43 | * @Controller) that generated the exception. 44 | */ 45 | @Override 46 | protected ModelAndView doResolveException(HttpServletRequest request, 47 | HttpServletResponse response, Object handler, Exception exception) { 48 | 49 | // Get the ModelAndView to use 50 | ModelAndView mav = super.doResolveException(request, response, handler, 51 | exception); 52 | 53 | // Make more information available to the view - note that 54 | // SimpleMappingExceptionResolver adds the exception already 55 | mav.addObject("url", request.getRequestURL()); 56 | mav.addObject("timestamp", new Date()); 57 | mav.addObject("status", 500); 58 | return mav; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/demo/example/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Example classes not used in the application that you might find them useful 3 | * in your own project. 4 | * 5 | * @author Paul Chapman 6 | */ 7 | package demo.example; -------------------------------------------------------------------------------- /src/main/java/demo/exceptions/CustomException.java: -------------------------------------------------------------------------------- 1 | package demo.exceptions; 2 | 3 | public class CustomException extends RuntimeException { 4 | 5 | /** 6 | * Unique ID for Serialized object 7 | */ 8 | private static final long serialVersionUID = 4657491283614755649L; 9 | 10 | public CustomException(String msg) { 11 | super(msg); 12 | } 13 | 14 | public CustomException(String msg, Throwable t) { 15 | super(msg, t); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/demo/exceptions/DatabaseException.java: -------------------------------------------------------------------------------- 1 | package demo.exceptions; 2 | 3 | public class DatabaseException extends RuntimeException { 4 | 5 | /** 6 | * Unique ID for Serialized object 7 | */ 8 | private static final long serialVersionUID = 4657491283614455649L; 9 | 10 | public DatabaseException(String msg) { 11 | super(msg); 12 | } 13 | 14 | public DatabaseException(String msg, Throwable t) { 15 | super(msg, t); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/demo/exceptions/InvalidCreditCardException.java: -------------------------------------------------------------------------------- 1 | package demo.exceptions; 2 | 3 | public class InvalidCreditCardException extends RuntimeException { 4 | 5 | /** 6 | * Unique ID for Serialized object 7 | */ 8 | private static final long serialVersionUID = 6648725043534411041L; 9 | 10 | public InvalidCreditCardException(String msg) { 11 | super(msg); 12 | } 13 | 14 | public InvalidCreditCardException(String msg, Throwable t) { 15 | super(msg, t); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/demo/exceptions/OrderNotFoundException.java: -------------------------------------------------------------------------------- 1 | package demo.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * An example of an application-specific exception. Defined here for convenience 8 | * as we don't have a real domain model or its associated business logic. 9 | */ 10 | @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such order") 11 | public class OrderNotFoundException extends RuntimeException { 12 | 13 | /** 14 | * Unique ID for Serialized object 15 | */ 16 | private static final long serialVersionUID = -8790211652911971729L; 17 | 18 | public OrderNotFoundException(String orderId) { 19 | super(orderId + " not found"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/demo/exceptions/SupportInfoException.java: -------------------------------------------------------------------------------- 1 | package demo.exceptions; 2 | 3 | public class SupportInfoException extends RuntimeException { 4 | 5 | /** 6 | * Unique ID for Serialized object 7 | */ 8 | private static final long serialVersionUID = 4657491283614755649L; 9 | 10 | public SupportInfoException(String msg) { 11 | super(msg); 12 | } 13 | 14 | public SupportInfoException(String msg, Throwable t) { 15 | super(msg, t); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/demo/exceptions/UnhandledException.java: -------------------------------------------------------------------------------- 1 | package demo.exceptions; 2 | 3 | public class UnhandledException extends RuntimeException { 4 | 5 | /** 6 | * Unique ID for Serialized object 7 | */ 8 | private static final long serialVersionUID = 4657422283614755649L; 9 | 10 | public UnhandledException(String msg) { 11 | super(msg); 12 | } 13 | 14 | public UnhandledException(String msg, Throwable t) { 15 | super(msg, t); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/demo/exceptions/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The exceptions used by the demo. 3 | * 4 | * @author Paul Chapman 5 | */ 6 | package demo.exceptions; 7 | -------------------------------------------------------------------------------- /src/main/java/demo/filter/BrokenFilter.java: -------------------------------------------------------------------------------- 1 | package demo.filter; 2 | 3 | import java.io.IOException; 4 | import java.util.logging.Logger; 5 | 6 | import javax.servlet.Filter; 7 | import javax.servlet.FilterChain; 8 | import javax.servlet.FilterConfig; 9 | import javax.servlet.ServletException; 10 | import javax.servlet.ServletRequest; 11 | import javax.servlet.ServletResponse; 12 | import javax.servlet.http.HttpServletRequest; 13 | 14 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 15 | import org.springframework.stereotype.Component; 16 | 17 | /** 18 | * A demo filter that deliberately throws an exception if the URL "/broken" is 19 | * invoked. Due to Spring Boot auto-configuration, the exception will be 20 | * reported via the usual Spring Boot page (by default the Whitelabel error page 21 | * but in this application, our Thymeleaf error-page). 22 | *

23 | * As this is a Spring Boot application, the filter can be created as a Spring 24 | * Bean by the component scanner in the usual way and Spring Boot will register 25 | * it as a filter, mapped to "/*" by default. For more control consider creating 26 | * a {@link FilterRegistrationBean} to configure it. 27 | */ 28 | @Component 29 | public class BrokenFilter implements Filter { 30 | 31 | @SuppressWarnings("serial") 32 | protected static class FilterException extends RuntimeException { 33 | 34 | public FilterException(String message) { 35 | super(message); 36 | } 37 | } 38 | 39 | private Logger logger = Logger.getLogger(BrokenFilter.class.getName()); 40 | 41 | @Override 42 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 43 | throws IOException, ServletException { 44 | 45 | if (request instanceof HttpServletRequest) { 46 | if (((HttpServletRequest) request).getRequestURI().endsWith("broken")) { 47 | logger.severe("BROKEN FILTER FORCES AN EXCEPTION"); 48 | throw new FilterException("Failure in BrokenFilter"); 49 | } 50 | } 51 | 52 | chain.doFilter(request, response); 53 | } 54 | 55 | @Override 56 | public void init(FilterConfig arg0) throws ServletException { 57 | } 58 | 59 | @Override 60 | public void destroy() { 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/demo/main/Main.java: -------------------------------------------------------------------------------- 1 | package demo.main; 2 | 3 | import java.util.Properties; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.boot.builder.SpringApplicationBuilder; 9 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 10 | import org.springframework.context.annotation.ImportResource; 11 | 12 | import demo.config.ExceptionConfiguration; 13 | 14 | /** 15 | * Main entry point for our application using Spring Boot. It can be run as an 16 | * executable or as a standard war file. 17 | *

18 | * Details of annotations used: 19 | *

26 | * 27 | * @author Paul Chapman 28 | */ 29 | @SpringBootApplication(scanBasePackages = { "demo", "demo1", "demo2", "demo3", "demo5" }) 30 | @ImportResource("classpath:mvc-configuration.xml") 31 | public class Main extends SpringBootServletInitializer { 32 | 33 | /** 34 | * How should a SimpleMappingExceptionResolver be created? 35 | * 44 | *

45 | * Demo mode is the default - set to "java-config" or "xml-config" to match 46 | * however you intend to use Spring for a more realistic setup. 47 | * 48 | * @see Profiles 49 | */ 50 | public static final String activeProfile = Profiles.DEMO_CONFIG_PROFILE; 51 | 52 | // Local logger 53 | protected Logger logger; 54 | 55 | // Holds Spring Boot configuration properties 56 | protected Properties props = new Properties(); 57 | 58 | /** 59 | * Retrieve requested Profiles. Depends on the value of {@link #activeProfile}. 60 | * 61 | * @return Comma-separated list of profiles. 62 | */ 63 | public static String getProfiles() { 64 | return activeProfile; 65 | } 66 | 67 | /** 68 | * We are using the constructor to perform some useful initializations: 69 | *

    70 | *
  1. Set the Spring Profile to use 'controller' or 'global' which in turn 71 | * selects how exceptions will be handled. Profiles are a Spring feature from 72 | * V3.1 onwards. 73 | *
  2. Disable Thymeleaf caching so templates (HTML files with Thymeleaf 74 | * namespace attributes) can be modified whilst the application is running.
  3. 75 | *
  4. Enable DEBUG level logging so you can see Spring MVC as its working.
  5. 76 | *
77 | */ 78 | public Main() { 79 | logger = LoggerFactory.getLogger(getClass()); 80 | logger.info("Application starting "); 81 | } 82 | 83 | /** 84 | * Back to the future: run the application as a Java application and it will 85 | * pick up a container (Tomcat, Jetty) automatically if present. Pulls in Tomcat 86 | * by default, running in embedded mode. 87 | *

88 | * This application can also run as a traditional war file because it extends 89 | * SpringBootServletInitializer as well. 90 | * 91 | * @param args 92 | * @throws Exception 93 | */ 94 | public static void main(String[] args) throws Exception { 95 | // Create an instance and invoke run(); Allows the contructor to perform 96 | // initialisation regardless of whether we are running as an application 97 | // or in a container. 98 | new Main().runAsJavaApplication(args); 99 | } 100 | 101 | /** 102 | * Run the application using Spring Boot. SpringApplication.run 103 | * tells Spring Boot to use this class as the initialiser for the whole 104 | * application (via the class annotations above). This method is only used when 105 | * running as a Java application. 106 | * 107 | * @param args 108 | * Any command line arguments. 109 | */ 110 | protected void runAsJavaApplication(String[] args) { 111 | SpringApplicationBuilder application = new SpringApplicationBuilder(); 112 | configure(application); 113 | application.run(args); 114 | logger.info("Go to this URL: http://localhost:8080/"); 115 | } 116 | 117 | /** 118 | * Configure the application using the supplied builder when running as a WAR. 119 | * This method is invoked automatically when running in a container and 120 | * explicitly by {@link #runAsJavaApplication(String[])}. 121 | * 122 | * @param application 123 | * Spring Boot application builder. 124 | */ 125 | @Override 126 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 127 | application.sources(Main.class); 128 | 129 | logger.info("Spring Boot configuration: profiles = " + activeProfile); 130 | application.profiles(activeProfile); 131 | 132 | // Set additional properties. 133 | logger.info("Spring Boot configuratoon: properties = " + props); 134 | application.properties(props); 135 | 136 | return application; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/demo/main/Profiles.java: -------------------------------------------------------------------------------- 1 | package demo.main; 2 | 3 | import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; 4 | 5 | import demo.config.ExceptionConfiguration; 6 | import demo1.web.ExceptionHandlingController; 7 | import demo2.web.ControllerWithoutExceptionHandlers; 8 | import demo2.web.GlobalExceptionHandlingControllerAdvice; 9 | import demo3.config.DemoExceptionConfiguration; 10 | 11 | /** 12 | * Spring Bean configuration profiles used in this application. Three profile 13 | * combinations are currently in use: 14 | *

27 | * The ability to switch between Java or XML configuration for the 28 | * SimpleMappingExceptionResolver is just for demonstration 29 | * purposes. In a real application use one or the other, but not both. You are 30 | * very likely to have an existing XML definition for this bean and there is no 31 | * need to change it. 32 | * 33 | * @author Paul Chapman 34 | */ 35 | public interface Profiles { 36 | 37 | /** 38 | * Java configuration profile - see {@link ExceptionConfiguration}. Value = 39 | * {@value} 40 | */ 41 | public static final String JAVA_CONFIG_PROFILE = "java-config"; 42 | 43 | /** 44 | * XML configuration property - see mvc-configuration.xml. Value = 45 | * {@value} 46 | */ 47 | public static final String XML_CONFIG_PROFILE = "xml-config"; 48 | 49 | /** 50 | * DEMO mode configuration property - see {@link DemoExceptionConfiguration}. 51 | * Value = {@value} 52 | */ 53 | public static final String DEMO_CONFIG_PROFILE = "demo-config"; 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/demo/main/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Main entry point for Application plus available profiles. 3 | * 4 | * @author Paul Chapman 5 | */ 6 | package demo.main; -------------------------------------------------------------------------------- /src/main/java/demo/utils/BeanLogger.java: -------------------------------------------------------------------------------- 1 | package demo.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.BeansException; 6 | import org.springframework.beans.factory.config.BeanPostProcessor; 7 | import org.springframework.core.Ordered; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 10 | import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite; 11 | 12 | /** 13 | * A simple implementation of a BeanPostProcessor that logs every bean 14 | * created by Spring. When working with Spring Boot, a lot of beans get created 15 | * "auto-magically". This logger allows you to see what beans have been setup. 16 | *

17 | * You should remember that most of the web-layer beans are created by Spring 18 | * MVC automatically, whether you are using Spring Boot or not. To see what 19 | * beans are created, examine the @Bean methods in 20 | * {@link WebMvcConfigurationSupport}. 21 | * 22 | * @author Paul Chapman 23 | */ 24 | @Component 25 | public class BeanLogger implements BeanPostProcessor { 26 | 27 | protected Logger logger; 28 | protected boolean enabled = false; 29 | 30 | public BeanLogger() { 31 | logger = LoggerFactory.getLogger(getClass()); 32 | } 33 | 34 | @Override 35 | public Object postProcessBeforeInitialization(Object bean, String beanName) 36 | throws BeansException { 37 | // Nothing to do. Must return the bean or we lose it! 38 | return bean; 39 | } 40 | 41 | @Override 42 | public Object postProcessAfterInitialization(Object bean, String beanName) 43 | throws BeansException { 44 | if (!enabled) 45 | return bean; // Must return the bean or we lose it! 46 | 47 | // Display the bean. If it is Ordered, print its order also. Several MVC 48 | // classes are chained by Order and it is often useful to see what order 49 | // they are configured to run. 50 | if (bean instanceof Ordered) { 51 | String order = (bean instanceof Ordered) ? String 52 | .valueOf(((Ordered) bean).getOrder()) : "unknown"; 53 | logger.info(bean.getClass().getName() + " - Order: " + order); 54 | } else { 55 | logger.info(bean.getClass().getName()); 56 | } 57 | 58 | // Since we are concerned with exception-resolvers, lets print extra 59 | // info about this one. It delegates in turn to all the default 60 | // resolvers - see "Going Deeper" in the Blog for more details. 61 | if (bean instanceof HandlerExceptionResolverComposite) { 62 | logger.info(" resolvers: " 63 | + ((HandlerExceptionResolverComposite) bean) 64 | .getExceptionResolvers()); 65 | } 66 | 67 | // Must return the bean or we lose it! 68 | return bean; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/demo/web/ViewMappingController.java: -------------------------------------------------------------------------------- 1 | package demo.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | 7 | import demo.config.ResponseDataControllerAdvice; 8 | 9 | /** 10 | * Maps URLs directly to views. Unlike View Controllers, using an actual 11 | * Controller causes the {@link ResponseDataControllerAdvice} to be invoked and 12 | * setup model data needed by these views. 13 | * 14 | * @author Paul Chapman 15 | */ 16 | @Controller 17 | public class ViewMappingController { 18 | 19 | @GetMapping("/") 20 | public String home() { 21 | return "index"; 22 | } 23 | 24 | @GetMapping("/unannotated") 25 | public String unannotated() { 26 | return "unannotated"; 27 | } 28 | 29 | @GetMapping("/no-handler") 30 | public String noHandler() { 31 | return "no-handler"; 32 | } 33 | 34 | @GetMapping("/demo5") 35 | public String demo4() { 36 | return "demo5"; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/demo/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Web artifacts used by all three demos. 3 | * 4 | * @author Paul Chapman 5 | */ 6 | package demo.web; -------------------------------------------------------------------------------- /src/main/java/demo1/web/ExceptionHandlingController.java: -------------------------------------------------------------------------------- 1 | package demo1.web; 2 | 3 | import java.sql.SQLException; 4 | import java.util.Date; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.core.annotation.AnnotationUtils; 11 | import org.springframework.dao.DataAccessException; 12 | import org.springframework.dao.DataIntegrityViolationException; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.stereotype.Controller; 15 | import org.springframework.web.bind.annotation.ExceptionHandler; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.ResponseStatus; 19 | import org.springframework.web.servlet.ModelAndView; 20 | 21 | import demo.exceptions.DatabaseException; 22 | import demo.exceptions.InvalidCreditCardException; 23 | import demo.exceptions.OrderNotFoundException; 24 | import demo.exceptions.SupportInfoException; 25 | import demo.exceptions.UnhandledException; 26 | 27 | /** 28 | * A controller whose request-handler methods deliberately throw exceptions to 29 | * demonstrate the points discussed in the Blog. 30 | *

31 | * Contains its own @ExceptionHandler methods to handle (most of) the 32 | * exceptions it raises. 33 | * 34 | * @author Paul Chapman 35 | */ 36 | @Controller 37 | @RequestMapping("/local") 38 | public class ExceptionHandlingController { 39 | 40 | protected Logger logger; 41 | 42 | public ExceptionHandlingController() { 43 | logger = LoggerFactory.getLogger(getClass()); 44 | } 45 | 46 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 47 | /* . . . . . . . . . . . . . . REQUEST HANDLERS . . . . . . . . . . . . .. */ 48 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 49 | 50 | /** 51 | * Home page. 52 | * 53 | * @return The view name (an HTML page with Thymeleaf markup). 54 | */ 55 | @GetMapping("") 56 | String home1() { 57 | logger.info("Local home page 1"); 58 | return "local"; 59 | } 60 | 61 | /** 62 | * Home page. 63 | * 64 | * @return The view name (an HTML page with Thymeleaf markup). 65 | */ 66 | @GetMapping("/") 67 | String home2() { 68 | logger.info("Local home page 2"); 69 | return "local"; 70 | } 71 | 72 | /** 73 | * No handler is needed for this exception since it is annotated with 74 | * @ResponseStatus. 75 | * 76 | * @return Nothing - it always throws the exception. 77 | * @throws OrderNotFoundException 78 | * Always thrown. 79 | */ 80 | @GetMapping("/orderNotFound") 81 | String throwOrderNotFoundException() { 82 | logger.info("Throw OrderNotFoundException for unknown order 12345"); 83 | throw new OrderNotFoundException("12345"); 84 | } 85 | 86 | /** 87 | * Throws an unannotated DataIntegrityViolationException. Must be 88 | * caught by an exception handler. 89 | * 90 | * @return Nothing - it always throws the exception. 91 | * @throws DataIntegrityViolationException 92 | * Always thrown. 93 | */ 94 | @GetMapping("/dataIntegrityViolation") 95 | String throwDataIntegrityViolationException() throws SQLException { 96 | logger.info("Throw DataIntegrityViolationException"); 97 | throw new DataIntegrityViolationException("Duplicate id"); 98 | } 99 | 100 | /** 101 | * Simulates a database exception by always throwing SQLException. 102 | * Must be caught by an exception handler. 103 | * 104 | * @return Nothing - it always throws the exception. 105 | * @throws SQLException 106 | * Always thrown. 107 | */ 108 | @GetMapping("/databaseError1") 109 | String throwDatabaseException1() throws SQLException { 110 | logger.info("Throw SQLException"); 111 | throw new SQLException(); 112 | } 113 | 114 | /** 115 | * Simulates a database exception by always throwing 116 | * DataAccessException. Must be caught by an exception handler. 117 | * 118 | * @return Nothing - it always throws the exception. 119 | * @throws DataAccessException 120 | * Always thrown. 121 | */ 122 | @GetMapping("/databaseError2") 123 | String throwDatabaseException2() throws DataAccessException { 124 | logger.info("Throw DataAccessException"); 125 | throw new DataAccessException("Error accessing database"); 126 | } 127 | 128 | /** 129 | * Simulates an illegal credit-card exception by always throwing 130 | * InvalidCreditCardException. Handled by 131 | * SimpleMappingExceptionResolver. 132 | * 133 | * @return Nothing - it always throws the exception. 134 | * @throws InvalidCreditCardException 135 | * Always thrown. 136 | */ 137 | @GetMapping("/invalidCreditCard") 138 | String throwInvalidCreditCard() throws Exception { 139 | logger.info("Throw InvalidCreditCardException"); 140 | throw new InvalidCreditCardException("1234123412341234"); 141 | } 142 | 143 | /** 144 | * Simulates a database exception by always throwing 145 | * DatabaseException. Handled by 146 | * SimpleMappingExceptionResolver. 147 | * 148 | * @return Nothing - it always throws the exception. 149 | * @throws DatabaseException 150 | * Always thrown. 151 | */ 152 | @GetMapping("/databaseException") 153 | String throwDatabaseException() throws Exception { 154 | logger.info("Throw InvalidCreditCardException"); 155 | throw new DatabaseException("Database not found: info.db"); 156 | } 157 | 158 | /** 159 | * Always throws a SupportInfoException. Must be caught by an 160 | * exception handler. 161 | * 162 | * @return Nothing - it always throws the exception. 163 | * @throws SupportInfoException 164 | * Always thrown. 165 | */ 166 | @GetMapping("/supportInfoException") 167 | String throwCustomException() throws Exception { 168 | logger.info("Throw SupportInfoException"); 169 | throw new SupportInfoException("Custom exception occurred"); 170 | } 171 | 172 | /** 173 | * Simulates a database exception by always throwing 174 | * UnhandledException. Must be caught by an exception handler. 175 | * 176 | * @return Nothing - it always throws the exception. 177 | * @throws UnhandledException 178 | * Always thrown. 179 | */ 180 | @GetMapping("/unhandledException") 181 | String throwUnhandledException() throws Exception { 182 | logger.info("Throw UnhandledException"); 183 | throw new UnhandledException("Some exception occurred"); 184 | } 185 | 186 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 187 | /* . . . . . . . . . . . . . EXCEPTION HANDLERS . . . . . . . . . . . . .. */ 188 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 189 | 190 | /** 191 | * Convert a predefined exception to an HTTP Status code 192 | */ 193 | @ResponseStatus(value = HttpStatus.CONFLICT, reason = "Data integrity violation") 194 | // 409 195 | @ExceptionHandler(DataIntegrityViolationException.class) 196 | public void conflict() { 197 | logger.error("Request raised a DataIntegrityViolationException"); 198 | // Nothing to do 199 | } 200 | 201 | /** 202 | * Convert a predefined exception to an HTTP Status code and specify the 203 | * name of a specific view that will be used to display the error. 204 | * 205 | * @return Exception view. 206 | */ 207 | @ExceptionHandler({ SQLException.class, DataAccessException.class }) 208 | public String databaseError(Exception exception) { 209 | // Nothing to do. Return value 'databaseError' used as logical view name 210 | // of an error page, passed to view-resolver(s) in usual way. 211 | logger.error("Request raised " + exception.getClass().getSimpleName()); 212 | return "databaseError"; 213 | } 214 | 215 | /** 216 | * Demonstrates how to take total control - setup a model, add useful 217 | * information and return the "support" view name. This method explicitly 218 | * creates and returns 219 | * 220 | * @param req 221 | * Current HTTP request. 222 | * @param exception 223 | * The exception thrown - always {@link SupportInfoException}. 224 | * @return The model and view used by the DispatcherServlet to generate 225 | * output. 226 | * @throws Exception 227 | */ 228 | @ExceptionHandler(SupportInfoException.class) 229 | public ModelAndView handleError(HttpServletRequest req, Exception exception) 230 | throws Exception { 231 | 232 | // Rethrow annotated exceptions or they will be processed here instead. 233 | if (AnnotationUtils.findAnnotation(exception.getClass(), 234 | ResponseStatus.class) != null) 235 | throw exception; 236 | 237 | logger.error("Request: " + req.getRequestURI() + " raised " + exception); 238 | 239 | ModelAndView mav = new ModelAndView(); 240 | mav.addObject("exception", exception); 241 | mav.addObject("url", req.getRequestURL()); 242 | mav.addObject("timestamp", new Date().toString()); 243 | mav.addObject("status", 500); 244 | 245 | mav.setViewName("support"); 246 | return mav; 247 | } 248 | } -------------------------------------------------------------------------------- /src/main/java/demo1/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Artifacts used by Demo 1 - a Controller that handles its own exceptions. 3 | * 4 | * @author Paul Chapman 5 | */ 6 | package demo1.web; -------------------------------------------------------------------------------- /src/main/java/demo2/web/ControllerWithoutExceptionHandlers.java: -------------------------------------------------------------------------------- 1 | package demo2.web; 2 | 3 | import java.sql.SQLException; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.dao.DataAccessException; 8 | import org.springframework.dao.DataIntegrityViolationException; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import demo.exceptions.OrderNotFoundException; 14 | import demo.exceptions.SupportInfoException; 15 | 16 | /** 17 | * A controller whose request-handler methods deliberately throw exceptions to 18 | * demonstrate the points discussed in the Blog. 19 | *

20 | * Expects a @ControllerAdvice to handle its exceptions - see 21 | * {@link GlobalExceptionHandlingControllerAdvice}. 22 | * 23 | * @author Paul Chapman 24 | */ 25 | @Controller 26 | @RequestMapping("/global") 27 | public class ControllerWithoutExceptionHandlers { 28 | 29 | protected Logger logger; 30 | 31 | public ControllerWithoutExceptionHandlers() { 32 | logger = LoggerFactory.getLogger(getClass()); 33 | } 34 | 35 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 36 | /* . . . . . . . . . . . . . . REQUEST HANDLERS . . . . . . . . . . . . . . */ 37 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 38 | 39 | /** 40 | * Controller to demonstrate exception handling. 41 | * 42 | * @return The view name (an HTML page with Thymeleaf markup). 43 | */ 44 | @GetMapping("") 45 | String home1() { 46 | logger.info("Global home page 1"); 47 | return "global"; 48 | } 49 | 50 | /** 51 | * Controller to demonstrate exception handling.. 52 | * 53 | * @return The view name (an HTML page with Thymeleaf markup). 54 | */ 55 | @GetMapping("/") 56 | String home2() { 57 | logger.info("Global home page 2"); 58 | return "global"; 59 | } 60 | 61 | /** 62 | * No handler is needed for this exception since it is annotated with 63 | * @ResponseStatus. 64 | * 65 | * @return Nothing - it always throws the exception. 66 | * @throws OrderNotFoundException 67 | * Always thrown. 68 | */ 69 | @GetMapping("/orderNotFound") 70 | String throwOrderNotFoundException() { 71 | logger.info("Throw OrderNotFoundException for unknown order 12345"); 72 | throw new OrderNotFoundException("12345"); 73 | } 74 | 75 | /** 76 | * Throws an unannotated DataIntegrityViolationException. Must be 77 | * caught by an exception handler. 78 | * 79 | * @return Nothing - it always throws the exception. 80 | * @throws DataIntegrityViolationException 81 | * Always thrown. 82 | */ 83 | @GetMapping("/dataIntegrityViolation") 84 | String throwDataIntegrityViolationException() throws SQLException { 85 | logger.info("Throw DataIntegrityViolationException"); 86 | throw new DataIntegrityViolationException("Duplicate id"); 87 | } 88 | 89 | /** 90 | * Simulates a database exception by always throwing SQLException. 91 | * Must be caught by an exception handler. 92 | * 93 | * @return Nothing - it always throws the exception. 94 | * @throws SQLException 95 | * Always thrown. 96 | */ 97 | @GetMapping("/databaseError1") 98 | String throwDatabaseException1() throws SQLException { 99 | logger.info("Throw SQLException"); 100 | throw new SQLException(); 101 | } 102 | 103 | /** 104 | * Simulates a database exception by always throwing 105 | * DataAccessException. Must be caught by an exception handler. 106 | * 107 | * @return Nothing - it always throws the exception. 108 | * @throws DataAccessException 109 | * Always thrown. 110 | */ 111 | @GetMapping("/databaseError2") 112 | String throwDatabaseException2() throws DataAccessException { 113 | logger.info("Throw DataAccessException"); 114 | throw new DataAccessException("Error accessing database"); 115 | } 116 | 117 | /** 118 | * Always throws a SupportInfoException. Must be caught by an 119 | * exception handler. 120 | * 121 | * @return Nothing - it always throws the exception. 122 | * @throws SupportInfoException 123 | * Always thrown. 124 | */ 125 | @GetMapping("/supportInfoException") 126 | String throwCustomException() throws Exception { 127 | logger.info("Throw SupportInfoException"); 128 | throw new SupportInfoException("Custom exception occurred"); 129 | } 130 | 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/demo2/web/GlobalExceptionHandlingControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package demo2.web; 2 | 3 | import java.sql.SQLException; 4 | import java.util.Date; 5 | 6 | import javax.servlet.http.HttpServletRequest; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.core.annotation.AnnotationUtils; 11 | import org.springframework.dao.DataAccessException; 12 | import org.springframework.dao.DataIntegrityViolationException; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.web.bind.annotation.ControllerAdvice; 15 | import org.springframework.web.bind.annotation.ExceptionHandler; 16 | import org.springframework.web.bind.annotation.ResponseStatus; 17 | import org.springframework.web.servlet.ModelAndView; 18 | 19 | import demo.exceptions.SupportInfoException; 20 | import demo1.web.ExceptionHandlingController; 21 | 22 | /** 23 | * Performs the same exception handling as {@link ExceptionHandlingController} 24 | * but offers them globally. The exceptions below could be raised by any 25 | * controller and they would be handled here, if not handled in the controller 26 | * already. 27 | * 28 | * @author Paul Chapman 29 | */ 30 | @ControllerAdvice 31 | public class GlobalExceptionHandlingControllerAdvice { 32 | 33 | protected Logger logger; 34 | 35 | public GlobalExceptionHandlingControllerAdvice() { 36 | logger = LoggerFactory.getLogger(getClass()); 37 | } 38 | 39 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 40 | /* . . . . . . . . . . . . . EXCEPTION HANDLERS . . . . . . . . . . . . . . */ 41 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 42 | 43 | /** 44 | * Convert a predefined exception to an HTTP Status code 45 | */ 46 | @ResponseStatus(value = HttpStatus.CONFLICT, reason = "Data integrity violation") 47 | // 409 48 | @ExceptionHandler(DataIntegrityViolationException.class) 49 | public void conflict() { 50 | logger.error("Request raised a DataIntegrityViolationException"); 51 | // Nothing to do 52 | } 53 | 54 | /** 55 | * Convert a predefined exception to an HTTP Status code and specify the 56 | * name of a specific view that will be used to display the error. 57 | * 58 | * @return Exception view. 59 | */ 60 | @ExceptionHandler({ SQLException.class, DataAccessException.class }) 61 | public String databaseError(Exception exception) { 62 | // Nothing to do. Return value 'databaseError' used as logical view name 63 | // of an error page, passed to view-resolver(s) in usual way. 64 | logger.error("Request raised " + exception.getClass().getSimpleName()); 65 | return "databaseError"; 66 | } 67 | 68 | /** 69 | * Demonstrates how to take total control - setup a model, add useful 70 | * information and return the "support" view name. This method explicitly 71 | * creates and returns 72 | * 73 | * @param req 74 | * Current HTTP request. 75 | * @param exception 76 | * The exception thrown - always {@link SupportInfoException}. 77 | * @return The model and view used by the DispatcherServlet to generate 78 | * output. 79 | * @throws Exception 80 | */ 81 | @ExceptionHandler(SupportInfoException.class) 82 | public ModelAndView handleError(HttpServletRequest req, Exception exception) 83 | throws Exception { 84 | 85 | // Rethrow annotated exceptions or they will be processed here instead. 86 | if (AnnotationUtils.findAnnotation(exception.getClass(), 87 | ResponseStatus.class) != null) 88 | throw exception; 89 | 90 | logger.error("Request: " + req.getRequestURI() + " raised " + exception); 91 | 92 | ModelAndView mav = new ModelAndView(); 93 | mav.addObject("exception", exception); 94 | mav.addObject("url", req.getRequestURL()); 95 | mav.addObject("timestamp", new Date().toString()); 96 | mav.addObject("status", 500); 97 | 98 | mav.setViewName("support"); 99 | return mav; 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/java/demo2/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Artifacts used by Demo 2 - a Controller that relies on a global Controller 3 | * Advice to handle its exceptions. 4 | * 5 | * @author Paul Chapman 6 | */ 7 | package demo2.web; -------------------------------------------------------------------------------- /src/main/java/demo3/config/DemoExceptionConfiguration.java: -------------------------------------------------------------------------------- 1 | package demo3.config; 2 | 3 | import java.util.Properties; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; 11 | 12 | import demo.main.Profiles; 13 | import demo3.web.SwitchController; 14 | import demo3.web.SwitchableSimpleMappingExceptionResolver; 15 | 16 | /** 17 | * Setup for exception handling using a {@link SimpleMappingExceptionResolver} 18 | * sub-class that can be enabled or disabled programmatically. 19 | *

20 | * The use of the "demo-config" profile here is for demonstration only. A real 21 | * application would either create a {@link SimpleMappingExceptionResolver} or 22 | * it wouldn't. 23 | * 24 | * @author Paul Chapman 25 | */ 26 | @Configuration 27 | @Profile(Profiles.DEMO_CONFIG_PROFILE) 28 | public class DemoExceptionConfiguration { 29 | 30 | protected Logger logger; 31 | 32 | public DemoExceptionConfiguration() { 33 | logger = LoggerFactory.getLogger(getClass()); 34 | logger.info("Creating DemoExceptionConfiguration"); 35 | } 36 | 37 | /** 38 | * Setup the switchable {@link SimpleMappingExceptionResolver} to provide 39 | * useful defaults for logging and handling exceptions. 40 | * 41 | * @return The new resolver 42 | */ 43 | @Bean(name = "simpleMappingExceptionResolver") 44 | public SwitchableSimpleMappingExceptionResolver createSwitchableSimpleMappingExceptionResolver() { 45 | logger.info("Creating SwitchableSimpleMappingExceptionResolver in disabled mode"); 46 | 47 | // Turn exception resolving off to start 48 | boolean initialState = false; 49 | 50 | SwitchableSimpleMappingExceptionResolver resolver = new SwitchableSimpleMappingExceptionResolver( 51 | initialState); 52 | 53 | Properties mappings = new Properties(); 54 | mappings.setProperty("DatabaseException", "databaseException"); 55 | mappings.setProperty("InvalidCreditCardException", "creditCardError"); 56 | 57 | resolver.setExceptionMappings(mappings); // None by default 58 | resolver.setExceptionAttribute("ex"); // Default is "exception" 59 | resolver.setWarnLogCategory("demo1.ExceptionLogger"); // No default 60 | 61 | // See comment in ExceptionConfiguration 62 | resolver.setDefaultErrorView("defaultErrorPage"); 63 | return resolver; 64 | } 65 | 66 | /** 67 | * Create the {@link SwitchController}. 68 | */ 69 | @Bean(name = "switchController") 70 | public SwitchController createSwitchController() { 71 | logger.info("Creating SwitchController"); 72 | return new SwitchController( 73 | createSwitchableSimpleMappingExceptionResolver()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/demo3/config/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for demonstrating the use of the 3 | * {@link org.springframework.web.servlet.handler.SimpleMappingExceptionResolver}. 4 | * 5 | * @author Paul Chapman 6 | */ 7 | package demo3.config; 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/demo3/web/ExceptionThrowingController.java: -------------------------------------------------------------------------------- 1 | package demo3.web; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; 10 | 11 | import demo.exceptions.CustomException; 12 | import demo.exceptions.DatabaseException; 13 | import demo.exceptions.InvalidCreditCardException; 14 | import demo.exceptions.SupportInfoException; 15 | import demo.exceptions.UnhandledException; 16 | 17 | /** 18 | * A controller whose request-handler methods deliberately throw exceptions to 19 | * demonstrate the points discussed in the Blog. 20 | *

21 | * Expects a SimpleMappingExceptionResolver to handle its exceptions. 22 | * 23 | * @author Paul Chapman 24 | */ 25 | @Controller 26 | @RequestMapping("/throw") 27 | public class ExceptionThrowingController { 28 | 29 | protected Logger logger; 30 | protected SimpleMappingExceptionResolver resolver; 31 | 32 | public ExceptionThrowingController() { 33 | logger = LoggerFactory.getLogger(getClass()); 34 | } 35 | 36 | /** 37 | * Need to see if the {@link SwitchableSimpleMappingExceptionResolver} is 38 | * being used and if so, is it enabled? 39 | * 40 | * @param resolver 41 | */ 42 | @Autowired(required = false) 43 | public void setSimpleMappingExceptionResolver( 44 | SimpleMappingExceptionResolver resolver) { 45 | this.resolver = resolver; 46 | } 47 | 48 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 49 | /* . . . . . . . . . . . . . . REQUEST HANDLERS . . . . . . . . . . . . .. */ 50 | /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ 51 | 52 | /** 53 | * Home page. 54 | * 55 | * @return The view name (an HTML page with Thymeleaf markup). 56 | */ 57 | @GetMapping("/") 58 | String home() { 59 | logger.info("Throw home page"); 60 | 61 | // Check if the SwitchableSimpleMappingExceptionResolver is in use and 62 | // enabled, or not. 63 | if (resolver == null) 64 | return "no-handler"; 65 | else if (resolver instanceof SwitchableSimpleMappingExceptionResolver) { 66 | return ((SwitchableSimpleMappingExceptionResolver) resolver) 67 | .isEnabled() ? "unannotated" : "no-handler"; 68 | } else 69 | return "unannotated"; 70 | } 71 | 72 | /** 73 | * Simulates a database exception by always throwing 74 | * DatabaseException. Handled by 75 | * SimpleMappingExceptionResolver. 76 | * 77 | * @return Nothing - it always throws the exception. 78 | * @throws DatabaseException 79 | * Always thrown. 80 | */ 81 | @GetMapping("/databaseException") 82 | String throwDatabaseException() throws Exception { 83 | logger.info("Throw DatabaseException"); 84 | throw new DatabaseException("Database not found: info.db"); 85 | } 86 | 87 | /** 88 | * Simulates an illegal credit-card exception by always throwing 89 | * InvalidCreditCardException. Handled by 90 | * SimpleMappingExceptionResolver. 91 | * 92 | * @return Nothing - it always throws the exception. 93 | * @throws InvalidCreditCardException 94 | * Always thrown. 95 | */ 96 | @GetMapping("/invalidCreditCard") 97 | String throwInvalidCreditCard() throws Exception { 98 | logger.info("Throw InvalidCreditCardException"); 99 | throw new InvalidCreditCardException("1234123412341234"); 100 | } 101 | 102 | /** 103 | * Simulates a database exception by always throwing 104 | * CustomException. Must be caught by an exception handler. 105 | * 106 | * @return Nothing - it always throws the exception. 107 | * @throws CustomException 108 | * Always thrown. 109 | */ 110 | @GetMapping("/supportInfoException") 111 | String throwCustomException() throws Exception { 112 | logger.info("Throw SupportInfoException"); 113 | throw new SupportInfoException("Exception occurred"); 114 | } 115 | 116 | /** 117 | * Simulates a database exception by always throwing 118 | * UnhandledException. Must be caught by an exception handler. 119 | * 120 | * @return Nothing - it always throws the exception. 121 | * @throws UnhandledException 122 | * Always thrown. 123 | */ 124 | @GetMapping("/unhandledException") 125 | String throwUnhandledException() throws Exception { 126 | logger.info("Throw UnhandledException"); 127 | throw new UnhandledException("Some exception occurred"); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/demo3/web/SwitchController.java: -------------------------------------------------------------------------------- 1 | package demo3.web; 2 | 3 | import org.slf4j.LoggerFactory; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Profile; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.ModelAttribute; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import demo.main.Main; 14 | 15 | /** 16 | * A simple controller for enabling and disabling the 17 | * {@link SwitchableSimpleMappingExceptionResolver}. 18 | */ 19 | @Controller 20 | @Profile("no-auto-creation") 21 | public class SwitchController { 22 | 23 | protected SwitchableSimpleMappingExceptionResolver resolver; 24 | 25 | @Autowired 26 | public SwitchController(SwitchableSimpleMappingExceptionResolver resolver) { 27 | this.resolver = resolver; 28 | } 29 | 30 | /** 31 | * Must return the currently active profiles for the home page (index.html) 32 | * to use. 33 | * 34 | * @return Currently active profiles. 35 | */ 36 | @ModelAttribute("profiles") 37 | public String getProfiles() { 38 | return Main.getProfiles(); 39 | } 40 | 41 | /** 42 | * Enable the profile if action is "on". 43 | * 44 | * @param action 45 | * What to do. Resolver is enabled if set to "ON" (case ignored). 46 | * @return Redirection back to the home page. 47 | */ 48 | @GetMapping("/simpleMappingExceptionResolver/{action}") 49 | public String enable(@PathVariable("action") String action, Model model) { 50 | LoggerFactory.getLogger(getClass()).info( 51 | "SwitchableSimpleMappingExceptionResolver is " + action); 52 | // Anything other than ON, is treated as OFF 53 | boolean enabled = action.equalsIgnoreCase("on"); 54 | resolver.setEnabled(enabled); 55 | 56 | // The ResponseDataControllerAdvice sets this for every request, but 57 | // before it is changed by this controller. So we need to set it 58 | // again - to reflect the new value. 59 | model.addAttribute("switchState", enabled ? "on" : "off"); 60 | 61 | // Which view to return depends on resolver status 62 | // - Enabled? Show use of exception handing without annotations. 63 | // - Disabled? Show what happens with no excpetion handling at all. 64 | return enabled ? "unannotated" : "no-handler"; 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/demo3/web/SwitchableSimpleMappingExceptionResolver.java: -------------------------------------------------------------------------------- 1 | package demo3.web; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; 6 | import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; 7 | 8 | /** 9 | * A sub-class of {@link SimpleMappingExceptionResolver} that can be turned on 10 | * and off for demonstration purposes (you wouldn't do this in a real 11 | * application). 12 | */ 13 | public class SwitchableSimpleMappingExceptionResolver extends 14 | SimpleMappingExceptionResolver { 15 | 16 | protected boolean enabled = false; 17 | 18 | public SwitchableSimpleMappingExceptionResolver(boolean enabled) { 19 | this.enabled = enabled; 20 | } 21 | 22 | /** 23 | * Enabled or not? 24 | * 25 | * @return Is enabled? 26 | */ 27 | public boolean isEnabled() { 28 | return enabled; 29 | } 30 | 31 | /** 32 | * Allow this resolver to be turned on and off whilst the application is 33 | * running. 34 | * 35 | * @param enabled 36 | * Set to enabled? 37 | */ 38 | public void setEnabled(boolean enabled) { 39 | this.enabled = enabled; 40 | } 41 | 42 | /** 43 | * Resolver only handles exceptions if enabled. Overrides method inherited 44 | * from {@link AbstractHandlerExceptionResolver} 45 | */ 46 | @Override 47 | protected boolean shouldApplyTo(HttpServletRequest request, Object handler) { 48 | return enabled && super.shouldApplyTo(request, handler); 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/main/java/demo3/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Artifacts used by Demo 3 - a Controller that relies on a Simple Mapping 3 | * Exception Resolver to handle its exceptions. 4 | * 5 | * @author Paul Chapman 6 | */ 7 | package demo3.web; -------------------------------------------------------------------------------- /src/main/java/demo5/web/ReturnOrRedirectController.java: -------------------------------------------------------------------------------- 1 | package demo5.web; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.ExceptionHandler; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | import demo.exceptions.OrderNotFoundException; 14 | 15 | /** 16 | * Demonstrate the difference between returning the error view and redirecting 17 | * to the Spring Boot error page (which uses the same error view). 18 | * 19 | * @author Paul Chapman 20 | * 21 | */ 22 | @Controller 23 | public class ReturnOrRedirectController { 24 | 25 | enum Action { 26 | RETURN_ACTION, FORWARD_ACTION 27 | } 28 | 29 | @SuppressWarnings("serial") 30 | protected static class DemoException extends RuntimeException { 31 | final public Action action; 32 | 33 | public DemoException(Action action) { 34 | this.action = action; 35 | } 36 | } 37 | 38 | protected Logger logger; 39 | 40 | public ReturnOrRedirectController() { 41 | logger = LoggerFactory.getLogger(getClass()); 42 | } 43 | 44 | /** 45 | * Throws a {@link DemoException} with the action "return" - see 46 | * {@link 47 | * 48 | * @return Nothing - it always throws the exception. 49 | * @throws OrderNotFoundException 50 | * Always thrown. 51 | */ 52 | @GetMapping("/demo5/return") 53 | String throwDemoException1() { 54 | logger.info("Throw DemoException - ask hander to return 'error' view"); 55 | throw new DemoException(Action.RETURN_ACTION); 56 | } 57 | 58 | /** 59 | * Throws a {@link DemoException} with the action "return" - see 60 | * {@link 61 | * 62 | * @return Nothing - it always throws the exception. 63 | * @throws OrderNotFoundException 64 | * Always thrown. 65 | */ 66 | @GetMapping("/demo5/forward") 67 | String throwDemoException2() { 68 | logger.info("Throw DemoException - ask hander to return 'error' view"); 69 | throw new DemoException(Action.FORWARD_ACTION); 70 | } 71 | 72 | /** 73 | * Handle a {@link DemoException} by returning the "error" view by name or 74 | * redirecting to the "/error" URL. The first shows the error page with no 75 | * additional information (ExceptionResolvers by default don't provide any). 76 | * The second forces a redirect via Spring Boot's internal 77 | * BasicErrorController whose @GetMapping method adds 78 | * exception and other details into the Model for the error view to use. 79 | * 80 | * @return Exception view. 81 | */ 82 | @ExceptionHandler 83 | public String handleDemoException(DemoException exception, 84 | HttpServletRequest req) { 85 | logger.error("Handle DemoExeption - action is " + exception.action); 86 | 87 | // Because we are handling the error, the server thinks everything is 88 | // OK, so the status is 200. So let's set it to something else. 89 | req.setAttribute("javax.servlet.error.status_code", 90 | HttpStatus.BAD_REQUEST.value()); 91 | return exception.action == Action.RETURN_ACTION ? "error" 92 | : "forward:/error"; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/demo5/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Controller that demos exception handling using Spring Boot's internal setup. 3 | * 4 | * @author Paul Chapman 5 | */ 6 | package demo5.web; -------------------------------------------------------------------------------- /src/main/java/mvc-configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | 16 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 39 | 40 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/dao/DataAccessException.java: -------------------------------------------------------------------------------- 1 | package org.springframework.dao; 2 | 3 | /** 4 | * Spring's database support defines this exception. Our demo application just 5 | * needs it to demonstrate how it might be handled. The definition is copied 6 | * here for convenience rather than pulling in the Spring JDBC dependencies. 7 | * 8 | * @author Copied from Spring 9 | */ 10 | public class DataAccessException extends RuntimeException { 11 | 12 | /** 13 | * Unique ID for Serialized object 14 | */ 15 | private static final long serialVersionUID = 3620199505147655172L; 16 | 17 | public DataAccessException(String msg) { 18 | super(msg); 19 | } 20 | 21 | public DataAccessException(String msg, Throwable t) { 22 | super(msg, t); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/springframework/dao/DataIntegrityViolationException.java: -------------------------------------------------------------------------------- 1 | package org.springframework.dao; 2 | 3 | /** 4 | * Spring's database support defines this exception. Our demo application just 5 | * needs it to demonstrate how it might be handled. The definition is copied 6 | * here for convenience rather than pulling in the Spring JDBC dependencies. 7 | * 8 | * @author Copied from Spring 9 | */ 10 | public class DataIntegrityViolationException extends DataAccessException { 11 | 12 | /** 13 | * Unique ID for Serialized object 14 | */ 15 | private static final long serialVersionUID = -8146834359701827537L; 16 | 17 | public DataIntegrityViolationException(String msg) { 18 | super(msg); 19 | } 20 | 21 | public DataIntegrityViolationException(String msg, Throwable t) { 22 | super(msg, t); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2 | # CONFIGURE SPRING BOOT 3 | # 4 | # By convention Spring Boot properties go in this file and 5 | # Spring Boot automatically loads it as a PropertySource 6 | # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 7 | 8 | # Allow Thymeleaf templates to be reloaded at dev time by disabling the cache. 9 | # 10 | # During development if a page is changed, the changes will be seen straight 11 | # away, the next time that page is rendered. 12 | # Property should be set to 'true' in production for efficiency and consistency 13 | spring.thymeleaf.cache=false 14 | 15 | # Set the error path (this is actually the default). 16 | # 17 | # Spring boot assumes the fallback error page maps to /error. You can set this 18 | # property to specify an alternative mapping. 19 | # If using a SimpleMappingExceptionResolver, make sure it's defaultErrorView 20 | # corresponds to the same page (see Boot's ErrorMvcAutoConfiguration for more 21 | # details). 22 | server.error.path=/error 23 | 24 | # Enable Spring Boots 'white-label' error page (this is actually the default) 25 | # 26 | # Set to false to turn-off Spring Boot's error page. If your view resolution 27 | # setup provides no mapping for /error (or you aren't using view-resolvers) 28 | # any unhandled exceptions will be handled by the container in the usual way. 29 | server.error.whitelabel.enabled=true 30 | 31 | # Enable logging for Spring Web 32 | logging.level.org.springframework.web=DEBUG 33 | 34 | # Enable actuator access without security checks - DEVELOPMENT ONLY 35 | # Spring Boot 1.5 36 | # management.security.enabled: false 37 | # Spring Boot 2.x 38 | management.endpoints.web.exposure.include=* -------------------------------------------------------------------------------- /src/main/resources/mvc-configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | 16 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 39 | 40 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/resources/public/extlink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulc4/mvc-exceptions/e72c53595671ab6b4e7d0d8e33efa56eae06e10c/src/main/resources/public/extlink.png -------------------------------------------------------------------------------- /src/main/resources/public/logo-vmware-tanzu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulc4/mvc-exceptions/e72c53595671ab6b4e7d0d8e33efa56eae06e10c/src/main/resources/public/logo-vmware-tanzu.png -------------------------------------------------------------------------------- /src/main/resources/public/pws-header-logo_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulc4/mvc-exceptions/e72c53595671ab6b4e7d0d8e33efa56eae06e10c/src/main/resources/public/pws-header-logo_new.png -------------------------------------------------------------------------------- /src/main/resources/public/spring-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulc4/mvc-exceptions/e72c53595671ab6b4e7d0d8e33efa56eae06e10c/src/main/resources/public/spring-trans.png -------------------------------------------------------------------------------- /src/main/resources/public/styles.css: -------------------------------------------------------------------------------- 1 | /* - - - - - - - - - - - * 2 | Local Styles 3 | * - - - - - - - - - - - */ 4 | 5 | body { 6 | font-family: helvetica; 7 | font-size: 13pt; 8 | } 9 | 10 | .navbar, .navbar > a { 11 | font-family: helvetica; 12 | font-size: 15pt; 13 | } 14 | 15 | ul.openlist li { 16 | margin-bottom: 1em; 17 | } 18 | 19 | li { 20 | margin-bottom: 1em; 21 | } 22 | 23 | a { 24 | font-family: helvetica; 25 | } 26 | 27 | h1, h3 { 28 | color: #206090; 29 | } 30 | 31 | h3 { 32 | font-size:16pt; 33 | } 34 | 35 | code { 36 | background-color: white; 37 | border-radius: 0px; 38 | color: #80A080; 39 | font-size: 100%; 40 | font-family: monospace; 41 | padding: 0px; 42 | white-space: nowrap; 43 | } 44 | 45 | footer { 46 | margin-top: 3em; 47 | border-top: 1px black solid; 48 | text-align: center; 49 | color:#6DB33F; /* Spring Green */ 50 | font-style: italic; 51 | } 52 | 53 | code, .bg-none { 54 | background:none !important; 55 | } 56 | 57 | pre { 58 | margin-left: 2em; 59 | } 60 | 61 | .filler { 62 | height: 5em; 63 | } -------------------------------------------------------------------------------- /src/main/resources/templates/creditCardError.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 |

Credit Card Error

9 |

10 | Your credit card has been rejected: 11 | error here 12 |

13 | Note: You would never generate a page that displays a Java 14 | exception to an end-user in a real application 15 |
16 |

17 | 18 |

19 | 20 |
21 | 22 |

23 | You are seeing this error page due to the definition of the 24 | SimpleMappingExceptionResolver. 25 |

26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/templates/databaseError.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 |

Database Error

9 |

An internal data storage error has occurred accessing:

10 |

11 | Request URL 12 |

13 | 14 | 19 |
21 | Cause: exception ... 22 | exception ... 23 | message ... 25 |
26 | Note: You would never generate a page that displays a Java 27 | exception to an end-user in a real application 28 |
29 |
30 | 31 |

33 | Cause unknown (no exception details available)

34 | 35 |

36 | 37 |
38 | 39 |
40 |

Notes on this Page

41 |

Notice that there is very little information here (above the 42 | line). Firstly, end-users should not be shown internal exception 43 | details, but secondly, no exception details are available anyway.

44 | 79 |
80 | 81 |
82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/resources/templates/databaseException.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 |

Database Failure

9 |

An internal data storage error has occurred accessing:

10 |

11 | Request URL 12 |

13 |

14 | Exception 15 |

16 | Note: You would never generate a page that displays a Java 17 | exception to an end-user in a real application 18 |
19 |

20 | 21 |

22 | 23 |
24 | 25 |

26 | You are seeing this error page due to the definition of the 27 | SimpleMappingExceptionResolver 28 | . 29 |

30 | 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/templates/defaultErrorPage.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 18 |

Default Error Page

19 | 20 | 23 |

24 | Page: Page URL 25 |

26 | 27 |

28 | URL: Request 29 | URL 30 |

31 | 32 |

33 | Occurred: Timestamp 34 |

35 | 36 |

37 | Response Status: status-code error ... 39 |

40 | 41 | 43 |
45 | Cause: exception 46 | ... exception 47 | ... message ... 49 |
50 | Note: You would never generate a page that displays a Java 51 | exception to an end-user in a real application 52 |
53 |
54 | 55 |

56 | 57 |
58 | 59 |

60 | You are seeing this error page due to the definition of the 61 | SimpleMappingExceptionResolver. 62 |

63 | 64 |
65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/resources/templates/demo5.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo 5 - Using Spring Boot

10 | 11 |

Exception handling with Spring Boot.

12 | 13 |

Summary

14 | 22 | 23 |

Details

24 |

25 | Spring Boot generates its error page by forwarding to it via an 26 | internal controller (see below for details). This demo shows this at 27 | work. The exceptions below are thrown and handled within the same 28 | Controller - 29 | demo5.web.ReturnOrRedirectController 30 | 33 | . 34 |

35 |

To fully understand this demo, review the output in the server 36 | (console) log as you click the links below.

37 | 38 |

Demo

39 | 40 |

Note: After clicking on any of the links below, use your browser's back button to return to this page.

41 | 42 | 80 | 81 |

82 | Return to Home page. 83 |

84 | 85 |
86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 18 |

Default Spring Boot Error Page

19 | 20 | 24 |

25 | Page: Page URL 26 |

27 | 28 |

29 | Occurred: Timestamp 30 |

31 | 32 |

33 | Response Status: status-code error ... 35 |

36 | 37 | 39 |
41 | Cause: exception ... 42 | exception ... 43 | message ... 45 |
46 | Note: You would never generate a page that displays a Java 47 | exception to an end-user in a real application 48 |
49 |
50 | 51 |

53 | Cause unknown (no exception details available)

54 | 55 |

56 | 57 |
58 | 59 | 60 |

61 | You are seeing this error page due to Spring Boot (which returns a 62 | view called "error" in response to uncaught exceptions. Since we are 63 | using Thymeleaf this corresponds to the template 64 | error.html 65 | ) 66 |

67 | 68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/resources/templates/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/templates/global.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo 2 - Global Exception Handling

10 |

Handling exceptions using a global Controller Advice.

11 | 12 |

Summary

13 | 22 | 23 |

Details

24 |

25 | The exceptions below are are not handled by the controller that threw 26 | them, but rely on a 27 | ControllerAdvice 28 | 30 | 31 | instead. 32 |

33 | 45 | 46 |

To fully understand this demo, review the output in the server 47 | (console) log as you click the links below.

48 | 49 |

50 | The behavior of this demo is the same as Demo 1 - 51 | just implemented differently. 52 |

53 | 54 |

Demo

55 | 56 |

Note: After clicking on any of the links below, use your browser's back button to return to this page.

57 | 58 | 79 | 80 |

81 | Demo 3: Using a 82 | Simple Mapping Exception Handler. 83 |

84 |

85 | Demo 3: Using a Simple Mapping Exception 86 | Handler. 87 |

88 | 89 |
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/main/resources/templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Exception Blog Demo Code 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 61 | 62 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 |

Exceptions Demo V2.1

10 |

Details

11 | 25 |

26 | Spring-Boot sets up a view called error as the default error-page and we 27 | are using Thymeleaf to define it via the error.html template 28 | (if you don't configure your own error view you see the default 29 | "Whitelabel Error Page"). 30 |

31 |

32 | Without Spring Boot you would see your container's default error page. 33 | Rename error.html to error2.html and restart to see this 34 | for yourself. 35 |

36 | 37 |

The previous version of this demo tried to show everything on 38 | one page. It has been broken down into different pages for clarity.

39 | 40 |

The Demo

41 | 42 |
    43 |
  1. Local Exception Handling - Exceptions 44 | handled in the Controller.
    URLs on this page are handled by 45 | the demo1.web.ExceptionHandlingController 48 | which handles its own exceptions using its own @ExceptionHandler 49 | methods.
  2. 50 | 51 |
  3. Global Exception Handling - Exceptions 52 | handled by a Controller Advice.
    URLs on this page are handled 53 | by the demo2.web.ControllerWithoutExceptionHandlers 56 | which relies on a Controller Advice demo2.web.GlobalExceptionHandlingControllerAdvice 59 | to handle any exceptions thrown.
  4. 60 | 61 | 72 |
  5. 73 | With Exception 75 | Resolver - Exceptions handled by a customized bean 77 | defined in mvc-configuration.xml. 81 | With 82 | Exception Resolver - Exceptions handled by a customized bean 84 | defined in demo.config.ExceptionConfiguration. With Exception 90 | Resolver - Exceptions handled by a runtime switchable (for demo 91 | purposes only) bean 93 | defined by demo3.config.DemoExceptionConfiguration.
    Exceptions raised by demo3.web.ExceptionThrowingController 99 |
  6. 100 | 101 | 102 |
  7. No Resolver for 104 | Exceptions
    For comparison, the same exceptions as previous 105 | option, but with the resolver disabled to see what you get by 106 | default. Exceptions are not totally unhandled because they are caught 107 | by Spring Boot's default error page - mapped to error.html.
  8. 108 | 109 |
  9. Using Spring Boot - demonstrates how 110 | Spring Boot's error-handling setup works.
  10. 111 | 112 |
  11. Throwing an exception in a filter - this link causes the 113 | BrokenFilter 114 | 115 | 116 | 117 | to throw an exception. Note it is still reported by our default Error page.
  12. 118 |
119 | 120 |

Spring Boot URLs

121 |

For those interested, Spring Boot provides RESTful URLs for 122 | interrogating your application (they return JSON format data):

123 | 124 | 132 | 133 |
134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /src/main/resources/templates/link.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 10 | 11 | Blog 12 | 13 | SimpleMappingExceptionResolver 14 | 16 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/templates/local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Demo 1 - Local Exception Handling

8 |

Handling exceptions in the Controller.

9 | 10 |

Summary

11 | 19 | 20 |

Details

21 |

22 | The exceptions below are thrown and handled within the same Controller 23 | using its own 24 | @ExceptionHandler 25 | methods. The controller is 26 | demo1.web.ExceptionHandlingController 27 | 29 | 30 | . 31 |

32 |

To fully understand this demo, review the output in the server 33 | (console) log as you click the links below.

34 | 35 |

36 | The behavior of this demo is the same as Demo 2 37 | - just implemented differently. 38 |

39 | 40 |

Demo

41 | 42 |

Note: After clicking on any of the links below, use your browser's back button to return to this page.

43 | 44 | 65 | 66 |

67 | Demo 2: Global Exception Handling - using a 68 | Controller Advice 69 |

70 | 71 |
72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/resources/templates/no-handler.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo 4 - No Handlers Enabled

10 | 11 |

With no handlers enabled, everything defaults to Spring Boot. 12 | 13 |

Summary

14 | 55 | 56 |

Details

57 |

The exceptions below have no handler defined for them when 58 | running the demo-config profile. They will generate the default error-page 59 | supplied by Spring Boot. A non Spring-Boot application would generate 60 | the container's error page.

61 | 62 |

To fully understand this demo, review the output in the server 63 | (console) log as you click the links below.

64 | 65 |

Demo

66 | 67 |

Note: After clicking on any of the links below, use your browser's back button to return to this page.

68 | 69 | 86 | 87 |

88 | Demo 5: Working with Spring Boot. 89 |

90 | 91 | 92 |
93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/resources/templates/support.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 |

Support Friendly Error Page

9 | 10 | 14 |

15 | Page: Page URL 16 |

17 | 18 |

19 | Occurred: Timestamp 20 |

21 | 22 |

23 | Response Status: status-code error ... 25 |

26 | 27 |

Application has encountered an error. Please contact support on 28 | ...

29 | 30 |

Support may ask you to right click to view page source.

31 | 32 | 36 |
37 |
${url}
38 |
${exception.message}
39 | 43 |
44 | 45 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/resources/templates/unannotated.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 |

Demo 3 - Simple Mapping Exception Resolver

10 |

Using the Simple Mapping Exception Resolver instead of annotations. 11 | 12 |

Summary

13 | 57 | 58 |

Details

59 |

60 | The exceptions below show the use of a 61 | SimpleMappingExceptionResolver 62 | - the oldest way of handling exceptions in Spring dating back to V1 63 | and still commonly used. It is equivalent to using a ControllerAdvice 64 | - your choice. 65 |

66 | 67 |

To fully understand this demo, review the output in the server 68 | (console) log as you click the links below.

69 | 70 |

Demo

71 | 72 |

Note: After clicking on any of the links below, use your browser's back button to return to this page.

73 | 74 | 106 | 107 |

108 | Demo 4: No 109 | Handler. For comparison, the same exceptions with no Simple Mapping 110 | Exception Resolver. 111 |

112 |

113 | Demo 5: Working with Spring Boot. 114 |

115 | 116 |
117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulc4/mvc-exceptions/e72c53595671ab6b4e7d0d8e33efa56eae06e10c/support.png --------------------------------------------------------------------------------