SpringCloud + Nacos + Seata1.2.0 最新版

關於Seata配置, 一步一個坑, 配置過程中0.9.0~1.2.0版本全下載也沒有成功(非Feign降級可以回滾, 但是數據庫事務相關表沒任何數據出現), 官方文檔和Demo描述都是1.0.0之前老版本的, 1.0.0以上要麼只有簡單說明, 要麼直接項目貼jar包, 終於是自己摸索出來了, 希望能讓Springcloud整合Seata的同學少走點坑.
下面開始詳細的配置說明.

版本信息

Jar版本信息

JDK : 1.8.0
Springboot: 2.2.6.RELEASE
Nacos: 2.2.1.RELEASE
OpenFeign: 2.2.2.RELEASE
MybatisPlus: 3.3.1.tmp

Nacos 及 Seata 版本

Nacos: 1.2.1
Seata: 1.2.0

項目相關其他Jar版本

Sentinel-dashboard: 1.7.2
spring-cloud-starter-alibaba-sentinel: 2.2.1.RELEASE
druid: 1.1.22

這裏也不描述官方的轉賬Demo, 沒什麼用, 只要配置好了事務過程中Seata數據庫有信息就是可用的.

Seata下載地址
Nacos跳過

解壓後修改三個配置文件. file.conf, file.conf.example, registry.conf.

file.conf 修改mode=‘db’, 下方Seata數據庫地址賬號密碼

## transaction log store, only used in seata-server
store {
  ## store mode: file、db
  mode = "db"

  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "mysql"
    password = "mysql"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }
}

file.conf.example 修改mode=‘db’, 下方Seata數據庫地址賬號密碼
transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = false
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThreadPrefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}

## transaction log store, only used in server side
store {
  ## store mode: file、db
  mode = "db"
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "mysql"
    password = "mysql"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
  }
}
## server configuration, only used in server side
server {
  recovery {
    #schedule committing retry period in milliseconds
    committingRetryPeriod = 1000
    #schedule asyn committing retry period in milliseconds
    asynCommittingRetryPeriod = 1000
    #schedule rollbacking retry period in milliseconds
    rollbackingRetryPeriod = 1000
    #schedule timeout retry period in milliseconds
    timeoutRetryPeriod = 1000
  }
  undo {
    logSaveDays = 7
    #schedule delete expired undo_log in milliseconds
    logDeletePeriod = 86400000
  }
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  maxCommitRetryTimeout = "-1"
  maxRollbackRetryTimeout = "-1"
  rollbackRetryTimeoutUnlockEnable = false
}

## metrics configuration, only used in server side
metrics {
  enabled = false
  registryType = "compact"
  # multi exporters use comma divided
  exporterList = "prometheus"
  exporterPrometheusPort = 9898
}
registry.conf registry.type=‘nacos’ 並標明nacos端口
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

啓動Nacos, 啓動Seata, 若Seata運行無報錯且Nacos服務列表存在Seata服務則第一步成功.

Seata數據庫配置, 創建表
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `global_table` (
  `xid` varchar(128)  not null,
  `transaction_id` bigint,
  `status` tinyint not null,
  `application_id` varchar(32),
  `transaction_service_group` varchar(32),
  `transaction_name` varchar(128),
  `timeout` int,
  `begin_time` bigint,
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`xid`),
  key `idx_gmt_modified_status` (`gmt_modified`, `status`),
  key `idx_transaction_id` (`transaction_id`)
);

-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
  `branch_id` bigint not null,
  `xid` varchar(128) not null,
  `transaction_id` bigint ,
  `resource_group_id` varchar(32),
  `resource_id` varchar(256) ,
  `lock_key` varchar(128) ,
  `branch_type` varchar(8) ,
  `status` tinyint,
  `client_id` varchar(64),
  `application_data` varchar(2000),
  `gmt_create` datetime,
  `gmt_modified` datetime,
  primary key (`branch_id`),
  key `idx_xid` (`xid`)
);

-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
  `row_key` varchar(128) not null,
  `xid` varchar(96),
  `transaction_id` long ,
  `branch_id` long,
  `resource_id` varchar(256) ,
  `table_name` varchar(32) ,
  `pk` varchar(36) ,
  `gmt_create` datetime ,
  `gmt_modified` datetime,
  primary key(`row_key`)
);

其他各服務數據庫添加undo_log表
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此腳本必須初始化在你當前的業務數據庫中,用於AT 模式XID記錄。與server端無關(注:業務數據庫)
-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

服務配置

Pom.xml 導入Jar. 每個服務都需導入Seata相關包
<properties>
   <java.version>1.8</java.version>
   <nacos.version>2.2.1.RELEASE</nacos.version>
   <boot.version>2.2.6.RELEASE</boot.version>
   <feign.version>2.2.2.RELEASE</feign.version>
</properties>

<!--Seata 包-->
<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
   <version>2.2.1.RELEASE</version>
   <exclusions>
       <exclusion>
           <groupId>io.seata</groupId>
           <artifactId>seata-all</artifactId>
       </exclusion>
       <exclusion>
           <groupId>io.seata</groupId>
           <artifactId>seata-spring-boot-starter</artifactId>
       </exclusion>
   </exclusions>
  </dependency>
  <dependency>
      <groupId>io.seata</groupId>
      <artifactId>seata-spring-boot-starter</artifactId>
      <version>1.2.0</version>
  </dependency>
  <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.22</version>
  </dependency>

<!--openfeign 支持解析MVC註解接口-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      <version>${feign.version}</version>
  </dependency>
  <!--Nacos配置-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      <version>2.2.1.RELEASE</version>
  </dependency>
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      <version>${nacos.version}</version>
  </dependency>
  <!--Sentinel 流量哨兵-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
      <version>${nacos.version}</version>
  </dependency>
  <!--Sentinel-Nacos動態配置-->
  <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-datasource-nacos</artifactId>
      <version>1.7.2</version>
  </dependency>
 <!--ORM-->
 <dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <version>3.3.1.tmp</version>
     <exclusions>
         <exclusion>
             <groupId>com.github.jsqlparser</groupId>
             <artifactId>jsqlparser</artifactId>
         </exclusion>
     </exclusions>
 </dependency>
yml配置

項目使用Nacos動態配置, 我這裏做了拆分
bootstrap優先級大於application, 所有拉取Nacos配置的要寫在此處, 不然讀取不到就會報錯.
bootstrap.yml 主要是讀取Nacos配置, 如下:

bootstrap.yml
spring:
  nacos-host: localhost
  nacos-port: 8848
  application:
    name: xxx-server
  cloud:
    nacos:
      discovery:
        server-addr: ${spring.nacos-host}:${spring.nacos-port}
      config:
        server-addr: ${spring.nacos-host}:${spring.nacos-port}
        file-extension: yaml
    #Sentinel-Nacos 配置
    sentinel:
      datasource:
        ds:
          nacos:
            server-addr: ${spring.nacos-host}:${spring.nacos-port}
            data-id: ${spring.application.name}-sentinel
            groupId: FLOW
            rule-type: flow

application.yml
spring:
  profiles:
    active: dev
application-dev.yml
server:
  port: 8002
spring:
  application:
    name: xxx-server # 應用名稱 自定義
    host: localhost
  # druid 數據源配置
  datasource:
    url: jdbc:mysql://localhost:3308/xxx業務數據庫?allowMultiQueries=true&autoReconnect=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useUnicode=true&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
    username: mysql
    password: mysql
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    filters: stat,wall,slf4j
    maxActive: 20
    initialSize: 5
    maxWait: 60000
    minIdle: 5
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 打開PSCache,並且指定每個連接上PSCache的大小
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
    # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

  main:
    allow-bean-definition-overriding: true
  # 連接控制檯
  cloud:
    sentinel:
      transport:
        port: 7001
        dashboard: ${spring.application.host}:8080

mybatis-plus:
  global-config:
    db-config:
      id-type: auto
      # 表名前綴
      table-prefix: 'xxx'
  configuration:
    # 駝峯下劃線映射
    map-underscore-to-camel-case: true
    cache-enabled: false
  #  config-location: classpath:mybatis/mybatis.cfg.xml
  mapper-locations: classpath:mapper/*.xml


# 開啓Sentinel熔斷器
feign:
  sentinel:
    enabled: true
# 餓加載開啓 Feign 預加載, 防止第一次請求超時
ribbon:
  eager-load:
    enabled: true
    clients: xxx-server, xxx-server

seata:
  application-id: ${spring.application.name} # Seata 應用名稱,默認使用 ${spring.application.name}
  tx-service-group: default # Seata 事務組, 高版本沒找到相關配置, 是否可配置未知 選用默認default
  # 服務配置項
  service:
    # 虛擬組和分組的映射 1.0.0以上好像將vgroup-mapping 改爲 vgroupMapping, 此處是否影響未測試
    vgroupMapping:
      # 此處Key對應 tx-service-group 的 Value, 此處 value 默認 default
      default: default
    # 分組和 Seata 服務的映射 默認端口8091
    grouplist:
      default: 127.0.0.1:8091

調用說明: A服務調用B服務, 兩邊都是insert, 在B服務插入成功後拋出異常, 測試是否回滾.

A服務啓動類添加Feign註解, @EnableFeignClients, @MapperScan(“dao包路徑”)

A服務編寫業務代碼, 使用MybatisPlus內置方法, 其他寫法注入mapper接口調用方法就好.
主分支方法添加事務註解@GlobalTransactional(rollbackFor = Exception.class),
分支事務註解無需添加

package com.mis.rbac.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mis.common.constants.RespData;
import com.mis.common.rbac.entity.Dept;
import com.mis.common.utils.DateUtils;
import com.mis.rbac.dao.DeptMapper;
import com.mis.rbac.feign.BankServiceFeign;
import com.mis.rbac.service.DeptService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements DeptService {

    @Autowired
    BankServiceFeign bankServiceFeign;

    @Override
    @GlobalTransactional(rollbackFor = Exception.class)
    public RespData addDept() {
        Dept dept = new Dept();
        dept.setDeptName("asdasdsa");
        dept.setDeptPid(0L);
        dept.setCreateTime(DateUtils.getCurrentDate("", String.class));
        if(dept.insert()){
            bankServiceFeign.tran();
        }
        return RespData.success();
    }

}

Feign接口

package com.mis.rbac.feign;

import com.mis.common.constants.RespData;
import com.mis.rbac.feign.impl.BankServiceFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;

@FeignClient(name = "firm-server"/*, fallbackFactory= BankServiceFallbackFactory.class*/)
public interface BankServiceFeign {

    @PostMapping("tran")
    public RespData tran();

}

Controller 跳過.

B服務
package com.mis.firm.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mis.common.constants.RespData;
import com.mis.common.firm.Bank;
import com.mis.firm.dao.BankMapper;
import com.mis.firm.service.BankService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class BankServiceImpl extends ServiceImpl<BankMapper, Bank> implements BankService {

    @Override
    public RespData tran() throws Exception {
        System.out.println("執行添加Bank.");
        Bank bank = new Bank();
        bank.setBankAccount("123456");
        bank.setBankAddr("Transactional");
        bank.setBankName("事務");
        bank.setBankPhone("123456789");
        bank.setDel(0);
        if(bank.insert()){
            throw new Exception("手動異常, 事務回滾");
        }
        return RespData.success();
    }
}

啓動Nacos, Seata後, 啓動服務

若Seata控制檯輸出服務相關信息及數據庫信息則連接成功, 如下:
在這裏插入圖片描述

若啓動過程中出現 no available service ‘null’ found, please make sure registry config correct或DataSource錯誤,

說明未連接上Seata.
一般是Jar有衝突

spring-cloud-starter-alibaba-seata: 2.2.1.RELEASE 此版本與 seata-spring-boot-starter:1.1.0 衝突

更換爲 seata-spring-boot-starter:1.2.0

其他情況: ClassNotFoundException:io.seata.spring.annotation.datasource.SeataDataSourceBeanPostProcessor
缺少Jar : seata-all
推薦使用seata-spring-boot-starter


啓動成功並控制檯無錯誤信息, 開始調用接口測試.

若主事務與分支事務均回滾, 則又進一步成功,

BUG斷點測試, 攔截過程中查看Seata數據庫表和服務undo_log表
若表存在數據, 如:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

則完成配置

Feign降級回滾(動態回滾)

使用Feign降級後, Seata不回滾.
需手動配置回滾
官方文檔
在這裏插入圖片描述

RootContext.getXID() 獲取當前XID後執行手動回滾
編寫AOP, 切點事務註解所在的實現類, 各服務都添加此切面類

package com.mis.rbac.aop;

import java.lang.reflect.Method;

import com.mis.common.constants.RespData;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.tm.api.GlobalTransaction;
import io.seata.tm.api.GlobalTransactionContext;
import lombok.extern.slf4j.Slf4j;

/**
 * Seata 動態事務回滾切面類 (Feign熔斷降級回滾)
 */
@Slf4j
@Aspect
@Component
public class AtFeignTransactionalAop {

    @Before("execution(* com.mis.rbac.service.*.*(..))")
    public void before(JoinPoint joinPoint) throws TransactionException {
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        log.info("攔截到需要分佈式事務的方法," + method.getName());
        GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
        // 超時時間 , 所在服務
        tx.begin(300000, "rbac-server");
        log.info("創建分佈式事務id:{}", tx.getXid());
    }

    @AfterThrowing(throwing = "e", pointcut = "execution(* com.mis.rbac.service.*.*(..))")
    public void doRecoveryActions(Throwable e) throws TransactionException {
        log.info("方法執行異常:{}", e.getMessage());
        if (!StringUtils.isBlank(RootContext.getXID())) {
            log.info("分佈式事務Id:{}, 手動回滾!", RootContext.getXID());
            GlobalTransactionContext.reload(RootContext.getXID()).rollback();
        }
    }

    @AfterReturning(value = "execution(* com.mis.rbac.service.*.*(..))", returning = "result")
    public void afterReturning(JoinPoint point, Object result) throws TransactionException {
        log.info("方法執行結束:{}", result);
        // 方法返回值 RespData是自定義的統一返回類
        RespData respData = (RespData)result;
        if (respData.getCode() != 0) {
            if (!StringUtils.isBlank(RootContext.getXID())) {
                log.info("分佈式事務Id:{}, 手動回滾!", RootContext.getXID());
                GlobalTransactionContext.reload(RootContext.getXID()).rollback();
            }
        }
    }
}

統一異常
package com.mis.rbac.exception;


import com.mis.common.enums.Code;

/**
 * @ClassName GlobalException
 * @description: 自定義異常
 **/
public class GlobalException extends RuntimeException {

    /**
     * 異常狀態碼
     */
    private Integer code;

    /**
     * 異常信息
     */
    private String message;

    private String e;

    public GlobalException(Code enums){
        super();
        this.code=enums.getCode();
        this.message=enums.getMsg();
    }

    public GlobalException(Code enums, String e){
        super();
        this.code=enums.getCode();
        this.message=enums.getMsg();
        this.e = e;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getE() {
        return e;
    }

    public void setE(String e) {
        this.e = e;
    }
}

package com.mis.rbac.exception;

import com.mis.common.constants.RespData;
import com.mis.common.enums.Code;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
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.ResponseBody;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

/**
 * @ClassName GlobalExceptionHandler
 * @Description TODO 全局異常處理類
 * @Version 1.0.0
 **/
@Slf4j
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(GlobalException.class)
    public RespData handlerGlobalException(GlobalException e) {
        e.printStackTrace();
        log.error("\r\n異常信息 : \r\n[\r\n 錯誤代碼 >>>>>>> { "+ e.getCode() +" } , 錯誤信息 >>>>>>> { "+e.getE()+" } \r\n].");
        return RespData.error(Code.EXCEPTION_ERROR);
    }

    @ExceptionHandler(Exception.class)
    public  RespData handleException(Exception e) {
        e.printStackTrace();
        log.error("\r\n系統系統異常 >>>>>> { "+e.getMessage()+" } , \r\n錯誤信息 >>>>>>> {} ,\r\n { "+getExceptionAllinfo(e)+" } \r\n");
        return RespData.error(Code.EXCEPTION_ERROR);
    }

    /**
     * 驗證異常
     * @param e
     * @return
     * @throws BindException
     */
    @ExceptionHandler(value = BindException.class)
    public RespData handleBindException(BindException e) {
        FieldError fieldError = e.getFieldError();
        StringBuilder sb = new StringBuilder();
        sb.append(fieldError.getField()).append(" =[ ").append(fieldError.getRejectedValue()).append(" ] ")
                .append(fieldError.getDefaultMessage());
        log.error("\r\n參數驗證異常 >>>>>> 錯誤信息 : " + sb.toString());
        return RespData.error(-1,sb.toString());
    }

    @ExceptionHandler(value = ConstraintViolationException.class)
    public Object ConstraintViolationExceptionHandler(ConstraintViolationException exception){
        StringBuilder sb = new StringBuilder();
        int i = 0;
        for (ConstraintViolation<?> violation : exception.getConstraintViolations()) {
            if(i == 0){
                sb.append(violation.getMessageTemplate());
            }
            i++;
        }
        log.error("\r\n參數驗證異常 >>>>>> 錯誤信息 : " + sb.toString() + "\r\n");
        return RespData.error(4000,sb.toString());
    }

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Object MethodArgumentNotValidHandler(MethodArgumentNotValidException exception){
        StringBuilder sb = new StringBuilder();
        for (FieldError error : exception.getBindingResult().getFieldErrors()) {
            sb.append(error.getField()).append(" =[ ").append(error.getRejectedValue()).append(" ] ")
                    .append(error.getDefaultMessage());
        }
        log.error("\r\n參數驗證異常 >>>>>> 錯誤信息 : " + sb.toString());
        return RespData.error(4000,sb.toString());
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public  RespData handleException(HttpRequestMethodNotSupportedException e) {
        System.err.println("\n************************************************************************************");
        System.err.println("\n*     請求異常 : ---> [ { "+e.getMessage()+" } ] .                                 *");
        System.err.println("\n************************************************************************************\n");
        return RespData.error(4000, e.getMessage());
    }


    public String getExceptionAllinfo(Exception ex) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        PrintStream pout = new PrintStream(out);
        ex.printStackTrace(pout);
        String ret = new String(out.toByteArray());
        pout.close();
        try {
            out.close();
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
        return ret;
    }
}

動態回滾 已完成.

參考:
https://seata.io/zh-cn/blog/seata-spring-boot-aop-aspectj.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章