阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式

在网上很多关于dubbo异常统一处理的博文,90%都是抄来抄去。大多都是先上一段dubbo中对于异常的统一处理的原码,然后说一堆的(甚至有12345,五种)不靠谱方案,最后再说“本篇使用的是方案4”,然后再对所谓的方案4写了一段文字,最后还说不清!!!

本篇解决方案不会那么罗里吧嗦也不会贴dubbo源码来凑字数,我就直接从刚结束不久的双11保卫战性能全链路优化中我们的面对10万级别TPS的方案中提取的代码来说明这个dubbo统一处理异常是怎么个处理方式吧!

1. 为什么要抛异常?

不同开发团队间便于追溯异常的来源以及为了便于定位问题的需要

往往实际开发中的架构是这么一个样子的:

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第1张图片 dubbo微服务架构简图

不同层的开发人员都是不同的人或者是不同的几波人马;

无状态的API层(一组Tomcat对Nginx Web层的API暴露)是一组开发团队;

微服务Dubbo层是另一组开发团队;

在调试、测试、上线后我们经常会发生各种Exception,此时这几个不同的开发团队间会互相扯皮、打架,并且大家都要忙于定位这个Exception到底是发生在哪一层,甚至需要追溯Exception发生在哪个点(stackTrace)。

Service层有数据库事务一致性的问题必须抛出异常

我们都知道在spring中的Service层必须抛出Runtime Exception,否则Service层的方法如果有涉及数据库的修改操作是不会回滚的。

2. 给出解决方案

其实解决方案真正的无外乎就2种:

  • provider向远程consumer层直接抛RuntimeException即可;
  • provider端把所有的Exception进行统一包装,向consumer端返回json报文体的类似message:xxx,code:500,data{xxx:xxx,xxx:xxx}这样的消息而在provider端进行“logger.error”的记录即可;

本文把这2种实现方式都给实现了,下面开始直接show me the code的方式来说话吧。

3. 两种抛异常的实例解说

环境搭建

nacos1.1.4

我们这边不用dubbo admin,因为dubbo admin太老且使用不方便,缺少了很多管理微服务所需要的基本功能。并且dubbo从2.6开始已经把dubbo admin从它的主工程里分离了出去,同时dubbo2.6开始支持nacos registry了。

目前来说nacos是最方便、效率最高、功能最强大的微服务发现组件(甚至支持spring cloud)。

下载地址在这里(请戳):阿里nacos最新下载地址

下载后直接解压,然后进行nacos配置

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第2张图片

编辑这个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+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第3张图片

登录界面中使用nacos/nacos即可进行登录了。

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第4张图片

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第5张图片

登录后看到nacos管理界面就说明nacos配置和启动成功了。接下来我们就要开始书写dubbo的provider端与consumer端了。

dubbo工程搭建

nacos-parent工程

整个工程我已经放在git上了,地址请戳这里:nacos-dubbo-demo

工程的依赖结构如下:

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第6张图片

 

由于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}
					${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的方式注入。

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第7张图片

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表数据

插两张表时,只要有一点点错误那么整个插入事务回滚,否则成功。这边需要注意的就是:

  1. springboot service只有接到RuntimeException才会回滚;
  2. 要把RuntimeException从provider远程传递到consumer端,包括把stackTrace这些信息也远程传递到consumer端,那么这个exception必须是serializable的;
  3. 暴露成dubbo provider service的service方法必须加上@Service注解,这个Service可不是spring annotation的service而是ali dubbo的service,在2.7.3开始变成了org.apache.dubbo包了。它配合着springboot的主启动文件中的@EnableDubbo来启作用,它在启动后会通过application.properties中的dubbo.scan.base-packages中所指的路径把这个路径下所有的类寻找是否带有@Service注解,如有那么就把它通过nacos-registry给注册到nacos中去;

 

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+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第8张图片

nacos-consumer运行示例:

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第9张图片

然后我们去nacos的管理界面查看一下,就能发现provider和consumer都注册成功了。

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第10张图片

接着我们使用postman对consumer发一个json请求

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第11张图片

得到返回如下所示

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第12张图片

再看数据库中

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第13张图片

这说明我们的dubbo+nacos搭建完全运行正常,接下来就要演示两种Exception的抛出了。

第1种:直接从provider端抛RuntimeException到consumer端

在provider端我们对ProductServiceImpl进行一个小修改如下:

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第14张图片

我们写了一句:

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+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第15张图片

来看nacos-service端的日志,这是我们在provider端人为手工抛出的一条日志:

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第16张图片

来看nacos-consumer端的日志,我们可以看到provider端的异常甚至包括它的stackTrace信息都已经传递到了consumer端了:

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第17张图片

这样的话consumer端的开发人员一看传过来了这个错误就会跑到dubbo开发团队处吼一下:喂,生产上有一个bug,你看这就是你们provider端抛出来的,改吧!

为了确保我们的ExceptionHandler拦截的是否成功,我们来看数据库端:

t_product表没有插入coffee的记录

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第18张图片

t_stock表也没有插入相关coffee的库存

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第19张图片

说明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使用一个叫DubboResponse的请求体来返回provider端的service报文;
  • 如果provider端出错,那么也把错误的系统code与系统message“包”在DubboResponse内

等等等等。。。。。。出问题了!此处还没全完,为什么?

一切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+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第20张图片

得到返回:

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第21张图片

再来看nacos-service端、nacos-consumer端以及数据库

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第22张图片

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第23张图片

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第24张图片

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第25张图片

可以看到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)。

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第26张图片

然后我们来运行起来看一下效果:

我们可以看到,这一次在去除了@Transactional注解后,当Service方法抛错时,请求端拿到的是我们经过包装过的DubboResponse内的东西

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第27张图片

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+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第28张图片

来看看nacos-consumer端的日志输出

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第29张图片

哈哈,这次nacos-consumer端无任何抛错,因为错误已经被provider端包装起来了。

当然,当我们看我们的DB端时,肯定,是有数据插入成功的。

因为前文说了,对于无@Transactional注解的方法,我们的aop handler类会把一切错误 “吃掉”,在后台仅作记录然后包成正常返回结果给到consumer端的,那么provider端的Service方法既无RuntimeException抛出,何来回滚?

当然是插入成功的!

t_product表

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第30张图片

t_stock表

阿里的nacos+springboot+dubbo2.7.3集成以及统一处理异常的两种方式_第31张图片

总结

  1. 所以在dubbo的provider端的RuntimeExeption并且是"implements Serializable"的就可以连着stackTrace抛到远程的consumer端;
  2. 实际项目中dubbo的provider(dubbo群)与dubbo的consumer(一堆无状态的tomcat为容器布署的api controller)间如果只是为了传stackTrace来消耗网络硬件等资源只是为了“排查定位问题”方便,这么做是不值的,那么就要包一层,包时不要包的太过了,记得涉及数据库事务的方法一定要抛RuntimeException,要不然就是插进去一堆脏数据;

你可能感兴趣的:(架构师之路)