阿里nacos+springboot+dubbo2.7.3統(tǒng)一處理異常的兩種方式
在網(wǎng)上很多關于dubbo異常統(tǒng)一處理的博文,90%都是抄來抄去。大多都是先上一段dubbo中對于異常的統(tǒng)一處理的原碼,然后說一堆的(甚至有12345,五種)不靠譜方案,最后再說“本篇使用的是方案4”,然后再對所謂的方案4寫了一段文字,最后還說不清?。。?/p>
本篇解決方案不會那么羅里吧嗦也不會貼dubbo源碼來湊字數(shù),我就直接從剛結束不久的雙11保衛(wèi)戰(zhàn)性能全鏈路優(yōu)化中我們的面對10萬級別TPS的方案中提取的代碼來說明這個dubbo統(tǒng)一處理異常是怎么個處理方式吧!
1. 為什么要拋異常?
不同開發(fā)團隊間便于追溯異常的來源以及為了便于定位問題的需要
往往實際開發(fā)中的架構是這么一個樣子的:

dubbo微服務架構簡圖
不同層的開發(fā)人員都是不同的人或者是不同的幾波人馬;
無狀態(tài)的API層(一組Tomcat對Nginx Web層的API暴露)是一組開發(fā)團隊;
微服務Dubbo層是另一組開發(fā)團隊;
在調試、測試、上線后我們經(jīng)常會發(fā)生各種Exception,此時這幾個不同的開發(fā)團隊間會互相扯皮、打架,并且大家都要忙于定位這個Exception到底是發(fā)生在哪一層,甚至需要追溯Exception發(fā)生在哪個點(stackTrace)。
Service層有數(shù)據(jù)庫事務一致性的問題必須拋出異常
我們都知道在spring中的Service層必須拋出Runtime Exception,否則Service層的方法如果有涉及數(shù)據(jù)庫的修改操作是不會回滾的。
2. 給出解決方案
其實解決方案真正的無外乎就2種:
- provider向遠程consumer層直接拋RuntimeException即可;
- provider端把所有的Exception進行統(tǒng)一包裝,向consumer端返回json報文體的類似message:xxx,code:500,data{xxx:xxx,xxx:xxx}這樣的消息而在provider端進行“logger.error”的記錄即可;
本文把這2種實現(xiàn)方式都給實現(xiàn)了,下面開始直接show me the code的方式來說話吧。
3. 兩種拋異常的實例解說
環(huán)境搭建
nacos1.1.4
我們這邊不用dubbo admin,因為dubbo admin太老且使用不方便,缺少了很多管理微服務所需要的基本功能。并且dubbo從2.6開始已經(jīng)把dubbo admin從它的主工程里分離了出去,同時dubbo2.6開始支持nacos registry了。
目前來說nacos是最方便、效率最高、功能最強大的微服務發(fā)現(xiàn)組件(甚至支持spring cloud)。
下載地址在這里(請戳):阿里nacos最新下載地址
下載后直接解壓,然后進行nacos配置

編輯這個application.properties文件,我們把nacos自動服務發(fā)現(xiàn)管理端連上自己開發(fā)環(huán)境上的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工程
整個工程我已經(jīng)放在git上了,地址請戳這里:nacos-dubbo-demo
工程的依賴結構如下:

由于dubbo與springboot結合的項目不多,很多網(wǎng)上有的博客也充斥著亂抄、自己都沒有驗證過就上代碼的,因此大多網(wǎng)友們通過網(wǎng)上之言片語拼湊起來的項目在本地很難運行起來,不是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>演示用數(shù)據(jù)庫(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中對于數(shù)據(jù)庫一致性插入時在碰到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的部分配置是相對于我虛擬出來的模擬環(huán)境4C CPU,4GB內存來設的,具體更多設置參數(shù)可以直接參照于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協(xié)議依舊使用dubbo,這是因為dubbo2.6中已經(jīng)帶入了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申明該項目會使用數(shù)據(jù)庫事務;
把項目連接上數(shù)據(jù)庫
我們使用druid做數(shù)據(jù)庫的連接池。
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統(tǒng)一處理器
* 可以將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來對于遠程的用于具體實現(xiàn)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;
}具體業(yè)務邏輯實現(xiàn)類,ProductServiceImpl
該類做這么一件事:
1)插入t_product表數(shù)據(jù)
2)插入t_stock表數(shù)據(jù)
插兩張表時,只要有一點點錯誤那么整個插入事務回滾,否則成功。這邊需要注意的就是:
- springboot service只有接到RuntimeException才會回滾;
- 要把RuntimeException從provider遠程傳遞到consumer端,包括把stackTrace這些信息也遠程傳遞到consumer端,那么這個exception必須是serializable的;
- 暴露成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<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;
}
}這個類目前是正常狀態(tài),我們先調用一把正常的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參數(shù)設置。因為dubbo有3個核心參數(shù)集,provider, protocol, consumer。而在consumer做的設置由于這3者的優(yōu)先級問題,它是會覆蓋掉provider端的設置。如果是在大規(guī)模微服務開發(fā)場景中,每個consumer都做自己的個性化設置,這不利于全局上對系統(tǒng)性能進行集中統(tǒng)一的管控,因此這需要公司的架構師對這些規(guī)范進行provider端的統(tǒng)一管控,一定盡量避免在consumer端去設置本該屬于central(provider)端的一些參數(shù)。
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沒多少區(qū)別,注意要@EnableDubbo,要不然spring不會在項目啟動時把consumer端給注冊到nacos的注冊中心去。
consumer端的Controller
這個consumer端正是我第一張圖中的無狀態(tài)的API層,這一層會有一堆tomcat/netty/jboss一類的東西,它們做的事就是路由API,以json格式向客戶端(手機、網(wǎng)頁、小程序)進行返回。這一層是不會去和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相當?shù)暮唵?,直接通過遠程接口調用dubbo得到一個返回。
運行例子
確保我們的nacos1.1.4運行在那。
然后先運行nacos-service的Application.java再運行nacos-consumer的Application.java
nacos-service運行示例:

nacos-consumer運行示例:

然后我們去nacos的管理界面查看一下,就能發(fā)現(xiàn)provider和consumer都注冊成功了。

接著我們使用postman對consumer發(fā)一個json請求

得到返回如下所示

再看數(shù)據(jù)庫中


這說明我們的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才會讓數(shù)據(jù)庫事物回滾,但是我們也不用擔心,還記得我們在nacos-service中已經(jīng)注入了一個aop的攔截器叫“DemoRpcRuntimeExceptionHandler”嗎?
它的作用就是攔住一切Exception然后把它轉化成RuntimeException。
好,我們加完這一句話后重新依次運行nacose-service和nacos-consumer。然后同樣通過postman來訪問http://localhost:8082/nacosconsumer/addProductAndStock,然后我們使用新的產品品名,post請求體內的報文如下所 示:
{"productname":"coffee","stock":10000}看,我們這次請求過去后直接在response中出現(xiàn)的是什么?

來看nacos-service端的日志,這是我們在provider端人為手工拋出的一條日志:

來看nacos-consumer端的日志,我們可以看到provider端的異常甚至包括它的stackTrace信息都已經(jīng)傳遞到了consumer端了:

這樣的話consumer端的開發(fā)人員一看傳過來了這個錯誤就會跑到dubbo開發(fā)團隊處吼一下:喂,生產上有一個bug,你看這就是你們provider端拋出來的,改吧!
為了確保我們的ExceptionHandler攔截的是否成功,我們來看數(shù)據(jù)庫端:
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端的開發(fā)人員通過日志查詢和定位問題即可。
為什么還有這種做法?
很簡單,因為stackTrace是異常追溯,調用到了jvm的棧內信息了,這個是“很重”的一件活 。我們把一堆的異常Exception通過provider和consumer端拋來拋去,本來我們用dubbo就是用來做微服務的、就是為了應對大規(guī)模的并發(fā)請求的、就是為了做系統(tǒng)的彈性伸縮和高冗余的,你還在用這么大一陀stackTrace在兩端傳來傳去不說,還要加上傳時序列化、接到時反序列化,這不是增加了系統(tǒng)的開銷嗎?
下面直接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注解的方法異常處理切面 將自定義的業(yè)務異常轉為RuntimeException:
* 1.規(guī)避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 參數(shù)
* @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 入?yún)? {} \n 錯誤信息: {}", joinPoint.toLongString(), inputParam,
Throwables.getStackTraceAsString(throwable));
}
}它的作用就是:
- 把一切Exception使用一個叫DubboResponse的請求體來返回provider端的service報文;
- 如果provider端出錯,那么也把錯誤的系統(tǒng)code與系統(tǒng)message“包”在DubboResponse內
等等等等。。。。。。出問題了!此處還沒全完,為什么?
一切Exception?這樣一來那么包完后在Service層豈不是沒有Exception被拋出了?如果Service方法涉及到數(shù)據(jù)庫操作沒有拋RuntimeException時數(shù)據(jù)庫事務怎么回滾?
這才有了我們在這個handler類中有這么一段內容,它的作用就是對一切有@Transactional注解的Service方法在其出錯時,還是照樣要拋"RuntimeException",對于其它的就都包成DubboResponse返回給調用者了(如下對于非事務型Service方法的異常的統(tǒng)一包裝):
@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());
}
}好了,然后我們現(xiàn)在重新啟動我們的系統(tǒng),我們再來看下面的運行示例。。。。。。
等!
忘記一件事,下面我給出位于“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;
/**
* 狀態(tài)碼
*/
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;
}
}“正常”據(jù)有@Transactional的Service方法拋異常演示:
現(xiàn)在我們把nacose-service和nacos-consumer運行起來看效果,試圖插入一個新的prouct:

得到返回:

再來看nacos-service端、nacos-consumer端以及數(shù)據(jù)庫




可以看到provider與consumer端都正確拋錯且數(shù)據(jù)庫中沒有插進去值。
“不正常”的不含有Transactional的(普通)Service方法拋異常被封裝演示:
我們現(xiàn)在做點小手腳,我們把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調用數(shù)據(jù)庫,在這邊我們使用的是postman)。

然后我們來運行起來看一下效果:
我們可以看到,這一次在去除了@Transactional注解后,當Service方法拋錯時,請求端拿到的是我們經(jīng)過包裝過的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端無任何拋錯,因為錯誤已經(jīng)被provider端包裝起來了。
當然,當我們看我們的DB端時,肯定,是有數(shù)據(jù)插入成功的。
因為前文說了,對于無@Transactional注解的方法,我們的aop handler類會把一切錯誤 “吃掉”,在后臺僅作記錄然后包成正常返回結果給到consumer端的,那么provider端的Service方法既無RuntimeException拋出,何來回滾?
當然是插入成功的!
t_product表

t_stock表

總結
- 所以在dubbo的provider端的RuntimeExeption并且是"implements Serializable"的就可以連著stackTrace拋到遠程的consumer端;
- 實際項目中dubbo的provider(dubbo群)與dubbo的consumer(一堆無狀態(tài)的tomcat為容器布署的api controller)間如果只是為了傳stackTrace來消耗網(wǎng)絡硬件等資源只是為了“排查定位問題”方便,這么做是不值的,那么就要包一層,包時不要包的太過了,記得涉及數(shù)據(jù)庫事務的方法一定要拋RuntimeException,要不然就是插進去一堆臟數(shù)據(jù);
到此這篇關于阿里nacos+springboot+dubbo2.7.3統(tǒng)一處理異常的兩種方式的文章就介紹到這了,更多相關nacos+springboot+dubbo統(tǒng)一處理異常內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Java實現(xiàn)經(jīng)典游戲打磚塊游戲的示例代碼
這篇文章主要介紹了如何利用Java實現(xiàn)經(jīng)典的游戲—打磚塊。玩家操作一根螢幕上水平的“棒子”,讓一顆不斷彈來彈去的“球”在撞擊作為過關目標消去的“磚塊”的途中不會落到螢幕底下。感興趣的小伙伴可以了解一下2022-02-02
完美解決IDEA Ctrl+Shift+f快捷鍵突然無效的問題
這篇文章主要介紹了完美解決IDEA Ctrl+Shift+f快捷鍵突然無效的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02

