在网上很多关于dubbo异常统一处理的博文,90%都是抄来抄去。大多都是先上一段dubbo中对于异常的统一处理的原码,然后说一堆的(甚至有12345,五种)不靠谱方案,最后再说“本篇使用的是方案4”,然后再对所谓的方案4写了一段文字,最后还说不清!!!
本篇解决方案不会那么罗里吧嗦也不会贴dubbo源码来凑字数,我就直接从刚结束不久的双11保卫战性能全链路优化中我们的面对10万级别TPS的方案中提取的代码来说明这个dubbo统一处理异常是怎么个处理方式吧!
往往实际开发中的架构是这么一个样子的:
dubbo微服务架构简图不同层的开发人员都是不同的人或者是不同的几波人马;
无状态的API层(一组Tomcat对Nginx Web层的API暴露)是一组开发团队;
微服务Dubbo层是另一组开发团队;
在调试、测试、上线后我们经常会发生各种Exception,此时这几个不同的开发团队间会互相扯皮、打架,并且大家都要忙于定位这个Exception到底是发生在哪一层,甚至需要追溯Exception发生在哪个点(stackTrace)。
我们都知道在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端了。
nacos-parent工程
整个工程我已经放在git上了,地址请戳这里:nacos-dubbo-demo
工程的依赖结构如下:
由于dubbo与springboot结合的项目不多,很多网上有的博客也充斥着乱抄、自己都没有验证过就上代码的,因此大多网友们通过网上之言片语拼凑起来的项目在本地很难运行起来,不是maven包冲突就是少这个、那个包。下面给出工程的parent pom文件。
4.0.0
org.sky.demo
nacos-parent
0.0.1-SNAPSHOT
pom
Demo project for Spring Boot Dubbo Nacos
1.8
1.5.15.RELEASE
2.7.3
4.0.1
2.8.0
1.1.20
27.0.1-jre
1.2.59
2.7.3
1.1.4
5.1.46
3.4.2
1.8.13
0.0.1-SNAPSHOT
0.0.1-SNAPSHOT
${java.version}
${java.version}
3.8.1
3.2.3
3.1.2
UTF-8
UTF-8
org.springframework.boot
spring-boot-starter-web
${spring-boot.version}
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.apache.dubbo
dubbo-spring-boot-starter
${dubbo.version}
org.slf4j
slf4j-log4j12
org.apache.dubbo
dubbo
${dubbo.version}
org.apache.curator
curator-framework
${curator-framework.version}
org.apache.curator
curator-recipes
${curator-recipes.version}
mysql
mysql-connector-java
${mysql-connector-java.version}
com.alibaba
druid
${druid.version}
com.lmax
disruptor
${disruptor.version}
com.google.guava
guava
${guava.version}
com.alibaba
fastjson
${fastjson.version}
org.apache.dubbo
dubbo-registry-nacos
${dubbo-registry-nacos.version}
com.alibaba.nacos
nacos-client
${nacos-client.version}
org.aspectj
aspectjweaver
${aspectj.version}
org.apache.maven.plugins
maven-compiler-plugin
${compiler.plugin.version}
${java.version}
org.apache.maven.plugins
maven-war-plugin
${war.plugin.version}
org.apache.maven.plugins
maven-jar-plugin
${jar.plugin.version}
演示用数据库(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客户端的完整配置)
4.0.0
org.sky.demo
nacos-service
0.0.1-SNAPSHOT
nacos-service
服务者 Demo project for Spring Boot dubbo nacos
org.sky.demo
nacos-parent
0.0.1-SNAPSHOT
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-logging
org.apache.dubbo
dubbo
org.apache.curator
curator-framework
org.apache.curator
curator-recipes
mysql
mysql-connector-java
com.alibaba
druid
org.springframework.boot
spring-boot-starter-test
test
org.spockframework
spock-core
test
org.spockframework
spock-spring
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-log4j2
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-tomcat
org.aspectj
aspectjweaver
com.lmax
disruptor
redis.clients
jedis
com.google.guava
guava
com.alibaba
fastjson
org.apache.dubbo
dubbo-registry-nacos
com.alibaba.nacos
nacos-client
org.sky.demo
skycommon
${skycommon.version}
src/main/java
src/test/java
org.springframework.boot
spring-boot-maven-plugin
src/main/resources
src/main/webapp
META-INF/resources
**/**
src/main/resources
true
application.properties
application-${profileActive}.properties
然后我们设置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文件中引入
org.apache.dubbo
dubbo-registry-nacos
com.alibaba.nacos
nacos-client
org.apache.dubbo
dubbo
以及在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 addProductAndStock(ProductVO prod) throws DemoRpcRunTimeException {
DubboResponse 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
4.0.0
org.sky.demo
nacos-consumer
0.0.1-SNAPSHOT
nacos-service
消费者 Demo project for Spring Boot dubbo nacos
org.sky.demo
nacos-parent
0.0.1-SNAPSHOT
org.apache.dubbo
dubbo
org.apache.curator
curator-framework
org.apache.curator
curator-recipes
org.springframework.boot
spring-boot-starter-test
test
org.spockframework
spock-core
test
org.spockframework
spock-spring
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-log4j2
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-logging
com.lmax
disruptor
com.google.guava
guava
com.alibaba
fastjson
org.apache.dubbo
dubbo-registry-nacos
com.alibaba.nacos
nacos-client
org.aspectj
aspectjweaver
org.sky.demo
skycommon
${skycommon.version}
src/main/java
src/test/java
org.springframework.boot
spring-boot-maven-plugin
src/main/resources
src/main/webapp
META-INF/resources
**/**
src/main/resources
true
application.properties
application-${profileActive}.properties
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 sayHello(@RequestBody String params) throws Exception {
ResponseEntity response;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
Map resultMap = new HashMap<>();
JSONObject requestJsonObj = JSON.parseObject(params);
try {
String name = getHelloNameFromJson(requestJsonObj);
String answer = helloNacosService.sayHello(name);
logger.info("answer======>" + answer);
Map 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 addProduct(@RequestBody String params) throws Exception {
ResponseEntity response = null;
DubboResponse dubboResponse;
String returnResultStr;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
JSONObject requestJsonObj = JSON.parseObject(params);
Map 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的抛出了。
在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框架所捕捉然后进行了一次回滚。
我们希望把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 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 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 addProductAndStock(ProductVO prod) throws DemoRpcRunTimeException {
DubboResponse 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表