ws-springx

忆此些年来作为Javaer的coding经历,也遇到不少通用性的功能实现,闲来作此库以沉淀之,就叫做ws-springx吧

Github: https://github.com/WindShadow-mo/ws-springx.git

MethodPeeper方法观测器

在编写业务aop组件时,我们通常以@Aspect + 切点表达式的方式去开发。实际开发中会存在以下问题:

  • 被增强的类或方法往往在少数,但需要特别单独的写一套基于Aspect的切面组件
  • 需要开发者足够细心,如果切点表达式没写好,可能在这个版本能用,在后续版本中新增了其它类也满足了切点,就会出现意外的增强导致bug
  • 开发者编写通知(增强)逻辑时,容易忽略异常或者不正确的处理异常,导致“吞异常”的情况出现。现象如目标方法正常执行返回,但是上层调用却得不到正确的结果。或者目标方法执行时抛出了异常,但是切面吞掉了异常,返回了一个自以为的结果,上层仍接收到了返回,但是这和方法预期是不符的,即目标方法没有满足原先的预期约束和返回。
  • 增强的逻辑对目标方法的执行过程来说,往往更像是一种观察、窥探,而不是修改其原先的入参、执行逻辑和结果

使用MethodPeeper能更简便的编写aop组件逻辑,而不用担心上面的问题,

例:

使用@EnableMethodPeek开启MethodPeeper支持

1
2
3
4
5
6
7
@EnableMethodPeek
@SpringBootApplication
public class SpringxApp {
public static void main(String[] args) {
SpringApplication.run(SpringxApp.class, args);
}
}

在bean的public方法上使用@ExposurePoint注解来暴露被观察方法,我们称之为暴露点

1
2
3
4
5
6
7
8
9
@Slf4j
@Service
public class NormalService {
@ExposurePoint("consumerString")
public String consumerString(String str) {
log.info("consumerString, str : {}", str);
return "consumerString: " + str;
}
}

使用@Peeper注解声明观察者并指明观察的bean的class,使用@PeekPoint注解标记观测点,并通过返回ReturnValuePeeper对象来观测目标方法的返回值或异常。

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@Peeper(value = NormalService.class)
@Component
public class NormalServiceAdvice {
@PeekPoint("consumerString")
public ReturnValuePeeper<String> consumerString(String str) {

log.info("method consumerString str: {} - Args Advice", str);
return (v, ex) -> log.info("return-consumerString: {}, ex: {}", v, ex);
}
}

ReturnValuePeeper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 返回值观测者,实现此接口用于观测{@linkplain ws.spring.aop.annotation.ExposurePoint 暴露点}方法的返回值
*
* @author WindShadow
* @see ws.spring.aop.annotation.ExposurePoint
* @see MethodPeeper
*/

@FunctionalInterface
public interface ReturnValuePeeper<T> {

/**
* 观测暴露点方法的返回值
*
* @param returnValue 目标方法执行成功的返回值(void 的返回值为null)
* @param ex 目标方法执行期间抛出的异常,无异常时其值为 null
*/
void peekReturnValue(@Nullable T returnValue, @Nullable Exception ex);
}

若调用NormalService.consumerString,参数为”abc”,其日志输出将如下:

1
2
3
method consumerString str: abc - Args Advice
consumerString, str : abc
return-consumerString: consumerString: abc, ex: null

此外,你可以使用以下方式编写观测点方法。结合Order注解可以编排观测点方法的执行顺序。指定asyn 属性@PeekPoint(asyn = true)来表明对于返回值的观测是异步的,这样不会使暴露点方法执行的太久。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Slf4j
@Peeper(value = NormalService.class)
@Component
public class NormalServiceAdvice {

// case1: 声明参数,声明ReturnValuePeeper返回值,既观测参数=且异步观察执行结果
@PeekPoint(value = "consumerString", asyn = true)
public ReturnValuePeeper<String> consumerStringAync(String str) {

log.info("method consumerString str: {} - Args Advice", str);
return (v, ex) -> log.info("return-consumerString: {}, ex: {}", v, ex);
}

// case2: 不声明参数,声明ReturnValuePeeper返回值,既只观察执行结果
@PeekPoint("consumerString")
public ReturnValuePeeper<String> consumerString() {

log.info("method consumerString - Args Advice");
return (v, ex) -> log.info("return-consumerString: {}, ex: {}", v, ex);
}

// case3: 声明参数,不声明ReturnValuePeeper返回值,既只观察参数
@PeekPoint("consumerString")
public void consumerStringVoid(String str) {

log.info("method consumerStringVoid str: {} - Args Advice", str);
}

// case4: 不声明参数,不声明ReturnValuePeeper返回值,既不观测参数也不观察执行结果
@Order(1)
@PeekPoint("consumerString")
public void consumerStringVoid() {

log.info("method consumerStringVoid - Advice");
}
}

一种常见的声明方式,PeekPoint观测点方法与ExposurePoint暴露点方法的方法名、参数类型和参数顺序一致时,无需额外声明它们的名称,这将由默认的取名逻辑确定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface SomeService {

@ExposurePoint
String proccess(String arg);
}

@Peeper(value = SomeService.class)
@Service
public class SomeServiceAdvice {

@PeekPoint
public ReturnValuePeeper<String> proccess(String arg) {
// do peek
}
}

EnumValue

设计一个枚举类时,我会遇到枚举内部属性与枚举值本身存在一一对应的情况,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public enum OperateSys {
Windows("code_100"),
Linux("code_200");

private final String code;

OperateSys(String code) {
this.code = code;
}

public String getCode() {
return code;
}

private static final Map<String, OperateSys> MAPPING = new HashMap<>();

static {

for (OperateSys os : OperateSys.values()) {
MAPPING.put(os.getCode(), os);
}
}

public static OperateSys resolve(String code) {
return MAPPING.get(code);
}
}

为了更方便通过code获取OperateSys枚举,通常需要对外提供一个resolve方法。这样通过枚举方法getCode与静态方法resolve,我们可以在code与OperateSys之间进行来回转换。

随着枚举越来越多,这种固定式写法也会不停地复制粘贴。

使用EnumValue接口与EnumValues工具类可以解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 枚举类型实现此接口提供一个value值反映枚举本身,
* 你可以通过{@linkplain EnumValues}来操作实现了该接口的枚举
*
* @author WindShadow
* @see EnumValues
*/

public interface EnumValue<T> {

/**
* 获取枚举的对应的其它类型的值,该值不能为null,多次调用必须返回同一个值,即满足{@linkplain Object#hashCode() hashCode}和{@linkplain Object#equals(Object) equals比较}的结果相同
*
* @return the value, not null
*/
@NonNull
T getValue();
}

OperateSys枚举改造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum OperateSys implements EnumValue<String> {
Windows("code_100"),
Linux("code_200");

private final String code;

OperateSys(String code) {
this.code = code;
}

public String getCode() {
return code;
}

@Override
public String getValue() {
return getCode();
}
}

使用EnumValues操作枚举

1
2
3
4
5
OperateSys os = EnumValues.getEnum(OperateSys.class, "code_100");
List<OperateSys> enums = EnumValues.getEnums(OperateSys.class, "code_100", "code_200");
Map<String, OperateSys> valueMap = EnumValues.getValueMap(OperateSys.class);
Set<String> valueSet = EnumValues.getValueSet(OperateSys.class);
boolean b = EnumValues.complyWith(OperateSys.class, "code_300"); // false

注册spring转换器

1
2
3
4
5
DefaultConversionService service = new DefaultConversionService();
ConverterRegistry registry = service;
EnumValues.registerEnumValueConverter(OperateSys.class, registry);
service.canConvert(OperateSys.class, String.class); // true
service.canConvert(String.class, OperateSys.class); // true

Logback日志扩展

log4j中可以通过file-permissions配置日志文件拥有的系统权限

1
2
3
4
5
6
<appender name="RollingFile" class="org.apache.log4j.RollingFileAppender">
<file value="logs/app.log">
<param name="file-permissions" value="600" />
</file>
<!-- 其他配置 -->
</appender>

而logback中没有此类配置,因此扩展Appender和RollingPolicy以实现

如下使用AccessControlRollingFileAppender、AccessControlBasedRollingPolicy,在filePermissions结点中你可以使用数字或表达式配置日志文件权限

1
2
3
4
5
6
7
8
9
10
11
12
13
<appender name="ROOT_WS" class="ws.spring.log.AccessControlRollingFileAppender">
<file>${LOG_HOME}/xxx.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${LOG_CHARSET}</charset>
</encoder>
<rollingPolicy class="ws.spring.log.AccessControlBasedRollingPolicy">
<!-- 扩展的属性,设置归档日志文件权限 -->
<filePermissions>600</filePermissions>
</rollingPolicy>
<!-- 扩展的属性,设置日志文件权限 -->
<filePermissions>rw-------</filePermissions>
</appender>

Escaper文本转义器

文本转义器,通过Escaper与Escape注解的组合,你可以自定义实现自己的转义器,通常用于在字符串类型的方法参数上进行转义。这可以使你无需在方法实现上额外处理参数

使用@EnableEscape开启Escaper支持

1
2
3
4
5
6
7
8
@EnableEscape
@SpringBootApplication
public class SpringxApp {

public static void main(String[] args) {
SpringApplication.run(SpringxApp.class, args);
}
}

例:实现SqlEscape注解,来对参数进行转义,避免恶意参数造成sql注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@FunctionalInterface
public interface Escaper {

String escape(String value);
}

public class SqlEscaper implements Escaper {

public static String escapeSqlValue(String value) {

if (StringUtils.hasText(value)) {

StringBuilder sb = new StringBuilder();
for (char c : value.toCharArray()) {

if (c == '_' || c == '%' || c == '\\') {
sb.append('\\');
}
sb.append(c);
}
return sb.toString();
}
return value;
}

@Override
public String escape(String value) {
return escapeSqlValue(value);
}
}

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Escape(SqlEscaper.class) // 指明使用的转义器
public @interface SqlEscape {
}

// 如在mapper中使用
public interface SomeMapper {
List<String> selectXxByKeywords(@SqlEscape String keywords); // "abc_" -> "abc\_"
}

Validation扩展

扩展了常用的Validation校验注解,部分如下:

  • @IPv4

    约束字符串为IPv4地址格式

  • @MAC

    约束字符串为MAC地址格式

  • @UUID

    约束字符串为UUID格式

  • @StringRange

    约束字符串的可选范围

    1
    public void aMethod(@StringRange({"aaa","bbb"}) String str) // str值只能为"aaa"或"bbb"
  • @EnumRange

    约束枚举值的可选范围

    1
    2
    3
    4
    5
    6
    public enum MyEnum {
    // 金木水火土
    JIN,MU,SHUI,HUO,TU
    }

    public void aMethod(@EnumRange(enumType = MyEnum.class, enums = {"JIN","MU","SHUI"}) MyEnum param)

Web

FormModel表单绑定

FormModel通常解决表单提交带有不同前缀相同后缀的参数绑定到java对象的痛点
假设有如下表单提交:

参数 参数值
user.name WindShadow
user.desc 一名软件工程师
city.name 北京
city.desc 中国首都

你可以使用 FormModel 注解进行如下参数映射,其中 @FormModel User user 的声明等价于 @FormModel(prefix = "user", separator = ".", required = true) User user

1
2
3
4
@GetMapping("/from-model")
public RestResponse<String> formModelBind(@FormModel User user, @FormModel City city) {
// ...
}

不仅如此,你可以使用@Validated 参数进行校验,类似其它spring的参数绑定机制,同样支持Optional对象的参数声明,即以下参数声明都是有效的

1
2
3
4
5
6
7
8
@GetMapping("/from-model")
public RestResponse<String> formModelBind(@Validated @FormModel User user) { /* */ }

@GetMapping("/from-model")
public RestResponse<String> formModelBind(@FormModel Optional<User> user) { /* */ }

@GetMapping("/from-model")
public RestResponse<String> formModelBind(@Validated @FormModel Optional<User> user) { /* */ }

注:FormModel 不支持基础类型与其包装类的绑定以及String类型,因为使用@RequestParam注解就可以达到相同的效果

开启条件,通过mvc配置接口,注册对应参数解析器

1
2
3
4
5
6
7
8
@Configuration
public class PracticalWebBindSupport implements WebMvcConfigurer {

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new FormModelResolver());
}
}

SingleEntity单属性实体

用于表示单一结构的对象,支持动态json key,常用于web环境中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface SingleBean<T> {
T getValue();
}

@JsonDeserialize(as = JacksonSingleEntity.class)
public interface SingleEntity<T> implements SingleBean<T> {
// ...
static <T> SingleEntity<T> of(String key, T value) {

JacksonSingleEntity<T> entity = new JacksonSingleEntity<>();
entity.putJsonValue(key, value);
return entity;
}
}

在设计接口响应数据结构时,如果遇到仅需要响应一个仅有一个键值对的简单json结构时,如{"name": "tom"}{"size": 100}{"flag": true}等,往往key名称不同,为了避免定义过多的相似结构的pojo类,可以使用SingleEntity来解决此问题。

同理,也可用于接收request body中的数据,且支持@Validated + @Valid方式的方法级别校验

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Validated
@RestController
@RequestMapping("/simple-body")
public class SimpleBodyEntityController {

@PostMapping("/request")
public RestResponse<String> simpleRequestBody(@Valid @RequestBody SingleEntity<@NotBlank String> reqBody) {
System.out.println(reqBody.getValue());
//...
}

@GetMapping("/response")
public RestResponse<SingleEntity<String>> simpleResponseBody() {
String name = "tom";
return GlobalRest.SUCCESS.of(SingleEntity.of("name", name));
// SimpleBodyEntity 将json序列化为 {"name": "tom"} 置于RestResponse中
}
}

SingleEntity继承了SingleBean接口,ws-springx 提供对SingleBean的validation提取器的支持,故而应用中有了SingleBean对象的 JSR-303的校验支持。

RestProxy rest请求简易代理

日常开发中,通常会遇到A服务请求B服务,B服务处理数据后,转发给C服务的情况。其实就是个小代理操作,但又没必要引入springcloud-gateway这种大组件。

使用RestProxy 你可以轻松完成上述操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class BServerController {

@GetMapping("/api/b/user")
public ResponseEntity<User> user(RestProxy proxy) {
// 替换请求路径 /api/b/user -> /api/c/user
proxy.replacePath("/api/c/user");
// 设置(或覆盖)token
proxy.header("X-Token", "xxxx")
// 调用到c服务
return proxy.proxy("http://162.168.100.100:8080", User.class);;
}
}

开启条件:通过mvc配置接口,注册对应参数解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class PracticalWebBindSupport implements WebMvcConfigurer {

private final RestTemplateBuilder builder;

public PracticalWebBindSupport(RestTemplateBuilder builder) {
this.builder = builder;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new RestProxyResolver(builder));
}
}

YamlSource yaml资源引入

spring提供了@PropertySource引入额外properties资源,这里提供了引入yaml资源的注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@YamlSource("classpath:redis/redis-server-config.yml")
@EnableConfigurationProperties(RedisServerProperties.class)
class RedisServerConfiguration {

@Bean
public static RedisServerFactory redisServerFactory(RedisServerProperties properties) {
return new DefaultRedisServerFactory(properties);
}

@Bean
public static RedisServerBean redisServerBean(RedisServerFactory providerFactory, RedisServerProperties properties) throws IOException {
return new RedisServerBean(providerFactory.buildRedisServer(), properties.getStopTimeout());
}
}

FrequencyRestrict频控器

当我们需要对接口进行限流时,常用策略是限制请求个数,即接口只能同时被请求n次,如令牌桶策略,这种策略的效果是系统只同时处理n次该接口的请求,其它一律拦截(熔断),这种策略很容易实现。

而基于时间窗口的策略限制频率策略很少被提及,它和令牌桶乍一看很像,其实不然。限频指的是指定时间内仅接受n次请求,比如1秒中内只能同时请求某个接口n次,即在时间轴上任意1秒跨度内,该接口的请求次数最多不超过n次。

FrequencyRestrict可以满足上述频控需求,使用方式如下

使用@EnableFrequencyRestrict开启频控器支持

1
2
3
4
5
6
7
8
@EnableFrequencyRestrict
@SpringBootApplication
public class SpringxApp {

public static void main(String[] args) {
SpringApplication.run(SpringxApp.class, args);
}
}

在spring bean方法上使用@FrequencyRestrict开启频控配置

1
2
3
4
5
6
7
8
9
@Slf4j
@Service
public class NeedRestrictService implements INeedRestrictService {

@FrequencyRestrict(refer = "T(java.lang.String).valueOf(#name)")
public void access(String name) {
log.info("call access. name: {}", name);
}
}

@FrequencyRestrict配置属性

  • name:频控器的名称,默认为方法限定名,频控器名全局唯一
  • refer:频率控制依据,基于refer计数以计算频率,支持spel表达式
  • frequency:限定的频次,正整数
  • duration:持续时长(单位:秒),默认1秒,最低为1秒

频控器的核心bean为 ws.spring.restrict.FrequencyRestrictService,默认实现是SimpleFrequencyRestrictService,你可以手动注册此bean来覆盖默认实现,这里提供了基于Redis的实现:RedisFrequencyRestrictService

SSH操作

ws-springx提供了ssh简单操作的模板:ws.spring.ssh.SshOperations

你可以通过它来对一个ssh连接进行以下操作:

  • exec:执行命令
  • shell:基于输入输出流建立一个shell交互通道
  • forward:端口转发

更多细节详见:ws.spring.ssh.SshOperations源代码

核心能力由ws.spring.ssh.SshService提供,默认基于sshd-mina实现的,如果有需要你可以自信覆盖实现。

使用方式:

pom.xml中额外引入sshd-mina

1
2
3
4
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-mina</artifactId>
</dependency>

在配置中开启ssh功能并注册ssh数据源,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
ext:
ssh:
enabled: true #开启ssh功能
accounts: #ssh数据源,账密
-[main]:
host: 192.168.100.100
port: 22
username: root
password: root123456
forwards: #端口转发,本地8001端口转发到远程服务端口192.168.100.101:8002
- local-host: 127.0.0.1
local-port: 8001
remote-host: 192.168.100.101
remote-port: 8002
key-pairs: #ssh数据源,rsa密钥
-[main-key]:
host: 192.168.100.100
port: 22
username: root
private-key: classpath:key/rsa.private
key-password: rsa123456

对于端口转发,你可以使用@SshProxy注解在java代码层面进行转发代理

1
2
3
4
5
6
7
8
@SshProxy(source = "main", // 数据源名称
localHost = "127.0.0.1",
localPort = "8001",
remoteHost = "192.168.100.102",
remotePort = "8002")
@Configuration(proxyBeanMethods = false)
public class SshProxyConfig {
}

@SshProxy注解配置属性均支持从配置获取,且支持多次配置

ExactConfiguration 精准配置

当我们使用@Configuration编写spring配置类时,希望此配置类通过@Import引入。如

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MyConfiguration {
// ...
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyConfiguration.class)
public @interface EnableMyFunction {}

上述代码,我们希望通过@EnableMyFunction注解引入MyConfiguration配置类来启用某些功能。尤其在开发二方库或公共组件时,这非常常见。

但是如果MyConfiguration被spring通过@ComponentScan扫描时,就非常不好,我们没法控制每个项目扫描包的路径。

使用@ExactConfiguration声明配置类就能避免这个问题

1
2
3
4
@ExactConfiguration
public class MyConfiguration {
// ...
}

其原理与spring排除自动装配配置类(@AutoConfiguration修饰的配置类)类似。

在此不作展开,详见:ws.spring.context.annotation.ExactConfiguration

工具

树节点工具

对于实现前向结点接口的pojo,可以通过Nodes工具类来为其构建对应的树形数据结构

1
2
3
4
5
6
7
public interface ForwardNode<K> {

K fetchKey(); // 获取当前结点的key

@Nullable
K fetchParentKey(); // 获取父结点的key
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 这是一个常见的部门表对应的结构体,每个部门的ID是唯一的
@NoArgsConstructor
@Data
@ToString
public static class Dept implements ForwardNode<String> {
private String deptId;
private String deptName;
private String parentDeptId;
@Override
public String fetchKey() {
return getDeptId();
} @Override
public String fetchParentKey() {
return getParentDeptId();
}
}

通过Nodes工具类将一个部门集合转换成一颗部门树

1
2
3
4
5
6
7
8
9
List<Dept> depts = Arrays.asList(
new Dept("H1", "name1", null),
new Dept("H2-1", "name2-1", "H1"),
new Dept("H2-2", "name2-2", "H1"),
new Dept("H3", "name2-2", "H2-1"),
new Dept("H4", "name2-2", "H2-2"));

TreeNode<Dept> node = Nodes.transToSingleTree(depts);
System.out.println(JacksonUtils.toJson(node));

输出将如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"identity": {
"deptId": "H1",
"depName": "name1"
},
"children": [
{
"identity": {
"deptId": "H2-1",
"depName": "name2-1",
"parentDeptId": "H1"
},
"children": [
{
"identity": {
"deptId": "H3",
"depName": "name2-2",
"parentDeptId": "H2-1"
}
}
]
},
{
"identity": {
"deptId": "H2-2",
"depName": "name2-2",
"parentDeptId": "H1"
},
"children": [
{
"identity": {
"deptId": "H4",
"depName": "name2-2",
"parentDeptId": "H2-2"
}
}
]
}
]
}

需要注意的是上述操作可行的前提是根部门只有一个,否则你应该使用Nodes.transformToTrees(depts)方法来构建多棵部门树。

根节点的确定:当某个节点无法在给定的集合中找到对应的父节点时,其成为树的根节点。

若pojo类未实现ForwardNode接口,可使用adapterTransToSingleTree方法,提供对应的Function接口来提取对应的key来进行树节点的构建

1
2
// 例
TreeNode<Dept> node = Nodes.adapterTransToSingleTree(depts, Dept::getDeptId, Dept::getParentDeptId);

更多操作见Nodes具体实现

Lambda表达式工具

ws.spring.util.lambda.Lambdas提供了对lambda表达式的解析支持。

  • 获取方法引用

    1
    String str = Lambdas.getImplMethodName((SC<String>) System.out::println); // println
  • 获取字段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @NoArgsConstructor
    @Data
    @ToString
    public class FakeUser {

    private String name;
    private boolean enabled;

    public int getAge() {
    return 0;
    }

    public void setEmail(String email) {

    }

    public boolean isFake() {
    return true;
    }

    public String fetchAddress() {
    return null;
    }
    }

    // 获取 FakeUser class的name字段
    Field f = Lambdas.findFieldForReferencedMethod((SF<FakeUser, String>) FakeUser::getName);
  • 获取java bean属性信息

    1
    2
    // 获取 FakeUser class的 email属性
    PropertyDescriptor pd = Lambdas.findPropertyForReferencedMethod((SC<String>) fu::setEmail);

Lambdas对组件开发提供了一定便利,如类似mybatis-plus从lambda表达式中提取字段注解信息从而解析出表的列信息等。

SegmentStream 分段流

使用SegmentStream,你可以很方便的对数据按顺序的进行分段处理,分段流核心参数包含分段间隔(多少个数据为一段)与偏移量(数据读取到哪)。

1
2
3
package ws.spring.util;
public interface SegmentStream<E> extends AutoCloseable, Iterable<E> {
}

通过SegmentStreams你可以轻视构建分段流。

1
2
3
4
5
6
7
8
9
int interval = 2;
// 通过数组创建
SegmentStreams.ofArray(interval, array);
// 通过集合创建
SegmentStreams.ofCollection(interval, collection);
// 通过查询器创建
SegmentStreams.ofSelector(interval, (offset, iv)-> {
// 如查询数据库
});

常见的分段流用法

如有以下分段流生成方法。

1
2
3
4
5
6
7
public SegmentStream<Integer> genStream() {

return SegmentStreams.ofArray(2, new Integer[]{

1, 2, 3, 4, 5, 6, 7, 8, 9, 10
});
}

常规迭代SegmentStream实现了Iterable接口,故可以如常规迭代器一般使用

1
2
3
4
SegmentStream<Integer> stream = genStream();
for (Integer i : stream) {
System.out.println(i);
}

刹车迭代

1
2
3
4
5
6
7
8
SegmentStream<Integer> stream = genStream();
stream.forEach(v -> {
if (v == 5) {
System.out.println("Found 5");
return false; // 停止迭代
}
return true; // 继续迭代
});

获取段

1
2
3
4
5
6
SegmentStream<Integer> stream = genStream();
List<Integer> segment = stream.nextSegment(); // [1, 2]
List<Integer> segment = stream.nextSegment(); // [3, 4]
for (Integer i : stream) {
System.out.println(i); // 将从5开始输出
}

按分段迭代

1
2
3
4
5
6
7
8
9
SegmentStream<Integer> stream = genStream();
stream.forEachSegment(segment -> {
System.out.println(segment);
});
// 输出
// [1, 2]
// [3, 4]
// ...
// [9, 10]

转换分段流

1
2
3
4
5
SegmentStream<Integer> stream = genStream();
List<Integer> segment = stream.nextSegment(); // 1, 2
SegmentStream<String> strStream = stream.map(3, i -> "Num:" + i); // stream关闭
List<String> strSegment = strStream.nextSegment(); // ["Num:3", "Num:4", "Num:5"]
stream.nextSegment(); // error

注:转换分段流之后,原来的分段流将关闭。

利用SegmentStream可以使我们更方便进行数据批处理。

TemporaryLock 临时独占锁

1
2
3
4
package ws.spring.util.concurrent;
public interface TemporaryLock<E> {
boolean exclusiveRun(@Nullable E identity, Runnable runnable);
}

TemporaryLock通常的实现是 HashTemporaryLock

使用临时独占锁可以让某个任务在系统中不会同时执行。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SomeService {

private static final TemporaryLock<String> LOCK = new HashTemporaryLock<>();

public void callSyncData() {

boolean success = LOCK.exclusiveRun("syncData", this::doSyncData);
if (success) {
System.out.println("任务调度成功");
} else {
System.out.println("有相同的任务在运行,此次任务调度取消");
}
}

private void doSyncData() {
// ...
}
}

上述代码可以保证同步数据的任务(callSyncData)在短时间被多个线程调用时,doSyncData方法的执行是顺序的。不会在两个线程中执行,导致数据同步出现问题。

另外,在常规实现时,我们往往需要对callSyncData方法单独使用一个ReentrantLock锁,并通过tryLock实现控制,使用临时独占锁就可以避免到处写重复的逻辑。

不难看出对于此类需求,TemporaryLock的将原本要声明的锁转换成了任务的key。

更多信息详见:ws.spring.util.concurrent.HashTemporaryLock