手把手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/SpringBoot-Dubbo-Docker-Jenkins数据库


1. 安全框架的设计

1.1 开发目标

在全部事情开始以前,咱们首先要搞清楚,咱们究竟要实现哪些功能?缓存

  1. 用户登陆 全部系统都须要登陆功能,这毫无疑问,也没必要多说。
  2. 角色管理 每一个用户都有且仅有一种角色,好比:系统管理员、普通用户、企业用户等等。管理员能够添加、删除、查询、修改角色信息。
  3. 权限管理 每种角色能够拥有不一样的权限,管理员能够建立、修改、查询、删除权限,也能够为某一种角色添加、删除权限。
  4. 权限检测 用户调用每个接口,都须要校验该用户是否具有调用该接口的权限。

当咱们明确了开发目标以后,下面就须要基于这些目标,设计咱们的系统。咱们首先要作的就是要搞清楚“用户”、“角色”、“权限”的定义以及他们之间的关系。这在领域驱动设计中被称为“领域模型”。安全

1.2 领域模型

title

  • 权限:
    • 权限表示某一用户是否具备操做某一资源的能力。
    • 权限通常用“资源名称:操做名称”来表示。好比:建立用户的权限能够用“user:create”来表示,删除用户的权限能够用“user:delete”来表示。
    • 在Web系统中,权限和接口呈一一对应关系,好比:“user:create”对应着建立用户的接口,“user:delete”对应着删除用户的接口。所以,权限也能够理解成一个用户是否具有操做某一个接口的能力。
  • 角色:
    • 角色是一组权限的集合。角色规定了某一类用户共同具有的权限集合。
    • 好比:超级管理员这种角色拥有“user:create”、“user:delete”等权限,而普通用户只有“user:create”权限。
    • 从领域模型中可知,角色和权限之间呈多对多的聚合关系,即一种角色能够包含多个权限,一个权限也能够属于多种角色,而且权限能够脱离于角色而单独存在,所以他们之间是一种弱依赖关系——聚合关系。
  • 用户:
    • 用户和角色之间呈多对一的聚合关系,即一个用户只能属于一种角色,而一种角色却能够包含多个用户。而且角色能够脱离于用户单独存在,所以他们之间是一种弱依赖关系——聚合关系。

1.3 数据结构设计

当咱们捋清楚了“权限”、“用户”、“角色”的定义和他们之间的关系后,下面咱们就能够基于这个领域模型设计出具体的数据存储结构。bash

为了可以方便地给每个接口标注权限,咱们须要自定义三个注解@Login@Role@Permissionsession

  • @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();
    }
}
复制代码

相关文章
相关标签/搜索