<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在網上很多關於dubbo異常統一處理的博文,90%都是抄來抄去。大多都是先上一段dubbo中對於異常的統一處理的原碼,然後說一堆的(甚至有12345,五種)不靠譜方案,最後再說“本篇使用的是方案4”,然後再對所謂的方案4寫了一段文字,最後還說不清!!!
本篇解決方案不會那麼羅裡吧嗦也不會貼dubbo原始碼來湊字數,我就直接從剛結束不久的雙11保衛戰效能全鏈路優化中我們的面對10萬級別TPS的方案中提取的程式碼來說明這個dubbo統一處理異常是怎麼個處理方式吧!
不同開發團隊間便於追溯異常的來源以及為了便於定位問題的需要
往往實際開發中的架構是這麼一個樣子的:
dubbo微服務架構簡圖
不同層的開發人員都是不同的人或者是不同的幾波人馬;
無狀態的API層(一組Tomcat對Nginx Web層的API暴露)是一組開發團隊;
微服務Dubbo層是另一組開發團隊;
在偵錯、測試、上線後我們經常會發生各種Exception,此時這幾個不同的開發團隊間會互相扯皮、打架,並且大家都要忙於定位這個Exception到底是發生在哪一層,甚至需要追溯Exception發生在哪個點(stackTrace)。
Service層有資料庫事務一致性的問題必須丟擲異常
我們都知道在spring中的Service層必須丟擲Runtime Exception,否則Service層的方法如果有涉及資料庫的修改操作是不會回滾的。
其實解決方案真正的無外乎就2種:
本文把這2種實現方式都給實現了,下面開始直接show me the code的方式來說話吧。
環境搭建
nacos1.1.4
我們這邊不用dubbo admin,因為dubbo admin太老且使用不方便,缺少了很多管理微服務所需要的基本功能。並且dubbo從2.6開始已經把dubbo admin從它的主工程裡分離了出去,同時dubbo2.6開始支援nacos registry了。
目前來說nacos是最方便、效率最高、功能最強大的微服務發現元件(甚至支援spring cloud)。
下載地址在這裡(請戳):阿里nacos最新下載地址
下載後直接解壓,然後進行nacos設定
編輯這個application.properties檔案,我們把nacos自動服務發現管理端連上自己開發環境上的mysql。
# spring spring.datasource.platform=mysql server.contextPath=/nacos server.servlet.contextPath=/nacos server.port=8848 db.num=1 db.url.0=jdbc:mysql://192.168.56.101:3306/nacos?useUnicode=true&characterEncoding=utf-8&useSSL=false db.user=nacos db.password=111111
配完後直接雙擊:startup.cmd啟動nacos
登入介面中使用nacos/nacos即可進行登入了。
登入後看到nacos管理介面就說明nacos設定和啟動成功了。接下來我們就要開始書寫dubbo的provider端與consumer端了。
dubbo工程搭建
nacos-parent工程
整個工程我已經放在git上了,地址請戳這裡:nacos-dubbo-demo
工程的依賴結構如下:
由於dubbo與springboot結合的專案不多,很多網上有的部落格也充斥著亂抄、自己都沒有驗證過就上程式碼的,因此大多網友們通過網上之言片語拼湊起來的專案在本地很難執行起來,不是maven包衝突就是少這個、那個包。下面給出工程的parent pom檔案。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.sky.demo</groupId> <artifactId>nacos-parent</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <description>Demo project for Spring Boot Dubbo Nacos</description> <modules> </modules> <properties> <java.version>1.8</java.version> <spring-boot.version>1.5.15.RELEASE</spring-boot.version> <dubbo.version>2.7.3</dubbo.version> <curator-framework.version>4.0.1</curator-framework.version> <curator-recipes.version>2.8.0</curator-recipes.version> <druid.version>1.1.20</druid.version> <guava.version>27.0.1-jre</guava.version> <fastjson.version>1.2.59</fastjson.version> <dubbo-registry-nacos.version>2.7.3</dubbo-registry-nacos.version> <nacos-client.version>1.1.4</nacos-client.version> <mysql-connector-java.version>5.1.46</mysql-connector-java.version> <disruptor.version>3.4.2</disruptor.version> <aspectj.version>1.8.13</aspectj.version> <nacos-service.version>0.0.1-SNAPSHOT</nacos-service.version> <skycommon.version>0.0.1-SNAPSHOT</skycommon.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <compiler.plugin.version>3.8.1</compiler.plugin.version> <war.plugin.version>3.2.3</war.plugin.version> <jar.plugin.version>3.1.2</jar.plugin.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>${dubbo.version}</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>${dubbo.version}</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>${curator-framework.version}</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>${curator-recipes.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql-connector-java.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>${disruptor.version}</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-nacos</artifactId> <version>${dubbo-registry-nacos.version}</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>${nacos-client.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectj.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${compiler.plugin.version}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>${war.plugin.version}</version> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>${jar.plugin.version}</version> </plugin> </plugins> </build> </project>
演示用資料庫(mySQL5.7)建表語句
CREATE TABLE `t_product` ( `product_id` int(11) NOT NULL AUTO_INCREMENT, `product_name` varchar(45) DEFAULT NULL, PRIMARY KEY (`product_id`) ); CREATE TABLE `t_stock` ( `stock_id` int(11) NOT NULL AUTO_INCREMENT, `stock` int(11) DEFAULT NULL, `product_id` int(11) NOT NULL, PRIMARY KEY (`stock_id`) );
它建了兩張表,t_product表和t_stock表。這兩張表我們會用於演示dubbo provider中對於資料庫一致性插入時在碰到Exception時怎麼處理回滾的場景。
nacos-service工程搭建說明
先上pom.xml(很重要,這裡面的依賴是正確的springboot+dubbo+nacos使用者端的完整設定)
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.sky.demo</groupId> <artifactId>nacos-service</artifactId> <version>0.0.1-SNAPSHOT</version> <name>nacos-service</name> <description>服務者 Demo project for Spring Boot dubbo nacos</description> <parent> <groupId>org.sky.demo</groupId> <artifactId>nacos-parent</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-spring</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <!-- Dubbo Registry Nacos --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-nacos</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> </dependency> <dependency> <groupId>org.sky.demo</groupId> <artifactId>skycommon</artifactId> <version>${skycommon.version}</version> </dependency> </dependencies> <build> <sourceDirectory>src/main/java</sourceDirectory> <testSourceDirectory>src/test/java</testSourceDirectory> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <resources> <resource> <directory>src/main/resources</directory> </resource> <resource> <directory>src/main/webapp</directory> <targetPath>META-INF/resources</targetPath> <includes> <include>**/**</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>application.properties</include> <include>application-${profileActive}.properties</include> </includes> </resource> </resources> </build> </project>
然後我們設定application.properties檔案內容
這邊dubbo的部分設定是相對於我虛擬出來的模擬環境4C CPU,4GB記憶體來設的,具體更多設定引數可以直接參照於dubbo官方檔案。
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://192.168.56.101:3306/mk?useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.username=mk spring.datasource.password=111111 server.port=8080 server.tomcat.max-connections=300 server.tomcat.max-threads=300 server.tomcat.uri-encoding=UTF-8 server.tomcat.max-http-post-size=0 #Dubbo provider configuration dubbo.application.name=nacos-service-demo dubbo.registry.protocol=dubbo dubbo.registry.address=nacos://127.0.0.1:8848 dubbo.protocol.name=dubbo dubbo.protocol.port=20880 dubbo.protocol.threads=200 dubbo.protocol.queues=100 dubbo.protocol.threadpool=cached dubbo.provider.retries = 3 dubbo.provider.threadpool = cached dubbo.provider.threads = 200 dubbo.provider.connections = 100 dubbo.scan.base-packages=org.sky.service logging.config=classpath:log4j2.xml
我們可以看到要把dubbo與nacos連線起來只需要在pom.xml檔案中引入
<!-- Dubbo Registry Nacos --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-nacos</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> </dependency>
以及在application.properties檔案中把相應的dubbo協定依舊使用dubbo,這是因為dubbo2.6中已經帶入了nacos-registry了,因此就必須把dubbo.registry.address設成指向你本機的nacos啟動範例(預設為8848埠)即可。
dubbo.registry.protocol=dubbo dubbo.registry.address=nacos://127.0.0.1:8848
springboot的啟動程式碼,Application.java
package org.sky; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableDubbo @EnableAutoConfiguration @ComponentScan(basePackages = { "org.sky" }) @EnableTransactionManagement public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
有兩個重要的註解
@EnableDubbo申明該專案啟用dubbo的自動註解;
@EnableTransactionManagement申明該專案會使用資料庫事務;
把專案連線上資料庫
我們使用druid做資料庫的連線池。
package org.sky.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; @Configuration @EnableAutoConfiguration public class DruidConfig { @ConfigurationProperties(prefix = "spring.datasource") @Bean public DruidDataSource dataSource() { return new DruidDataSource(); } }
製作一個自定義的全域性Exception,DemoRpcRunTimeException
把它放置於common專案內
package org.sky.exception; import java.io.Serializable; public class DemoRpcRunTimeException extends RuntimeException implements Serializable { public DemoRpcRunTimeException() { } public DemoRpcRunTimeException(String msg) { super(msg); } public DemoRpcRunTimeException(Throwable cause) { super(cause); } public DemoRpcRunTimeException(String message, Throwable cause) { super(message, cause); } }
製作一個AOP, DemoRpcRuntimeExceptionHandler
用於包裝自定的異常用,它位於nacos-service專案中,做它會以AOP的方式注入。
package org.sky.config; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.Aspect; import org.sky.exception.DemoRpcRunTimeException; import org.springframework.stereotype.Component; @Aspect @Component public class DemoRpcRuntimeExceptionHandler { protected Logger logger = LogManager.getLogger(this.getClass()); /** * service層的RuntimeException統一處理器 * 可以將RuntimeException分裝成RpcRuntimeException拋給呼叫端處理 或自行處理 * * @param exception */ @AfterThrowing(throwing = "exception", pointcut = "execution(* org.sky.service.*.*(..))") public void afterThrow(Throwable exception) { if (exception instanceof RuntimeException) { logger.error("DemoRpcRuntimeExceptionHandler side->exception occured: " + exception.getMessage(), exception); throw new DemoRpcRunTimeException(exception); } // logger.error("DemoRpcRuntimeExceptionHandler side->exception occured: " + // exception.getMessage(), exception); } }
開始進入核心provider Service端的製作。
ProductService介面
我們把它放置於common工程,這樣consumer工程也就可以通過nacos的註冊中心找到這個介面名,然後通過spring的invoke來對於遠端的用於具體實現service邏輯的xxxServiceImpl類進行呼叫了。
package org.sky.service; import org.sky.exception.DemoRpcRunTimeException; import org.sky.platform.util.DubboResponse; import org.sky.vo.ProductVO; public interface ProductService { public DubboResponse addProductAndStock(ProductVO prod) throws DemoRpcRunTimeException; }
具體業務邏輯實現類,ProductServiceImpl
該類做這麼一件事:
1)插入t_product表資料
2)插入t_stock表資料
插兩張表時,只要有一點點錯誤那麼整個插入事務回滾,否則成功。這邊需要注意的就是:
ProductServiceImpl.java
package org.sky.service; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import org.apache.dubbo.config.annotation.Service; import org.sky.exception.DemoRpcRunTimeException; import org.sky.platform.util.DubboResponse; import org.sky.vo.ProductVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.transaction.annotation.Transactional; @Service(version = "1.0.0", interfaceClass = ProductService.class, timeout = 120000) public class ProductServiceImpl extends BaseService implements ProductService { @Autowired JdbcTemplate jdbcTemplate; @Override @Transactional public DubboResponse<ProductVO> addProductAndStock(ProductVO prod) throws DemoRpcRunTimeException { DubboResponse<ProductVO> response = null; int newProdId = 0; String prodSql = "insert into t_product(product_name)values(?)"; String stockSql = "insert into t_stock(product_id,stock)values(?,?)"; try { if (prod != null) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection connection) throws SQLException { PreparedStatement ps = connection.prepareStatement(prodSql, new String[] { "id" }); ps.setString(1, prod.getProductName()); return ps; } }, keyHolder); newProdId = keyHolder.getKey().intValue(); logger.info("======>insert into t_product with product_id:" + newProdId); if (newProdId > 0) { jdbcTemplate.update(stockSql, newProdId, prod.getStock()); logger.info("======>insert into t_stock with successful"); ProductVO returnData = new ProductVO(); returnData.setProductId(newProdId); returnData.setProductName(prod.getProductName()); returnData.setStock(prod.getStock()); response = new DubboResponse(HttpStatus.OK.value(), "success", returnData); //throw new Exception("Mk throwed exception to enforce rollback[insert into t_stock]"); return response; } } else { throw new DemoRpcRunTimeException("error occured on ProductVO is null"); } } catch (Exception e) { logger.error("error occured on Dubbo Service Side: " + e.getMessage(), e); throw new DemoRpcRunTimeException("error occured on Dubbo Service Side: " + e.getMessage(), e); } return response; } }
這個類目前是正常狀態,我們先呼叫一把正常的provider到service端的過程然後接下來就來演示如何把exception遠端傳遞到consumer端。
nacos-consumer工程搭建說明
先上pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.sky.demo</groupId> <artifactId>nacos-consumer</artifactId> <version>0.0.1-SNAPSHOT</version> <name>nacos-service</name> <description>消費者 Demo project for Spring Boot dubbo nacos</description> <parent> <groupId>org.sky.demo</groupId> <artifactId>nacos-parent</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <dependencies> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-spring</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions><!-- 去掉預設設定 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <!-- Dubbo Registry Nacos --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-nacos</artifactId> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> <!-- import sky common package --> <dependency> <groupId>org.sky.demo</groupId> <artifactId>skycommon</artifactId> <version>${skycommon.version}</version> </dependency> </dependencies> <build> <sourceDirectory>src/main/java</sourceDirectory> <testSourceDirectory>src/test/java</testSourceDirectory> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <resources> <resource> <directory>src/main/resources</directory> </resource> <resource> <directory>src/main/webapp</directory> <targetPath>META-INF/resources</targetPath> <includes> <include>**/**</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>application.properties</include> <include>application-${profileActive}.properties</include> </includes> </resource> </resources> </build> </project>
nacos-consumer端的application.properties
server.port=8082 server.tomcat.max-connections=50 server.tomcat.max-threads=50 server.tomcat.uri-encoding=UTF-8 server.tomcat.max-http-post-size=0 #Dubbo provider configuration dubbo.application.name=nacos-consumer dubbo.registry.address=nacos://127.0.0.1:8848 #dubbo.consumer.time=120000 logging.config=classpath:log4j2.xml
同樣,consumer端也需要連上原生的nacos範例。
另外多說一點的是,不要在consumer端去做類似dubbo通訊超時或者是一些個性化的dubbo引數設定。因為dubbo有3個核心引數集,provider, protocol, consumer。而在consumer做的設定由於這3者的優先順序問題,它是會覆蓋掉provider端的設定。如果是在大規模微服務開發場景中,每個consumer都做自己的個性化設定,這不利於全域性上對系統效能進行集中統一的管控,因此這需要公司的架構師對這些規範進行provider端的統一管控,一定儘量避免在consumer端去設定本該屬於central(provider)端的一些引數。
consumer端的Application.java
package org.sky; import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; @EnableDubbo @ComponentScan(basePackages = { "org.sky" }) @EnableAutoConfiguration public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
和provider端的naco-service沒多少區別,注意要@EnableDubbo,要不然spring不會在專案啟動時把consumer端給註冊到nacos的註冊中心去。
consumer端的Controller
這個consumer端正是我第一張圖中的無狀態的API層,這一層會有一堆tomcat/netty/jboss一類的東西,它們做的事就是路由API,以json格式向用戶端(手機、網頁、小程式)進行返回。這一層是不會去和DB、NOSQL、快取一類的打交道的,它們要做的就是呼叫“後端”微服務的dubbo服務,因此我們在這一端基本以spring中的controller為主。
為了讓consumer端可以呼叫provider端的service方法,必須在注入時加上@Reference註解,這樣dubbos的consumer在註冊進“註冊中心”,如:nacos這一類東西時就知道要找哪個provider的service(殘根-stub)了(定址作用)。
package org.sky.controller; import org.springframework.web.bind.annotation.RestController; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import org.springframework.web.bind.annotation.RequestMapping; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Resource; import org.apache.dubbo.config.annotation.Reference; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.sky.platform.util.AppConstants; import org.sky.platform.util.DubboResponse; import org.sky.platform.util.ResponseResult; import org.sky.platform.util.ResponseStatusEnum; import org.sky.platform.util.ResponseUtil; import org.sky.service.HelloNacosService; import org.sky.service.ProductService; import org.sky.vo.ProductVO; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @RestController @RequestMapping("nacosconsumer") public class DemoDubboConsumer extends BaseController { @Reference(version = "1.0.0",loadbalance="roundrobin") private HelloNacosService helloNacosService; @Reference(version = "1.0.0") private ProductService productService; @PostMapping(value = "/sayHello", produces = "application/json") public ResponseEntity<String> sayHello(@RequestBody String params) throws Exception { ResponseEntity<String> response; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); Map<String, Object> resultMap = new HashMap<>(); JSONObject requestJsonObj = JSON.parseObject(params); try { String name = getHelloNameFromJson(requestJsonObj); String answer = helloNacosService.sayHello(name); logger.info("answer======>" + answer); Map<String, String> result = new HashMap<>(); result.put("result", answer); String resultStr = JSON.toJSONString(result); response = new ResponseEntity<>(resultStr, headers, HttpStatus.OK); } catch (Exception e) { logger.error("dubbo-clinet has an exception occured: " + e.getMessage(), e); String resultStr = e.getMessage(); response = new ResponseEntity<>(resultStr, headers, HttpStatus.EXPECTATION_FAILED); } return response; } @PostMapping(value = "/addProductAndStock", produces = "application/json") public ResponseEntity<String> addProduct(@RequestBody String params) throws Exception { ResponseEntity<String> response = null; DubboResponse<ProductVO> dubboResponse; String returnResultStr; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); JSONObject requestJsonObj = JSON.parseObject(params); Map<String, Object> result = new HashMap<>(); try { ProductVO inputProductPara = getProductFromJson(requestJsonObj); dubboResponse = productService.addProductAndStock(inputProductPara); ProductVO returnData = dubboResponse.getData(); if (returnData != null && dubboResponse.getCode() == HttpStatus.OK.value()) { result.put("code", HttpStatus.OK.value()); result.put("message", "add a new product successfully"); result.put("productid", returnData.getProductId()); result.put("productname", returnData.getProductName()); result.put("stock", returnData.getStock()); returnResultStr = JSON.toJSONString(result); response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.OK); } else { result.put("message", "dubbo service ProductService get nullpoint exception"); returnResultStr = JSON.toJSONString(result); response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.EXPECTATION_FAILED); } } catch (Exception e) { logger.error("add a new product with error: " + e.getMessage(), e); result.put("message", "add a new product with error: " + e.getMessage()); returnResultStr = JSON.toJSONString(result); response = new ResponseEntity<>(returnResultStr, headers, HttpStatus.EXPECTATION_FAILED); } return response; } private String getHelloNameFromJson(JSONObject requestObj) { String helloName = requestObj.getString("name"); return helloName; } private ProductVO getProductFromJson(JSONObject requestObj) { String productName = requestObj.getString("productname"); int stock = requestObj.getIntValue("stock"); ProductVO prod = new ProductVO(); prod.setProductName(productName); prod.setStock(stock); return prod; } }
這個consumer相當的簡單,直接通過遠端介面呼叫dubbo得到一個返回。
執行例子
確保我們的nacos1.1.4執行在那。
然後先執行nacos-service的Application.java再執行nacos-consumer的Application.java
nacos-service執行範例:
nacos-consumer執行範例:
然後我們去nacos的管理介面檢視一下,就能發現provider和consumer都註冊成功了。
接著我們使用postman對consumer發一個json請求
得到返回如下所示
再看資料庫中
這說明我們的dubbo+nacos搭建完全執行正常,接下來就要演示兩種Exception的丟擲了。
第1種:直接從provider端拋RuntimeException到consumer端
在provider端我們對ProductServiceImpl進行一個小修改如下:
我們寫了一句:
throw new Exception("Mk throwed exception to enforce rollback[insert into t_stock]");
我們前文說過,在provider端的service裡一定要丟擲RuntimeException才會讓資料庫事物回滾,但是我們也不用擔心,還記得我們在nacos-service中已經注入了一個aop的攔截器叫“DemoRpcRuntimeExceptionHandler”嗎?
它的作用就是攔住一切Exception然後把它轉化成RuntimeException。
好,我們加完這一句話後重新依次執行nacose-service和nacos-consumer。然後同樣通過postman來存取http://localhost:8082/nacosconsumer/addProductAndStock,然後我們使用新的產品品名,post請求體內的報文如下所 示:
{"productname":"coffee","stock":10000}
看,我們這次請求過去後直接在response中出現的是什麼?
來看nacos-service端的紀錄檔,這是我們在provider端人為手工丟擲的一條紀錄檔:
來看nacos-consumer端的紀錄檔,我們可以看到provider端的異常甚至包括它的stackTrace資訊都已經傳遞到了consumer端了:
這樣的話consumer端的開發人員一看傳過來了這個錯誤就會跑到dubbo開發團隊處吼一下:喂,生產上有一個bug,你看這就是你們provider端丟擲來的,改吧!
為了確保我們的ExceptionHandler攔截的是否成功,我們來看資料庫端:
t_product表沒有插入coffee的記錄
t_stock表也沒有插入相關coffee的庫存
說明Exception確實是被轉成了RuntimeException並被spring框架所捕捉然後進行了一次回滾。
第2種:把一切Exception包裝成json返回報文不向consumer端輸出異常具體資訊
我們希望把provider端的Exception包裝成如下這種json報文:
{ "message" : "exception", "code" : "500", "add new product failed", "productid" : xxx, "productname" : xxx, "stock" : xxx }
轉而把:
異常的stackTrace以log方式記錄在provider端,在出了問題讓provider端的開發人員通過紀錄檔查詢和定位問題即可。
為什麼還有這種做法?
很簡單,因為stackTrace是異常追溯,呼叫到了jvm的棧內資訊了,這個是“很重”的一件活 。我們把一堆的異常Exception通過provider和consumer端拋來拋去,本來我們用dubbo就是用來做微服務的、就是為了應對大規模的並行請求的、就是為了做系統的彈性伸縮和高冗餘的,你還在用這麼大一陀stackTrace在兩端傳來傳去不說,還要加上傳時序列化、接到時反序列化,這不是增加了系統的開銷嗎?
下面直接show me the code,在nacos-service的org.sky.config處增加一個aop叫“ServiceExceptionHandler”,程式碼如下:
package org.sky.config; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.sky.platform.util.DubboResponse; import org.springframework.stereotype.Component; import com.google.common.base.Throwables; @Component @Aspect public class ServiceExceptionHandler { protected Logger logger = LogManager.getLogger(this.getClass()); /** * 返回值型別為Response的Service */ @Pointcut(value = "execution(* org.sky.service.*.*(..))") private void servicePointcut() { } /** * 任何持有@Transactional註解的方法 */ @Pointcut(value = "@annotation(org.springframework.transaction.annotation.Transactional)") private void transactionalPointcut() { } /** * 例外處理切面 將異常包裝為Response,避免dubbo進行包裝 * * @param pjp 處理點 * @return Object */ @Around("servicePointcut() && !transactionalPointcut()") public Object doAround(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); try { return pjp.proceed(); } catch (Exception e) { processException(pjp, args, e); return DubboResponse.error("exception occured on dubbo service side: " + e.getMessage()); } catch (Throwable throwable) { processException(pjp, args, throwable); return DubboResponse.error("exception occured on dubbo service side: " + throwable.getMessage()); } } /** * 任何持有@Transactional註解的方法例外處理切面 將自定義的業務異常轉為RuntimeException: * 1.規避dubbo的包裝,讓customer可以正常獲取message 2.丟擲RuntimeException使事務可以正確回滾 其他異常不處理 * * @param pjp 處理點 * @return Object */ @Around("servicePointcut() && transactionalPointcut()") public Object doTransactionalAround(ProceedingJoinPoint pjp) throws Throwable { try { return pjp.proceed(); } catch (Exception e) { Object[] args = pjp.getArgs(); // dubbo會將異常捕獲進行列印,這裡就不列印了 processException(pjp, args, e); // logger.error("service with @Transactional exception occured on dubbo service // side: " + e.getMessage(), e); throw new RuntimeException(e.getMessage(), e); } } /** * 處理異常 * * @param joinPoint 切點 * @param args 引數 * @param throwable 異常 */ private void processException(final ProceedingJoinPoint joinPoint, final Object[] args, Throwable throwable) { String inputParam = ""; if (args != null && args.length > 0) { StringBuilder sb = new StringBuilder(); for (Object arg : args) { sb.append(","); sb.append(arg); } inputParam = sb.toString().substring(1); } logger.error("n 方法: {}n 入參: {} n 錯誤資訊: {}", joinPoint.toLongString(), inputParam, Throwables.getStackTraceAsString(throwable)); } }
它的作用就是:
等等等等。。。。。。出問題了!此處還沒全完,為什麼?
一切Exception?這樣一來那麼包完後在Service層豈不是沒有Exception被丟擲了?如果Service方法涉及到資料庫操作沒有拋RuntimeException時資料庫事務怎麼回滾?
這才有了我們在這個handler類中有這麼一段內容,它的作用就是對一切有@Transactional註解的Service方法在其出錯時,還是照樣要拋"RuntimeException",對於其它的就都包成DubboResponse返回給呼叫者了(如下對於非事務型Service方法的異常的統一包裝):
@Around("servicePointcut() && !transactionalPointcut()") public Object doAround(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); try { return pjp.proceed(); } catch (Exception e) { processException(pjp, args, e); return DubboResponse.error("exception occured on dubbo service side: " + e.getMessage()); } catch (Throwable throwable) { processException(pjp, args, throwable); return DubboResponse.error("exception occured on dubbo service side: " + throwable.getMessage()); } }
好了,然後我們現在重新啟動我們的系統,我們再來看下面的執行範例。。。。。。
等!
忘記一件事,下面我給出位於“common”工程中的ProductVO和DubboResponse這兩個類的結構先,我寫博文不喜歡“藏”一手。
ProductVO.java
package org.sky.vo; import java.io.Serializable; public class ProductVO implements Serializable { private int stock = 0; public int getStock() { return stock; } public void setStock(int stock) { this.stock = stock; } public String getProductName() { return productName; } public int getProductId() { return productId; } public void setProductId(int productId) { this.productId = productId; } public void setProductName(String productName) { this.productName = productName; } private int productId = 0; private String productName = ""; }
DubboResponse.java
package org.sky.platform.util; import java.io.Serializable; import org.springframework.http.HttpStatus; import com.alibaba.fastjson.JSON; public class DubboResponse<T> implements Serializable { /** * */ private static final long serialVersionUID = 1L; /** * 狀態碼 */ private int code; /** * 返回資訊 */ private String message; /** * * 返回json物件 */ private T data; public DubboResponse(int code, String message) { this.code = code; this.message = message; } public DubboResponse(int code, String message, T data) { this.code = code; this.message = message; this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } public static <T> DubboResponse success(String message, T data) { String resultStr = JSON.toJSONString(data); return new DubboResponse(HttpStatus.OK.value(), message, data); } public static DubboResponse success(String message) { return success(message, null); } public static DubboResponse error(String message) { return new DubboResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), message, null); } public static DubboResponse error(int code, String message) { return new DubboResponse(code, message, null); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
“正常”據有@Transactional的Service方法拋異常演示:
現在我們把nacose-service和nacos-consumer執行起來看效果,試圖插入一個新的prouct:
得到返回:
再來看nacos-service端、nacos-consumer端以及資料庫
可以看到provider與consumer端都正確拋錯且資料庫中沒有插進去值。
“不正常”的不含有Transactional的(普通)Service方法拋異常被封裝演示:
我們現在做點小手腳,我們把provider端的“addProductAndStock(ProductVO prod)”方法上的@Transactional拿走來看看效果。
@Override public DubboResponse<ProductVO> addProductAndStock(ProductVO prod) throws DemoRpcRunTimeException { DubboResponse<ProductVO> response = null; int newProdId = 0; String prodSql = "insert into t_product(product_name)values(?)"; String stockSql = "insert into t_stock(product_id,stock)values(?,?)"; try { if (prod != null) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(new PreparedStatementCreator() { @Override
請像上面這樣的程式碼片端
我們再在nacos-consume端做一個小小的修改,如下所示,讓consumer端直接把provider端組裝好的{ "message" : "xxxx..."}顯示在“最前端”(一切通過 nginx端來存取consumer,consumer再通過provider呼叫資料庫,在這邊我們使用的是postman)。
然後我們來執行起來看一下效果:
我們可以看到,這一次在去除了@Transactional註解後,當Service方法拋錯時,請求端拿到的是我們經過包裝過的DubboResponse內的東西
provider端包裝普通Service丟擲的異常的核心程式碼:
@Around("servicePointcut() && !transactionalPointcut()") public Object doAround(ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); try { return pjp.proceed(); } catch (Exception e) { processException(pjp, args, e); return DubboResponse.error("exception occured on dubbo service side: " + e.getMessage()); } catch (Throwable throwable) { processException(pjp, args, throwable); return DubboResponse.error("exception occured on dubbo service side: " + throwable.getMessage()); } }
我們檢視我們的Provider端,它正是通過上述程式碼catch(Exception e)中的這一段來進行伺服器端紀錄檔的記錯和把錯誤包裝後返回給到consumer端的,就是下面這兩句:
processException(pjp, args, e); return DubboResponse.error("exception occured on dubbo service side: " + e.getMessage());
來看看nacos-service端的紀錄檔輸出:
來看看nacos-consumer端的紀錄檔輸出:
哈哈,這次nacos-consumer端無任何拋錯,因為錯誤已經被provider端包裝起來了。
當然,當我們看我們的DB端時,肯定,是有資料插入成功的。
因為前文說了,對於無@Transactional註解的方法,我們的aop handler類會把一切錯誤 “吃掉”,在後臺僅作記錄然後包成正常返回結果給到consumer端的,那麼provider端的Service方法既無RuntimeException丟擲,何來回滾?
當然是插入成功的!
t_product表
t_stock表
到此這篇關於阿里nacos+springboot+dubbo2.7.3統一處理異常的兩種方式的文章就介紹到這了,更多相關nacos+springboot+dubbo統一處理異常內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45