Dubbo 概述
什么是分布式系统
分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。
分布式系统(distributed system)是建立在网络之上的软件系统。
简单来说:多个(不同职责)人共同来完成一件事。
任何一台服务器都无法满足淘宝的双十一的数据吞吐量,一定是很多台服务器公共来完成的。
-
单一应用架构
当网站流量很小时,只需要一个应用,将所有的功能部署到一起(所有业务都放在一个 tomcat 里),从而减少部署节点和成本;
此时,用于简化增删改查工作量的数据访问框架 (ORM)是关键;
例如:某个超市的收银系统,某个公司的员工管理系统。
ORM:对象关系映射(Object Relational Mapping)。
优点:
- 小项目开发快,成本低
- 架构简单
- 易于测试
- 易于部署
缺点:
- 大项目模块耦合严重,不易开发,维护、沟通成本高
- 新增业务困难
- 核心业务与边缘业务混合在一块,出现问题互相影响
并发:1 - 10
-
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成几个互不相干的几个应用,以提高效率。
大模块按照 mvc 分层模式,进行拆分成多个互不相关的小模块,并且每个小模块都有独立的服务器。
此时,用于加速前端页面开发的 web 框架(MVC)是关键,因为每个小应用都有独立的页面。
MVC - 模型视图控制器 (Model View Controller)。
缺点:
- 模块之间不可能完全没有交集,公用模块无法重复利用,开发性的浪费。
并发:10 - 1000
-
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的业务,逐渐形成稳健的服务中心,使前端应用能更快速的响应多变的市场需求;
此时,用户提高业务复用及整合的分布式服务框架(RPC)远程调用是关键;
RPC:独立的应用服务器之间,要依靠 RPC(Romote Procedure Call)才能调用
当物流服务不忙,有 100 台服务器;而商品服务特别忙,也是 100 台服务器;那么,如何做到资源优化调配?
并发:1000 - 10000
-
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐呈现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率;
此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键;
SOA:面向服务架构(Service-Oriented Architecture),简单理解就是“服务治理”;例如,公交车站的“调度员”。
并发:10000+
Dubbo 简介
Dubbo 是分布式服务框架,是阿里巴巴的开源项目,现交给 apache 进行维护
Dubbo 致力于提高性能和透明化的 RPC 远程服务调用方案,以及 SOA 服务治理方案
简单来说,dubbo 是个服务框架,如果没有分布式的需求,是不需要用的
-
RPC
RPC - Remote Procedure Call 是指远程过程调用,是一种进程间通信方式。
RPC 基本的通信原理:
- 在客户端将对象进行序列化
- 底层通信框架使用 netty(基于 tcp 协议的 socket),将序列化的对象发给服务方提供方
- 服务提供方通过 socket 得到数据文件之后,进行反序列化,获得要操作的对象
- 对象数据操作完毕,将新的对象序列化,再通过服务提供方的 socket 返回给客户端
- 客户端获得序列化数据,再反序列化,得到最新的数据对象,至此,完成一次请求
RPC 两个核心模块:通讯(netty 的 socket),序列化。
-
节点角色
Provider - - 服务的提供方,暴露服务的服务方
Consumer - - 服务的消费方
Registry - - 服务注册与发现的注册中心
Monitor - - 监控服务的统计中心
Container - - 服务运行容器
0)Container 启动 Provider
1)Provider 注册 Registry
2)Consumer 订阅 Registry
3)Registry 通知 Consumer
4)Consumer 调用 Provider
5)Monitor 进行计数
-
调用关系
-
服务容器负责启动,加载,运行服务提供者
-
服务提供者在启动时,向注册中心注册自己提供的服务
-
服务消费者在启动时,向注册中心订阅自己所需的服务
-
在注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
-
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用
-
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
快速入门
Dubbo 官网:http://dubbo.apache.org/
注册中心
-
Zookeeper
官方推荐使用 zookeeper 注册中心;
注册中心负责服务地址的注册与查找,相当于目录服务;
服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小;
Zookeeper 是 apache hadoop 的子项目,是一个树形的目录服务,支持变更推送,适合作为 dubbo 的服务注册中心,工业强度较高,可用于生产环境。
Dubbo 既是求职的人(服务消费者),也是招聘单位(服务提供者),而 zookeeper 就是人才市场 / 招聘网站。
-
安装
1、安装 jdk
2、拷贝 apache-zookeeper-3.6.0-bin.tar.gz 到 opt 目录
3、解压安装包
tar -zxvf apache-zookeeper-3.6.0-bin.tar.gz
4、重命名
mv apache-zookeeper-3.6.0-bin zookeeper
5、在 /opt/zookeeper/ 这个目录上创建 zkData 和 zkLog 目录
mkdir zkData
mkdir zkLog
6、进入 /opt/zookeeper/conf 这个路径,复制一份 zoo_sample.cfg 文件并命名为 zoo.cfg
cp zoo_sample.cfg zoo.cfg
7、编辑 zoo.cfg 文件,修改 dataDir 路径
dataDir=/opt/zookeeper/zkData
dataLogDir=/opt/zookeeper/zkLog
8、启动 Zookeeper
./zkServer.sh start
9、查看状态
./zkServer.sh status
服务提供方
1、一个空的 maven 项目 dubbo-server
2、提供一个服务接口即可
-
服务方的 pom.xml
设置 tomcat 的端口为 8001
<packaging>war</packaging>
<!-- 指定编码及版本 -->
<properties>
<spring.version>5.0.6.RELEASE</spring.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!--dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.7</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.11.0.GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven </groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
-
服务方接口
public interface HelloService {
String sayHello(String name);
}
-
服务方实现
@com.alibaba.dubbo.config.annotation.Service
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
return "Hello " + name;
}
}
-
服务方的配置文件 spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 1.服务提供方在 zookeeper 中的“别名” -->
<dubbo:application name="dubbo-server"/>
<!-- 2.注册中心的地址 -->
<dubbo:registry address="zookeeper://192.168.186.128:2181"/>
<!-- 3.扫描类(将什么包下的类作为服务提供类) -->
<dubbo:annotation package="com.zm.service"/>
</beans>
-
服务方的 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</context-param>
</web-app>
服务消费方
另建一个 Maven 项目 dubbo-consumer
-
消费方的 pom.xml
与服务方一致,只需要修改 tomcat 的端口为 8002
-
消费方的 Controller
@Controller
public class HelloAction {
// 远程去服务提供方将 service 的实现类注入进来
@Reference
private HelloService helloService;
@GetMapping("hello")
@ResponseBody
public String sayHi(String name){
return helloService.sayHello(name);
}
}
-
消费方的接口
注意:
Controller 中要依赖 HelloService,所以创建一个接口;
这里是消费方,不需要实现接口,因为服务方会实现接口。
public interface HelloService {
String sayHello(String name);
}
-
消费方的 web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
-
消费方的 springmvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 1.服务提供方在 zookeeper 中的“别名” -->
<dubbo:application name="dubbo-consumer"/>
<!-- 2.注册中心的地址 -->
<dubbo:registry address="zookeeper://192.168.186.128:2181"/>
<!-- 3.扫描类(将什么包下的类作为服务消费类) -->
<dubbo:annotation package="com.zm.controller"/>
</beans>
-
启动服务测试
首先启动服务方,再启动消费方。
访问:http://localhost:8002/hello?name=renda
在 Zookeeper 客户端中运行以下代码可以查看注册的节点:
ls /dubbo/com.zm.service.HelloService/providers
ls /dubbo/com.zm.service.HelloService/consumers
监控中心
在开发时,需要知道注册中心都注册了哪些服务,以便开发和测试。
可以通过部署一个 web 应用版的管理中心来实现图形化显示注册中心的服务列表。
服务管理端
-
安装管理端
1)下载解压 dubbo-admin-master.zip:https://github.com/apache/dubbo-admin/tree/master
2)修改配置文件 dubbo-admin-master\dubbo-admin\src\main\resources\application.properties
的端口号和注册中心的地址
# 管理端的端口号,可以随意改为一个没有被占用的端口号
server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
dubbo.registry.address=zookeeper://192.168.186.128:2181
3)返回到项目根目录dubbo-admin-master\dubbo-admin
,使用 maven 打包:mvn clean package
,生成 target 目录
4)在 dos 下运行 target 目录中的 jar 文件: java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
5) 此时打开浏览器输入:http://localhost:7001/
;第一次访问时,需要登录,帐号密码都是 root
-
管理端使用
1)启动服务提供方,将服务注册到 zookeeper
2)启动 dubbo-server 服务方后,刷新管理端,服务注册成功,只是没有消费者
3)点击服务名,进入服务提供者页面
4)把消费者也运行起来,刷新服务,显示正常
5)查看消费者
监控统计中心
Monitor:统计中心 ,记录服务被调用多少次等
1)下载并解压 dubbo-monitor-simple-2.5.3.zip
2)修改 dubbo-monitor-simple-2.5.3\conf\dubbo.properties:
dubbo.container=log4j,spring,registry,jetty
dubbo.application.name=simple-monitor
dubbo.application.owner=
#dubbo.registry.address=multicast://192.168.186.128:2181
dubbo.registry.address=zookeeper://192.168.186.128:2181
#dubbo.registry.address=redis://127.0.0.1:6379
#dubbo.registry.address=dubbo://127.0.0.1:9090
dubbo.protocol.port=7070
dubbo.jetty.port=8080
dubbo.jetty.directory=${user.home}/monitor
dubbo.charts.directory=${dubbo.jetty.directory}/charts
dubbo.statistics.directory=${user.home}/monitor/statistics
dubbo.log4j.file=logs/dubbo-monitor-simple.log
dubbo.log4j.level=WARN
3)双击运行 dubbo-monitor-simple-2.5.3\bin\start.bat
4)分别修改 dubbo-server 和 dubbo-consumer 的 spring.xml,加入下面标签
<!-- 让监控去注册中心自动找服务 -->
<dubbo:monitor protocol="registry"/>
综合实战
配置说明
-
启动时检查
启动时会在注册中心检查依赖的服务是否可用,不可用时会抛出异常。
在消费方编写初始化容器的 main 方法启动(tomcat 启动方式,必须访问一次 action 才能初始化 spring;tomcat 启动方式会直接初始化 Spring,所以不使用这种方式启动)。
public class TestCheckException {
public static void main(String[] args) throws IOException {
// 初始化spring
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring.xml");
System.in.read();
}
}
<!-- 默认是 true:抛异常;false:不抛异常 -->
<dubbo:consumer check="false"/>
系统级别日志,需要配合 log4j 才输出,在 resources 下添加 log4j.properties,内容如下:
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=dubbo.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n
log4j.rootLogger=error, stdout,file
-
超时时间
由于网络或服务端不可靠,会导致调用过程中出现不确定的阻塞状态(超时)。
为了避免超时导致客户端资源(线程)挂起耗尽,必须设置超时时间。
在服务提供者添加如下配置:
<!-- 设置超时时间为 2 秒,默认为 1 秒 -->
<dubbo:provider timeout="2000"/>
可以将服务实现 HelloServiceImpl.java 中加入模拟的网络延迟进行测试:
@Override
public String sayHello(String name) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello " + name;
}
超时设置 2 秒,而模拟的网络延迟有 3 秒,超出时限,报错。
配置原则 - dubbo 推荐在 Provider 上尽量多配置 Consumer 端属性:
- 作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等
- 在 Provider 配置后,Consumer 不配置则会使用 Provider 的配置值,即 Provider 配置可以作消费者的默认值。
-
重试次数
当出现失败,自动切换并重试其它服务器,dubbo 重试的默认值是 2 次,可以自行设置
在 provider 提供方配置:
<!-- 设置超时时间为 2 秒,默认为 1 秒;设置重试次数为 3,一共最多执行 4 次 -->
<dubbo:provider timeout="2000" retries="3"/>
@Override
public String sayHello(String name) {
System.out.println("=============sayHello 被调用 1 次===============");
try {
// 模拟网络延迟
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello " + name;
}
并不是所有的方法都适合设置重试次数:
- 幂等方法:适合设置重试(当参数一样,无论执行多少次,结果是一样的;例如,查询,修改)
- 非幂等方法:不适合设置重试(当参数一样,执行结果不一样;例如,删除,添加)
单独设置某个方法
1)提供方接口添加 sayNo() 方法并实现
public interface HelloService {
String sayHello(String name);
String sayNo();
}
@Service
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
System.out.println("=============sayHello 被调用 1 次===============");
try {
// 模拟网络延迟
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello " + name;
}
@Override
public String sayNo() {
System.out.println("=============sayNo 被调用 1 次===============");
return "no";
}
}
2)消费方接口添加 sayNo() 方法声明
public interface HelloService {
String sayHello(String name);
String sayNo();
}
3)消费方 controller
@Controller
public class HelloAction {
// 远程去服务提供方将 service 的实现类注入进来
// @Reference // 此注解已经在 xml 文件中被 <dubbo:reference> 顶替,所以自动注入即可
@Autowired
private HelloService helloService;
@GetMapping("hello")
@ResponseBody
public String sayHi(String name){
return helloService.sayHello(name);
}
@GetMapping("no")
@ResponseBody
public String no(){
return helloService.sayNo();
}
}
4)消费方配置方法重试次数
<dubbo:reference interface="com.zm.service.HelloService" id="helloService">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
-
多版本
一个接口,多个(版本的)实现类,可以使用定义版本的方式引入
为 HelloService 接口定义两个实现类,提供者修改配置:
<dubbo:service interface="com.zm.service.HelloService" class="com.zm.service.impl.HelloService01Impl"
version="1.0.0"/>
<dubbo:service interface="com.zm.service.HelloService" class="com.zm.service.impl.HelloService02Impl"
version="2.0.0"/>
消费者就可以根据 version 的版本,选择具体的服务版本
<dubbo:reference interface="com.zm.service.HelloService" id="helloService" version="1.0.0">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
注意:消费者的控制层要改为自动注入,因为 @Reference 注解和 <dubbo:reference> 在这里冲突
@Controller
public class HelloAction {
@Autowired
private HelloService helloService;
...
}
当消费者的版本修改为 version="*",那么就会随机调用服务提供者的版本
==========01===sayNo 被调用 1 次===============
==========02===sayNo 被调用 1 次===============
==========01===sayNo 被调用 1 次===============
==========02===sayNo 被调用 1 次===============
==========01===sayNo 被调用 1 次===============
-
本地存根
远程服务后,客户端通常只剩下接口,而实现全在服务器端,但提供方有些时候想在客户端也执行部分逻辑,比如:做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy。
目前分布式架构搭建起来有一个问题就是所有的操作全都是消费者发起,由服务提供者执行
消费者动动嘴皮子却什么活都不干,这样会让提供者很累,例如简单的参数验证,消费者完全能够胜任,把合法的参数再发送给提供者执行,效率高了,提供者也没那么累了
先在消费者处理一些业务逻辑,再调用提供者的过程,就是“本地存根”
代码实现肯定在消费者,创建一个 HelloServiceStub 类并且实现 HelloService 接口
注意:必须使用构造方法的方式注入
Stub 必须有可传入 Proxy 的构造函数。
在消费者创建一个 Stub 实现 HelloService 接口,并有一个传入远程 HelloService 实例的构造函数:
public class HelloServiceStub implements HelloService {
// helloService 的代理对象
private final HelloService helloService;
/**
* 本地存根必须以构造方法的形式注入
*/
public HelloServiceStub(HelloService helloService) {
this.helloService = helloService;
}
@Override
public String sayHello(String name) {
System.out.println("HelloServiceStub:本地存根");
if(!StringUtils.isEmpty(name)){
return helloService.sayHello(name);
}
return "Name is Empty";
}
@Override
public String sayNo() {
return helloService.sayNo();
}
}
修改消费者配置:
<dubbo:reference interface="com.zm.service.HelloService" id="helloService" version="2.0.0"
stub="com.zm.service.stub.HelloServiceStub">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
负载均衡策略
负载均衡 - Load Balance,其实就是将请求分摊到多个操作单元上进行执行,从而共同完成工作任务。
简单的说,好多台服务器,不能总是让一台服务器干活,应该“雨露均沾”。
Dubbo 提供了 4 种负载均衡实现,分别是基于权重随机算法的RandomLoadBalance(默认)、基于最少活跃调用数算法的 LeastActiveLoadBalance、基于 hash 一致性的 ConsistentHashLoadBalance,以及基于加权轮询算法的 RoundRobinLoadBalance。
修改提供者配置并启动 3 个提供者,让消费者对其进行访问:
- tomcat 端口 8001,8002,8003
- provider 端口 20881,20882,20883
<dubbo:provider timeout="2000" port="20881"/>
- HelloService01Impl 类,服务器 1,服务器 2,服务器 3
@Override
public String sayHello(String name) {
System.out.println("=====服务器1=====02===sayHello 被调用 1 次===============");
return "Hello " + name;
}
- 启动 consumer 进行测试
修改消费方的端口防止端口冲突,然后修改负载均衡策略
<dubbo:reference loadbalance="roundrobin" interface="com.zm.service.HelloService" id="helloService"
version="2.0.0" stub="com.zm.service.stub.HelloServiceStub">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
也可以使用 dubbo-admin 管理端修改权重
高可用
-
ZooKeeper 宕机
ZooKeeper 注册中心宕机,还可以消费 dubbo 暴露的服务
-- 监控中心宕掉不影响使用,只是丢失部分采样数据
-- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
-- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
-- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
-- 服务提供者无状态,任意一台宕掉后,不影响使用
-- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
测试“注册中心全部宕掉”的情况:
-- 正常发出请求
-- 关闭 zookeeper:./zkServer.sh stop
-- 消费者仍然可以正常消费
服务降级
壁虎遇到危险会自动脱落尾巴,目的是损失不重要的东西,保住重要的。
服务降级,就是根据实际的情况和流量,对一些服务有策略的停止或换种简单的方式处理,从而释放服务器的资源来保证核心业务的正常运行。
-
为什么要服务降级
而为什么要使用服务降级,这是防止分布式服务发生雪崩效应。
什么是雪崩?就是蝴蝶效应,当一个请求发生超时,一直等待着服务响应,那么在高并发情况下,很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪崩。
-
服务降级实现方式
在管理控制台配置服务降级:屏蔽和容错。
屏蔽:mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用,用来屏蔽不重要服务不可用时对调用方的影响。
容错:mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
整合 MyBatis 实现用户注册
-
初始化数据库
CREATE DATABASE IF NOT EXISTS `smd` DEFAULT CHARACTER SET utf8mb4;
USE `smd`;
DROP TABLE IF EXISTS users;
CREATE TABLE users (
`uid` INT (11) AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR (50) NOT NULL,
`password` VARCHAR (50) NOT NULL,
`phone` VARCHAR (50) NOT NULL,
`createtime` VARCHAR (50) NOT NULL
);
-- 查询
SELECT * FROM `users`;
-
创建聚合项目 - 项目模块化
zm-dubbo-parent
父工程,聚合项目 - 定义所有模块用的依赖版本
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>zm-dubbo-entity</module>
<module>zm-dubbo-dao</module>
<module>zm-dubbo-interface</module>
<module>zm-dubbo-service</module>
<module>zm-dubbo-web</module>
</modules>
<packaging>pom</packaging>
<!-- 指定编码及版本 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<java.version>1.11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring.version>5.0.6.RELEASE</spring.version>
</properties>
<dependencies>
<!-- JSP相关 -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
<version>2.5</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<scope>provided</scope>
<version>2.0</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<!--dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.7</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.11.0.GA</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
renda-dubbo-entity 模块
实体工程,jar 项目
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zm-dubbo-parent</artifactId>
<groupId>com.zm</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zm-dubbo-entity</artifactId>
</project>
com.zm.entity.Users
public class Users implements Serializable {
private int uid;
private String username;
private String password;
private String phone;
private String createtime;
public Users() {
}
public Users(String username, String password, String phone, String createtime) {
this.username = username;
this.password = password;
this.phone = phone;
this.createtime = createtime;
}
// getter, setter, toString ...
}
zm-dubbo-dao 模块
数据访问层工程,jar 项目
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zm-dubbo-parent</artifactId>
<groupId>com.zm</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zm-dubbo-dao</artifactId>
<dependencies>
<dependency>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-entity</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
com.zm.mapper.UserMapper
@Transactional
public interface UserMapper {
int register(User user);
}
src\main\resources\com\zm\mapper\UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zm.mapper.UserMapper">
<insert id="register">
insert into users (username,password,phone,createtime)
values (#{username},#{password},#{phone},#{createtime})
</insert>
</mapper>
src\main\resources\mybatis\mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 后台的日志输出:针对开发者 -->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
src\main\resources\log4j.properties
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=dubbo.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n
log4j.rootLogger=error, stdout,file
src\main\resources\jdbc.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.186.128:3306/smd?serverTimezone=GMT&characterEncoding=utf8&useSSL=false
jdbc.username=root
jdbc.password=RendaZhang@666
jdbc.maxActive = 10
jdbc.minIdle = 5
src\main\resources\spring\spring-dao.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 引入 jdbc.properties -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 数据连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="maxActive" value="${jdbc.maxActive}"/>
<property name="minIdle" value="${jdbc.minIdle}"/>
</bean>
<!-- sqlsessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
</bean>
<!-- mapper 映射扫描 MapperScannerConfigurer 扫描该包下所有接口,生成代理对象存到 IOC 容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.zm.mapper"/>
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开始事务注解的支持 -->
<tx:annotation-driven/>
</beans>
com.zm.DaoTest
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:spring/spring-dao.xml"})
public class DaoTest {
@Autowired
private UserMapper userMapper;
@Test
public void register() {
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
User user = new User("张三", "123456", "123456789456", date);
userMapper.register(user);
System.out.println("注册成功");
}
}
zm-dubbo-interface 模块
服务接口定义工程,jar 项目
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zm-dubbo-parent</artifactId>
<groupId>com.zm</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zm-dubbo-interface</artifactId>
<dependencies>
<dependency>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-dao</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-entity</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
com.zm.service.UserService
public interface UserService {
int register(User user);
}
zm-dubbo-service 模块
privoder 服务提供者工程,war 项目
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zm-dubbo-parent</artifactId>
<groupId>com.zm</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<artifactId>zm-dubbo-service</artifactId>
<dependencies>
<dependency>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-dao</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
com.zm.service.impl.UserServiceImpl
@Service // 使用 Dubbo 的 @Service 暴露服务(向 zookeeper 注册服务)
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public int register(User user) {
return userMapper.register(user);
}
}
src\main\resources\spring\spring-service.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 服务提供方在 zookeeper 中的“别名” -->
<dubbo:application name="zm-dubbo-service"/>
<!-- 注册中心的地址 -->
<dubbo:registry address="zookeeper://192.168.186.128:2181"/>
<!-- 扫描类(将什么包下的类作为服务提供类) -->
<dubbo:annotation package="com.zm.service.impl"/>
<!-- 设置超时时间为 60 秒,默认为 1 秒 -->
<dubbo:provider timeout="60000"/>
</beans>
src\main\webapp\WEB-INF\web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-*.xml</param-value>
</context-param>
</web-app>
com.zm.TestService
@RunWith(SpringJUnit4ClassRunner.class)
// 扫描所有模块下的 spring-*.xml 的配置文件
@ContextConfiguration(locations = {"classpath*:spring/spring-*.xml"})
public class TestService {
// 这个 userService 会标红是因为使用了 Dubbo 来存放实现类,Spring 容器中没有存放,而 IDEA 没有识别出来
@Autowired
private UserService userService;
@Test
public void register() {
String time = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
User user = new User("张三", "cvws", "256561652", time);
userService.register(user);
System.out.println("注册成功!");
}
}
zm-dubbo-web 模块
consumer 服务消费者工程,war 项目
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zm-dubbo-parent</artifactId>
<groupId>com.zm</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<artifactId>zm-dubbo-web</artifactId>
<dependencies>
<dependency>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.zm</groupId>
<artifactId>zm-dubbo-entity</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8002</port>
<path>/</path>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
src\main\webapp\WEB-INF\web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<!-- 解决 post 乱码 -->
<filter>
<filter-name>charset</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>charset</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
src\main\resources\spring\spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 处理器映射器-处理器适配器。进行了功能的增强:支持 json 的读写 -->
<mvc:annotation-driven>
<!-- json 转换器 -->
<mvc:message-converters register-defaults="true">
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes" value="application/json"/>
<property name="features">
<array>
<value>WriteMapNullValue</value>
<value>WriteDateUseDateFormat</value>
</array>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- 配置放行静态资源 -->
<mvc:default-servlet-handler/>
<!-- 服务提供方在 zookeeper 中的“别名” -->
<dubbo:application name="zm-dubbo-web"/>
<!-- 注册中心的地址 -->
<dubbo:registry address="zookeeper://192.168.186.128:2181"/>
<!-- 扫描类(将什么包下的类作为消费类) -->
<dubbo:annotation package="com.zm.controller"/>
</beans>
com.zm.controller.UserAction
@Controller
public class UserAction {
@Reference
private UserService userService;
@RequestMapping("register")
@ResponseBody
public String register(User user) {
System.out.println("欢迎注册");
try {
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
user.setCreatetime(date);
userService.register(user);
return "注册成功";
} catch (Exception e) {
e.printStackTrace();
return "注册失败";
}
}
}
src\main\webapp\index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>用户注册</title>
</head>
<body>
<form>
<p>username: <input name="username"></p>
<p>password: <input name="password"></p>
<p>phone: <input name="phone"></p>
<p><input type="button" id="btn" value="register"></p>
</form>
</body>
<script src="js/jquery-3.3.1.min.js"></script>
<script>
$("#btn").on("click", function () {
$.ajax({
type: "post",
url: "register",
data: $("form").serialize(),
success: function (data) {
console.log(data);
alert("注册成功");
},
error: function (xhr, type, errorThrown) {
console.log("xhr:" + xhr + " type:" + type + " errorThrown:" + errorThrown);
}
});
});
</script>
</html>
-
启动测试
-
先选择父项目(聚合工程)进行全员安装成 jar
-
启动服务 service
-
启动调用者 web
网友评论