# java-practices **Repository Path**: lxp4352/java-practices ## Basic Information - **Project Name**: java-practices - **Description**: 🚀🚀🚀 Mybatis-Flex/Plus、Knife4j、Redisson、javers、drools、sa-token、liteflow、elasticjob等常用工具类 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: https://youxiaxiaomage.gitee.io/blog/ - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 16 - **Created**: 2023-09-15 - **Last Updated**: 2023-09-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

java-practices日常开发中的开发工具,编码技巧以及设计思想等,持续更新中

| 目录(部分直接可以点击链链接跳转模块明细) | 说明 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | `yxxmg-mybatis-plus-sample` | 动态表名,`Mybatis-Plus`自定义枚举,`Mapstruct`,`JSR303`,`JSR330`,`Caffeine`,`Knife4j`,`Openfegin`,`JustAuth`,`Easy-Excel`,`Easy-ES` | | `yxxmg-mybatis-flex-sample` | `mybatis-flex`新的`orm`框架,目前是`1.6.3`版本(目前版本可能存在问题,当前版本也是紧急修复的版本) | | `yxxmg-pay-spring-boot-starter` | `springboot`自动装配,`SPI`自定义支付`starter` | | `yxxmg-drools-sample` | 规则引擎 | | `yxxmg-java-helper` | `java`语法糖 | | `yxxmg-spring-boot-sample` | `springboot`相关内容 | | `yxxmg-flowable` | `flowable`工作流引擎 | | `yxxmg-elasticjob` | `elasticjob`任务调度 | | `yxxmg-smart-doc` | `smart-doc` | | `yxxmg-exception-sample` | 自定义异常或国际化 | | `yxxmg-sensitive-sample` | `jackson`序列化与反序列化,例如字段脱敏,字符串去除首尾空格等 | | `yxxmg-liteflow-sample` | `liteflow`服务编排 | | `yxxmg-magic-api-sample` | `magic-api` | | `yxxmg-event-sample` | `ApplicationEvent`,`ApplicationEventPublisher` | | `yxxmg-javers-sample` | 对象前后变化对比(未开始) | | `yxxmg-oos-sample` | 阿里云`oos` | | `yxxmg-nlp-sample` | `Stanford nlp` | | `yxxmg-magic-api-sample` | `magic-api` | | `yxxmg-gof-sample` | `Gof`设计模式 | | `yxxmg-distribute-redis-lock-sample` | `redis`实现分布式锁 | | `yxxmg-sa-token-sample` | `sa-token`功能 | | `yxxmg-dynamic-feign-sample` | 动态`feign` | | `yxxmg-okhttp-sample` | `okhttp3` | | [`yxxmg-mybatis-spring-sample`](./yxxmg-mybatis-spring-sample/readme_zh.md) | `mybatis`基础研究 | ### 介绍 1. `MybatisPlus`重写`MybatisPlusAutoConfiguration`、自定义枚举类转换、自动填充; 2. `JSR303`校验,如分组校验规则等,校验后台数据重复; 3. 集成`Knife4j`,新增枚举插件转换; 4. 集成`mapstruct`; 5. 全局异常切面 6. `PageHelper`,如果与缓存配合使用,切记ThreadLocal能引发血案 7. `OpenFeign`调用外部接口 8. `Caffeine` 配置缓存,目前配置的jvm缓存 9. `SpringBoot`自动装配 10. `JustAuth`第三方授权 11. `elasticsearch orm`框架,`easy-es`类似`mybatis-plus`,代码后期列举 12. 自定义SPI `spring-boot-starter`组件 13. `elasticjob` 14. `flowable`工作流引擎 15. `Drools`规则引擎 16. `JaVers`对象前后变化对比 17. 阿里云`oos` 18. `Stanford nlp`自然语言 19. `magic-api` 20. `Gof` 设计模式 21. `sa-token` 22. `dynamic-feign` 23. `okhttp3` 24. `mybatis-flex`,官方插件`mybatis-flex-helper` 25. 自定义`ClassLoader` ### 核心组件以及功能介绍 #### 枚举类自动转换 mybatis-plus配置 ```yaml mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl default-enum-type-handler: com.yxxmg.mybatisplussample.handler.CustomEnumHandler ``` 实例代码 ```java @MappedTypes(value = BaseEnum.class) public class CustomEnumHandler extends BaseTypeHandler { private final Class type; public CustomEnumHandler(Class type) { if (type == null) { throw new IllegalArgumentException("Type argument cannot be null"); } this.type = type; } @Override public void setNonNullParameter(PreparedStatement ps, int i, BaseEnum parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter.getCode()); } @Override public BaseEnum getNullableResult(ResultSet rs, String columnName) throws SQLException { return convert(rs.getString(columnName)); } @Override public BaseEnum getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return convert(rs.getString(columnIndex)); } @Override public BaseEnum getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return convert(cs.getString(columnIndex)); } private T convert(String value) { return BaseEnum.getEnum(this.type, StringUtils.isBlank(value) ? null : Integer.parseInt(value)); } } ``` ![](images/列表查询列表展示.png) #### Knife4j文档显示对应的枚举项 ![](images/swagger.png) 新增修改使用同一个DTO但是Knife4j新增不显示Id ![](images/新增.png) ![](images/修改.png) 实体类只需配置 ```java @PostMapping("/add") @ApiOperationSupport(ignoreParameters = "userId") @ApiOperation("用户新增") public String add(@RequestBody @Validated(AddGroup.class) UserDTO userDTO){ return this.userService.add(userDTO); } @PutMapping("/update") @ApiOperation("用户修改") public String update(@RequestBody @Validated(UpdateGroup.class) UserDTO userDTO){ return this.userService.update(userDTO); } ``` #### mapstruct简化领域对象、DTO、DO、VO之间的转换 不在使用apache的BeanUtils或者spring的BeanUtils去动态加载, 实际还是类似lombok实现代码的简化工作 ```java @Mapper interface UserMapper { UserMapper MAPPER = Mappers.getMapper(UserMapper.class); /** * DTO转DO * * @param userDTO DTO * @return DO */ User from(UserDTO userDTO); } ``` #### JSR303实现参数的重复校验 核心代码 ```Java @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) @Constraint(validatedBy = ValidUserConstraintValidator.class) public @interface ValidUser { String message() default "用户已存在"; Class[] groups() default {}; Class[] payload() default {}; } ``` ```java public class ValidUserConstraintValidator implements ConstraintValidator { @Autowired private UserService userService; @Override public boolean isValid(UserDTO userDTO, ConstraintValidatorContext context) { String validResult = this.userService.valid(userDTO); if (StringUtils.isBlank(validResult)) { return true; } context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(validResult).addConstraintViolation(); return false; } } ``` #### Bean注入方式 1. 构造器注入 ```java @Service @RequiredArgsConstructor public class ConstructorService { private final UserService userService; public void test() { this.userService.list(); } } ``` 2. @Autowired注入 示例代码 ```java @Service @RequiredArgsConstructor public class ConstructorService { private final UserService userService; @Autowired private CustomService customService; public void test() { this.userService.list(); } public void test2() { this.customService.custom(); } } ``` 3. JSR330注入 pom.xml ```xml javax.inject javax.inject 1 ``` ```java @Service @RequiredArgsConstructor public class ConstructorService { private final UserService userService; @Autowired private CustomService customService; public void test() { this.userService.list(); } public void test2() { this.customService.custom(); } } ``` #### 手写支付`SPI` `Starter` ![image-20221202110333902](images/image-20221202110333902.png) 1. `spi`定义的接口要在增加`META-INF/services`下`com.yxxmg.pay.spi.PayService` ```properties com.yxxmg.pay.spi.AlipayServiceImpl com.yxxmg.pay.spi.UnionPayServiceImpl com.yxxmg.pay.spi.WechatPayServiceImpl ``` 2. 自定义配置类 ```java @ConfigurationProperties(prefix = "yxxmg.pay") @Data public class YxxmgPayProperties { PayMethod payMethod; } ``` 3. 自动装配类 ```java @Configuration @ConditionalOnMissingBean(PayService.class) @EnableConfigurationProperties(YxxmgPayProperties.class) public class YxxmgPayAutoConfigure { @Bean public PayService payService(YxxmgPayProperties yxxmgPayProperties) { ServiceLoader serviceLoader = ServiceLoader.load(PayService.class); Iterator iterator = serviceLoader.iterator(); PayService payService = null; while (iterator.hasNext()) { payService = iterator.next(); if (payService instanceof AlipayServiceImpl && PayMethod.ALIPAY.equals(yxxmgPayProperties.getPayMethod())) { break; } if (payService instanceof UnionPayServiceImpl && PayMethod.UNION.equals(yxxmgPayProperties.getPayMethod())) { break; } if (payService instanceof WechatPayServiceImpl && PayMethod.WECHAT.equals(yxxmgPayProperties.getPayMethod())) { break; } if (Objects.isNull(payService)) { payService = new ErrorPayServiceImpl(); } } return payService; } } ``` 4. `META-INF/spring.factories` ```properties org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.yxxmg.pay.config.YxxmgPayAutoConfigure ``` 5. 引入starter模块 ```xml com.yxxmg yxxmg-pay-spring-boot-starter ${project.version} ``` 6. 配置文件配置 ```yaml yxxmg: pay: pay-method: alipay ``` 7. 测试用例 ```java @SpringBootTest(classes = MybatisPlusSampleApplication.class) @RunWith(SpringRunner.class) @ActiveProfiles("dev") public class MybatisPlusSampleApplicationTest { @Resource private PayService payService; @Test public void testPayService() { String pay = this.payService.pay("iPhone14", Float.parseFloat("6999.99")); System.out.println(pay); } } ``` 运行结果 ![image-20221202111352927](images/spi测试用例运行结果.png) #### Jackson序列化与反序列化 1. 字段脱敏 ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveJsonSerializer.class) public @interface Sensitive { SensitiveStrategy strategy(); } ``` ```java public class SensitiveJsonSerializer extends JsonSerializer implements ContextualSerializer { private SensitiveStrategy strategy; @Override public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { jsonGenerator.writeString(strategy.desensitizer().apply(s)); } @Override public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { Sensitive annotation = beanProperty.getAnnotation(Sensitive.class); if (Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())) { this.strategy = annotation.strategy(); return this; } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } } ``` ```java public enum SensitiveStrategy { /** * 手机 */ PHONE(DesensitizedUtil::mobilePhone), ID(id -> DesensitizedUtil.idCardNum(id, 3, 4)), EMAIL(DesensitizedUtil::email); private final Function desensitizer; SensitiveStrategy(Function desensitizer) { this.desensitizer = desensitizer; } public Function desensitizer() { return desensitizer; } } ``` ```java @Data @Accessors(chain = true) public class Student implements Serializable { private static final long serialVersionUID = 1270380814231996333L; private String userId; @Sensitive(strategy = SensitiveStrategy.PHONE) private String phoneNumber; @Sensitive(strategy = SensitiveStrategy.EMAIL) private String email; } ``` 2. 请求去首尾空格 ```java public class TrimStringSerializer extends JsonSerializer { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(StringUtils.trim(value)); } } ``` ```java @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { // 创建一个 ObjectMapper 对象 ObjectMapper objectMapper = new ObjectMapper(); // 创建一个 SimpleModule 对象,用于注册序列化器 SimpleModule module = new SimpleModule(); // 将自定义的 TrimStringSerializer 序列化器注册到 SimpleModule 对象中 module.addSerializer(String.class, new TrimStringSerializer()); // 将 SimpleModule 对象注册到 ObjectMapper 中 objectMapper.registerModule(module); // 返回 ObjectMapper 对象 return objectMapper; } } ``` #### 动态`feign` 基于serviceId(服务发现如nacos) ```java public interface DynamicService { /** * POST请求 * * @param url 请求路径 * @param params 请求参数 * @return 响应结果 */ @PostMapping("{url}") String post(@PathVariable String url, @RequestBody Object params); /** * GET请求 * * @param url 请求路径 * @param params 请求参数 * @return 响应结果 */ @GetMapping("{url}") String get(@PathVariable String url, @SpringQueryMap Object params); /** * PUT请求 * * @param url 请求路径 * @param params 请求参数 * @return 响应结果 */ @PutMapping("{url}") String put(@PathVariable String url, @RequestBody Object params); /** * DELETE请求 * * @param url 请求路径 * @param params 请求参数 * @return 响应结果 */ @DeleteMapping("{url}") String delete(@PathVariable String url, @RequestBody Object params); } ``` ```java public class DynamicFeignClientFactory { private final FeignClientBuilder feignClientBuilder; public DynamicFeignClientFactory(ApplicationContext applicationContext) { this.feignClientBuilder = new FeignClientBuilder(applicationContext); } public T getFeignClient(final Class type, String serviceId) { return this.feignClientBuilder.forType(type, serviceId).build(); } } ``` ```java @RequiredArgsConstructor public class DynamicClient { private final DynamicFeignClientFactory dynamicFeignClientFactory; public String post(String serviceId, String url, Object params) { return dynamicFeignClientFactory.getFeignClient(DynamicService.class, serviceId).post(url, params); } public String get(String serviceId, String url, Object params) { return dynamicFeignClientFactory.getFeignClient(DynamicService.class, serviceId).get(url, params); } public String put(String serviceId, String url, Object params) { return dynamicFeignClientFactory.getFeignClient(DynamicService.class, serviceId).put(url, params); } public String delete(String serviceId, String url, Object params) { return dynamicFeignClientFactory.getFeignClient(DynamicService.class, serviceId).delete(url, params); } } ``` #### 动态feign基于URL 例子展示的是按照`POST`请求方式 ```java @FeignClient(name = "dynamic-url-feign", url = "EMPTY") public interface DynamicUrlClient { @RequestLine("POST /") @Headers({"Content-Type: application/json", "Accept: application/json"}) String post(URI uri, @RequestBody String request); } ``` ```java @Component @Import(FeignClientsConfiguration.class) public class DynamicUrlService { private final DynamicUrlClient dynamicUrlClient; @Autowired public DynamicUrlService(Encoder encoder, Decoder decoder) { this.dynamicUrlClient = Feign.builder() .encoder(encoder) .decoder(decoder) .target(Target.EmptyTarget.create(DynamicUrlClient.class)); } public String post(String url, String request) { return this.dynamicUrlClient.post(URI.create(url), request); } } ``` #### `redis`实现分布式锁 ```java @Inherited @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RedisDistributeLock { String key() default ""; long lockTime() default 5000; long releaseLock() default 5000; } ``` ```java Component @Slf4j @RequiredArgsConstructor @Aspect public class RedisDistributeLockAspect { private final RedissonClientDelegate redissonClientDelegate; @Pointcut("@annotation(com.yxxmg.distribute.redis.lock.annotation.RedisDistributeLock)") public void pointCut() {} public Object proceed(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { RedisDistributeLock redisDistributeLock = getAnnotation(proceedingJoinPoint); String valueKey = generateDistributeLockKey(proceedingJoinPoint, redisDistributeLock, proceedingJoinPoint.getArgs()); try (CloseableRLock lock = this.redissonClientDelegate.getFairLock(valueKey)) { if (lock.tryLock(redisDistributeLock.lockTime(), redisDistributeLock.releaseLock(), TimeUnit.MILLISECONDS)) { return proceedingJoinPoint.proceed(); } else { throw new IllegalArgumentException("current thread get lock failed"); } } } private static RedisDistributeLock getAnnotation(ProceedingJoinPoint proceedingJoinPoint) { Signature signature = proceedingJoinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature)signature; Method method = methodSignature.getMethod(); return method.getAnnotation(RedisDistributeLock.class); } private String generateDistributeLockKey(ProceedingJoinPoint proceedingJoinPoint, RedisDistributeLock redisDistributeLock, Object[] args) { LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); MethodSignature signature = (MethodSignature)proceedingJoinPoint.getSignature(); String[] parameterNames = localVariableTableParameterNameDiscoverer.getParameterNames(signature.getMethod()); if (ObjectUtils.isEmpty(parameterNames)) { throw new IllegalArgumentException("redis distribute lock must have key param"); } ExpressionParser expressionParser = new SpelExpressionParser(); StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext(); for (int i = 0; i < parameterNames.length; i++) { standardEvaluationContext.setVariable(parameterNames[i], args[i]); } try { return expressionParser.parseExpression(redisDistributeLock.key()).getValue(standardEvaluationContext, String.class); } catch (Exception e) { throw new RuntimeException(e); } } } ``` ### 开发使用到的工具 #### 开发工具Idea #### 开发中常用插件 1. lombok 简化代码,一般idea会集成,有些版本不自带,需要手动配置 2. MybatisX mybatis快速开发定位 3. Save Actions 自动保存,以及格式化代码需要配合代码风格插件使用 4. Code for Eclipse Code Formater eclipse代码风格 5. Maven Helper 用于Maven管理 6. Arthas Idea Plugin 性能工具 7. Key Promoter X 快捷键 8. Rainbow Brackets 代码括号彩虹色,方便看代码 9. Alibaba Java Coding Guidelines 阿里代码检视 10. CamelCase 驼峰命名变量 方法 类等 11. GitToolBox Git工具,方便提交更新代码等 12. RestfulToolKit 方便定位接口 13. Yapi 接口管理类似Knife4j 14. MeterSphere 接口自动化 15. Jenkins 流水线发版 16. xxl-job 分布式任务调度(light-task-scheduler) 17. Sonar Lint 代码漏洞 18. Confluence 文档管理 内部文档的传递 ##### 使用的中间件 1. rabbitmq 2. activemq 3. kafka 4. elasticsearch 5. hbase 6. redis数据缓存以及基于Redisson实现的分布式锁 ##### 多环境打包 1. pom.xml配置 ```xml org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 org.springframework.boot spring-boot-maven-plugin 2.7.5 true repackage org.apache.maven.plugins maven-war-plugin 3.3.2 false ${project.artifactId} ${project.artifactId} dev dev true prod prod ``` 2. application.yml配置 ```yaml spring: profiles: active: @spring.profiles.active@ ``` 3. maven打包命令 ```yaml # 测试环境打包,默认方式dev mvn clean package -P dev或mvn clean package # 生产环境打包 mvn clean package -P prod ```