ws-springx
忆此些年来作为Javaer的coding经历,也遇到不少通用性的功能实现,闲来作此库以沉淀之,就叫做ws-springx吧
Github: https://github.com/WindMo/ws-springx
AOP
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
|
@FunctionalInterface public interface ReturnValuePeeper<T> {
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 { @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); }
@PeekPoint("consumerString") public ReturnValuePeeper<String> consumerString() {
log.info("method consumerString - Args Advice"); return (v, ex) -> log.info("return-consumerString: {}, ex: {}", v, ex); }
@PeekPoint("consumerString") public void consumerStringVoid(String str) {
log.info("method consumerStringVoid str: {} - Args Advice", str); }
@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) { } }
|
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
|
public interface EnumValue<T> {
@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");
|
注册spring转换器
1 2 3 4 5
| DefaultConversionService service = new DefaultConversionService(); ConverterRegistry registry = service; EnumValues.registerEnumValueConverter(OperateSys.class, registry); service.canConvert(OperateSys.class, String.class); service.canConvert(String.class, OperateSys.class);
|
Log
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>
|
Text
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 { }
public interface SomeMapper { List<String> selectXxByKeywords(@SqlEscape String keywords); }
|
Validation扩展
扩展了常用的Validation校验注解,部分如下:
@IPv4
约束字符串为IPv4地址格式
@MAC
约束字符串为MAC地址格式
@UUID
约束字符串为UUID格式
@StringRange
约束字符串的可选范围
例
1
| public void aMethod(@StringRange({"aaa","bbb"}) String str)
|
@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)
|
树节点工具
对于实现前向结点接口的pojo,可以通过Nodes工具类来为其构建对应的树形数据结构
1 2 3 4 5 6 7
| public interface ForwardNode<K> { K fetchKey();
@Nullable K fetchParentKey(); }
|
例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @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
具体实现
Web
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
注解就可以达到相同的效果
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)); } }
|
SingleEntity继承了SingleBean接口,ws-springx 提供对SingleBean的validation提取器的支持,故而应用中有了SingleBean对象的 JSR-303的校验支持。