手把手0基礎項目實戰(三)——教你開發一套電商平臺的安全框架

寫在最前

本文是《手把手項目實戰系列》的第三篇文章,預告一下,整個系列會介紹以下內容:java

  • 《手把手0基礎項目實戰(一)——教你搭建一套可自動化構建的微服務框架(SpringBoot+Dubbo+Docker+Jenkins)》
  • 《手把手0基礎項目實戰(二)——微服務架構下的數據庫分庫分表實戰》
  • 《手把手0基礎項目實戰(三)——教你開發一套安全框架》
  • 《手把手0基礎項目實戰(四)——電商訂單系統架構設計與實戰(分佈式事務一致性保證)》
  • 《手把手0基礎項目實戰(五)——電商系統的緩存策略》
  • 《手把手0基礎項目實戰(六)——基於配置中心實現集羣配置的集中管理和熔斷機制》
  • 《手把手0基礎項目實戰(七)——電商系統的日誌監控方案》
  • 《手把手0基礎項目實戰(八)——基於JMeter的系統性能測試》

幾乎全部的Web系統都須要登陸、權限管理、角色管理等功能,並且這些功能每每具備較大的普適性,與系統具體的業務關聯性較小。所以,這些功能徹底能夠被封裝成一個可配置、可插拔的框架,當開發一個新系統的時候直接將其引入、並做簡單配置便可,無需再從頭開發,極大節約了人力成本、時間成本。git

在Java Web領域,有兩大主流的安全框架,Spring Security和Apache Shiro。他們都能實現用戶鑑權、權限管理、角色管理、防止Web攻擊等功能,並且這兩套開源框架都已通過大量項目的驗證,趨於穩定成熟,能夠很好地爲咱們的項目服務。github

本文將帶領你們從頭開始實現一套安全框架,該框架與Spring Boot深度融合,從而可以幫助你們加深對Spring Boot的理解。這套框架中將涉及到以下內容:redis

  • Spring Boot AOP
  • Spring Boot 全局異常處理
  • Spring Boot CommandLineRunner
  • Java 反射機制
  • 分佈式系統中Session的集中式管理

本文將從安全框架的設計與實現兩個角度帶領你們完成安全框架的開發,廢話很少說,如今開始吧~spring

項目完整源碼下載

https://github.com/bz51/Sprin...數據庫


1. 安全框架的設計

1.1 開發目標

在全部事情開始以前,咱們首先要搞清楚,咱們究竟要實現哪些功能?緩存

  1. 用戶登陸
    全部系統都須要登陸功能,這毫無疑問,也沒必要多說。
  2. 角色管理
    每一個用戶都有且僅有一種角色,好比:系統管理員、普通用戶、企業用戶等等。管理員能夠添加、刪除、查詢、修改角色信息。
  3. 權限管理
    每種角色能夠擁有不一樣的權限,管理員能夠建立、修改、查詢、刪除權限,也能夠爲某一種角色添加、刪除權限。
  4. 權限檢測
    用戶調用每個接口,都須要校驗該用戶是否具有調用該接口的權限。

當咱們明確了開發目標以後,下面就須要基於這些目標,設計咱們的系統。咱們首先要作的就是要搞清楚「用戶」、「角色」、「權限」的定義以及他們之間的關係。這在領域驅動設計中被稱爲「領域模型」。安全

1.2 領域模型

title

  • 權限:session

    • 權限表示某一用戶是否具備操做某一資源的能力。
    • 權限通常用「資源名稱:操做名稱」來表示。好比:建立用戶的權限能夠用「user:create」來表示,刪除用戶的權限能夠用「user:delete」來表示。
    • 在Web系統中,權限和接口呈一一對應關係,好比:「user:create」對應着建立用戶的接口,「user:delete」對應着刪除用戶的接口。所以,權限也能夠理解成一個用戶是否具有操做某一個接口的能力。
  • 角色:數據結構

    • 角色是一組權限的集合。角色規定了某一類用戶共同具有的權限集合。
    • 好比:超級管理員這種角色擁有「user:create」、「user:delete」等權限,而普通用戶只有「user:create」權限。
    • 從領域模型中可知,角色和權限之間呈多對多的聚合關係,即一種角色能夠包含多個權限,一個權限也能夠屬於多種角色,而且權限能夠脫離於角色而單獨存在,所以他們之間是一種弱依賴關係——聚合關係。
  • 用戶:

    • 用戶和角色之間呈多對一的聚合關係,即一個用戶只能屬於一種角色,而一種角色卻能夠包含多個用戶。而且角色能夠脫離於用戶單獨存在,所以他們之間是一種弱依賴關係——聚合關係。

1.3 數據結構設計

當咱們捋清楚了「權限」、「用戶」、「角色」的定義和他們之間的關係後,下面咱們就能夠基於這個領域模型設計出具體的數據存儲結構。

爲了可以方便地給每個接口標註權限,咱們須要自定義三個註解@Login@Role@Permission

  • @Login:用於標識當前接口是否須要登陸。當接口使用了這個註解後,用戶只有在登陸後才能訪問。
  • @Role("角色名"):用於標識容許調用當前接口的角色。當接口使用了這個註解後,只有指定角色的用戶才能調用本接口。
  • @Permission("權限名"):用於標識容許調用當前接口的權限。當接口使用了這個註解後,只有具有指定權限的用戶才能調用本接口。

1.4 接口權限信息初始化流程

要使得這個安全框架運行起來,首先就須要在系統初始化完成前,初始化全部接口的權限、角色等信息,這個過程即爲「接口權限信息初始化流程」;而後在系統運行期間,若是有用戶請求接口,就能夠根據這些權限信息判斷該用戶是否有權限訪問接口。

這一小節主要介紹接口權限信息初始化流程,不涉及任何實現細節,實現的細節將在本文的實現部分介紹。

  1. 當Spring完成上下文的初始化後,須要掃描本項目中全部Controller類;
  2. 再依次掃描Controller類中的全部方法,獲取方法上的@GetMapping@PostMapping@PutMapping@DeleteMapping,經過這些註解獲取接口的URL、請求方式等信息;
  3. 同時,獲取方法上的@Login@Role@Permission,經過這些註解,獲取該接口是否須要登陸、容許訪問的角色以及容許訪問的權限信息;
  4. 將每一個接口的權限信息、URL、請求方式存儲在Redis中,供用戶調用接口是鑑權使用。

1.5 用戶鑑權流程

  1. 全部的用戶請求在被執行前都會被系統攔截,從請求中獲取請求的URL和請求方式;
  2. 而後從Redis中查詢該接口對應的權限信息;
  3. 若該接口須要登陸,而且當前用戶還沒有登陸,則直接拒絕;
  4. 若該接口須要登陸,而且擁有已經登陸,那麼須要從請求頭中解析出SessionID,併到Redis中查詢該用戶的權限信息,而後拿着用戶的權限信息、角色信息和該接口的權限信息、角色信息進行比對。若經過鑑權,則執行該接口;若未經過鑑權,則直接拒絕請求。

2. 安全框架的實現

2.1 註解的實現

本套安全框架一共定義了四個註解:@AuthScan@Login@Role@Permission

2.1.1 @AuthScan

該註解用來告訴安全框架,本項目中全部Controller類所在的包,從而可以幫助安全框架快速找到Controller類,避免了全部類的掃描。

它有且僅有一個參數,用來指定Controller所在的包:@AuthScan("com.gaoxi.controller")。它的代碼實現以下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthScan {
    public String value();
}

註解顧名思義,它是用來在代碼中進行標註,它自己不承載任何邏輯,經過註解

  • @Retention
    它解釋說明了這個註解的的存活時間。它的取值以下:

    • RetentionPolicy.SOURCE 註解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
    • RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被加載到 JVM 中。
    • RetentionPolicy.RUNTIME 註解能夠保留到程序運行的時候,它會被加載進入到 JVM 中,因此在程序運行時能夠獲取到它們。
  • @Documented
    顧名思義,這個元註解確定是和文檔有關。它的做用是可以將註解中的元素包含到 Javadoc 中去。
  • @Target
    當一個註解被 @Target 註解時,這個註解就被限定了運用的場景。

    • ElementType.ANNOTATION_TYPE:能夠給一個註解進行註解
    • ElementType.CONSTRUCTOR:能夠給構造方法進行註解
    • ElementType.FIELD:能夠給屬性進行註解
    • ElementType.LOCAL_VARIABLE:能夠給局部變量進行註解
    • ElementType.METHOD:能夠給方法進行註解
    • ElementType.PACKAGE:能夠給一個包進行註解
    • ElementType.PARAMETER:能夠給一個方法內的參數進行註解
    • ElementType.TYPE:能夠給一個類型進行註解,好比類、接口、枚舉

2.1.2 @Login

這個註解用於標識指定接口是否須要登陸後才能訪問,它有一個默認的boolean類型的值,用於表示是否須要登陸,其代碼以下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Login {

    // 是否須要登陸(默認爲true)
    public boolean value() default true;

}

2.1.3 @Role

該註解用於指定容許訪問當前接口的角色,其代碼以下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Role {
    public String value();
}

2.1.4 @Permission

該註解用於指定容許訪問當前接口的權限,其代碼以下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Permission {
    public String value();
}

2.2 權限信息初始化過程

上文中提到,註解自己不含任何業務邏輯,它只是在代碼中起一個標識的做用,那麼怎麼才能讓註解「活」起來?這就須要經過反射機制來獲取註解。

2.2.1 在接口上聲明權限信息

當完成這些註解的定義後,接下來就須要使用他們,以下面代碼所示:

public interface ProductController {

    /**
     * 建立產品
     * @param prodInsertReq 產品詳情
     * @return 是否建立成功
     */
    @PostMapping("product")
    @Login
    @Permission("product:create")
    public Result createProduct(ProdInsertReq prodInsertReq);
    
}

ProductController是一個Controller類,它提供了處理產品的各類接口。簡單起見,這裏只列出了一個建立產品的接口。
@PostMapping是SpringMVC提供的註解,用於標識該接口的訪問路徑和訪問方式。
@Login聲明瞭該接口須要登陸後才能訪問。
@Permission聲明瞭用戶只有擁有product:create權限才能訪問該接口。

2.2.2 初始化權限信息

當系統初始化的時候,須要加載接口上的這些權限信息,存儲在Redis中。在系統運行期間,當有用戶請求接口的時候,系統會根據接口的權限信息判斷用戶是否有訪問接口的權限。權限信息初始化過程的代碼以下:

/**
 * @author 大閒人柴毛毛
 * @date 2017/11/1 上午10:04
 *
 * @description 初始化權限信息
 */
@AuthScan("com.gaoxi.controller")
@Component
public class InitAuth implements CommandLineRunner {
    @Override
    public void run(String... strings) throws Exception {
        // 加載接口訪問權限
        loadAccessAuth();
    }
    
    ……
}
  • 上述代碼定義了一個InitAuth類,該類實現了CommandLineRunner接口,該接口中含有run()方法,當Spring的上下文初始化完成後,就會調用run(),從而完成權限信息的初始化過程。
  • 該類使用了@AuthScan("com.gaoxi.controller")註解,用於標識當前項目Controller類所在的包名,從而避免掃描全部類,必定程度上加速系統初始化的速度。
  • @Component註解會在Spring容器初始化完成後,建立本類的對象,並加入IoC容器中。

下面來看一下loadAccessAuth()方法的具體實現:

/**
     * 加載接口訪問權限
     */
    private void loadAccessAuth() throws IOException {
        // 獲取待掃描的包名
        AuthScan authScan = AnnotationUtil.getAnnotationValueByClass(this.getClass(), AuthScan.class);
        String pkgName = authScan.value();

        // 獲取包下全部類
        List<Class<?>> classes = ClassUtil.getClasses(pkgName);
        if (CollectionUtils.isEmpty(classes)) {
            return;
        }

        // 遍歷類
        for (Class clazz : classes) {
            Method[] methods = clazz.getMethods();
            if (methods==null || methods.length==0) {
                continue;
            }

            // 遍歷函數
            for (Method method : methods) {
                AccessAuthEntity accessAuthEntity = buildAccessAuthEntity(method);
                if (accessAuthEntity!=null) {
                    // 生成key
                    String key = generateKey(accessAuthEntity);
                    // 存至本地Map
                    accessAuthMap.put(key, accessAuthEntity);
                    logger.debug("",accessAuthEntity);
                }
            }
        }
        // 存至Redis
        redisService.setMap(RedisPrefixUtil.Access_Auth_Prefix, accessAuthMap, null);
        logger.info("接口訪問權限已加載完畢!"+accessAuthMap);
    }
  • 首先會讀取本類上的@AuthScan註解,並獲取註解中聲明瞭Controller類所在的包pkgName
  • pkgName是一個字符串,所以須要使用Java反射機制將字符串解析成Class對象。其解析過程經過工具包ClassUtil.getClasses(pkgName)完成,具體解析過程這裏就不作詳細介紹了,感興趣的同窗能夠參閱本項目源碼。
  • ClassUtil.getClasses(pkgName)解析以後,該包下的全部Controller類將會被解析成List<Class<?>>對象,而後遍歷全部的Class對象;
  • 而後依次獲取每一個Class對象中的Method對象,並依次遍歷Method對象,經過buildAccessAuthEntity(method)方法將一個個Method對象解析成AccessAuthEntity對象(具體解析過程在稍後介紹);
  • 最後將AccessAuthEntity對象存儲在Redis中,供用戶訪問接口時使用。

這就是整個權限信息初始化的過程,下面詳細介紹buildAccessAuthEntity(method)方法的解析過程,它到底是如何將一個Mehtod對象解析成AccessAuthEntity對象?而且AccessAuthEntity對象的結構到底是怎樣的?

首先來看一下AccessAuthEntity的數據結構:

/**
 * @author 大閒人柴毛毛
 * @date 2017/11/1 上午11:05
 * @description 接口訪問權限的實體類
 */
public class AccessAuthEntity implements Serializable {

    /** 請求 URL */
    private String url;

    /** 接口方法名 */
    private String methodName;

    /** HTTP 請求方式 */
    private HttpMethodEnum httpMethodEnum;

    /** 當前接口是否須要登陸 */
    private boolean isLogin;

    /** 當前接口的訪問權限 */
    private String permission;
    
    // setter/getter省略
}

AccessAuthEntity用於存儲一個接口的訪問路徑、訪問方式和權限信息。在系統初始化的時候,Controller類中的每一個Mehtod對象都會被buildAccessAuthEntity()方法解析成AccessAuthEntity對象。buildAccessAuthEntity()方法的代碼以下所示:

/**
 * 構造AccessAuthEntity對象
 * @param method
 * @return
 */
private AccessAuthEntity buildAccessAuthEntity(Method method) {
    GetMapping getMapping = AnnotationUtil.getAnnotationValueByMethod(method, GetMapping.class);
    PostMapping postMapping = AnnotationUtil.getAnnotationValueByMethod(method, PostMapping.class);
    PutMapping putMapping= AnnotationUtil.getAnnotationValueByMethod(method, PutMapping.class);
    DeleteMapping deleteMapping = AnnotationUtil.getAnnotationValueByMethod(method, DeleteMapping.class);

    AccessAuthEntity accessAuthEntity = null;
    if (getMapping!=null
            && getMapping.value()!=null
            && getMapping.value().length==1
            && StringUtils.isNotEmpty(getMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.GET);
        accessAuthEntity.setUrl(trimUrl(getMapping.value()[0]));
    }
    else if (postMapping!=null
            && postMapping.value()!=null
            && postMapping.value().length==1
            && StringUtils.isNotEmpty(postMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.POST);
        accessAuthEntity.setUrl(trimUrl(postMapping.value()[0]));
    }
    else if (putMapping!=null
            && putMapping.value()!=null
            && putMapping.value().length==1
            && StringUtils.isNotEmpty(putMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.PUT);
        accessAuthEntity.setUrl(trimUrl(putMapping.value()[0]));
    }
    else if (deleteMapping!=null
            && deleteMapping.value()!=null
            && deleteMapping.value().length==1
            && StringUtils.isNotEmpty(deleteMapping.value()[0])) {
        accessAuthEntity = new AccessAuthEntity();
        accessAuthEntity.setHttpMethodEnum(HttpMethodEnum.DELETE);
        accessAuthEntity.setUrl(trimUrl(deleteMapping.value()[0]));
    }

    // 解析@Login 和 @Permission
    if (accessAuthEntity!=null) {
        accessAuthEntity = getLoginAndPermission(method, accessAuthEntity);
        accessAuthEntity.setMethodName(method.getName());
    }

    return accessAuthEntity;
}

該方法首先會獲取當前Method上的XXXMapping四個註解,經過解析這些註解可以獲取到當前接口的訪問路徑和請求方式,並將這二者存儲在AccessAuthEntity對象中。

而後經過getLoginAndPermission方法,解析當前Method對象中的@Login 和@Permission信息,其代碼以下所示:

/**
 * 獲取指定方法上的@Login的值和@Permission的值
 * @param method 目標方法
 * @param accessAuthEntity
 * @return
 */
private AccessAuthEntity getLoginAndPermission(Method method, AccessAuthEntity accessAuthEntity) {
    // 獲取@Permission的值
    Permission permission = AnnotationUtil.getAnnotationValueByMethod(method, Permission.class);
    if (permission!=null && StringUtils.isNotEmpty(permission.value())) {
        accessAuthEntity.setPermission(permission.value());
        accessAuthEntity.setLogin(true);
        return accessAuthEntity;
    }

    // 獲取@Login的值
    Login login = AnnotationUtil.getAnnotationValueByMethod(method, Login.class);
    if (login!=null) {
        accessAuthEntity.setLogin(true);
    }

    accessAuthEntity.setLogin(false);
    return accessAuthEntity;
}

該註解的解析過程由註解工具包AnnotationUtil.getAnnotationValueByMethod完成,具體的解析過程這裏就再也不贅述,感興趣的同窗請參閱項目源碼。

到此爲止,接口的訪問路徑、請求方式、是否須要登陸、權限信息都已經解析成一個個AccessAuthEntity對象,並以「請求方式+訪問路徑」做爲key,存儲在Redis中。接口權限信息的初始化過程也就完成了!

2.2.3 用戶鑑權

當用戶請求全部接口前,系統都應該攔截這些請求,只有在權限校驗經過的狀況下才運行調用接口,不然直接拒絕請求。

基於上述需求,咱們須要給Controller中全部方法執行前增長切面,並將用於權限校驗的代碼織入到該切面中,從而在方法執行前完成權限校驗。下面就詳細介紹在SpringBoot中AOP的使用。

  • 首先,咱們須要在項目的pom中引入AOP的依賴:
<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 建立切面類:

    • 在類上必須添加@Aspect註解,用於標識當前類是一個AOP切面類
    • 該類也必須添加@Component註解,讓Spring初始化完成後建立本類的對象,並加入IoC容器中
    • 而後須要使用@Pointcut註解定義切點;切點描述了哪些類中的哪些方法須要織入權限校驗代碼。咱們這裏將全部Controller類中的全部方法做爲切點。
    • 當完成切點的定義後,咱們須要使用@Before註解聲明切面織入的時機;因爲咱們須要在方法執行前攔截全部的請求,所以使用@Before註解。
    • 當完成上述設置以後,全部Controller類中的函數在被調用前,都會執行權限校驗代碼。權限校驗的詳細過程在authentication()方法中完成。
/**
 * @author 大閒人柴毛毛
 * @date 2017/11/2 下午7:06
 *
 * @description 訪問權限處理類(全部請求都要通過此類)
 */
@Aspect
@Component
public class AccessAuthHandle {
    /** 定義切點 */
    @Pointcut("execution(public * com.gaoxi.controller..*.*(..))")
    public void accessAuth(){}


    /**
     * 攔截全部請求
     */
    @Before("accessAuth()")
    public void doBefore() {

        // 訪問鑑權
        authentication();

    }
}
  • 權限校驗過程

    • 該方法首先會獲取當前請求的訪問路徑和請求方法;
    • 而後獲取HTTP請求頭中的SessionID,並從Redis中獲取該SessionID對應的用戶信息;
    • 而後根據接口訪問路徑和訪問方法,從Redis中獲取該接口的權限信息;到此爲止,權限校驗前的準備工做都已完成,下面就要進入權限校驗過程了;
/**
 * 檢查當前用戶是否容許訪問該接口
 */
private void authentication() {
    // 獲取 HttpServletRequest
    HttpServletRequest request = getHttpServletRequest();

    // 獲取 method 和 url
    String method = request.getMethod();
    String url = request.getServletPath();

    // 獲取 SessionID
    String sessionID = getSessionID(request);

    // 獲取SessionID對應的用戶信息
    UserEntity userEntity = getUserEntity(sessionID);

    // 獲取接口權限信息
    AccessAuthEntity accessAuthEntity = getAccessAuthEntity(method, url);

    // 檢查權限
    authentication(userEntity, accessAuthEntity);
}
  • authentication():

    • 首先判斷當前接口是否須要登陸後才容許訪問,若是無需登陸,那麼直接容許訪問;
    • 若當前接口須要登陸後才能訪問,那麼判斷當前用戶是否已經登陸;若還沒有登陸,則直接拒絕請求(經過拋出throw new CommonBizException(ExpCodeEnum.NO_PERMISSION)異常來拒絕請求,這由SpringBoot統一異常處理機制來完成,稍後會詳細介紹);若已經登陸,則開始檢查權限信息;
    • 權限檢查由checkPermission()方法完成,它會將用戶所具有的權限和接口要求的權限進行比對;若是用戶所具有的權限包含接口要求的權限,那麼權限校驗經過;反之,則經過拋異常的方式拒絕請求。
/**
 * 檢查權限
 * @param userEntity 當前用戶的信息
 * @param accessAuthEntity 當前接口的訪問權限
 */
private void authentication(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 無需登陸
    if (!accessAuthEntity.isLogin()) {
        return;
    }

    // 檢查是否登陸
    checkLogin(userEntity, accessAuthEntity);

    // 檢查是否擁有權限
    checkPermission(userEntity, accessAuthEntity);
}

/**
 * 檢查當前用戶是否擁有訪問該接口的權限
 * @param userEntity 用戶信息
 * @param accessAuthEntity 接口權限信息
 */
private void checkPermission(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 獲取接口權限
    String accessPermission = accessAuthEntity.getPermission();

    // 獲取用戶權限
    List<PermissionEntity> userPermissionList = userEntity.getRoleEntity().getPermissionList();

    // 判斷用戶是否包含接口權限
    if (CollectionUtils.isNotEmpty(userPermissionList)) {
        for (PermissionEntity permissionEntity : userPermissionList) {
            if (permissionEntity.getPermission().equals(accessPermission)) {
                return;
            }
        }
    }

    // 沒有權限
    throw new CommonBizException(ExpCodeEnum.NO_PERMISSION);
}


/**
 * 檢查當前接口是否須要登陸
 * @param userEntity 用戶信息
 * @param accessAuthEntity 接口訪問權限
 */
private void checkLogin(UserEntity userEntity, AccessAuthEntity accessAuthEntity) {
    // 還沒有登陸
    if (accessAuthEntity.isLogin() && userEntity==null) {
        throw new CommonBizException(ExpCodeEnum.UNLOGIN);
    }
}
  • 全局異常處理
    爲了是得代碼具有良好的可讀性,這裏使用了SpringBoot提供的全局異常處理機制。咱們只需拋出異常便可,這些異常會被咱們預先設置的全局異常處理類捕獲並處理。全局異常處理本質上藉助於AOP完成。

    • 咱們須要定義全局異常處理類,它只是一個普通類,咱們只要用@ControllerAdvice註解聲明便可
    • 咱們還須要在這個類上增長@ResponseBody註解,它可以幫助咱們當處理完異常後,直接向用戶返回JSON格式的錯誤信息,而無需咱們手動處理。
    • 在這個類中,咱們根據異常類型不一樣,定義了兩個異常處理函數,分別用於捕獲業務異常、系統異常。而且須要使用@ExceptionHandler註解告訴Spring,該方法用於處理什麼類型的異常。
    • 當咱們完成上述配置後,只要項目中任何地方拋出異常,都會被這個全局異常處理類捕獲,並根據拋出異常的類型選擇相應的異常處理函數。
/**
 * @Author 大閒人柴毛毛
 * @Date 2017/10/27 下午11:02
 * REST接口的通用異常處理
 */
@ControllerAdvice
@ResponseBody
public class ExceptionHandle {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 業務異常處理
     * @param exception
     * @param <T>
     * @return
     */
    @ExceptionHandler(CommonBizException.class)
    public <T> Result<T> exceptionHandler(CommonBizException exception) {
        return Result.newFailureResult(exception);
    }

    /**
     * 系統異常處理
     * @param exception
     * @return
     */
    @ExceptionHandler(Exception.class)
    public <T> Result<T> sysExpHandler(Exception exception) {
        logger.error("系統異常 ",exception);
        return Result.newFailureResult();
    }
}

相關文章
相關標籤/搜索