📌 术语定义

Spring Boot

Spring Boot 是启动 Spring 项目的工具,是一些库的集合,能够被任意项目的构建系统所使用,用于简化 Spring 应用的初始搭建以及开发过程,使研发不再需要定义样板化的配置。更多详情请参见 Spring 官网

starter

starterSpring Boot 提供的一种管理依赖的方式,借助 Spring Boot 中自动配置的能力,各个功能组件都直接以 starter 的形式存在,使用时只需要引入一个 starter 依赖即可。
Spring Boot 提供了很多官方的 starter 内容。
Shoulder 中,各个功能也是以 starter 的形态存在的,starter 中包含了该模块所需的所有 jar 与相关完整依赖、组件的初始化逻辑以及一些默认配置参数等。更多关于 starter 的描述和逻辑关系请参见 Spring Boot Starters 内容。

本文档讲述内容与代码示例可参见 Shoulder-Demo1

能力激活方式:引入 Shoulder 任意一个 starter

Maven
<dependency>
    <groupId>cn.itlym</groupId>
    <artifactId>shoulder-starter</artifactId>
    <version>1.0.0<version>
</dependency>
Gradle
compile 'cn.itlym:shoulder-starter:1.0.0'

全局ID生成器

Shoulder 提供了两类开箱即用的全局id生成器:StringGuidGeneratorLongGuidGenerator,您可以像这样使用它们。

    @Autowired
    private StringGuidGenerator stringGuidGenerator;

    @Autowired
    private LongGuidGenerator longGuidGenerator;

    public void testGuid() {
        long guidLong = longGuidGenerator.nextId();
        String guidString = stringGuidGenerator.nextId();
        // 批量获取
        long[] guidLongArr = longGuidGenerator.nextIds(10);
        String[] guidStringArr = stringGuidGenerator.nextIds(10);
    }

动态字典

Shoulder 提供了一套开箱即用的字典管理能力,极大的降低了字典模块的编码成本,一个 @EnableDictionaryItemEnum 注解实现以下功能:

  • 枚举 ←→ String/Number/DTO 自动转换

  • 结合 Shoulder-Web 获得更丰富的功能:

    • 支持 JSR 303 规范的校验注解

    • 开箱即用的字典搜索、管理接口

    • 可视化的查询页面

  • 结合 Shoulder-Data-Db

    • 可定制的存储模式,并提供多种默认的存储模式实现

    • 带有缓存的查询与搜索

    • 不停机添加枚举项

    • …​

字典在各行各业都会又,举例:在租借系统,以车为例,汽车品牌出租用途缴费状态 这些可能又多种可选项作为动态字典是比较合适的;同样的,在办公系统中 办公方式办公园区考勤方式 等也是合适的项,实际业务系统中非常常见。

增强的类型转换器

Spring 官方 ConversionService 接口基础上新增更多类型转换,如:

  • String ←→ DataSize ←→ Long

  • String ←→ Date ←→ LocalDateTime ←→ Instant

  • String ←→ Date ←→ LocalDate ←→ Instant

  • 支持多级 ConversionService 供高级用户使用已有的 ConversionService 并支持自定义优先级

✅ 请放心使用,这些转换代码几乎都是 JDK / Sping 中的工具类,无需担心 BUG 与稳定性,Shoulder 拥抱复用与优雅,杜绝造轮子。

全局锁

基于 JDK 的标准 Lock 接口,提供了 ServerLock,在其基础之上增加了更丰富的并发接口:如 tryLockisHoldLock ,且支持通过配置 cluster=true/false 快速支持集群模式的锁。

ServerLock
    @Autowired
    private ServerLock serverLock;

    public void tryLock() {
        LockInfo lockInfo = new LockInfo(shareKey, Duration.ofSeconds(10));
        jdbcLock.lock(lockInfo);

        try {
            // 做你的事情
            String xx = "do something";
        } catch (Exception) {
            log.xxx
        } finally {
            jdbcLock.unlock(lockInfo);
        }
    }

基于 Shoulder 全局锁 ,即使开始为单机项目,以后集群改造时无需修改代码,通过配置即可支持集群模式下的全局锁。

默认为 单机模式,若想支持集群,可以引入数据库或 Redis 相关依赖,设置 shoulder.lock.type=jdbc/reedis 即可,无需修改任何带代码。

Shoulder 默认提供了以下几个实现:

  • JdkLock: 基于 JDKReentrantLock,不支持集群。

  • FileSystemLock: 基于 操作系统-文件系统 的锁实现,支持同一机器的多个应用共享资源控制,适用于小型项目,无数据库场景。

  • JdbcLock: 基于 DB + JDBC 的分布式锁实现,支持集群,高可用。

  • RedisLock: 基于 Redis 的分布式锁实现,支持集群,高并发下与 JdbcLock 相比性能更佳,实现借鉴了 Redisson

  • MemoryLock:基于内存的锁,主要用于演示工程中使用,不支持集群。【⚠️ 仅用于 Demo 中】

用锁时,牢记口诀 一锁、二判、三更新 用锁不出错哦!

运行上下文 AppContext

Shoulder 提供了基于 ThreadLocalAppContext,用于存储单次业务处理信息的上下文,可据此实现 tracer 追踪流量染色压测识别流量血缘识别 等高级能力。

字典枚举示例
public void createUser(CreateUserRequest req, HttpHeaders httpHeaders) {
    // 操作上下文
    AppContext.setUserId(httpHeaders.get("userId"));
    AppContext.setUserName(httpHeaders.get("userName"));
    AppContext.setTenantCode(httpHeaders.get("tenant"));
    AppContext.setLocale(httpHeaders.get("language"));
    AppContext.setTraceId(httpHeaders.get("x-trace-id"));

    testB(req);
}

public void testB(CreateUserRequest req) {
    // 从上下文获取
    AppContext.getUserId();
    AppContext.getUserName();
    AppContext.getTenantCode();
    AppContext.getLocale();
    AppContext.getTraceId();
}

日志

Shoulder 使用 Slf4j 记录日志,故您可以自行选择 LogBackLog4JCommonns-LoggingJUL…​ 遵循 Spring Boot 日志的默认项:LogBack

自动的链路追踪

使用 org.shoulder.core.log.Logger 打印的日志,都会自动在日志中加上本次请求的 traceId 而无需您手动指定,默认格式如下:

# 未引入 Shoulder,无 trace
2025-01-09 21:34:54.313  DEBUG [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : 日志内容
# 引入 Shoulder 后,自动记录 trace 🔍
2025-01-09 21:34:54.360 <00341eda1736415294268100107f000001,> DEBUG  [77898] --- [nio-8080-exec-1] o.s.w.t.d.c.DictionaryEnumController     : 日志内容

有了 traceId线程 id,还原代码执行现场更方便了!

Shoulder WEB 会自动在 HTTP 请求时生成 traceId 放在上下文中,并在日志、响应的 HttpHeader 中体现。

错误码支持

在成熟的系统中,规范的错误码是可长期维护的必要条件,Shoulder 不仅提供了规范参考,还支持日志中方便地打印错误码(ErrorCode)与异常(BaseRuntimeException)。

部分代码法示例:

public interface Logger extends org.slf4j.Logger {
    void error(ErrorCode errorCode);
    void error(ErrorCode errorCode, Throwable t);
    void error(String errorCode, String msg);
    //...
}
请尽情享用 org.shoulder.core.log.Logger ,以便统一您的日志格式,更有效管理和分析日志,无需再为 日志格式 问题发愁。

默认 Logger

Shoulder 默认日志输出已经为您自动配置好一些 Logger,直接在 org.shoulder.core.log.AppLoggers 使用即可。

示例代码:

    AppLoggers.APP_SERVICE_DIGEST.info("url={}, success={}, cost={}, result={}", requestUrl, true, cost, returnStr);

打印在 ${logging.file.path}/${shoulder.application.name} 目录中

LoggerName 打印建议 文件名

APP_DEFAULT

APP启动/重启等时逻辑,默认包路径日志打在这里

default.log

APP_WARN

APP启动/重启等运维时逻辑

warn.log

APP_ERROR

所有需要关注的 error 日志都在这里出现

error.log

APP_BIZ

业务逻辑处理过程

biz.log

APP_ROUTER

业务路由,新老链路/逻辑切流日志:决策因子、规则、切流结果

router.log

APP_INTEGRATION

调用其他应用 / 服务的入参、出参、错误码等详细信息

integration.log

APP_INTEGRATION_DIGEST

调用其他应用 / 服务的接口、方法、成功、耗时等关键信息

integration-digest.log

APP_SERVICE

对外暴露接口/服务的日志详情,入参、出参等

service.log

APP_SERVICE_DIGEST

对外暴露接口/服务的接口、方法、成功、耗时等关键信息

service-digest.log

APP_SERVICE_WARN

对外暴露接口/服务的接口执行出现预期内异常记录(参数校验失败等)

service-warn.log

APP_SERVICE_ERROR

对外暴露接口/服务的接口执行失败记录

service-error.log

APP_DAL

数据访问层-详细日志

dal.log

APP_DAL_DIGEST

数据访问层-摘要日志

dal-digest.log

APP_CAL

数据访问层-缓存日志

cal.log

APP_CAL_DIGEST

数据访问层-缓存-摘要日志

cal-digest.log

APP_DAEMON

定时任务触发记录

daemon.log

APP_MSG_CONSUMER

消费消息记录,如消息内容,消息来源等

msg-consumer.log

APP_MSG_PRODUCER

发布消息记录,如消息内容等

msg-producer.log

APP_NOTIFY

向其他系统发送通知,如外部

notify.log

APP_CONFIG

收到配置变更,读取到配置中心的配置内容

config.log

APP_CODE_MAPPING

错误码映射记录,如调用 x 应用返回 errorCode_123,对于本服务接口应该返回 errorCode_567 时记录

code-mapping.log

Shoulder 内置了一些仅适用于框架内部使用 Logger,若您想了解,可前往 Shoulder 框架内置日志说明

高性能的日志拼接

Shoulder 内置的 org.shoulder.core.log.LoggerSlf4j 基础上进一步增强了复杂参数打印的性能,见示例代码:

import java.util.function.Supplier;
import org.shoulder.core.log.LoggerFactory;
import org.slf4j.Logger;

public class Demo {

    private static final Logger log = LoggerFactory.getLogger(Demo.class);

    public void testLog() {
        if(oldLog.isInfoEnable()) { // ⚠️ 传统需要 isDebugEnable 判断日志级别是否激活,避免无效运算
            String argX = mockCalculateArgs();
            oldLog.info("打印一个耗时的参数 {}", argX);
        }

        Supplier<String> argX = Demo::mockCalculateArgs;
        log.info("打印一个耗时的参数 {}", argX); //  👈 Shoulder 中自动判断,扔掉冗余的 if isXxxEnable
    }

    public static String mockCalculateArgs() {

        try {
            Thread.sleep(5_000); // 模拟组装、拼接一个大对象

            return "模拟组装、拼接一个大对象,涉及很多耗时操作";

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

    }

}

日志格式规范化

默认的 Logger 中已经预设置了 规范的日志格式,且支持用户自行调整与扩展(如自定义 logback.xml

TODO 举例打印格式

日志文件划分

Shoulder 默认提供了 Logback 的配置文件,已为您提供了默认的日志 内容分类文件拆分,默认会按照以下划分日志文件:

Shoulder 默认将其他组件的日志(如 redisson)打印在 ${logging.file.path}/other/common.log 中,若您需要特殊管理,按照 Spring Boot 提供的方案自行定义 logger 即可。

可以在 shoulder-autoconfigurationlogback-spring.xml 查看配置内容。

该功能基于 Spring Boot,但 Spring Boot 官方文档说明该配置不能与 Logback配置扫描 一起使用。故在使用时请不要主动开启 Logback配置扫描

错误码与异常

为了降低编码 + 维护成本,Shoulder 提供了统一的规范类,对于自定义错误码枚举/异常可用直接继承 org.shoulder.core.exception.ErrorCode 接口即可,并且 Shoulder 提供了错误码多语言、转异常等丰富功能。

并发工具

Threads 线程工具类

提供了常用并发操作的封装,确保安全、高效地执行并发任务,以简化Java并发编程。

Threads 提供了多种线程池拒绝策略实现,如 Discard, DiscardOldest, AbortBlock,以多样应对线程池任务队列满的情况。
您可以通过 setExecutorService() 方法更改默认内置线程池。
Threads 类依赖于 ContextUtilsLogHelper 进行上下文获取和日志美化,确保这些类的可用。

异步执行

您可以通过 Threads.execute() 让代码在异步执行,如希望异步打印 "hello, shoulder!"

异步执行示例
    Threads.execute(() -> System.out.printLn("hello, shoulder!"));

延迟执行

您可以通过 Threads.delay() 让代码延迟执行,如希望一分钟后打印 "hello, shoulder!"

延迟执行示例
    Threads.delay(() -> System.out.printLn("hello, shoulder!"), Duration.ofMinutes(1));

批量异步执行且等待

您可以通过 Threads.executeAndWait() 让代码延迟执行,如希望执行一批任务,10个线程分别打印 "shoulder-processing-task-i"(i: 1~10):

批量异步执行且等待-示例
    List<Runnable> tasks = IntStream.range(1, 11)
        .mapToObj(i -> new Runnable(() -> System.out.println("shoulder-processing-task-" + i)))
        .toList();
    Threads.executeAndWait(tasks);

全局异步增强器 ThreadEnhancer

若希望所有异步线程执行前后做一些操作,可以实现 org.shoulder.core.concurrent.enhance.ThreadEnhancer 接口,如 监控记录、自动拷贝/清理 ThreadLocal 等高级能力。

以下是实现【自动统计在线程池队列内等待时间等指标】的例子:

异步增强器-示例
/**
 * 自动统计在线程池队列内等待时间等指标
 */
public class MonitorRunnableEnhancer implements ThreadEnhancer {
    @Override
    public EnhancedRunnable doEnhance(EnhancedRunnable runnable) {
        return new DefaultMonitorableRunnable(runnable);
    }
    @Override
    public <T> EnhancedCallable<T> doEnhance(EnhancedCallable<T> callable) {
        return new DefaultMonitorableCallable<>(callable);
    }
}
//  将该类注入进 Spring 上下文
@Configuration
public class MonitorRunnableAutoConfiguration {
    @Bean
    public MonitorRunnableEnhancer enqueueTimeEnhancer() {
        return new MonitorRunnableEnhancer();
    }
}

工具类

Shoulder 提供了一些高频的工具类(基于成熟的工具包:apache-commonsSpring 内的工具类google.guavahutools

对于 JSON 处理,与 Spring 的默认选型一致:采用安全、扩展良好的 jackson

扩展:为什么选 jackson 而非 fastjson 作为默认
  • 官方默认:jacksonSpring 充分考量后的默认选择。

  • 安全方面:近些年 jackson 出现恶行漏洞较 fastjson 更少。

  • 扩展性:jackson 有更多的扩展接口,如字段转换、映射等等大量的高级功能。

  • 代码质量:jackson 代码更规范,版本升级兼容性问题更少。

  • 社区成熟与稳定性:jackson 代码活跃维护者更多,不需要担心无人维护。

  • 整体成本:考虑到两者的序列化性能几乎一致,反序列化 fastjson 性能仅略优,结合代码维护人力成本、机器成本看,jackson 远低于 fastjson

  • 当然:FastJson 由于其简单的用法在一些非开放网络环境下、工具型软件、或非长期维护的代码确实更香!请合理选择。

shoulder core util

Shoulder 在 JSON 序列化上默认使用的是 Jackson(与 Spring-Boot 一致)。

多语言/国际化工具 Translator

简化您的多语言应用开发 —— Translator,精心设计的接口,在 Spring 的国际化功能(MessageSource)基础上升级,使用更简单、便捷。

亮点

  • 去除资源文件命名限制:命名无须为 messages_Shoulder 允许您更自由地命名以更方便的按模块管理您的多语言文件。

  • 语言自动识别:根据当前用户设置或系统默认值自动选择合适的语言环境([_运行上下文_appcontext]),实现上下文感知,高度个性化的翻译。

  • 简化的方法签名:提供 getMessageOrDefault 方法,允许指定默认值,避免因找不到翻译而中断程序。

  • 链式调用友好:默认值和参数化的翻译方法,支持直接使用或结合参数动态生成消息。

  • 智能默认处理:当特定翻译不可用时,立即回退到提供的默认消息,保证应用流畅运行。

  • 改良的资源文件目录:让多语言资源文件的管理更轻松。

NOTE: .去除资源文件命名限制

无资源文件命名限制, Spring / JDK 要求翻译文件命名必须为 messages_,—— 这陈旧的规则完全没必要,Shoulder 允许您更自由地命名以更方便的按模块管理您的多语言文件,如 user.properties dictionary.properties,这在大型的项目中更加容易开发、管理和维护。

配置

在您的 resources 目录下添加 language/zh_CN/loginpage.properties,就可以使用 Translator 轻松翻译多语言。

properties 配置
i18n.page.button.login=您好,{}!
代码使用
@Autowried
private Translator translator;

public String genHelloTip() {
    String msg = "i18n.page.button.login";
    String userName = "shoulder";

    return translator.getMessageOrDefault(msg, "welcome, {}", userName); // 不指定语言时默认从上下文取 AppContext.getLocaleOrDefault()
    // 返回:您好,shoulder! (若未配置,返回 welcome, shoulder)
}

应用元信息 AppInfo

Shoulder 提供了获取应用元信息的统一门面:org.shoulder.core.context.AppInfo

取应用基本信息示例代码
    // 应用id:默认使用 spring.application.name
    String appId = AppInfo.appId();
    // 应用版本号:默认使用 v1
    String version = AppInfo.version();
    // 应用示例id:默认为 0
    String instanceId = AppInfo.instanceId();
    // 运行环境,如 dev、test、prod
    String env = AppInfo.env();

借助 Spring-Boot,您可以在 application.xml / yml 中配置他们,应用启动时,Shoulder 将自动从 application.properties 中获取这些配置项。

扩展:Shoulder 的理论指南-基础软件管理规范 中讲述了应用相关的概念和定义。

更多应用元信息配置请参考 应用元信息配置总览