1. Dew-Framework

dew
Codacy code quality
Apache License 2

对Spring Cloud/Boot的封装扩展、整合公司现有能力、提供最佳实践,做为基础服务框架,支撑公司新项目地研发。

Dew [du:] 意为`露水`,希望此框架可以像晨间的露水一样透明、静谧、丰盈。让使用者尽量不要感知框架的存在,专注业务实现。

1.1. 设计理念

1.1.1. 服务框架的尴尬

几乎每个软件公司都会研发企业内部的服务框架以满足自身业务发展的需要,但几乎所有框架都会存在这样的尴尬:

  1. 无法传承,框架的研发人员离职后没有可以接手

  2. 上手难度大,很多框架喜欢重复造轮子,做出来的与业界主流思想/标准格格不入,导致学习培训成本很高

  3. 功能片面,不通用,服务框架讲求通用性,尽量让整个公司使用同一套规范以方便维护,但很多框架只实现了某些特定场景的功能,无法通用化

  4. 维护成本高,尤其是对于完全自研的框架,往往需要专职人员维护

1.1.2. Dew架构思想

上述问题是Dew框架必须面对的,应对的设计核心理念是:基于成熟框架扩展 ,具体要做到:

  1. 简单容易,用最通用的、标准的、开发人员都熟悉的开发模型

  2. 功能全面,尽量重用市场已有能力实现,减少框架自身的维护成本

  3. 轻量,原则上不引入高侵入性的三方框架/类库

  4. 可替换,只做扩展,尽量不修改基础框架代码,开发人员完全可以直接基于基础框架开发

实现上我们选择 Spring Boot/Cloud 这一业界主流框架。

2. 使用手册

使用申明

Dew有多个版本通道,使用时请谨慎选择:

  1. GA General Availability,正式版本,通过内部测试没有已知错误并且经过生产验证,生产环境首选!

  2. RC Release Candidate,发行候选版本,通过内部测试没有已知错误,可用于生产环境。

  3. Beta 公开测试版本,没有已知的Major类型Bug,但允许存在个别minor类型Bugs,生产环境使用需要谨慎评估!

  4. Alpha 内部测试版本,很早期的测试版本,未经过内部测试,可能存在较多Bugs,此版本类似技术预览版(Technical Preview),切 不可 用于生产环境!

  5. SNAPSHOT 快照版本,类似Nightly版本,更新频繁此不保证质量,切 不可 用于生产环境!

Dew 框架是对 Spring Boot/Cloud 的扩展,使用之前务必了解相关框架的基础知识。
本手册只介绍Dew扩展的功能!

2.1. 结构说明

此章节关联示例:bone-example

Dew 所有模块均为Maven结构,使用如下:

<!--引入Dew父依赖,也可以使用import方式-->
<parent>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>parent-starter</artifactId>
    <!--生产环境请选择合适的版本!!!!!!!!-->
    <version>${dew.version}</version>
</parent>
...
<dependencies>
    <!--引入需要的模块-->
    <dependency>
        <groupId>com.tairanchina.csp.dew</groupId>
        <artifactId><模块名></artifactId>
    </dependency>
</dependencies>
...
<!--开发者-->
<developers>
    <developer>
        <name></name>
        <email></email>
    </developer>
</developers>
<!--SCM信息-->
<scm>
    <connection></connection>
    <developerConnection></developerConnection>
    <url></url>
</scm>
parent-starter 中已包含各模块的版本,引用模块依赖时可省略版本号。

Dew推荐的代码结构为:

|- common-service
|   |- src
|   |   |- main
|   |   |   |- java
|   |   |   |   |- src
|   |   |   |   |   |- x.y.z
|   |   |   |   |   |   |- XApplication (1)
|   |   |   |   |   |   |- XConfig (2)
|   |   |   |   |   |   |- XInitiator (3)
1 XApplication : 服务启动类
2 XConfig:当前服务的配置文件(Spring Cloud格式)
3 XInitiator:当前服务启动初始化器,原则上所有初始化操作都应该从此类发起,如初始化脚本、定时任务等,以方便排错

2.2. 功能模块

模块名 核心功能

parent-starter

Dew的工程父依赖

boot-starter

基于Spring Boot的扩展,Dew的核心模块

cloud-starter

基于Spring Cloud的扩展

idempotent-starter

幂性处理模块

cluster-common

集群能力接口

cluster-redis

集群能力-Redis实现

cluster-hazelcast

集群能力-Hazelcast实现

cluster-rabbit

集群能力-Rabbit实现

cluster-eureka

集群能力-Eureka实现

test-starter

Dew的单元测试封装

config

统一配置中心组件

registry

服务注册中心组件

gateway

服务网关组件

monitor

服务监控组件

auth

多租户的权限组件(Scala语言编写)

<x>-example

各类示例

2.3. 核心功能

2.3.1. 常用工具集

Dew 框架的常用工具由 Dew-Common 包提供( https://github.com/gudaoxuri/dew-common ),功能如下:

  1. Json与Java对象互转,支持泛型

  2. Java Bean操作,Bean复制、反射获取/设置注解、字段、方法等

  3. Java Class扫描操作,根据注解或名称过滤

  4. Shell脚本操作,Shell内容获取、成功捕获及进度报告等

  5. 加解密操作,Base64、MD5/BCrypt/SHA等对称算法和RSA等非对称算法

  6. Http操作,包含Get/Post/Put/Delete/Head/Options操作

  7. 金额操作,金额转大写操作

  8. 通用拦截器栈,前/后置、错误处理等

  9. 定时器操作,定时和周期性任务

  10. 常用文件操作,根据不同情况获取文件内容

  11. 常用字段操作,各类字段验证、身份证提取、UUID创建等

  12. 常用时间处理,常规时间格式化模板

  13. 主流文件MIME整理,MIME分类

  14. 响应处理及分页模型

Dew Common 的使用

Dew Common 功能均以$开始,比如:

  • Json转成Java对象: $.json.toObject(json,JavaModel.class)

  • Json字符串转成List对象: $.json.toList(jsonArray, JavaModel.class)

  • Bean复制:$.bean.copyProperties(ori, dist)

  • 获取Class的注解信息: $.bean.getClassAnnotation(IdxController.class, TestAnnotation.RPC.class)

  • 非对称加密: $.encrypt.Asymmetric.encrypt(d.getBytes("UTF-8"), publicKey, 1024, "RSA")

  • Http Get: $.http.get("https://httpbin.org/get")

  • 验证手机号格式是否合法: $.field.validateMobile("18657120000")

  • …​

2.3.2. 集群功能

此章节关联示例:cluster-example

Dew 的集群支持 分布式缓存 分布式Map 分布式锁 MQ 领导者选举, 并且做了接口抽象以适配不同的实现,目前支持 Redis Hazelcast Rabbit Eureka

各实现对应的支持如下:

功能 Redis Hazelcast Rabbit Eureka

分布式缓存

*

/

/

/

分布式Map

*

*

/

/

分布式锁

*

*

/

/

MQ

*

*

*

/

领导者选举

*

/

/

*

各实现的差异
  • Redis实现了所有功能,但其MQ上不适用于高可靠场景

  • 只有Rabbit的MQ支持跟踪日志(见跟踪日志章节)

  • Eureka只支持一个节点一个领导者,Redis支持领导者分类,即一个节点可设置不同的类别对应不同的领导者

使用
依赖
<dependency>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>boot-starter</artifactId>
</dependency>
<!--引入集群依赖,可选redis/hazelcast/rabbit/eureka-->
<dependency>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>cluster-spi-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>cluster-spi-hazelcast</artifactId>
</dependency>
<dependency>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>cluster-spi-rabbit</artifactId>
</dependency>
<!--此实现需要引用 cloud-starter -->
<dependency>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>cluster-spi-eureka</artifactId>
</dependency>
增加配置
dew:
    cluster: # 集群功能
        cache: # 分布式缓存实现,默认为 redis
        map: # 分布式Map实现,默认为 redis
        lock: # 分布式锁实现,默认为 redis
        mq: # MQ实现,默认为 redis
        election: # 领导者选举实现,默认为 redis

spring:
    redis:
        host: # redis主机
        port: # redis端口
        database: # redis数据库
        password: # redis密码
        pool: # 连接池配置
    rabbitmq:
      host: # rabbit主机
      port: # rabbit端口
      username: # rabbit用户名
      password: # rabbit密码
      virtual-host: # rabbit VH
    hazelcast:
        addresses: [] # hazelcast地址,端口可选
eureka 实现了领导者选择,必须为 Spring Cloud 工程。

集群服务的使用入口统一为: Dew.cluster.XX

分布式缓存
接口见:com.tairanchina.csp.dew.core.cluster.ClusterCache
示例
Dew.cluster.cache.flushdb();
Dew.cluster.cache.del("n_test");
assert !Dew.cluster.cache.exists("n_test");
Dew.cluster.cache.set("n_test", "{\"name\":\"jzy\"}", 1);
assert Dew.cluster.cache.exists("n_test");
assert "jzy".equals($.json.toJson(Dew.cluster.cache.get("n_test")).get("name").asText());
Thread.sleep(1000);
assert !Dew.cluster.cache.exists("n_test");
assert null == Dew.cluster.cache.get("n_test");
Dew的缓存默认只实现了String、List、Set、Hash等结构常用的,时间复杂度低的操作, 如需要的操作Dew没有提供可使用Spring Boot Data Redis原生的RedisTemplate<String,String>
分布式Map
接口见:com.tairanchina.csp.dew.core.cluster.ClusterMap
示例
ClusterMap<TestMapObj> mapObj = Dew.cluster.map.instance("test_obj_map", TestMapObj.class);
mapObj.clear();
TestMapObj obj = new TestMapObj();
obj.a = "测试";
mapObj.put("test", obj);
assert "测试".equals(mapObj.get("test").a);
分布式锁
接口见:com.tairanchina.csp.dew.core.cluster.ClusterLock
示例
// dist lock
ClusterLock lock = Dew.cluster.lock.instance("test_lock");
// tryLock 示例,等待0ms,忘了手工unLock或出异常时1s后自动解锁
if (lock.tryLock(0, 1000)) {
    try {
        // 已加锁,执行业务方法
    } finally {
        // 必须手工解锁
        lock.unLock();
    }
}
// tryLockWithFun 示例
lock.tryLockWithFun(0, 1000, () -> {
    // 已加锁,执行业务方法,tryLockWithFun会将业务方法包裹在try-cache中,无需手工解锁
});
MQ
接口见:com.tairanchina.csp.dew.core.cluster.ClusterMQ
示例
// pub-sub
Dew.cluster.mq.subscribe("test_pub_sub", message ->
        logger.info("pub_sub>>" + message));
Thread.sleep(1000);
Dew.cluster.mq.publish("test_pub_sub", "msgA");
Dew.cluster.mq.publish("test_pub_sub", "msgB");
// req-resp
Dew.cluster.mq.response("test_rep_resp", message ->
        logger.info("req_resp>>" + message));
Dew.cluster.mq.request("test_rep_resp", "msg1");
Dew.cluster.mq.request("test_rep_resp", "msg2");
// rabbit confirm
if (Dew.cluster.mq instanceof RabbitClusterMQ) {
    boolean success = ((RabbitClusterMQ) Dew.cluster.mq).publish("test_pub_sub", "confirm message", true);
    success = ((RabbitClusterMQ) Dew.cluster.mq).request("test_rep_resp", "confirm message", true);
}
发布订阅模式时,发布前 topic 必须已经存在,可先使用 subscribe 订阅,此操作会自动创建 topic
Rabbit 实现支持单条 confirm 模式。
MQ的HA功能

MQ的HA(高可用)支持,默认MQ启用HA(可通过dew.cluster.config.ha-enabled=false关闭)。 可实现。

Dew的MQ仅在数据处理完成后才做commit,这限制了对同一个队列只能串行处理, MQ的HA开启后,您可以以多线程的方式消费消息,处理过程中如发生服务宕机重启后仍可从未处理完成的消息开始消费。

领导者选举
接口见:com.tairanchina.csp.dew.core.cluster.ClusterElection
示例
// 实例化fun1类型的领导者选举,Redis的实现支持多类型领导者
ClusterElection electionFun1 = Dew.cluster.election.instance("fun1");
// ...
if (electionFun1.isLeader()) {
   // 当前节点是fun1类型的领导者
   // ...
}

2.3.3. 幂等处理

此章节关联示例:idempotent-example

支持HTTP和非HTTP幂等操作,对于HTTP操作,要求请求方在请求头或URL参数中加上操作ID标识,非HTTP操作由可自由指定操作类型和操作ID标识的来源。

依赖
<!--引入幂等支持-->
<dependency>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>idempotent-starter</artifactId>
</dependency>
配置
dew:
  cluster:
    cache: redis # 启用Redis支持
  idempotent:
    default-expire-ms: 3600000 # 设置默认过期时间,1小时
    default-strategy: item # 设置默认策略,支持 bloom(Bloom Filter)和item(逐条记录),目前只支持item
    default-opt-id-flag: __IDEMPOTENT_OPT_ID__ # 指定幂等操作ID标识,可以位于HTTP Header或请求参数中
HTTP操作
@GetMapping(xxx)
// 启用幂等支持
// 请求头部或参数加上__IDEMPOTENT_OPT_ID__ = xx
@Idempotent
public void test(xxx) {
    // 业务操作
    // ...
    // 业务失败,在保证业务操作的原子性的情况下,在catch中取消幂等,并抛出异常
    DewIdempotent.cancel();
    // 手工确认
    DewIdempotent.confirm();
}

Idempotent注解说明:

  • optIdFlag:指定幂等操作ID标识,可以位于HTTP Header或请求参数中

  • expireMs:设置过期时间,单位毫秒

  • strategy:设置默认策略

  • needConfirm:设置是否需要显式确认,true时,需要进行显式确认操作: DewIdempotent.confirm() 或 DewIdempotent.confirm(String optType, String optId) 前者要求与请求入口在同一线程中

非HTTP操作
// 初始化类型为transfer_a的幂等操作,需要手工确认,过期时间为1秒
DewIdempotent.initOptTypeInfo("transfer_a", true, 1000, StrategyEnum.ITEM);
// 第一次请求transfer_a类型下的xxxxxxx这个ID,返回不存在,表示可以下一步操作
Assert.assertEquals(StatusEnum.NOT_EXIST, DewIdempotent.process("transfer_a", "xxxxxxx"));
// 第二次请求transfer_a类型下的xxxxxxx这个ID,返回未确认,表示上次操作还在进行中
Assert.assertEquals(StatusEnum.UN_CONFIRM, DewIdempotent.process("transfer_a", "xxxxxxx"));
// 确认操作完成
DewIdempotent.confirm("transfer_a", "xxxxxxx");
// 第三次请求transfer_a类型下的xxxxxxx这个ID,返回已确认,但未过期,仍不能操作
Assert.assertEquals(StatusEnum.CONFIRMED, DewIdempotent.process("transfer_a", "xxxxxxx"));
// 延时1秒
Thread.sleep(1000);
// 再次请求transfer_a类型下的xxxxxxx这个ID,返回不存在(上次请求已过期),表示可以下一步操作
Assert.assertEquals(StatusEnum.NOT_EXIST, DewIdempotent.process("transfer_a", "xxxxxxx"));

2.3.4. 统一响应

Dew 推荐使用 协议无关的响应格式,此格式在 方法间调用 非HTTP协议RPC MQ 等数据交互场景做到真正的 统一响应格式。 要求返回的格式为Resp对象,格式为:

{
    code: "", // 响应编码,与http状态码类似,200表示成功
    message:"", // 响应附加消息,多有于错误描述
    body: // 响应正文
}
示例
public Resp<String> test(){
    return Resp.success("enjoy!");
    // or return Resp.notFound("…")/conflict("…")/badRequest("…")/…
}

Resp类提供了常用操作:详见 https://gudaoxuri.github.io/dew-common/#true-resp

Dew使用返回格式中的code表示操作状态码,此状态码与HTTP状态码无关,一般情况下HTTP状态码均为200,如需要降级处理时返回500。

500 Http状态码说明

500 状态码仅用于告诉 Hystrix 这次请求是需要降级的错误,对于 Resp 中的 code 没有影响。

dew 框架会把所有 5xx(服务端错误,需要降级) 的异常统一转换成 500 的Http状态码返回给调用方。

Resp.xxx.fallback() 用于显示声明当前返回需要降级, 比如 Resp.serverError("some message") 不会降级,返回http状态码为200,body为 {"code":"500","message":"some message","body":null}, 但 Resp.serverError("some message").fallback() 会降级,返回http状态码为500,body为 同上。

2.3.5. 消息通知

Dew 支持发送消息到钉钉或邮件,默认支持对未捕获异常及Hystrix错误的通知。

通知配置
# 格式
dew:
    notifies:
        "": # 通知的标识
            type: DD # 通知的类型,DD=钉钉 MAIL=邮件,邮件方式需要有配置spring.mail下相关的smtp信息 HTTP=自定义HTTP Hook
            defaultReceivers: # 默认接收人列表,钉钉为手机号,邮件为邮箱
            dndTimeReceivers: # 免扰时间内的接收人列表,只有该列表中的接收人才能在免扰时间内接收通知
            args: # 不同类型的参数,邮件不需要设置
                url: # 钉钉及自定义HTTP的推送地址,钉钉URL说明详见:https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.karFPe&treeId=257&articleId=105735&docType=1
            strategy: # 通知策略
                minIntervalSec: 0 # 最小间隔的通知时间,0表示不设置,如为10则表示10s内只会发送一次
                dndTime: # 开启免扰时间,HH:mm-HH:mm 如,18:00-06:00
                forceSendTimes: 3 # 同一免扰周期间通知调用达到几次后强制发送

# 示例
dew:
  notifies:
    __DEW_ERROR__:
      type: DD
      defaultReceivers: xxxx
      args:
        url: https://oapi.dingtalk.com/robot/send?access_token=8ff65c48001c1981df7d3269
      strategy:
        minIntervalSec: 5
    sendMail:
      type: MAIL
      defaultReceivers: x@y.z
    custom:
      type: HTTP
      defaultReceivers: x@y.z
      args:
        url: https://...
通知使用
# 最简单的调用
Resp<Void> result = Dew.notify.send("<通知的标识>", "<通知的内容或Throwable>");
# 带通知标题,标题会加上``Dew.Info.instance``
Resp<Void> result = Dew.notify.send("<通知的标识>", "<通知的内容或Throwable>", "<通知标题>");
# 加上特殊接收人列表,非免扰时间内的接收人=配置默认接收人列表+特殊接收人列表,免扰时间内的接收人=配置的免扰时间内的接收人列表
Resp<Void> result = Dew.notify.send("<通知的标识>", "<通知的内容或Throwable>", "<通知标题>", "<特殊接收人列表>");
# 上述三个方法都有异步的重载方法,如
Dew.notify.sendAsync("<通知的标识>", "<通知的内容或Throwable>");
默认通知标识
  1. 非捕获异常: DEW_ERROR,所有非捕获异常(ErrorController)调用此标识发送错误,可通过dew.basic.format.error-flag 修改

  2. Hystrix错误: HYSTRIX,所有Hystrix错误调用此标识发送错误,可通过dew.cloud.error.notify-flag 修改

要启用以上两个通知请确保dew.notifies有相应的配置。

HTTP自定义通知格式

POST请求,Body格式为:

{ "title": "", // 标题 "content": "", // 内容 "receivers": [] // 接收人列表 }

调用正常需要返回200状态码

2.3.6. 异常处理

Dew 会把程序没有捕获的异常统一上抛,同时框架提供了常用的异常检查:

异常检查,异常类型要求为RuntimeException及其子类
Dew.E.check(VoidPredicate notExpected, E ex)
Dew.E.check(boolean notExpected, E ex)
Dew.E.checkNotEmpty(Map<?, ?> objects, E ex)
Dew.E.checkNotEmpty(Iterable<?> objects, E ex)
Dew.E.checkNotNull(Object obj, E ex)
自定义异常配置,启用后此类异常均使用此模块
dew:
  basic:
    error-mapping:
      "[<异常类名>]":
        http-code: # http状态码,不存在时使用实例级http状态码
        business-code: # 业务编码,不存在时使用实例级业务编码
        message: # 错误描述,不存在时使用实例级错误描述

2.3.7. 数据验证

Dew集成了Spring validate 机制,支持针对 URLBean 的验证。

  • 在 java bean 中添加各项validation,支持标准`javax.validation.constraints`包下的诸如:NotNull ,同时框架扩展了几个检查,如: IdNumber、Phone

  • 在Controller中添加 @Validated 注解 ( Spring还支持@Vaild,但这一注解不支持分组 )

  • 支持Spring原生分组校验

  • URL 类型的验证必须在类头添加 @Validated 注解

  • Dew 框架内置了 CreateGroup UpdateGroup 两个验证组,验证组仅是一个标识,可为任何java对象

2.3.8. CORS支持

配置
dew:
  security:
    cors:
      allow-origin: # 允许来源,默认 *
      allow-methods: # 允许方法,默认 POST,GET,OPTIONS,PUT,DELETE,HEAD
      allow-headers: # 允许头信息 x-requested-with,content-type

2.3.9. 权限认证

此章节关联示例:auth-example

支持`认证缓存`,即支持将鉴权系统生成的登录信息缓存到业务系统中方便即时调用,并提供三方适配。

配置认证缓存
dew:
    security:
        token-flag: # token key的名称
        token-in-header: # token key是否在http header中,为false是会从url query中获取
        token-hash: # token 值是否做hash(MD5)处理
认证缓存需要 集群缓存 服务支持,请引入相关的依赖并配置对应的连接信息等。
basic 认证缓存接口
// 添加登录信息,optInfo封装自鉴权系统过来的登录信息
// 一般在登录认证后操作
Dew.auth.setOptInfo(OptInfo optInfo);
// 获取登录信息,要求在http请求加上token信息
Dew.context().optInfo();
// 删除登录信息
// 一般在注销登录后操作
Dew.auth.removeOptInfo();

// 登录信息
public class OptInfo {
    // Token
    String token;
    // 账号编码
    String accountCode;
}
OptInfo 为认证缓存信息的基类,使用时可以继承并扩展自己的属性。
使用 OptInfo 扩展类型时需要在工程启动时指定扩展类: DewContext.setOptInfoClazz(<扩展类型>)
basic 认证缓存示例
/**
 * 模拟用户注册
 */
@PostMapping(value = "user/register")
public Resp<Void> register(@RequestBody User user) {
    // 实际注册处理
    user.setId($.field.createUUID());
    MOCK_USER_CONTAINER.put(user.getId(), user);
    return Resp.success(null);
}

/**
 * 模拟用户登录
 */
@PostMapping(value = "auth/login")
public Resp<String> login(@RequestBody LoginDTO loginDTO) {
    // 实际登录处理
    User user = MOCK_USER_CONTAINER.values().stream().filter(u -> u.getIdCard().equals(loginDTO.getIdCard())).findFirst().get();
    String token = $.field.createUUID();
    Dew.auth.setOptInfo(new OptInfoExt()
            .setIdCard(user.getIdCard())
            .setAccountCode($.field.createShortUUID())
            .setToken(token)
            .setName(user.getName())
            .setMobile(user.getPhone()));
    return Resp.success(token);
}

/**
 * 模拟业务操作
 */
@GetMapping(value = "business/someopt")
public Resp<Void> someOpt() {
    // 获取登录用户信息
    Optional<OptInfoExt> optInfoExtOpt = Dew.auth.getOptInfo();
    if (!optInfoExtOpt.isPresent()) {
        return Resp.unAuthorized("用户认证错误");
    }
    // 登录用户的信息
    optInfoExtOpt.get();
    return Resp.success(null);
}

/**
 * 模拟用户注销
 */
@DeleteMapping(value = "auth/logout")
public Resp<Void> logout() {
    // 实际注册处理
    Dew.auth.removeOptInfo();
    return Resp.success(null);
}

2.3.10. 测试支持

良好的单元测试可以保证代码的高质量,单测的重要原则是内聚、无依赖,好的单测应该是"函数化"的——结果的变化只与传入参数有关。 但实际上我们会的代码往往会与数据库、缓存、MQ等外部工具交互,这会使单测的结果不可控,通常的解决方案是使用Mock,但这无行中引入了单测撰写的成本, Dew使用"内嵌式"工具解决,数据库使用 H2 ,Redis使用 embedded redis ,由于 Dew 集群的 Cache Map Lock MQ 都支持 Redis 实现,所以可以做到对主流操作的全覆盖。

依赖
<dependency>
    <groupId>com.tairanchina.csp.dew</groupId>
    <artifactId>test-starter</artifactId>
</dependency>
配置
dew:
  cluster: #所有集群操作都使用reids模拟
    cache: redis
    lock: redis
    map: redis
    mq: redis

spring:
  redis:
    host: 127.0.0.1
    port: 6379
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test

2.4. 工程化

2.4.1. API文档

用于生成Asciidoc格式的离线API文档,可通过软件转换成HTML及PDF版本。

配置
dew:
  basic:
    name: # 文档名称
    version: 1.0 # 文档版本
    desc: # 文档说明
    web-site: # 文档站点
    doc:
        base-package: # API文档要扫描的根包,多指定到 Controller 包中
        contact: # 联系人信息,可为空
            name: # 联系人姓名
            url: # 联系人URL
            email: # 联系人邮箱

management: # 文档生成地址前缀为 /management.context-path
  security:
    enabled: false # 建议关闭管理功能的安全认证
  context-path: /management-admin # 建议修改此前缀以便在网关上统一过滤管理请求
使用
访问任意服务实例:

PUT /{management.context-path}/doc/offline

{
	"docName":"", # 文档名称
	"docDesc":"", # 文档说明
	"visitUrls":{ # 不同环境的访问地址
		"":""
	},
	"swaggerJsonUrls":[] # swagger.json的URL列表(http://.../v2/api-docs),可选
}

swaggerJsonUrls 不为空时根据传入的swaggerJsonUrls合成接口文档,为空时分两种情况:

1. 如果是非集群模式(没有注册中心)则只生成本服务的接口文档
2. 如果是集群模式(注册到注册中心)则会合成此集群下所有的接口文档

示例:

{
	"docName":"Dew API 文档",
	"docDesc":"此为Dew的接口文档\n描述可以换行",
	"visitUrls":{
		"开发环境":"http://dev.dew.ecfront.com",
		"测试环境":"http://test.dew.ecfront.com"
	}
}

调用此接口后返回Asciidoc格式的文档内容,复制此内容到相应的转换工具中即可生成对应的HTTP或PDF文档。

2.4.2. 代码质量检查

Dew 已集成 Sonar 插件,只需要在maven中配置 sonar.host.url 为目标地址, 然后执行 mvn clean verify sonar:sonar -P qa -Dsonar.login=<用户名> -Dsonar.password=<密码> 即可。

也可以设置 sonar.forceAuthentication=false ,但要注意安全管控。
使用 <maven.test.skip>true</maven.test.skip> 可跳过特定模块的测试,<sonar.skip>true</sonar.skip> 可跳过特定模块的Sonar检查。

2.4.3. 降级通知

此章节关联 `hystrix-feign-example` 示例
为能更及时的对服务异常做出处理, dew 增加邮件通知功能。
# 通知条件配置示例
dew:
  cloud:
    error:
      enabled: true
      notify-event-types: FAILURE,SHORT_CIRCUITED,TIMEOUT,THREAD_POOL_REJECTED,SEMAPHORE_REJECTED
      notify-include-keys: ["ExampleClient#deleteExe(int,String)","ExampleClient#postExe(int,String)"]

# 邮箱配置示例
spring:
  mail:
    host: smtp.163.com
    username: <邮件地址>
    password: <password为smtp授权码,非邮箱密码>
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true

2.4.4. 跟踪日志

此章节关联 `sleuth-invokeX-example` 示例

用于记录 服务API调用 (追踪)日志到 Slf4j

开启追踪日志
dew:
  cloud:
    trace-log:
        enabled: true # 默认为true

一次调用日志的查看,以 ES 为例,过滤条件是: logger:com.tairanchina.csp.dew.core.logger.TraceLogWrap & trace:<对应的traceID>

2.4.5. Spring Admin 集成

此章节关联示例:monitor-example

Dew 集成了 Spring Admin ,封装成 monitor 组件, 示例 monitor-example 演示了如何与 monitor 交互。

monitor 关键配置
spring:
  application:
    name: monitor # 监控服务名称
  boot:
    admin:
      routes:
        endpoints: env,metrics,dump,jolokia,info,configprops,trace,logfile,refresh,flyway,liquibase,heapdump,loggers,auditevents,hystrix.stream # 要统计的内容
      turbine: # turbine集成配置
        clusters: default # 集群名称
        location: monitor # 聚合到的服务名称,这里要与 `spring.application.name` 相同

turbine: # turbine配置
  aggregator:
    clusterConfig: default # 集群名称
  appConfig: monitor-example # 要聚合的服务名称,需要把各个服务添加上去
  clusterNameExpression: metadata['cluster']

server:
  port: # 端口号

eureka:
  client:
    serviceUrl:
      defaultZone: # eureka 服务地址
要监控的服务 关键配置
spring:
  application:
    name: monitor-example # 服务名称,必须在上文 `turbine.appConfig` 添加上去

eureka:
  client:
    serviceUrl: # eureka 服务地址,必须和监控服务在同一集群中
  instance:
    metadata-map:
      cluster: default # 集群名称

management.security.enabled: false # 需要关闭安全管理,可通过IP来限制

3. 最佳实践

3.1. JDBC框架的选择

主流JDBC框架有Hibernate、MyBatis、Spring JDBC Template、Ebean、DBUtils等,Dew基于Spring Boot,所以对于这些框架都提供了很好的兼容。那么如何选择呢?

  • 先说Hibernate,如果你的数据表结构与对象模型基本吻合,那么使用JPA会带来很大的便捷,推荐Hibernate

  • 如果你的数据表结构与对象模型严重不吻合或是你希望对SQL有更多主动权(SQL优化、复杂查询等),那JPA就没什么优势了,这时:

    • 如果你追求极简、不需要ORMPPING,那么DBUtils会是最佳选择

    • 如果你喜欢敏捷开发,推崇充血模型,那么尝试一下Ebean吧,与Play!结合最合适不过

    • 如果你既要有一定的ORMPPING能力,又希望自己写SQL,那么MyBatis会是不错的选择

    • 如果你使用了Spring,希望框架简单些,可以接受自己写ORMPPING,未来无切换关系型数据库的计划,那么Spring JDBC Template将是个很好的选择

3.2. 服务调用开发期使用

Spring Cloud 体系下,服务调用需要启动 Eureka 服务(对于 Dew 中的 Registry 组件),这对开发阶段并不友好:

  1. 开发期间会不断启停服务,Eureka 保护机制会影响服务注册(当然这是可以关闭的)

  2. 多人协作时可能会出现调用到他人服务的情况(同一服务多个实例)

  3. 需要启动 Eureka 服务,多了一个依赖

为解决上述问题,在使用 Spring CloudRestTemplate 时,增加 Ribbon 的服务配置.

# <client>为service-id
<client>.ribbon.listOfServers: <直接访问的IPs>
# 如
x-service.ribbon.listOfServers: 127.0.0.1:8812

3.3. @Validated 注解

  • 在Spring Controller类里,@Validated 注解初使用会比较不易上手,在此做下总结

    1. 对于基本数据类型和String类型,要使校验的注解生效,需在该类上方加 @Validated 注解

    2. 对于抽象数据类型,需在形式参数前加@Validated注解

Spring对抽象数据类型校验抛出异常为MethodArgumentNotValidException,http状态码为400,对基本数据类型校验抛出异常为ConstraintViolationException,http状态码为500,dew对这两种异常做了统一处理,http状态码均返回200,code为400

3.4. jackson 对于 Java8 时间转换( SpringMVCjackson 接收 json 数据)

  1. 对于 LocalDateTime 类型,需在参数上加 @JsonFormat 注解,如下:@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")

  2. LocalDate,LocalTime,Instant 等,无需配置可自行转换

jackson 对于 LocalDateTime 类型的支持与其他三种类型不具有一致性,这是 jackson 需要优化的一个点

3.5. Ribbon 负载均衡

example: service-dew.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

service-dew为服务名,配置时自行选取规则,类均在com.netflix.loadbalancer包下
若指定zone,默认会优先调用相同zone的服务,此优先级高于策略配置,配置如下
#指定属于哪个zone
eureka:
  instance:
    metadata-map:
      zone: #zone 名称

#指定region(此处region为项目在不同区域的部署,为项目规范,不同region间能互相调用)
eureka:
  client:
    region: #region名称

3.6. Feign 配置特定方法超时时间

Hystrix 超时时间配置

# 配置默认的hystrix超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
# 配置特定方法的超时时间,优于默认配置
hystrix.command.<hystrixcommandkey>.execution.isolation.thread.timeoutInMilliseconds=10000
# <hystrixcommandkey>的format为FeignClassName#methodSignature,下面是示例配置
hystrix.command.PressureService#getBalance(int).execution.isolation.thread.timeoutInMilliseconds=10000

Ribbon 超时时间配置

# 配置默认ribbon超时时间
ribbon.ReadTimeout=60000
# 配置特定服务超时时间,优于默认配置
<client>.ribbon.ReadTimeout=6000
# <client>为实际服务名,下面是示例配置
x-service.ribbon.ReadTimeout=5000

HystrixRibbon 的超时时间配置相互独立,以低为准,使用时请根据实际情况进行配置

如果要针对某个服务做超时设置,建议使用 Ribbon 的配置;在同时使用 RibbonHystrix 时,请特别注意超时时间的配置。

3.7. Feign 接口添加 Http 请求头信息

@FeignClient 修饰类中的接口方法里添加新的形参,并加上 @RequestHeader 注解指定key值
示例
@PostMapping(value = "ca/all", consumes = MediaType.APPLICATION_JSON_VALUE)
Resp<CustomerInfoVO> applyCA(@RequestBody CAIdentificationDTO params,
     @RequestHeader Map<String, Object> headers);

3.8. Feign 文件上传实践

  • SDK 工程处,添加包依赖

pom
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form</artifactId>
            <version>3.0.1</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form-spring</artifactId>
            <version>3.0.1</version>
        </dependency>
  • SDK 工程处,创建一个 Configuration

MultipartSupportConfig
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.netflix.feign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MultipartSupportConfig {

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    public Encoder feignFormEncoder() {
        return new SpringFormEncoder(new SpringEncoder(messageConverters));
    }

}
  • 修改接口

FeginExample
@FeignClient(name = "demo")
public interface FeginExample {
@PostMapping(value = "images", consumes = MULTIPART_FORM_DATA_VALUE)
 Resp<String> uploadImage(
            @RequestParam MultipartFile image,
            @RequestParam("id") String id);
}

@RequestPart@RequestParam 效果是一样的,大家就不用花时间在这上面了。

  • 修改服务器接口

FeginServiceExample
@RestController
public class FeginServiceExample {
  @PostMapping(value = "images", consumes = MULTIPART_FORM_DATA_VALUE)
    public Resp<String> uploadImage(
            @RequestParam("image") MultipartFile image,
            @RequestParam("id") String id,
            HttpServletRequest request) {
              return Resp.success(null);
            }
}

常见问题:

  • HTTP Status 400 - Required request part 'file' is not present

请求文件参数的名称与实际接口接受名称不一致
  • feign.codec.EncodeException: Could not write request: no suitable HttpMessageConverter found for request type [org.springframework.mock.web.MockMultipartFile] and content type [multipart/form-data]

转换器没有生效,检查一下MultipartSupportConfig

3.9. 自定义降级方法

构建类继承HystrixCommand抽象类,重写run方法,getFallback方法,getFallback为run的降级,再执行excute方法即可
每个HystrixCommand的子类的实例只能execute一次。
下面附上代码
public class HelloHystrixCommand extends HystrixCommand<HelloHystrixCommand.Model> {

    public static final Logger logger = LoggerFactory.getLogger(HelloHystrixCommand.class);

    private Model model;

    protected HelloHystrixCommand(HystrixCommandGroupKey group) {
        super(group);
    }

    protected HelloHystrixCommand(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool) {
        super(group, threadPool);
    }

    protected HelloHystrixCommand(HystrixCommandGroupKey group, int executionIsolationThreadTimeoutInMilliseconds) {
        super(group, executionIsolationThreadTimeoutInMilliseconds);
    }

    protected HelloHystrixCommand(HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool, int executionIsolationThreadTimeoutInMilliseconds) {
        super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds);
    }

    protected HelloHystrixCommand(Setter setter) {
        super(setter);
    }

    public static HelloHystrixCommand getInstance(String key){
        return new HelloHystrixCommand(HystrixCommandGroupKey.Factory.asKey(key));
    }

    @Override
    protected Model run() throws Exception {
        int i = 1 / 0;
        logger.info("run:   thread id:  " + Thread.currentThread().getId());
        return model;
    }

    @Override
    protected Model getFallback() {
        return new Model("fallback");
    }

    public static void main(String[] args) throws Exception {
        HelloHystrixCommand helloHystrixCommand = HelloHystrixCommand.getInstance("dew");
        helloHystrixCommand.model = helloHystrixCommand.new Model("run");
        logger.info("main:      " + helloHystrixCommand.model + "thread id: " + Thread.currentThread().getId());
        System.out.println(helloHystrixCommand.execute());

    }


    class Model {

        public Model(String name) {
            this.name = name;
        }

        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Model{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}

3.10. 断路保护

Hystrix配置
# 执行的隔离策略 THREAD, SEMAPHORE 默认THREAD
hystrix.command.default.execution.isolation.strategy=THREAD
# 执行hystrix command的超时时间,超时后会进入fallback方法 默认1000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000
# 执行hystrix command是否限制超时,默认是true
hystrix.command.default.execution.timeout.enabled=true
# hystrix command 执行超时后是否中断 默认true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
# 使用信号量隔离时,信号量大小,默认10
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=10
# fallback方法最大并发请求数 默认是10
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests=10
# 服务降级是否开启,默认为true
hystrix.command.default.fallback.enabled=true
# 是否使用断路器来跟踪健康指标和熔断请求
hystrix.command.default.circuitBreaker.enabled=true
# 熔断器的最小请求数,默认20. (这个不是很理解,欢迎补充)
hystrix.command.default.circuitBreaker.requestVolumeThreshold=20
# 断路器打开后的休眠时间,默认5000
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=5000
# 断路器打开的容错比,默认50
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 强制打开断路器,拒绝所有请求. 默认false, 优先级高于forceClosed
hystrix.command.default.circuitBreaker.forceOpen=false
# 强制关闭断路器,接收所有请求,默认false,优先级低于forceOpen
hystrix.command.default.circuitBreaker.forceClosed=false

# hystrix command 命令执行核心线程数,最大并发 默认10
hystrix.threadpool.default.coreSize=10

使用断路保护可有效果的防止系统雪崩,Spring CloudHystrix 做了封装,详见: http://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#_circuit_breaker_hystrix_clients

需要说明的是 Hystrix 使用新线程执行代码,导致 Threadlocal 数据不能同步,使用时需要将用到的数据做为参数传入,如果需要使用 Dew 框架的上下文(请求链路/用户等获取)需要先传入再设值,e.g.

Hystrix Command 示例,及Context处理
public class HystrixExampleService {
    @HystrixCommand(fallbackMethod = "defaultFallback", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
    })
    public String someMethod(Map<String, Object> parameters, DewContext context) {
        // !!! Hystrix使用新线程执行代码,导致Threadlocal数据不能同步,
        // 使用时需要将用到的数据做为参数传入,如果需要使用Dew框架的上下文需要先传入再设值
        DewContext.setContext(context);
        try {
            Thread.sleep(new Random().nextInt(3000));
            logger.info("Normal Service Token:" + Dew.context().getToken());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return "ok";
    }

    // 降级处理方法定义
    public String defaultFallback(Map<String, Object> parameters, DewContext context, Throwable e) {
        DewContext.setContext(context);
        logger.info("Error Service Token:" + Dew.context().getToken());
        return "fail";
    }
}

3.11. 定时任务

使用 Spring Config 配置中心 refresh 时,在 @RefreshScope 注解的类中, @Scheduled 注解的自动任务会失效。 建议使用实现 SchedulingConfigurer 接口的方式添加自动任务。

自动任务添加
@Configuration
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer {

    private Logger logger = LoggerFactory.getLogger(SchedulingConfiguration.class);

    @Autowired
    private ConfigExampleConfig config;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> logger.info("task1: " + config.getVersion()), triggerContext -> {
            Instant instant = Instant.now().plus(5, SECONDS);
            return Date.from(instant);
        });

        taskRegistrar.addTriggerTask(() -> logger.info("task2: " + config.getVersion()), new CronTrigger("1/3 * * * * ?"));
    }
}

3.12. 主要性能影响参数

  • 内置 Tomcat 参数调整效果并不大,如果需要调整,建议适当调大 max-treadsaccept-count

    # 最大等待请求数 默认100
    server.tomcat.accept-count=1000
    # 最大并发数 默认200
    server.tomcat.max-threads=1000
    # 最大连接数 默认BIO:200 NIO:10000 APR:8192
    server.tomcat.max-connections=2000
  • Zuul 性能参数说明

    # 连接池最大连接,默认是200
    zuul.host.maxTotalConnections=1000
    每个route可用的最大连接数,默认值是20
    zuul.host.maxPerRouteConnections=1000
    Hystrix最大的并发请求 默认值是100
    zuul.semaphore.maxSemaphores=1000
Zuul 的最大并发数主要调整 maxSemaphores 优先级高于 Hystrix 的最大线程数配置.
  • Ribbon 性能参数说明调整 MaxTotalConnectionsMaxConnectionsPerHost 时建议同比调整 Pool 相关的参数

    # ribbon 单主机最大连接数,默认50
    ribbon.MaxConnectionsPerHost=500
    # ribbon 总连接数,默认 200
    ribbon.MaxTotalConnections=1000
    # 默认200
    ribbon.PoolMaxThreads=1000
    # 默认1
    ribbon.PoolMinThreads=500
Zuul 和其它使用 Ribbon 的服务一样,TPS主要调整 RibbonMaxConnectionsPerHostMaxTotalConnections
  • Hystrix 性能参数说明

    # 并发执行的最大线程数,默认10
    hystrix.threadpool.default.coreSize=100
普通 Service 使用 Hystrix 时,最大并发主要调整 hystrix.threadpool.default.coreSize
Hystrix 的默认超时时间为1s,在高并发下容易出现超时,建议将默认超时时间适当调长, 特殊接口需要将时间调短或更长的,使用特定配置,见上面 Feign 配置特定方法超时时间.

3.13. Zuul 保护(隐藏)内部服务的 Http 接口

在yml配置文件里配置(ignored-patterns,ignored-services)这两项中的一项即可

配置示例
zuul: #配置一项即可!
  ignored-patterns: /dew-example/**   #排除此路径
  ignored-services: dew-example       #排除此服务

3.14. 缓存处理

Spring Cache 提供了很好的注解式缓存,但默认没有超时,需要根据使用的缓存容器特殊配置

Redis缓存过期时间设置
@Bean
RedisCacheManager cacheManager() {
    final RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
    redisCacheManager.setUsePrefix(true);
    redisCacheManager.setDefaultExpiration(<过期秒数>);
    return redisCacheManager;
}

3.15. Spring Boot Admin 监控实践

Spring Boot Actuator 中提供很多像 healthmetrics 等实时监控接口,可以方便我们随时跟踪服务的性能指标。 Spring Boot 默认是开放这些接口提供调用的,那么就问题来了,如果这些接口公开在外网中,很容易被不法分子所利用,这肯定不是我们想要的结果。 在这里我们提供一种比较好的解决方案

  • 被监控的服务配置

management:
  security:
    enabled: false # 关闭管理认证
  context-path: /management (1)
eureka:
  instance:
    metadata-map:
      cluster: default (2)
1 管理前缀
2 集群名称
  • Zuul 网关配置

zuul:
  ignoredPatterns: /*/management/** (1)
1 同上文 management.context-path , 这里之所以不是 /management/** ,由于网关存在项目前缀,需要往前一级,大家可以具体场景具体配置
  • Spring Boot Admin 配置

spring:
  application:
    name: monitor
  boot:
    admin:
      discovery:
        converter:
          management-context-path: ${management.context-path}
      routes:
        endpoints: env,metrics,dump,jolokia,info,configprops,trace,logfile,refresh,flyway,liquibase,heapdump,loggers,auditevents,hystrix.stream,turbine.stream  (1)
      turbine:
        clusters: default  (2)
        location: ${spring.application.name}

turbine:
  instanceUrlSuffix: ${management.context-path}/hystrix.stream
  aggregator:
    clusterConfig: default (2)
  appConfig: monitor-example,hystrix-example (3)
  clusterNameExpression: metadata['cluster']

security:
  basic:
    enabled: false

server:
  port: ...

eureka:
  instance:
    metadata-map:
      cluster: default (2)
  client:
    serviceUrl:
      defaultZone: ...

management:
  security:
    enabled: false
  context-path: /management (4)
1 要监控的内容
2 要监控的集群名称
3 添加需要被监控的应用 Service-Id ,以逗号分隔
4 同上文 management.context-path

3.16. jdbc 批量插入性能问题

如果不开启rewriteBatchedStatements=true,那么jdbc会把批量插入当做一行行的单条处理,也就没有达到批量插入的效果

jdbc配置示例
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/dew?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=true
    username: root
    password: 123456
  • 对于一张七列的表,插入1500条数据,分别对mybatis和jdbctemplate进行测试,记录三次数据如下,可以看到,该配置对于jdbctemplate影响是极大的,而对于mybatis影响却不大,后续有时间再继续深入了解

Table 1. 测试数据
rewriteBatchedStatements mybatis(ms) jdbctemplate(ms) dew(ms)

true

401

88

174

true

427

78

167

true

422

75

176

false

428

1967

2065

false

410

2641

2744

false

369

2299

2398

3.17. http请求并发数性能瓶颈

  • 当策略为Thread时(默认是Thread),hystrix.threadpool.default.maximumSize为第一个性能瓶颈,默认值为10.

修改值时,需要先设置hystrix.threadpool.default.allowMaximumSizeToDivergeFromCoreSize为true,默认为false
hystrix详细配置参见https://github.com/Netflix/Hystrix/wiki/configuration#allowMaximumSizeToDivergeFromCoreSize
  • 第二个瓶颈为springboot内置的tomcat的最大连接数,参数为server.tomcat.maxThreads,默认值为200

3.18. 日志中解析message,动态显示property

  1. 在启动类的main方法中注册converter,如下PatternLayout.defaultConverterMap.put("dew", TestConverter.class.getName());

这个是解析%dew的内容
  1. 自定义Converter继承DynamicConverter<ILoggingEvent>,解析message,获取有效信息并返回解析后得到的字符串。

Converter代码示例
public class TestConverter extends DynamicConverter<ILoggingEvent> {

    @Override
    public String convert(ILoggingEvent event) {
        // 这里未做解析,示例代码
        return event.getMessage();
    }
}

4. 开发指南

4.2. 环境要求

基础环境: >=java8

调试环境: 推荐使用Docker环境准备各个容器

# MySQL环境
docker run -d --name dew-mysql -p 3306:3306 -e MYSQL_DATABASE=dew -e MYSQL_ROOT_PASSWORD=123456 mysql
# Redis环境
docker run -d --name dew-redis -p 6379:6379 redis
# Rabbit环境
docker run -d --name dew-rabbit --hostname dew-rabbit -p 15671:15671 -p 15672:15672 -p 4369:4369 -p 5671:5671 -p 5672:5672 -p 25672:25672 -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=123456 -e RABBITMQ_DEFAULT_VHOST=dew rabbitmq:3-management-alpine
# Hazelcast环境
docker run -d --name dew-hazelcast -p 5701:5701 hazelcast/hazelcast
# Zookeeper环境
docker run -d --name dew-zookeeper -p 2181 -t wurstmeister/zookeeper
# Kafka环境
docker run --name dew-kafka -e HOST_IP=localhost -e KAFKA_ADVERTISED_PORT=9092 -e KAFKA_BROKER_ID=1 -e ZK=zk -p 9092:9092 --link dew-zookeeper:zk -t wurstmeister/kafka

5. 编译部署

5.1. 开发期热部署

引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>
  • 设置IDE(以Intellij IDEA为例)

    • SettingBuild,Executiion,Deployment>Compiler,勾选 Build project automatically

    • Shift+Ctrl+Alt+/ → 选择 Registry,出现 Maintenance 窗口,勾选 compiler.automake.allow.when.app.running

5.2. 打包部署

  1. mvn clean package -P fatjar 即可打出一个包含所有依赖的包

  2. 手工或使用CI工具执行部署

6. 配置速查

6.1. Dew 参数

dew: # Dew 参数key前缀
    basic: # 基础信息
        name: # 服务名称,用于API文档显示等
        version: 1.0 # 服务版本号,用于API文档显示等
        desc: # 服务描述,用于API文档显示等
        webSite: # 官网,用于API文档显示等
        doc:
            enabled: true # 是否启用默认文档配置,关闭后可自定义文档管理,默认为true
            base-package: # API文档要扫描的根包,多指定到 Controller 包中
            contact: # 联系人信息
                name: # 联系人姓名
                url: # 联系人URL
                email: # 联系人邮箱
        format:
            use-unity-error: true # 是否启用统一响应
            error-flag: __DEW_ERROR__ # 默认的通知标识
        error-mapping: # 自定义错误映射
            "[<异常类名>]":
                http-code: # http状态码,不存在时使用实例级http状态码
                business-code: # 业务编码,不存在时使用实例级业务编码
                message: # 错误描述,不存在时使用实例级错误描述
    cluster: # 集群功能
        cache: redis # 缓存实现
        lock: redis # 分布式锁实现,可选 redis/hazelcast,默认redis
        map: redis # 分布式Map实现,可选 redis/hazelcast,默认redis
        mq: redis # MQ实现,可选 redis/hazelcast/rabbit,默认redis
        election: redis # 领导者选举实现,可选 redis/eureka,默认redis
        config: #集群相关配置
            election-period-sec: 60 #领导者选举时间区间,默认60秒
            ha-enabled: true #是否启用HA,默认为true
    notifies:
        "": # 通知的标识
            type: DD # 通知的类型,DD=钉钉 MAIL=邮件,邮件方式需要有配置spring.mail下相关的smtp信息 HTTP=自定义HTTP Hook
            defaultReceivers: # 默认接收人列表,钉钉为手机号,邮件为邮箱
            dndTimeReceivers: # 免扰时间内的接收人列表,只有该列表中的接收人才能在免扰时间内接收通知
            args: # 不同类型的参数,邮件不需要设置
                url: # 钉钉的推送地址,说明详见:https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.karFPe&treeId=257&articleId=105735&docType=1
            strategy: # 通知策略
                minIntervalSec: 0 # 最小间隔的通知时间,0表示不设置,如为10则表示10s内只会发送一次
                dndTime: # 免扰时间,HH:mm-HH:mm 如,18:00-06:00
                forceSendTimes: 3 # 同一免扰周期间通知调用达到几次后强制发送
    security: # 安全功能
        cors:
            allow-origin: * # 允许的来源,建议修改
            allow-methods: POST,GET,OPTIONS,PUT,DELETE,HEAD # 允许的方法
            allow-headers: x-requested-with,content-type # 允许的头信息
        token-flag: __dew_token__ # token 标识
        token-in-header: true # true:token标识在 header 中,反之在url参数中
        token-hash: false # token值是否需要hash,用于解决token值有特殊字符的情况
        include-services: [""] # 服务白名单,要求源服务头部带上 Request-From
        exclude-services: [""] # 服务黑名单,要求源服务头部带上 Request-From
    component:
        auth:
          token-expire-sec: 60 * 60 * 24 * 30 # token有效期,默认为30天
          max-login-error-times: 3 # 最大登录错误次数,默认3次
          clean-error-time-min: 10 # 登录错误限制登录时间,默认为10分钟
          password-salt: # 密码加盐
          email-account: # 发通知邮件的邮箱账号,比如用来发送用户注册成功邮件
          sdk:
              server-url: # 服务器地址
              white-list: # 白名单,逗号分隔
    cloud:
        trace-log:
            enabled: true # 是否启用服务API调用(追踪)日志,默认为true
        error:
             enabled: true # 启用降级邮件通知,默认为false
             notify-flag: __HYSTRIX__ # 默认的通知标识
             notify-event-types: FAILURE,SHORT_CIRCUITED,TIMEOUT,THREAD_POOL_REJECTED,SEMAPHORE_REJECTED # 通知的事件类型
             notify-include-keys: # 需监控的方法key值,与notify-exclude-keys互斥,client类名+#+方法名,for example:  ExampleClient#deleteExe(int,String)
             notify-exclude-keys: # 不需要监控的方法key值,与notify-include-keys互斥,client类名+#+方法名,for example:  ExampleClient#deleteExe(int,String)
    idempotent:
        default-expire-ms: 3600000 # 设置默认过期时间,1小时
        default-strategy: item # 设置默认策略,目前支持item(逐条记录)
        default-opt-id-flag: __IDEMPOTENT_OPT_ID__ # 指定幂等操作ID标识,可以位于HTTP Header或请求参数中

spring:
    application:
         name:  # 项目名称,若使用Dew,请配置
    mail: #mail配置,需要hystrix降级通知需加以下配置
         host: smtp.163.com
         username:
         password:
         properties:
           mail:
             smtp:
               auth: true
                 starttls:
                   enable: true
                   required: true
    hazelcast: # hazelcast相关配置
         username:
         password:
         addresses: ["127.0.0.1"]
         connection-timeout: #默认5000
         connection-attempt-limit: #默认2
         connection-attempt-period: #默认3000

7. 版本说明

7.1. 1.5.0-RC

Features
  • 使用小泰科技Fork版本做为开源版本

  • 添加领导者选举的Redis实现

  • 添加消息通知(钉钉或邮件)

  • 添加生成系统级(多服务)统一离线文档功能

  • 添加MQ消费的HA功能

  • 默认使用micrometer做为指标采集工具

  • 添加对Scala的支持

Improvement
  • 分布式锁中删除lock、lockWithFun操作

  • 分布式锁由可重入改为不可重入

  • redis增加hash incr操作 和 hash decr操作

  • 增加swagger-bootstrap-ui,优化swaggerUI的显示

  • spring-boot升级至1.5.13.RELEASE版本

  • spring-cloud升级到Edgware.SR4版本

  • dew-common升级到1.4.7版本

  • boot-starter默认启用HTTP服务

  • 移除ShardingJDBC的内容

  • 移除服务脚手架功能

  • 移除mybatis-starter模块

  • 暂时移除Dew JDBC模块

Fixed
  • 修复指标采集内存溢出问题

迁移指南(从1.3.4-RC到此版本)
  • 配置变更: 拆分dew.cluster.distdew.cluster.lockdew.cluster.map

  • 配置变更: dew.cluster.election.config.election-period-sec to dew.cluster.config.election-period-sec

  • 功能变更: 领导者选举、分布式锁、分布式Map的实例化方式由 dew.cluster.election/lock/map 修改成 dew.cluster.election/lock/map.instance(…​)

  • 功能变更: 领导者选举isLeader接口需要等待选举产生后再返回(之前逻辑是每次启动时会设置成false再执行选举)

  • 功能变更: 相同Dew.Info.instance的实例在选举过期周期内重启任能保持原先状态

  • 功能变更: 移除服务脚手架,需要手工添加需要的接口服务

  • 功能变更: 移除mybatis-starter模块,请使用mybatis官方方案

  • 功能变更: swagger-ui.html 变更成 doc.html

  • 功能变更: Dew.Info.instanceUUID修改成服务名@Profile@IP:端口

  • 功能变更: 升级后的Tomcat版本不支持Host中带有'_'这种非规范符号

7.2. 1.3.4-RC

Features
  • rabbitmq 增加topic exchange

7.3. 1.3.2-RC

Features
  • 去掉logback-es依赖,使用logstash从日志文件进行采集

7.4. 1.3.1-RC

Fixed
  • #93 修复mybatis-starter对于sharding-jdbc数据源的强制加载

7.5. 1.3.0-RC

Features
  • #87 局部使用sharding-jdbc,mybatis实现,增加mybatis-starter模块

  • #89 支持配置提示

  • #91 Dew实例加载机制优化

Improvement
  • #82 metrics指标增加线程、内存、cpu、磁盘等统计

  • #86 ErrorController增加zuul日志追踪支持

Fixed
  • #92 修复logback-elasticsearch日志压力过大时导致的内存泄漏

升级指南
  1. 修改pom.xml中dew版本号为1.3.0-RC

  2. 1.3.0-RC版本中已移除启动类配置,直接用@SpringBootApplication@SpringCloudApplication

  3. 启动类需要的注解不要忘记自行添加,如@EnableTransactionManagement@EnableScheduling

  4. 新增的mybatis-starter模块,详见使用说明

7.6. 1.2.2-RC

Fixed
  • #45 邮件通知修正

  • #85 日志配置优化

7.7. 1.2.1-RC

Fixed
  • #38 RabbitMQ消息未设置持久化

  • 使用 统一响应——协议无关 类型时,降级HTTP状态码改为500

7.8. 1.2.0-RC

Feature
  • #75 添加幂等处理功能, #77 可选策略类型Bloom Filter尚在开发中

  • #72 实现针对服务整体及每个接口的TPS、最大/平均/90%响应时间Metrics统计

Improvement
  • #68 支持自定义离线文档文件名

  • #70 更友好地获取本机Host

  • #76 cluster.cache 支持更多类型的操作

  • #53 统一响应——协议无关 降级由 1000 改成 555 以提升兼容性

  • #79 增加是否启用默认文档配置

  • #80 增加注解启用Dew功能

  • Swagger文档去除全局token参数

Fixed
  • #43 swagger2markup-maven-plugin 在使用 spring.content-path 无效

1.1.0-RC 迁移到 1.2.0-RC
  1. 使用 统一响应——协议无关 类型时,UI端由原来只需要获取200状态下的数据改成需要获取 200 和 555 状态下的数据,两者对UI端没有区别。( @See https://rep.360taihe.com/csp/dew-framework/issues/53 )

7.9. 1.1.0-RC

Feature
  • [功能] #45 支持服务调用( Hystrix )异常邮件通知

  • [功能] #51 适配新版 用户权限中心 SDK

  • [功能] #59 #49 #15 统一日志规范,适配 sleuth 日志到 ES

Improvement
  • [优化] #53 统一响应——协议无关 类型的http返回码由统一的200改成 2001000 ,前者表示操作成功或不需要降级的错误,后者表示需要做降级(Hystrix fallback)的错误

  • [优化] #50 Dew JDBC 更好地支持没有 Entity 注解的对象

  • [优化] #52 对于java8时间,url参数转换支持String转LocalDateTime,LocalDate、LocalTime,long转LocalDateTime(但json数据不支持),long转Instant

  • [优化] #55 #58 其它一些优化

1.1.0-beta1 迁移到 1.1.0-RC
  1. 使用 统一响应——协议无关 类型时,UI端由原来只需要获取200状态下的数据改成需要获取 200 和 1000 状态下的数据,两者对UI端没有区别。( @See https://rep.360taihe.com/csp/dew-framework/issues/53 )

7.10. 1.1.0-beta1

Feature
  • [功能] #19 支持局部 ShardingJDBC(由于ShardingJDBC 2.0还未RC,测试发现存在较多问题,此功能需要等待官方RC)

Improvement
  • [优化] 支持Java8时间处理

  • [优化] #34 模块Spring化,boot-core 更名为 boot-starter , cloud-core 更名为 cloud-starter

  • [优化] #40 Dew JDBC 独立成 jdbc-starter , 确保核心模块 boot-starter 更轻量

  • [优化] Dew JDBC 性能优化

  • [文档] #47 添加性能调优章节

Fixed
  • [修正] 统一错误拦截返回指定为 MediaType=APPLICATION_JSON_UTF8 以解决 Feign 调用解码错误

1.0.0-RC/betaX 迁移到 1.1.0

1.1.0 修正了 1.0.0 版本的几个设计缺陷,需要做如下的迁移操作:

  • Maven: Dew 框架的版本修正成 1.1.0-X,目前是 1.1.0-beta1

  • Maven: boot-core 更名为 boot-starter , cloud-core 更名为 cloud-starter

  • 核心代码: com.tairanchina.csp.dew.Dew 包路径改成 com.tairanchina.csp.dew.Dew

  • Dew JDBC 模块(使用MyBatis等其它持久化框架的项目可以忽略)

    • SafeEntity 的创建/更新时间 由 Date 换成了 LocalDateTime

    • 所有 entity 包 迁移到 com.tairanchina.csp.dew.jdbc.entity

    • 使用 JdbcTemplate 原生方法时 原来是: Dew.ds().jdbc.xx ,需要修改成 ((DewDS)Dew.ds).jdbc.xx

7.11. 1.0.0-RC

Feature
  • [功能]支持新版用户权限中心认证适配(* 新版用户权限中心Release后,此功能代码会有一定变更)

  • [功能]新增SqlBuilder用于快速构建SQL语句

  • [移除]由于 Spring Cloud Thrift RPC 测试不够充分,此版本中暂时移除

Improvement
  • [功能]支持rabbit confirm(单条)模式

    ((RabbitClusterMQ)Dew.cluster.mq).publish(String topic, String message, boolean confirm)
    ((RabbitClusterMQ)Dew.cluster.mq).request(String address, String message, boolean confirm)
  • [功能]支持 EnabledColumn 结果反转,EnabledColumn用于标识是否启用状态的注解,默认是true是否用,false是禁用,但有些情况下状态字段会使用`del_flag`表示是否删除,这时需要设置结果反转

  • [功能]统一Body及Url Path/Query的异常捕获

  • [功能] tryLock 支持重入

  • [测试]引入 embedded redis 以支持单元测试

  • [文档]添加 以宠物商店为例的 新手入门 章节

  • [修改]原 dew.dao.base-package 改成 dew.jdbc.base-packages 支持多个路径

Fixed
  • 修正Redis锁 Unlock 处理的线程问题

  • 修正jacoco单元测试覆盖率偏少的问题

7.12. 1.0.0-beta5

Feature
  • 添加服务调用限制(可定义A服务不允许B服务调用,防止服务双向依赖) e.g.

    dew.security.exclude-services:
     - serviceB
     - serviceC
  • 添加对Thrift的支持

  • 支持集群Leader Election(非严格模式)

  • 整合Spring Boot Cache

Improvement
  • 优化CURD脚手架

  • 支持UUID形式的主键

  • 优化注解自定义查询( @Select ),通过测试

  • 支持自定义异常配置,见 异常处理 章节

  • 添加Bean分组校验说明,见 异常处理 章节

  • 添加 Sonar 代码质量检查,配置 sonar.host.url 执行 mvn clean verify sonar:sonar

  • 【需要迁移】使用Druid数据库连接池(注意数据库连接配置变更)

  • 【需要迁移】删除 DaoImpl 兼容性类

  • 【需要迁移】将 Dew.e 移到 Dew.E.e,添加 Dew.E.checkXX`异常检查方法,见 `异常处理 章节

Fixed
  • 修正事务失败,重试成功后还是被回滚的问题

7.13. 1.0.0-beta4

Feature
  • 整合 Spring boot adminTurbine,可直观的监控各个性能及访问指标

spring boot admin
  • 添加实验功能:使用注解自定义查询( @Select

Improvement
  • 添加了几个自定义验证方式

  • 添加性能测试报告

  • 移除 DaoImpl ,改用接口 DewDao

为确保兼容, DaoImpl 在这一版本中未物理移除,如有条件请迁移至 DewDao

7.14. 1.0.0-beta3

Feature
  1. Cluster的MQ添加RabbitMQ SPI

Improvement
  1. 支持自定义http错误码( Dew.e(String code, E ex, StandardCode customHttpCode) )

  2. 对加了字段校验(@Valid)的对象,如果检验失败会返回错误详细

  3. 开放将ResultSet转成对象的方法( ds.convertRsToObj(Map<String, Object> rs, Class<E> entityClazz) )

7.15. 1.0.0-Beta2

Feature
  1. 支持生成Html及PDF版本的离线文档

Improvement
  1. 添加Dubbo整合示例,提供Dubbo服务提供无法处理`声明式事务`的方案

  2. 完善文档并改用asciidoc格式

  3. 统一依赖管理

  4. parent 中添加公司maven库

  5. Hazelcast Client升级到3.8.2

  6. Dew-Common升级到1.3.7

7.16. 1.0.0-beta1

Feature
  1. 多数据源支持,详见说明文档`多数据源支持`章节

原`Dew.ds.xx`接口弃用,改为`Dew.ds().xx`,如需要使用其它数据源请使用`Dew.ds(<DS Name>).xx`
Improvement
  1. 新增`mybatisplus-example`

  2. 改善`Swagger`文档支持

  3. 新增销毁时间支持:boolean tryLock(long waitMillSec, long leaseMillSec)

  4. 锁的等待、销毁时间单位由原来的`秒`改成`毫秒`

Fixed
  1. 修正`tryLock`锁(`Redis`实现),锁被其它线程或JVM占用时等待时间的计算错误

8. 路线图

8.1. 2.x

  1. 实现基于Service Mesh的微服务扩展

8.2. 1.5.0

  1. 迁移小泰科技Fork版本到开源版本

  2. Review及优化代码去除不常用功能

8.3. 1.x

实现核心功能,为公司各项目的研发提供能力支撑