springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;
spring boot 2.1.1
dubbo-spring-boot-starter 0.2.1-SNAPSHOT
dubbo 2.6.5
curator 4.1.0
zookeeper-3.5.4-beta
dubbo目前已经迁移到Apache下,作为Apache的顶级项目开源,并且其提供springboot的支持。dubbo-spring-boot-starter官网文档:incubator-dubbo-spring-boot-project
准备三个module:
这里我们演示一下dubbo参数的校验,固需要引入spring-boot-starter-validation
。引入依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-validationartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<scope>providedscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-jar-pluginartifactId>
<version>3.1.1version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>trueaddDefaultImplementationEntries>
manifest>
archive>
configuration>
plugin>
plugins>
build>
编写一个实体类
@Data
@Builder
@ToString
public class Stu implements Serializable {
private Integer id;
@NotEmpty
private String name;
@Range(min = 10, max = 20)
private Integer age;
private Date birthday;
}
编写测试接口
public interface DemoService {
String sayHello(String name);
Stu sayHello1(Stu stu);
}
API 编写完成,maven install 部署到本地仓库。
引入依赖:
<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.0modelVersion>
<artifactId>mysqltest4dubboServiceartifactId>
<packaging>jarpackaging>
<name>mysqltest4dubboServicename>
<parent>
<groupId>com.lblgroupId>
<artifactId>springboot2demoartifactId>
<version>1.0-SNAPSHOTversion>
parent>
<repositories>
<repository>
<id>sonatype-nexus-snapshotsid>
<url>https://oss.sonatype.org/content/repositories/snapshotsurl>
<releases>
<enabled>falseenabled>
releases>
<snapshots>
<enabled>trueenabled>
snapshots>
repository>
repositories>
<dependencies>
<dependency>
<groupId>com.lblgroupId>
<artifactId>mysqltest4dubboApiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.bootgroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>0.2.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubboartifactId>
<version>2.6.5version>
dependency>
<dependency>
<groupId>com.alibaba.springgroupId>
<artifactId>spring-context-supportartifactId>
<version>1.0.2version>
dependency>
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-frameworkartifactId>
<version>4.1.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
<scope>providedscope>
dependency>
<dependency>
<groupId>org.testnggroupId>
<artifactId>testngartifactId>
<version>${testng.version}version>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>${spring.boot.version}version>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
这里需要注意的是,由于我使用的是dubbo starter 是0.2.1-SNAPSHOT。它的依赖关系为:dubbo2.6.5;curator4.1.0;我这里ZK服务器也是使用的是zookeeper-3.5.4-beta版本。这几个需要手动引入并且指定版本号,dubbo做的不太好,应该直接依赖的
curator官方文档 是这样说的:
ZooKeeper Version Compatibility
While ZooKeeper 3.5.x is still considered "beta" by the ZooKeeper development team, the reality is that it is used in production by many users. However, ZooKeeper 3.4.x is also used in production. Prior to Apache Curator 4.0, both versions of ZooKeeper were supported via two versions of Apache Curator. Starting with Curator 4.0 both versions of ZooKeeper are supported via the same Curator libraries.
ZooKeeper 3.5.x
Curator 4.0 has a hard dependency on ZooKeeper 3.5.x
If you are using ZooKeeper 3.5.x there's nothing additional to do - just use Curator 4.0
ZooKeeper 3.4.x
Curator 4.0 supports ZooKeeper 3.4.x ensembles in a soft-compatibility mode. To use this mode you must exclude ZooKeeper when adding Curator to your dependency management tool.
也就是说3.5版本的ZK,对应3.5版本的ZK服务器,对应4.0版本的Curator 。4.0的Curator如果想支持3.4的ZK,那么需要将Curator中的ZK依赖移除:
<dependency>
<groupId>org.apache.curatorgroupId>
<artifactId>curator-recipesartifactId>
<version>${curator-version}version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
exclusion>
exclusions>
dependency>
application.yml配置内容:
dubbo:
application:
name: mydubboservice
id: mydubboservice
## ProtocolConfig
protocol:
id: dubbo
name: dubbo
port: 20880
scan:
base-packages: com.example.dubbo.provider
registry:
address: zookeeper://127.0.0.1:2181
provider:
validation: true
server:
port: 8888
编写dubbo服务提供方:
@Service
public class DefaultDemoService implements DemoService {
@Override
public String sayHello(String name) {
log.info("com.example.dubbo.provider.DefaultDemoService.sayHello");
return "Hello, " + name + " (from Spring Boot 2)";
}
@Override
public Stu sayHello1(Stu stu) {
log.info("com.example.dubbo.provider.DefaultDemoService.sayHello1");
stu.setBirthday(new Date());
return stu;
}
}
mainApplication(消费者和服务提供方相同):
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
服务提供方编写完成。
消费者的依赖与服务提供方相同。Application.java与服务器提供方相同。
application.yml:
dubbo:
application:
name: mydubboconsumer
id: mydubboconsumer
## ProtocolConfig
protocol:
id: dubbo
name: dubbo
port: 20880
scan:
base-packages: com.example.dubbo.consumer
registry:
address: zookeeper://127.0.0.1:2181
consumer:
validation: true
retries: 0
server:
port: 8889
controller:
@RestController
@Slf4j
public class DemoConsumerController {
@Reference
private DemoService demoService;
@RequestMapping("/test")
public String sayHello(@RequestParam String name) {
return demoService.sayHello(name);
}
@RequestMapping("/test1")
public Stu sayHello1(@RequestParam String name) {
Stu stu = demoService.sayHello1(Stu.builder().age(30).name(name).build());
log.info("stu={}", stu);
return stu;
}
}
分别启动服务提供方和消费者,访问http://127.0.0.1:8889/test1?name=123&age=12
结果正常。
然后我们将age设置为30 http://127.0.0.1:8889/test1?name=123&age=30
再次发送请求,将会发现dubbo消费者报错
Failed to validate service: com.example.dubbo.provider.DemoService, method: sayHello1, cause:
[ConstraintViolationImpl{interpolatedMessage='需要在10和20之间', propertyPath=age, rootBeanClass=class
com.example.dubbo.provider.model.Stu,
messageTemplate='{org.hibernate.validator.constraints.Range.message}'}]
然后可以看到服务提供方并没有输出日志com.example.dubbo.provider.DefaultDemoService.sayHello1
。所以是在消费者就进行了消费参数的验证,这样在实际应用中可以提前发现参数错误,避免无效的dubbo请求,减少资源浪费。使之生效的配置项是dubbo.consumer.validation=true
。
dubbo.provider.validation=true
同理。
主要利用dubbo的扩展点特性来实现。分三步:编写filter,注册扩展点,启用filter。
编写logFilter:
@Slf4j
public class DubboLogFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long start = System.currentTimeMillis();
Result result = invoker.invoke(invocation);
long elapsed = System.currentTimeMillis() - start;
if (invoker.getUrl() != null) {
try {
log.info("dubbo log [{}#{}], param={}, [result={}], [Exception={}], [elapsed={}] ",
invoker.getInterface(), invocation.getMethodName(),
JSONArray.toJSONString(invocation.getArguments()),
JSONObject.toJSONString(result.getValue()),
result.getException(),
elapsed);
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
}
return result;
}
}
注册扩展点:
resources下新建 META-INF/dubbo/com.alibaba.dubbo.rpc.Filter文件。
com.alibaba.dubbo.rpc.Filter文件内容dubboLogFilter=com.example.config.DubboLogFilter
启用filter:
在application.yml中配置dubbo.provider.filter=dubboLogFilter
即可。服务消费者可以配置dubbo.consumer.filter=dubboLogFilter
重启服务,调用test1
方法,可以看到服务提供方打印日志
dubbo log [interface com.example.dubbo.provider.DemoService#sayHello1],
param=[{"age":12,"birthday":1547465838388,"name":"123"}],
[result={"age":12,"birthday":1547465838388,"name":"123"}], [Exception=null], [elapsed=0]
原理:使用自定义filter及RpcContext两者相结合,实现dubbo的分布式日志追踪。当然还有MDC。
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。
MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
编写MDCUtil:
package com.example.config;
import org.slf4j.MDC;
public class MDCUtil {
public static void put(Type type,String value) {
MDC.put(type.name(),value);
}
public static String get(Type type) {
return MDC.get(type.name());
}
public static void remove(Type type) {
MDC.remove(type.name());
}
public static void clear() {
MDC.clear();
}
public enum Type {
TRACE_ID("traceid");
private String desc;
Type(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
}
}
扩展之前的DubboLogFilter.java。使用RpcContext添加traceID:
@Slf4j
public class DubboLogFilter implements Filter {
public static final String TRACE_ID = "TRACE_ID";
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long start = System.currentTimeMillis();
if (RpcContext.getContext().isConsumerSide()) {//dubbo消费者
String traceId = MDCUtil.get(MDCUtil.Type.TRACE_ID);
if (StringUtils.isBlank(traceId)) {
traceId = UUID.randomUUID().toString().replaceAll("-", "");
}
RpcContext.getContext().setAttachment(TRACE_ID, traceId);//设置traceid
}
if (RpcContext.getContext().isProviderSide()) {//dubbo提供者
String traceId = RpcContext.getContext().getAttachment(TRACE_ID);//获取traceID
if (StringUtils.isBlank(traceId)) {
traceId = UUID.randomUUID().toString().replaceAll("-", "");
}
MDCUtil.put(MDCUtil.Type.TRACE_ID, traceId);//重新设置traceID。因为调用一次RpcContext对象的attachment就会清空。目的是为了多重调用时传递traceID
}
Result result = invoker.invoke(invocation);
long elapsed = System.currentTimeMillis() - start;
if (invoker.getUrl() != null) {
try {
log.info("dubbo log [{}#{}], traceid={}, param={}, [result={}], [Exception={}], [elapsed={}] ",
invoker.getInterface(), invocation.getMethodName(),
RpcContext.getContext().getAttachment(TRACE_ID),
JSONArray.toJSONString(invocation.getArguments()),
JSONObject.toJSONString(result.getValue()),
result.getException(),
elapsed);
} catch (Exception e) {
log.warn(e.getMessage(), e);
}
}
return result;
}
}
到此结束,重启服务,调用http://127.0.0.1:8889/test1?name=123&age=12
可以看到日志中出现了traceID
dubbo log [interface com.example.dubbo.provider.DemoService#sayHello1],
traceid=caafae7078234c12a20359f5a980a0a9,
param=[{"age":12,"birthday":1547467250295,"name":"123"}],
[result={"age":12,"birthday":1547467250295,"name":"123"}], [Exception=null], [elapsed=1]
RpcContext是Thread local的,是一个临时状态持有者。每次发送请求或者接收请求时,其状态都会改变。例如:A调用B,然后B调用C.在服务B上,RpcContext在B开始调用C之前将调用信息从A保存到B,并在B调用C之后将调用信息从B保存到C.
/**
* Thread local context. (API, ThreadLocal, ThreadSafe)
*
* Note: RpcContext is a temporary state holder. States in RpcContext changes every time when request is sent or received.
* For example: A invokes B, then B invokes C. On service B, RpcContext saves invocation info from A to B before B
* starts invoking C, and saves invocation info from B to C after B invokes C.
*
* @export
* @see com.alibaba.dubbo.rpc.filter.ContextFilter
*/
public class RpcContext {
/**
* use internal thread local to improve performance
*/
private static final InternalThreadLocal<RpcContext> LOCAL = new InternalThreadLocal<RpcContext>() {
@Override
protected RpcContext initialValue() {
return new RpcContext();
}
};
private static final InternalThreadLocal<RpcContext> SERVER_LOCAL = new InternalThreadLocal<RpcContext>() {
@Override
protected RpcContext initialValue() {
return new RpcContext();
}
};
//省略
RpcContext里的attachments信息会填入到RpcInvocation对象中, 一起传递过去
上面提到了需要重新设置traceID,因为每次调用都会情空。源码参考ConsumerContextFilter
:
@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
RpcContext.getContext()
.setInvoker(invoker)
.setInvocation(invocation)
.setLocalAddress(NetUtils.getLocalHost(), 0)
.setRemoteAddress(invoker.getUrl().getHost(),
invoker.getUrl().getPort());
if (invocation instanceof RpcInvocation) {
((RpcInvocation) invocation).setInvoker(invoker);
}
try {
RpcResult result = (RpcResult) invoker.invoke(invocation);
RpcContext.getServerContext().setAttachments(result.getAttachments());
return result;
} finally {//每次都会清空Attachments
RpcContext.getContext().clearAttachments();
}
}
}
springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;