SpringBoot學習筆記十一:自定義 Web MVC配置

Spring MVC自動配置

當我們在項目中添加了spring-boot-starter-web依賴矮固,Spring Boot會為Spring MVC提供自動配置,自動配置類為org.springframework.boot.autoconfigure.web.servlet包下的WebMvcAutoConfiguration

自動配置在Spring的默認值之上添加了以下功能:

  • 配置InternalResourceViewResolver作為默認的視圖解析器喜庞,包含ContentNegotiatingViewResolverBeanNameViewResolver bean
  • 支持提供靜態(tài)資源台囱,包括對WebJars的支持
  • 自動注冊Converter屏积,GenericConverterFormatter bean
  • 支持HttpMessageConverters
  • 自動注冊MessageCodesResolver
  • 靜態(tài)index.html支持
  • 自定義Favicon支持
  • 自動使用ConfigurableWebBindingInitializer bean

如果要保留Spring Boot MVC功能并且想要添加其他 MVC配置(interceptors, formatters, view controllers以及其他功能)痹雅,可以添加自己的類型為WebMvcConfigurer的配置類(以@Configuration注解標注),但不包含@EnableWebMvc在验。如果希望提供RequestMappingHandlerMapping玷氏,RequestMappingHandlerAdapterExceptionHandlerExceptionResolver的自定義實例,則可以聲明WebMvcRegistrationsAdapter實例以提供此類組件腋舌。

如果想完全控制Spring MVC盏触,可以使用@EnableWebMvc注解添加自己的配置類。

注意:Spring Boot 1.x時我們可以使用WebMvcConfigurerAdapter類來自定義Spring MVC配置块饺,但Spring Boot 2.0已不推薦使用此類來進行自定義配置(已廢棄)赞辩,取而代之我們可以直接實現(xiàn)WebMvcConfigurer接口并重寫相應方法來達到自定義Spring MVC配置的目的

大致原理就是WebMvcConfigurer接口基于java 8提供了默認方法,其實里面基本上也就是空實現(xiàn)

以下是WebMvcConfigurerAdapter的部分源碼:

/**
 * An implementation of {@link WebMvcConfigurer} with empty methods allowing
 * subclasses to override only the methods they're interested in.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 * @deprecated as of 5.0 {@link WebMvcConfigurer} has default methods (made
 * possible by a Java 8 baseline) and can be implemented directly without the
 * need for this adapter
 */
@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
    ...
}

HttpMessageConverters

Spring MVC使用HttpMessageConverter接口來轉(zhuǎn)換HTTP請求和響應授艰。消息轉(zhuǎn)換器是在HttpMessageConvertersAutoConfiguration類中自動注冊的辨嗽。org.springframework.boot.autoconfigure.http包下HttpMessageConverters相關(guān)配置類如下:

消息轉(zhuǎn)換器相關(guān)配置類

StringHttpMessageConverter是Spring Boot默認自動配置的HttpMessageConverter,除了默認的StringHttpMessageConverter想诅,在HttpMessageConvertersAutoConfiguration配置類中還使用了@Import注解引入了JacksonHttpMessageConvertersConfiguration召庞、GsonHttpMessageConvertersConfigurationJsonbHttpMessageConvertersConfiguration来破,自動配置邏輯如下:

  1. 若jackson的相關(guān)jar包在類路徑下篮灼,則通過JacksonHttpMessageConvertersConfiguration配置MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter
  2. 若gson的相關(guān)jar包在類路徑下且Jackson、Jsonb未依賴徘禁, 亦或gson的相關(guān)jar包在類路徑下且設置了spring.http.converters.preferred-json-mapper屬性值為gson诅诱,則通過GsonHttpMessageConvertersConfiguration配置GsonHttpMessageConverter
  3. 若JSON-B的相關(guān)jar包在類路徑下且Jackson、Gson未依賴送朱,亦或JSON-B的相關(guān)jar包在類路徑下且設置了spring.http.converters.preferred-json-mapper屬性值為jsonb娘荡,則通過JsonbHttpMessageConvertersConfiguration配置JsonbHttpMessageConverter

一般情況下干旁,當我們在項目中添加了spring-boot-starter-web依賴,Spring Boot會默認引入jackson-core炮沐、jackson-databind争群、jackson-annotations依賴,但未引入jackson-dataformat-xml大年,因此會配置MappingJackson2HttpMessageConverter消息轉(zhuǎn)換器换薄。

最佳實踐1:整合Gson

一般情況

排除Jackson依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jdk8</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-parameter-names</artifactId>
        </exclusion>
    </exclusions>
</dependency>

這里完全排除了spring-boot-starter-web引入的jackson相關(guān)依賴,一般情況下排除jackson-databind依賴即可

添加Gson依賴

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

配置Gson相關(guān)屬性

Spring Boot關(guān)于Gson的自動配置類為GsonAutoConfiguration翔试,相關(guān)配置屬性封裝在GsonProperties類中
src/main/resources/application.yml

spring:
  gson:
    date-format: yyyy-MM-dd HH:mm:ss

這里貼出所有g(shù)son配置屬性

# GSON GsonProperties
spring.gson.date-format= # Format to use when serializing Date objects.
spring.gson.disable-html-escaping= # Whether to disable the escaping of HTML characters such as '<', '>', etc.
spring.gson.disable-inner-class-serialization= # Whether to exclude inner classes during serialization.
spring.gson.enable-complex-map-key-serialization= # Whether to enable serialization of complex map keys (i.e. non-primitives).
spring.gson.exclude-fields-without-expose-annotation= # Whether to exclude all fields from consideration for serialization or deserialization that do not have the "Expose" annotation.
spring.gson.field-naming-policy= # Naming policy that should be applied to an object's field during serialization and deserialization.
spring.gson.generate-non-executable-json= # Whether to generate non executable JSON by prefixing the output with some special text.
spring.gson.lenient= # Whether to be lenient about parsing JSON that doesn't conform to RFC 4627.
spring.gson.long-serialization-policy= # Serialization policy for Long and long types.
spring.gson.pretty-printing= # Whether to output serialized JSON that fits in a page for pretty printing.
spring.gson.serialize-nulls= # Whether to serialize null fields.

這里有個問題就是用上述這種排除Jackson來配置Gson的方法在項目引入spring-boot-starter-jpa依賴后會報如下錯誤:

java.lang.IllegalStateException: Failed to introspect Class [org.springframework.data.web.config.SpringDataWebConfiguration] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@4459eb14]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:659) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:556) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:541) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.util.ReflectionUtils.getUniqueDeclaredMethods(ReflectionUtils.java:599) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryMethod(AbstractAutowireCapableBeanFactory.java:718) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:659) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:627) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1489) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:419) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:389) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:510) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:502) ~[spring-beans-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.getBeansOfType(AbstractApplicationContext.java:1198) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.boot.SpringApplication.getExitCodeFromMappedException(SpringApplication.java:892) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
    at org.springframework.boot.SpringApplication.getExitCodeFromException(SpringApplication.java:878) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
    at org.springframework.boot.SpringApplication.handleExitCode(SpringApplication.java:864) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
    at org.springframework.boot.SpringApplication.handleRunFailure(SpringApplication.java:813) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:341) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1258) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
    at com.example.springbootmvc.SpringBootMvcApplication.main(SpringBootMvcApplication.java:10) [classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.0.4.RELEASE.jar:2.0.4.RELEASE]
Caused by: java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/ObjectMapper
    at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na]
    at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3119) ~[na:na]
    at java.base/java.lang.Class.getDeclaredMethods(Class.java:2268) ~[na:na]
    at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:641) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    ... 25 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499) ~[na:na]
    ... 29 common frames omitted

以上錯誤暫時未找到解決方法

特殊情況(無法排除jackson依賴)

添加Gson依賴(同上)

設置preferred-json-mapper屬性

spring:
  http:
    converters:
      preferred-json-mapper: gson

設置preferred-json-mapper屬性后依然可以使用spring.gson開頭的屬性在application.properties或application.yml文件中配置gson

最佳實踐2:整合Fastjson

Fastjson簡介

Fastjson是阿里巴巴旗下的一款開源json序列化與反序列化java類庫轻要,號稱是java中最快的json類庫
官方Github主頁:https://github.com/alibaba/fastjson

Spring Boot整合Fastjson

添加依賴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.49</version>
</dependency>

集成Fastjson

參考自官方文檔在 Spring 中集成 Fastjson
src/main/java/com/example/springbootmvc/config/WebMvcConfig.java

package com.example.springbootmvc.config;

import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 創(chuàng)建Fastjson消息轉(zhuǎn)換器
        FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
        // 創(chuàng)建Fastjson配置對象
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(
                SerializerFeature.WriteNullBooleanAsFalse,
                SerializerFeature.WriteNullListAsEmpty,
                SerializerFeature.WriteNullNumberAsZero,
                SerializerFeature.WriteNullStringAsEmpty
        );
        converter.setFastJsonConfig(fastJsonConfig);
        converters.add(converter);
    }
}

Fastjson SerializerFeatures常用枚舉值

枚舉值 含義 備注
WriteNullListAsEmpty List字段如果為null,輸出為[],而非null
WriteNullStringAsEmpty 字符類型字段如果為null,輸出為"",而非null
WriteNullBooleanAsFalse Boolean字段如果為null,輸出為false,而非null
WriteNullNumberAsZero 數(shù)值字段如果為null,輸出為0,而非null
WriteMapNullValue 是否輸出值為null的字段,默認為false

自定義Jackson ObjectMapper


靜態(tài)資源配置

Spring Boot中默認的靜態(tài)資源配置是將類路徑下的/static/public垦缅、/resources冲泥、/META-INF/resources文件夾中的靜態(tài)資源直接映射為/**。這個默認行為是在WebMvcAutoConfiguration內(nèi)部類WebMvcAutoConfigurationAdapteraddResourceHandlers方法中定義的壁涎,相關(guān)的屬性配置類為ResourceProperties凡恍、WebMvcProperties
以下是部分源碼:

@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/" };

    /**
     * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
     * /resources/, /static/, /public/].
     */
    private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
}
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
    ...
    /**
     * Path pattern used for static resources.
     */
    private String staticPathPattern = "/**";
    ...
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
        return;
    }
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache()
            .getCachecontrol().toHttpCacheControl();
    // webjars支持
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry
                .addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/")
                .setCachePeriod(getSeconds(cachePeriod))
                .setCacheControl(cacheControl));
    }
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    // 默認靜態(tài)資源處理
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(
                registry.addResourceHandler(staticPathPattern)
                        .addResourceLocations(getResourceLocations(
                                this.resourceProperties.getStaticLocations()))
                        .setCachePeriod(getSeconds(cachePeriod))
                        .setCacheControl(cacheControl));
    }
}

自定義靜態(tài)資源配置

使用屬性值配置

src/main/java/resources/application.yml

spring:
  mvc:
    static-path-pattern: /resources/**
  resources:
    static-locations: ["classpath:/META-INF/resources/", "classpath:/resources/",
                       "classpath:/static/", "classpath:/public/"]

重寫addResourceHandlers進行配置

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/META-INF/resources/", "classpath:/resources/",
                        "classpath:/static/", "classpath:/public/")
                .addResourceLocations("file:/Users/fulgens/Downloads/");
    }
    
}

以上代碼相當于在默認配置基礎(chǔ)上添加了虛擬目錄

WebJars支持

WebJars簡介

WebJars是將web前端資源(js,css等)打成jar包文件粹庞,然后借助Maven咳焚、Gradle等依賴管理及項目構(gòu)建工具,以jar包形式對web前端資源進行統(tǒng)一依賴管理庞溜,保證這些Web資源版本唯一性。
官網(wǎng)地址 : https://www.webjars.org/

WebJars官網(wǎng)

WebJars使用

引入依賴

官網(wǎng)首頁碑定,找到資源文件對應的maven依賴流码,寫入項目pom.xml文件

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator</artifactId>
    <version>0.34</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.3.1</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>4.1.3</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>layui</artifactId>
    <version>2.3.0</version>
</dependency>

這里引入了jquery、bootstrap延刘、layui

頁面引用

<link rel="stylesheet" href="/webjars/layui/css/layui.css"/>
<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<script src="/webjars/jquery/jquery.js"></script>
<script src="/webjars/bootstrap/js/bootstrap.js"></script>
<script src="/webjars/layui/layui.all.js"></script>

注意:這里由于添加了webjars-locator依賴漫试,在引入前端資源時省略了版本號,推薦使用
如果未添加webjars-locator依賴碘赖,在引入前端資源時你需要添加版本號驾荣,像下面這樣:

<script src="/webjars/jquery/3.3.1/jquery.js"></script>

歡迎頁Welcome Page

Spring Boot支持靜態(tài)和模板化歡迎頁面。 它首先在配置的靜態(tài)資源目錄中查找index.html文件普泡。 如果找不到播掷,則查找index模板。 如果找到任何一個撼班,將自動用作應用程序的歡迎頁面歧匈。
Spring Boot關(guān)于歡迎頁的處理類為WelcomePageHandlerMapping


自定義Favicon

Spring Boot默認的提供的favicon是片小葉子,我們也可以自定義favicon砰嘁,另Spring Boot關(guān)于Favicon的配置類為FaviconConfiguration件炉,其默認會在配置的靜態(tài)資源路徑和類路徑根目錄下查找名為favicon.ico的Favicon圖片勘究,如果存在,則自動應用為應用的Favicon

關(guān)閉Favicon

設置spring.mvc.favicon.enabled屬性值為false即可關(guān)閉Favicon斟冕,默認值為true
src/main/resources/application.yml

spring:
  mvc:
    favicon:
      enabled: false    # 禁用favicon

設置自己的Favicon

根據(jù)以上原理口糕,我們只需要在靜態(tài)資源路徑或類路徑根目錄下放一張名為favicon.ico的Favicon圖片即可

注意:需要注意瀏覽器緩存,導致favicon.ico沒能及時更新磕蛇,需瀏覽器清緩存景描。


模板引擎

整合Freemarker

添加依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

相關(guān)配置

src/main/resources/application.yml

spring:
  freemarker:
    suffix: .ftl
    cache: false
    charset: UTF-8
    content-type: text/html
    template-loader-path: "classpath:/templates/"
    expose-request-attributes: true
    expose-session-attributes: true
    expose-spring-macro-helpers: true
    request-context-attribute: request

這里貼出Freemarker所有配置屬性

# FREEMARKER FreeMarkerProperties
spring.freemarker.allow-request-override=false # Whether HttpServletRequest attributes are allowed to override (hide) controller generated model attributes of the same name.
spring.freemarker.allow-session-override=false # Whether HttpSession attributes are allowed to override (hide) controller generated model attributes of the same name.
spring.freemarker.cache=false # Whether to enable template caching.
spring.freemarker.charset=UTF-8 # Template encoding.
spring.freemarker.check-template-location=true # Whether to check that the templates location exists.
spring.freemarker.content-type=text/html # Content-Type value.
spring.freemarker.enabled=true # Whether to enable MVC view resolution for this technology.
spring.freemarker.expose-request-attributes=false # Whether all request attributes should be added to the model prior to merging with the template.
spring.freemarker.expose-session-attributes=false # Whether all HttpSession attributes should be added to the model prior to merging with the template.
spring.freemarker.expose-spring-macro-helpers=true # Whether to expose a RequestContext for use by Spring's macro library, under the name "springMacroRequestContext".
spring.freemarker.prefer-file-system-access=true # Whether to prefer file system access for template loading. File system access enables hot detection of template changes.
spring.freemarker.prefix= # Prefix that gets prepended to view names when building a URL.
spring.freemarker.request-context-attribute= # Name of the RequestContext attribute for all views.
spring.freemarker.settings.*= # Well-known FreeMarker keys which are passed to FreeMarker's Configuration.
spring.freemarker.suffix=.ftl # Suffix that gets appended to view names when building a URL.
spring.freemarker.template-loader-path=classpath:/templates/ # Comma-separated list of template paths.
spring.freemarker.view-names= # White list of view names that can be resolved.</pre>

簡單實踐

src/main/resources/templates/index.ftl

<#import "app.ftl" as app>
<base id="basePath" href="${app.basePath}/">
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>后臺首頁</title>
    <link rel="stylesheet" href="/webjars/layui/css/layui.css"/>
    <link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css"/>
    <script src="/webjars/jquery/jquery.js"></script>
    <script src="/webjars/bootstrap/js/bootstrap.js"></script>
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">

    <#include "layout/header.ftl">

    <#include "layout/menu.ftl">

    <div class="layui-body">
        <div style="padding: 15px;">Hello World!</div>
    </div>

    <#include "layout/footer.ftl">

</div>
<script src="/webjars/layui/layui.all.js"></script>
<script>
    layui.use('element', function(){
        var element = layui.element;

    });
</script>
</body>
</html> 

src/main/resources/templates/app.ftl

<#assign basePath=request.contextPath >

src/main/resources/templates/layout/header.ftl

<#--header start-->
<div class="layui-header">
    <div class="layui-logo">Xxx后臺管理系統(tǒng)</div>
    <ul class="layui-nav layui-layout-left">
        <li class="layui-nav-item"><a href="">控制臺</a></li>
        <li class="layui-nav-item"><a href="">商品管理</a></li>
        <li class="layui-nav-item"><a href="">用戶</a></li>
        <li class="layui-nav-item">
            <a href="javascript:;">其它系統(tǒng)</a>
            <dl class="layui-nav-child">
                <dd><a href="">郵件管理</a></dd>
                <dd><a href="">消息管理</a></dd>
                <dd><a href="">授權(quán)管理</a></dd>
            </dl>
        </li>
    </ul>
    <ul class="layui-nav layui-layout-right">
        <li class="layui-nav-item">
            <a href="javascript:;">
                <img src="http://t.cn/RCzsdCq" class="layui-nav-img">
                管理員
            </a>
            <dl class="layui-nav-child">
                <dd><a href="">基本資料</a></dd>
                <dd><a href="">安全設置</a></dd>
            </dl>
        </li>
        <li class="layui-nav-item"><a href="">退了</a></li>
    </ul>
</div>
<#--header end-->

src/main/resources/templates/layout/footer.ftl

<#--footer start-->
<div class="layui-footer">
    Copyright ? 20xx - 2018  example.com 版權(quán)所有
</div>
<#--footer end-->

src/main/resources/templates/layout/menu.ftl

<#--menu start-->
<div class="layui-side layui-bg-black">
    <div class="layui-side-scroll">
        <ul class="layui-nav layui-nav-tree"  lay-filter="test">
            <li class="layui-nav-item layui-nav-itemed">
                <a class="" href="javascript:;">用戶管理</a>
                <dl class="layui-nav-child">
                    <dd><a href="javascript:;">列表一</a></dd>
                    <dd><a href="javascript:;">列表二</a></dd>
                    <dd><a href="javascript:;">列表三</a></dd>
                    <dd><a href="">超鏈接</a></dd>
                </dl>
            </li>
            <li class="layui-nav-item">
                <a href="javascript:;">商品管理</a>
                <dl class="layui-nav-child">
                    <dd><a href="javascript:;">列表一</a></dd>
                    <dd><a href="javascript:;">列表二</a></dd>
                    <dd><a href="">超鏈接</a></dd>
                </dl>
            </li>
            <li class="layui-nav-item"><a href="">云市場</a></li>
            <li class="layui-nav-item"><a href="">發(fā)布商品</a></li>
        </ul>
    </div>
</div>
<#--menu end-->

視圖控制器配置

對于一個傳統(tǒng)的非前后端分離項目來說,視圖控制器的配置是至關(guān)重要的孤里,可以通過重寫WebMvcConfigureraddViewControllers(ViewControllerRegistry registry)方法來配置

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("/index");
    registry.addViewController("/index").setViewName("/index");
    registry.addViewController("/register").setViewName("/register");
    registry.addViewController("/login").setViewName("/login");
}

上面的代碼與我們自己寫一個Controller完成視圖映射是一樣的

@Controller
public class RouterController {

    @GetMapping(value = {"/", "/index"})
    public String toIndex() {
        return "/index";
    }

    @GetMapping("/register")
    public String toRegister() {
        return "/register";
    }

    @GetMapping("/login")
    public String toLogin() {
        return "/login";
    }

}

攔截器配置

這里是一個簡單的登錄校驗攔截器
src/main/java/com/example/springbootmvc/web/interceptor/LoginInterceptor

package com.example.springbootmvc.web.interceptor;

import com.alibaba.fastjson.JSON;
import com.example.springbootmvc.common.constants.CommonContext;
import com.example.springbootmvc.common.utils.ServerResponse;
import com.example.springbootmvc.entity.User;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;

/**
 * 登錄校驗攔截器
 */
public class LoginInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LoginInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            String methodName = handlerMethod.getMethod().getName();
            String className = handlerMethod.getBean().getClass().getSimpleName();
            log.info("攔截controller: {}伏伯, 攔截方法: {}", className, methodName);
        }

        // 獲取請求url
        String toURL = request.getRequestURI();
        String queryString = request.getQueryString();
        if (StringUtils.isNotEmpty(queryString)) {
            toURL += "?" + queryString;
        }
        log.info("攔截請求URL: {}", toURL);

        // 獲取攔截請求的請求參數(shù)
        StringBuffer sb = new StringBuffer();
        Map<String, String[]> parameterMap = request.getParameterMap();
        Iterator<Map.Entry<String, String[]>> iterator = parameterMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String[]> entry = iterator.next();
            String mapKey = entry.getKey();
            String mapValue = StringUtils.EMPTY;
            mapValue = Arrays.toString(entry.getValue());
            sb.append(mapKey).append("=").append(mapValue);
        }
        log.info("攔截請求入?yún)? {}", sb.toString());

        User currenUser = (User) request.getSession().getAttribute(CommonContext.CURRENT_USER_CONTEXT);
        if (currenUser == null) {
            // 用戶未登錄跳轉(zhuǎn)登錄頁面
            // response.sendRedirect("/login");
            request.getRequestDispatcher("/login").forward(request, response);
            // 保存用戶請求url用于登錄成功后跳轉(zhuǎn)
            request.getSession().setAttribute(CommonContext.LOGIN_REDIRECT_URL, toURL);
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

對于前后端分離的項目可能在攔截校驗不通過時需要向前臺返回一些信息

private void returnErrorMsg(HttpServletResponse response, ServerResponse serverResponse) {
    try (OutputStream os = response.getOutputStream()) {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        os.write(JSON.toJSONString(serverResponse).getBytes());
        os.flush();
    } catch (IOException e) {
        log.error("登錄校驗攔截器輸出錯誤信息發(fā)生異常,異常信息: {}", e);
    }
}

這里ServerResponse是封裝的一個通用服務端響應對象
通過重寫WebMvcConfigureraddInterceptors(InterceptorRegistry registry)方法配置攔截器

@Override
public void addInterceptors(InterceptorRegistry registry) {
    String[] excludePath = {"/login", "/doLogin", "/register", "/doRegister",
            "/error", "/**/*.js", "/**/*.css", "/**/*.jpg", "/**/*.jpeg",
            "/**/*.png", "/**/*.ico"};
    registry.addInterceptor(new LoginInterceptor())
            .addPathPatterns("/**")
            .excludePathPatterns(excludePath);
}

全局異常處理

Spring Boot默認的錯誤處理機制

默認情況下捌袜,Spring Boot提供/error錯誤映射说搅,以合理的方式處理所有錯誤,并在servlet容器中注冊為“全局”錯誤頁面虏等。對于機器客戶端(machine clients)弄唧,它會生成一個JSON響應,其中包含錯誤信霍衫、HTTP狀態(tài)和異常消息的詳細信息候引。對于瀏覽器客戶端(browser clients),有一個“whitelabel”錯誤視圖敦跌,以HTML格式呈現(xiàn)相同的數(shù)據(jù)(要自定義它澄干,須添加一個解析錯誤的視圖)。要完全替換默認行為柠傍,可以實現(xiàn)ErrorController接口并注冊該類型的bean麸俘,或者添加ErrorAttributes類型的bean以使用現(xiàn)有機制但替換內(nèi)容。
比如惧笛,我們自定義一個產(chǎn)生異常的映射:

@GetMapping("/test")
public void testException() {
    int i = 1/0;
}

瀏覽器訪問http://localhost:8080/test我們會看到

Spring Boot默認whitelabel錯誤頁面

再使用Restlet从媚、Postman等接口測試工具訪問


Spring Boot生成的JSON錯誤信息

其實,Spring Boot默認錯誤處理機制在BasicErrorController中定義患整,部分源碼如下:

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

    private final ErrorProperties errorProperties;
    // ...
        
    @RequestMapping(produces = "text/html")
    public ModelAndView errorHtml(HttpServletRequest request,
            HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
                request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
        response.setStatus(status.value());
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null ? modelAndView : new ModelAndView("error", model));
    }

    @RequestMapping
    @ResponseBody
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
                isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(body, status);
    }
    // ...
}
BasicErrorController類圖

自定義錯誤頁面

如果要顯示給定狀態(tài)碼的自定義HTML錯誤頁面拜效,可以將文件添加到/error目錄。 錯誤頁面可以是靜態(tài)HTML(即各谚,添加到任何靜態(tài)資源文件夾下)紧憾,也可以使用模板構(gòu)建。 文件名應該是確切的狀態(tài)代碼或系列掩碼嘲碧。
例如稻励,要映射404錯誤到靜態(tài)HTML文件,目錄結(jié)構(gòu)將如下所示:

src/
?+- main/
??+- java/
???| + <source code>
??+- resources/
???+- public/
????+- error/
?????| +- 404.html
????+- <other public assets>

同樣映射500錯誤我們可以在/error目錄下放一個500.html

使用FreeMarker模板映射所有5xx錯誤,目錄結(jié)構(gòu)如下:

src/
?+- main/
??+- java/
???| + <source code>
??+- resources/
???+- templates/
????+- error/
?????| +- 5xx.ftl
????+- <other templates>

這里給出一個簡單的5xx.ftl模板

<!DOCTYPE html>
<head>
    <meta charset="UTF-8"/>
    <title>5xx</title>
</head>
<body>
<div class="row border-bottom">
    <h1>Oh, There is something wrong</h1>
    <h3>timestamp: ${timestamp?datetime}</h3>
    <h3>status: ${status}</h3>
    <h3>error: ${error}</h3>
    <h3>message: ${message}</h3>
    <h3>path: ${path}</h3>
</div>
</body>
</html>

配置了以上5xx模板望抽,我們再次訪問http://localhost:8080/test

5xx模板頁面錯誤信息展示

你可能會問timestamp加矛、status...這些模型數(shù)據(jù)從哪來的呢?我們什么也沒做不是嗎煤篙?其實還是BasicErrorController在起作用斟览,Spring默認提供了ErrorAttributes接口的實現(xiàn)類DefaultErrorAttributes,感興趣可以去看一下其中的getErrorAttributes方法

對于更復雜的映射辑奈,還可以實現(xiàn)ErrorViewResolver接口苛茂,如以下示例所示:

public class MyErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request,
            HttpStatus status, Map<String, Object> model) {
        // Use the request or status to optionally return a ModelAndView
        return ...
    }

}

全局異常處理

Spring Boot默認的錯誤處理機制一般不會符合項目的要求,這個時候就需要我們自定義全局異常處理了

這里說一下鸠窗,沒有全局異常處理的系統(tǒng)妓羊,往往會使用像下面這樣的笨辦法,采用try-catch的方式稍计,手動捕獲來自service層的異常信息躁绸,然后返回對應的結(jié)果集,相信很多人都看到過類似的代碼(如:封裝成Result對象)臣嚣;該方法雖然間接性的解決錯誤暴露的問題净刮,同樣的弊端也很明顯,增加了代碼量硅则,當異常過多的情況下對應的catch層愈發(fā)的多了起來淹父,很難管理這些業(yè)務異常和錯誤碼之間的匹配,所以最好的方法就是通過簡單配置全局掌控….

@GetMapping("/test2")
public Map<String, String> test2() {
    Map<String, Object> resultMap = new HashMap<>();
    // TODO 采用catch手動捕獲怎虫,間接性的解決錯誤暴露的問題
    try {
        int i = 1 / 0;
        resultMap.put("code", "200");
        resultMap.put("data", "具體返回的結(jié)果集");
    } catch (Exception e) {
        resultMap.put("code", "500");
        resultMap.put("msg", "接口調(diào)用異常");
    }
    return resultMap;
}

自定義異常

src/main/java/com/example/springbootmvc/exception/CustomException.class

package com.example.springbootmvc.exception;

public class CustomException extends RuntimeException {

    private Integer code;

    public CustomException() {
        super();
    }

    public CustomException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }

}

通用服務端響應對象

src/main/java/com/example/springbootmvc/common/utils/ServerResponse.class

package com.example.springbootmvc.common.utils;

import com.alibaba.fastjson.annotation.JSONField;
import com.example.springbootmvc.common.enums.ResponseCode;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;

import java.io.Serializable;

// @JsonInclude(JsonInclude.Include.NON_NULL)
public class ServerResponse<T> implements Serializable {

    private static final long serialVersionUID = -4577255781088498763L;

    // 響應狀態(tài)
    private Integer status;

    // 響應消息
    private String msg;

    // 響應數(shù)據(jù)
    private T data;

    private ServerResponse() {

    }

    private ServerResponse(Integer status) {
        this.status = status;
    }

    private ServerResponse(Integer status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    private ServerResponse(Integer status, T data) {
        this.status = status;
        this.data = data;
    }

    private ServerResponse(Integer status, String msg, T data) {
        this.status = status;
        this.msg = msg;
        this.data = data;
    }

    // @JsonIgnore  // jackson
    @JSONField(serialize = false)
    public boolean isSuccess() {
        return this.status == ResponseCode.SUCCESS.getCode();
    }

    public Integer getStatus() {
        return status;
    }

    public String getMsg() {
        return msg;
    }

    public T getData() {
        return data;
    }

    public static <T> ServerResponse<T> success() {
        return new ServerResponse<>(ResponseCode.SUCCESS.getCode());
    }

    public static <T> ServerResponse<T> successWithMsg(String msg) {
        return new ServerResponse<>(ResponseCode.SUCCESS.getCode(), msg);
    }

    public static <T> ServerResponse<T> successWithData(T data) {
        return new ServerResponse<>(ResponseCode.SUCCESS.getCode(), data);
    }

    public static <T> ServerResponse<T> successWithMsgAndData(String msg, T data) {
        return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(), msg, data);
    }

    public static <T> ServerResponse<T> error() {
        return new ServerResponse<>(ResponseCode.ERROR.getCode());
    }

    public static <T> ServerResponse<T> errorWithMsg(String errorMsg) {
        return new ServerResponse<>(ResponseCode.ERROR.getCode(), errorMsg);
    }

    public static <T> ServerResponse<T> errorWithMsg(int errorCode, String errorMsg) {
        return new ServerResponse<>(errorCode, errorMsg);
    }

    public static <T> ServerResponse build(Integer status, String msg, T data) {
        return new ServerResponse(status, msg, data);
    }

}

全局異常處理

使用@ControllerAdvice及@ExceptionHandler

  • @ControllerAdvice 捕獲 Controller 層拋出的異常暑认,如果添加 @ResponseBody 返回信息則為JSON 格式。
  • @RestControllerAdvice 相當于 @ControllerAdvice 與 @ResponseBody 的結(jié)合體大审。
  • @ExceptionHandler 統(tǒng)一處理一種類的異常穷吮,減少代碼冗余度。
  • @ResponseStatus 返回Http響應狀態(tài)碼

對于非前后端分離的傳統(tǒng)項目(使用模板構(gòu)建)往往需要同時支持自定義錯誤頁面展示及Ajax請求返回錯誤信息
src/main/java/com/example/springbootmvc/aop/GlobalExceptionHandler.class

package com.example.springbootmvc.aop;

import com.alibaba.fastjson.JSON;
import com.example.springbootmvc.common.utils.ServerResponse;
import com.example.springbootmvc.exception.CustomException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(CustomException.class)
    public Object handleCustomException(HttpServletRequest request, HttpServletResponse response, Exception e) {
        CustomException exception = (CustomException) e;
        if (isAjax(request)) {
            ServerResponse serverResponse = ServerResponse.build(exception.getCode(), exception.getMessage(), null);
            return serverResponse;
        } else {
            ModelAndView modelAndView = new ModelAndView();
            modelAndView.setStatus(getStatus(request));
            modelAndView.addObject("path", request.getRequestURI());
            modelAndView.addObject("exception", exception.getMessage());
            modelAndView.setViewName("error/error");
            return modelAndView;
        }
    }

    private boolean isAjax(HttpServletRequest request) {
        return request.getHeader("X-Requested-With") != null
                && "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
    }

    private void writeErrorMsg(HttpServletResponse response, ServerResponse serverResponse) {
        try (OutputStream os = response.getOutputStream()) {
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=UTF-8");
            os.write(JSON.toJSONString(serverResponse).getBytes());
            os.flush();
        } catch (IOException e) {
            log.error("輸出錯誤信息發(fā)生異常饥努,異常信息: {}", e);
        }
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }

}

src/main/resources/templates/error/error.ftl

<!DOCTYPE html>
<head>
    <meta charset="UTF-8"/>
    <title>5xx</title>
</head>
<body>
<div class="row border-bottom">
    <h1>Oh, There is something wrong</h1>
    <h3>exception: ${exception}</h3>
    <h3>path: ${path}</h3>
</div>
</body>
</html>

對于前后端分離的項目只需要返回錯誤信息即可

package com.example.springbootmvc.aop;

import com.example.springbootmvc.common.utils.ServerResponse;
import com.example.springbootmvc.exception.CustomException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestControllerAdvice
public class GlobalExceptionHandler2 extends ResponseEntityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler2.class);

    @ExceptionHandler(CustomException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Object handleCustomException(HttpServletRequest request, HttpServletResponse response, Exception e) {
        CustomException exception = (CustomException) e;
        return ServerResponse.build(exception.getCode(), exception.getMessage(), null);
    }

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Object handleRuntimeException(HttpServletRequest request, HttpServletResponse response, Exception e) {
        RuntimeException exception = (RuntimeException) e;
        return ServerResponse.build(400, exception.getMessage(), null);
    }

    /**
     * 通用的接口映射異常處理方法
     */
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception e, Object body, HttpHeaders headers,
                                                             HttpStatus status, WebRequest request) {
        if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            return new ResponseEntity(ServerResponse.build(Integer.valueOf(status.value()), exception.getBindingResult().getAllErrors().get(0).getDefaultMessage(), null), status);
        }
        if (e instanceof MethodArgumentTypeMismatchException) {
            MethodArgumentTypeMismatchException exception = (MethodArgumentTypeMismatchException) e;
            log.error("參數(shù)轉(zhuǎn)換失敗,方法:{}八回,參數(shù):{}酷愧,信息:", exception.getParameter().getMethod().getName(),
                    exception.getParameter(), exception.getLocalizedMessage());
            return new ResponseEntity(ServerResponse.build(Integer.valueOf(status.value()), "參數(shù)轉(zhuǎn)換失敗", null), status);
        }
        return new ResponseEntity(ServerResponse.build(Integer.valueOf(status.value()), "參數(shù)轉(zhuǎn)換失敗", null), status);
    }

}

自定義異常使用

在Controller層直接向上拋出即可

@RequestMapping("/test")
public void testException(String param) {
    if (param == null) {
        throw new CustomException(400, "參數(shù)不能為空");
    }
    int i = 1/0;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缠诅,隨后出現(xiàn)的幾起案子溶浴,更是在濱河造成了極大的恐慌,老刑警劉巖管引,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件士败,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機谅将,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門漾狼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人饥臂,你說我怎么就攤上這事逊躁。” “怎么了隅熙?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵稽煤,是天一觀的道長。 經(jīng)常有香客問我囚戚,道長酵熙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任驰坊,我火速辦了婚禮匾二,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庐橙。我一直安慰自己假勿,他們只是感情好,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布态鳖。 她就那樣靜靜地躺著转培,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浆竭。 梳的紋絲不亂的頭發(fā)上浸须,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音邦泄,去河邊找鬼贡未。 笑死,一個胖子當著我的面吹牛魔招,可吹牛的內(nèi)容都是我干的砂碉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼特碳,長吁一口氣:“原來是場噩夢啊……” “哼诚亚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起午乓,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤站宗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后益愈,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梢灭,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了敏释。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片库快。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖颂暇,靈堂內(nèi)的尸體忽然破棺而出缺谴,到底是詐尸還是另有隱情,我是刑警寧澤耳鸯,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布湿蛔,位于F島的核電站,受9級特大地震影響县爬,放射性物質(zhì)發(fā)生泄漏阳啥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一财喳、第九天 我趴在偏房一處隱蔽的房頂上張望察迟。 院中可真熱鬧,春花似錦耳高、人聲如沸扎瓶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽概荷。三九已至,卻和暖如春碌燕,著一層夾襖步出監(jiān)牢的瞬間误证,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工修壕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留愈捅,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓慈鸠,卻偏偏與公主長得像蓝谨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子青团,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容