Skip to content

thebesteric/agile

Repository files navigation

Agile Framework

Agile Framework for building web applications easily

使用准备

开启 AOP 支持

@EnableAgile
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class AgileTestApplication {
    public static void main(String[] args) {
        SpringApplication.run(AgileTestApplication.class, args);
    }
}

引入 AspectJ 依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.25.1</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.25.1</version>
</dependency>

日志插件

相关配置

sourceflag:
  agile:
    logger:
      enable: true
      logger:
        log-mode: log
        curl-enable: true
        response-success-define:
          code-fields:
            - name: code
              value: 200
            - name: code
              value: 100
          message-fields: message, msg
      async:
        enable: true
        async-params:
          core-pool-size: 1
          maximum-pool-size: 2
          keep-alive-time: 60s
          queue-size: 1024
      local-log-recorder-config:
        enable: false
        record-levels: info, error
        record-tags: test, hello

自定义日志记录器

自定义一个 bean 实现 CustomRecorder,实现 process 方法

@Configuration
@EnableConfigurationProperties(AgileLoggerProperties.class)
public class AgileLoggerConfig {
    @Bean
    public Recorder customRecorder(AgileLoggerProperties properties) {
        return new CustomRecorder(properties) {
            @Override
            protected void doProcess(InvokeLog invokeLog) {
                System.out.println("This is my custom log: " + invokeLog.getLogId());
            }
        };
    }
}

自定义请求忽略或重写

@Bean
public RequestIgnoreProcessor headerIgnoreProcessor() {
    return new HeaderIgnoreProcessor() {
        @Override
        protected String[] doIgnore(RequestLog requestLog) {
            return new String[]{"apple", "banana"};
        }

        @Override
        protected Map<String, String> doRewrite(RequestLog requestLog) {
            return Map.of("phone", "*");
        }
    };
}

@Bean
public RequestIgnoreProcessor parameterIgnoreProcessor() {
    return new ParameterIgnoreProcessor() {
        @Override
        protected String[] doIgnore(RequestLog requestLog) {
            return new String[]{"apple", "banana"};
        }

        @Override
        protected Map<String, String> doRewrite(RequestLog requestLog) {
            return Map.of("name", "**");
        }
    };
}

幂等插件

主要作用:防止接口重复提交,可自定义幂等关键信息

使用方式

<dependency>
    <groupId>io.github.thebesteric.framework.agile.plugins</groupId>
    <artifactId>idempotent-plugin</artifactId>
    <version>${latest.version}</version>
</dependency>
  1. 使用方法参数的幂等信息
@GetMapping("/id1")
@Idempotent(timeout = 1000)
public R<String> id1(@IdempotentKey String name, @IdempotentKey Integer age) {
    return R.success(name + "-" + age);
}
  1. 使用对象内的幂等信息
@PostMapping("/id2")
@Idempotent(timeout = 1000)
public R<Id2Vo> id2(@RequestBody Id2Vo id2Vo) {
    return R.success(id2Vo);
}

@Data
public class Id2Vo {
    @IdempotentKey
    private String name;
    @IdempotentKey
    private Integer age;
}

幂等实现类配置

默认使用内存实现幂等操作,或自定义实现IdempotentProcessor接口,如:使用 Redis 实现幂等操作

<dependency>
    <groupId>io.github.thebesteric.framework.agile.plugins</groupId>
    <artifactId>idempotent-plugin-redis</artifactId>
    <version>${latest.version}</version>
</dependency>

代码实现

@Bean
public IdempotentProcessor redisIdempotentProcessor(RedissonClient redissonClient) {
    return new RedisIdempotentProcessor(redissonClient);
}

自定义幂等类匹配

如果没有定义幂等匹配类的话,默认会对 @Controller、@RestController、@Service、@Component、@Repository 等注解的类进行匹配
如果需要自定义匹配,则可以使用 @Bean 注解,名称必须为 idempotentCustomClassMatcher,自定义后,幂等匹配类则不会进行匹配

@Bean("idempotentCustomBeanClassMatcher")
public List<ClassMatcher> idempotentCustomBeanClassMatcher() {
    return List.of(new ClassMatcher() {
        @Override
        public boolean matcher(Class<?> clazz) {
            // 判断逻辑
            return true;
        }
    });
}

限流插件

主要作用:进行接口限流,同时支持 IP 地址限流

使用方式

<dependency>
    <groupId>io.github.thebesteric.framework.agile.plugins</groupId>
    <artifactId>limiter-plugin</artifactId>
    <version>${latest.version}</version>
</dependency>

使用方式:

  • timeout:表示时间窗口
  • count:表示时间窗口内允许多少个请求
  • type:限流类型,分为 RateLimitType.DEFAULTRateLimitType.IP
@PostMapping("/limit")
@RateLimiter(timeout = 10, count = 10)
public R<Id2Vo> limit(@RequestBody Id2Vo id2Vo) {
    return R.success(id2Vo);
}

使用 Redis 作为限流实现

<dependency>
    <groupId>io.github.thebesteric.framework.agile.plugins</groupId>
    <artifactId>limiter-plugin-redis</artifactId>
    <version>${latest.version}</version>
</dependency>

代码实现

@Bean
public RateLimiterProcessor redisRateLimiterProcessor(RedisTemplate<String, Object> redisTemplate) {
    return new RedisRateLimiterProcessor(redisTemplate);
}

数据库插件

支持正向创建和修改表结构

使用方式

<dependency>
    <groupId>io.github.thebesteric.framework.agile.plugins</groupId>
    <artifactId>database-plugin</artifactId>
    <version>${latest.version}</version>
</dependency>

相关配置项

sourceflag:
  agile:
    database:
      enable: true
      show-sql: true
      ddl-auto: update
      format-sql: true
      delete-column: true
@TableName("foo")
public class Foo extends BaseEntity {

    @EntityColumn(length = 32, unique = true, nullable = false, forUpdate = "hello", defaultExpression = "'foo'")
    private String name;

    @EntityColumn(name = "t_phone", unique = true, nullable = false, defaultExpression = "18", comment = "电话", unsigned = true)
    private Integer age;

    @EntityColumn(unique = true, defaultExpression = "'test'")
    private String address;

    @EntityColumn(length = 10, precision = 3, unique = true)
    private BigDecimal amount;

    @EntityColumn(nullable = false, type = EntityColumn.Type.SMALL_INT, unsigned = true)
    private Season season;

    @EntityColumn(length = 10, precision = 2)
    private Float state;

    @EntityColumn(type = EntityColumn.Type.DATETIME, defaultExpression = "now()")
    private Date createTime;

    @TableField("update_time")
    private Date updateTime;

    @TableField("t_test")
    @EntityColumn(length = 64, nullable = false)
    private String test;
}

工作流插件

相关配置

sourceflag:
  agile:
    workflow:
      ddl-auto: update

使用方式

class DeploymentServiceTest {

    @Autowired
    WorkflowEngine workflowEngine;

    /**
     * 创建一个流程定义
     */
    @Test
    void createWorkflow() {
        workflowEngine.setCurrentUser("admin");
        DeploymentService deploymentService = workflowEngine.getDeploymentService();
        WorkflowDefinition workflowDefinition = WorkflowDefinitionBuilder.builder().tenantId("1")
                .key("test-key").name("测试流程").type("测试").desc("这是一个测试流程").build();
        WorkflowDefinition workflow = deploymentService.create(workflowDefinition);
        System.out.println(workflow);
    }

    /**
     * 定义工作流程节点
     */
    @Test
    void createNodeDefinitions() {
        String tenantId = "8888";
        workflowEngine.setCurrentUser("admin");
        DeploymentService deploymentService = workflowEngine.getDeploymentService();
        WorkflowDefinition workflowDefinition = deploymentService.get(tenantId, "test-key");
        if (workflowDefinition == null) {
            workflowDefinition = WorkflowDefinitionBuilder.builder().tenantId(tenantId).key("test-key").name("测试流程").type("测试").desc("这是一个测试流程").build();
            workflowDefinition = deploymentService.create(workflowDefinition);
        }

        createNodeDefinitions(tenantId, workflowDefinition);

        WorkflowService workflowService = workflowEngine.getWorkflowService();
        workflowService.createRelations(tenantId, workflowDefinition.getId());

    }

    private void createNodeDefinitions(String tenantId, WorkflowDefinition workflowDefinition) {
        WorkflowService workflowService = workflowEngine.getWorkflowService();
        NodeDefinition nodeDefinition = NodeDefinitionBuilder.builderStartNode(tenantId, workflowDefinition.getId())
                .name("请假流程开始").desc("开始节点").build();
        nodeDefinition = workflowService.createNode(nodeDefinition);
        System.out.println(nodeDefinition);

        Conditions conditions = Conditions.defaultConditions();
        conditions.addCondition(Condition.of("day", "3", Operator.LESS_THAN));
        nodeDefinition = NodeDefinitionBuilder.builderTaskNode(tenantId, workflowDefinition.getId(), 1)
                .name("部门主管审批").desc("任务节点").conditions(conditions).approveType(ApproveType.ALL)
                .approverId("张三").approverId("李四")
                .build();
        nodeDefinition = workflowService.createNode(nodeDefinition);
        System.out.println(nodeDefinition);

        conditions = Conditions.defaultConditions();
        conditions.addCondition(Condition.of("day", "3", Operator.GREATER_THAN_AND_EQUAL));
        nodeDefinition = NodeDefinitionBuilder.builderTaskNode(tenantId, workflowDefinition.getId(), 1)
                .name("部门经理审批").desc("任务节点").conditions(conditions)
                .approverId("王五")
                .build();
        nodeDefinition = workflowService.createNode(nodeDefinition);
        System.out.println(nodeDefinition);

        nodeDefinition = NodeDefinitionBuilder.builderTaskNode(tenantId, workflowDefinition.getId(), 2)
                .name("人事主管审批").desc("任务节点")
                .approverId("赵六")
                .build();
        nodeDefinition = workflowService.createNode(nodeDefinition);
        System.out.println(nodeDefinition);

        nodeDefinition = NodeDefinitionBuilder.builderEndNode(tenantId, workflowDefinition.getId())
                .name("请假流程结束").desc("结束节点").build();
        nodeDefinition = workflowService.createNode(nodeDefinition);
        System.out.println(nodeDefinition);
    }

    /**
     * 提交流程
     */
    @Test
    void start() {
        String tenantId = "8888";
        String requesterId = "eric";
        workflowEngine.setCurrentUser(requesterId);
        RuntimeService runtimeService = workflowEngine.getRuntimeService();
        RequestConditions requestConditions = RequestConditions.newInstance();
        requestConditions.addRequestCondition(RequestCondition.of("day", "2"));
        runtimeService.start(tenantId, "test-key", requesterId, "123-789-3", "org.agile.workflow.Business.class", "请假申请单", requestConditions);
    }

    /**
     * 取消流程
     */
    @Test
    void cancel() {
        String tenantId = "8888";
        String requesterId = "eric";
        workflowEngine.setCurrentUser(requesterId);
        RuntimeService runtimeService = workflowEngine.getRuntimeService();
        List<WorkflowInstance> workflowInstances = runtimeService.findWorkflowInstancesByRequestId("1", requesterId, WorkflowStatus.IN_PROGRESS);
        for (WorkflowInstance workflowInstance : workflowInstances) {
            runtimeService.cancel(tenantId, workflowInstance.getId());
        }
    }

    /**
     * 同意流程
     */
    @Test
    void approve() {
        String tenantId = "8888";
        String approverId = "张三";
        workflowEngine.setCurrentUser(approverId);
        RuntimeService runtimeService = workflowEngine.getRuntimeService();
        List<TaskInstance> taskInstances = runtimeService.findTaskInstances(tenantId, approverId, NodeStatus.IN_PROGRESS, ApproveStatus.IN_PROGRESS);
        if (!taskInstances.isEmpty()) {
            for (TaskInstance taskInstance : taskInstances) {
                runtimeService.approve(tenantId, taskInstance.getId(), approverId, "同意");
            }
        }
    }

    /**
     * 拒绝流程
     */
    @Test
    void reject() {
        String tenantId = "8888";
        String approverId = "张三";
        workflowEngine.setCurrentUser(approverId);
        RuntimeService runtimeService = workflowEngine.getRuntimeService();
        List<TaskInstance> taskInstances = runtimeService.findTaskInstances(tenantId, approverId, NodeStatus.IN_PROGRESS, ApproveStatus.IN_PROGRESS);
        if (!taskInstances.isEmpty()) {
            for (TaskInstance taskInstance : taskInstances) {
                runtimeService.reject(tenantId, taskInstance.getId(), approverId, "不同意");
            }
        }
    }

    /**
     * 放弃流程
     */
    @Test
    void abandon() {
        String tenantId = "8888";
        String approverId = "张三";
        workflowEngine.setCurrentUser(approverId);
        RuntimeService runtimeService = workflowEngine.getRuntimeService();
        List<TaskInstance> taskInstances = runtimeService.findTaskInstances(tenantId, approverId, NodeStatus.IN_PROGRESS, ApproveStatus.IN_PROGRESS);
        if (!taskInstances.isEmpty()) {
            for (TaskInstance taskInstance : taskInstances) {
                runtimeService.abandon(tenantId, taskInstance.getId(), approverId, "弃权");
            }
        }
    }
}

注解扫描插件

  • 配置方式一:通过sourceflag.agile.annotation-scanner.annotation-class-names注册
sourceflag:
  agile:
    annotation-scanner:
      enable: true
      annotation-class-names:
        - org.springframework.web.bind.annotation.CrossOrigin
        - org.springframework.web.bind.annotation.RestController
  • 配置方式二:通过@Bean AnnotationRegister注册

Function<Parasitic, Boolean> filter 可以通过返回值来控制当前扫描到的注解是否注册到上下文中

@Bean
public AnnotationRegister annotationRegister() {
    AnnotationRegister annotationRegister = new AnnotationRegister();
    annotationRegister.register(CrossOrigin.class, parasitic -> true);
    annotationRegister.register(RestController.class, parasitic -> true);
    return annotationRegister;
}

注意:如果sourceflag.agile.annotation-scanner.annotation-class-names@Bean AnnotationRegister同时存在,则@Bean方式注册的注解优先级高

使用方式

通过List<Parasitic> parasites = AnnotationParasiticContext.get(RestController.class);获取注解对应的宿主

注意:注解可以通过annotation-class-names属性进行声明,或者通过@Bean的方式注册AnnotationRegister

@EnableAgile
@SpringBootApplication
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableTransactionManagement
public class AgileTestApplication implements CommandLineRunner {

    @Resource
    @Lazy
    private AnnotationParasiticContext annotationParasiticContext;

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

    @Override
    public void run(String... args) throws Exception {
        List<Parasitic> parasites = annotationParasiticContext.get(RestController.class);
        System.out.println(parasites);
    }
}

若需要监听注解的注册情况,需要实现AnnotationParasiticRegisteredListener接口

@Bean
public AnnotationParasiticRegisteredListener listener(){
    return new AnnotationParasiticRegisteredListener() {
        @Override
        public void onClassParasiticRegistered(Parasitic parasitic) {
            String annotation = parasitic.getAnnotation().annotationType().getName();
            System.out.println("onClassParasiticRegistered: " + annotation + " - " + parasitic.getClazz().getName());
        }

        @Override
        public void onMethodParasiticRegistered(Parasitic parasitic) {
            String annotation = parasitic.getAnnotation().annotationType().getName();
            System.out.println("onMethodParasiticRegistered: " + annotation + " - " + parasitic.getClazz().getName() + " - " + parasitic.getMethod().getName());
        }
    };
}

微信开放平台插件

sourceflag:
  agile:
    wechat:
      mini:
        enable: true
        app-id: 123456789
        app-secret: 123456789
      third:
        enable: true
        component-app-id: 123456789
        component-app-secret: 123456789
        verify-token: 123456789
        encrypt-aes-key: 123456789

使用方式

class DeploymentServiceTest {

    @Autowired
    private WechatMiniHelper wechatMiniHelper;

    @Autowired
    private WechatThirdPlatformHelper wechatThirdPlatformHelper;

}

分布式锁插件

注意:@DistributedLock 只能作用在方法上,如果需要使用动态参数使用#符号加上参数名称即可,使用+符号作为字符串连接符

sourceflag:
  agile:
    distribute-locks:
      enable: true
      message: "分布式锁加锁失败"

使用方式

@RestController
@RequestMapping("/hello")
@AgileLogger(tag = "hello")
public class HelloController {
    @PostMapping("/lock")
    @DistributedLock(key = "abc + #params.name + #params.id", waitTime = 5, message = "加锁失败咯")
    public R<String> lock(@RequestBody Map<String, Object> params) throws InterruptedException {
        TimeUnit.SECONDS.sleep(10);
        return R.success();
    }

}

敏感词插件

load-type: 敏感词数据加载类型,支持 JSON、TXT、OTHER
file-path: 敏感词文件路径
placeholder: 需要替换的占位符
symbols: 扩充符号字符

注意:

  • TXT 加载类型:不要求后缀名,每个敏感词占一行;
  • JSON 加载类型:不要求后缀名,但是格式必须符合 JSON Array 标准,如:[“keyword1", "keyword2"]
  • OTHER 加载类型:需要重写AgileSensitiveFilterloadOtherTypeSensitiveWords方法
sourceflag:
  agile:
    sensitive:
      enable: true
      file-type: json
      file-path: asserts/sensitive.json
      placeholder: "***"
      symbols:
        - 'x'

使用方式

直接使用

AgileSensitiveFilterProperties properties = new AgileSensitiveFilterProperties();
properties.setFilePath("asserts/sensitive.txt");
properties.getSymbols().add('x');
AgileSensitiveFilter sensitiveFilter = new AgileSensitiveFilter(properties, null);
sensitiveFilter.init();

SensitiveFilterResult result = sensitiveFilter.filter(MESSAGE);
System.out.println("original = " + result.getOriginal());
System.out.println("result = " + result.getResult());
System.out.println("placeholder = " + result.getPlaceholder());
for (SensitiveFilterResult.Sensitive sensitiveWord : result.getSensitiveWords()) {
    System.out.println(MessageUtils.format("sensitiveWord: start: {}, end: {}, keyword: {}", sensitiveWord.getStart(), sensitiveWord.getEnd(), sensitiveWord.getKeyword()));
}

配合 SpringBoot 使用

@Resource
AgileSensitiveFilter agileSensitiveFilter;

void test() {
    SensitiveFilterResult result = agileSensitiveFilter.filter(MESSAGE);
    System.out.println("original = " + result.getOriginal());
    System.out.println("result = " + result.getResult());
    System.out.println("placeholder = " + result.getPlaceholder());
    for (SensitiveFilterResult.Sensitive sensitiveWord : result.getSensitiveWords()) {
        System.out.println(MessageUtils.format("sensitiveWord: start: {}, end: {}, keyword: {}", sensitiveWord.getStart(), sensitiveWord.getEnd(), sensitiveWord.getKeyword()));
    }
}

若需要处理返回结果情况,需要实现AgileSensitiveResultProcessor接口

@Bean
public AgileSensitiveResultProcessor sensitiveResultProcessor() {
    return new AgileSensitiveResultProcessor() {
        @Override
        public void process(SensitiveFilterResult result) {
            result.setResult(result.getResult() + " => 稍微修改了一下");
        }
    };
}

若自定义读取文件格式类型,需要将file-type设置为other,并重写AgileSensitiveFilterloadOtherTypeSensitiveWords方法

@Bean
public AgileSensitiveFilter agileSensitiveFilter(AgileSensitiveFilterProperties properties) {
    return new AgileSensitiveFilter(properties, sensitiveResultProcessor()) {
        @Override
        public List<String> loadOtherTypeSensitiveWords() {
            return List.of("嫖娼", "赌博");
        }
    };
}

@Bean
public AgileSensitiveResultProcessor sensitiveResultProcessor() {
    return new AgileSensitiveResultProcessor() {
        @Override
        public void process(SensitiveFilterResult result) {
            result.setResult(result.getResult() + " => 稍微修改了一下");
        }
    };
}

JSON 格式敏感词库: https://github.com/konsheng/Sensitive-lexicon

Mock 插件

主要作用:测试时返回模拟数据,减少测试代码量

使用方式

<dependency>
    <groupId>io.github.thebesteric.framework.agile.plugins</groupId>
    <artifactId>mocker-plugin</artifactId>
    <version>${latest.version}</version>
</dependency>
  • condition: 表达式,使用#开头,表示引用当前方法的参数
  • type: 模拟数据类型,支持CLASSFILEURL
  • targetClass: 模拟数据类,当 type 为 CLASS 时,必须指定,要继承Mocker接口
  • path: 模拟数据文件路径,当 type 为 FILE 或 URL 时,必须指定,支持 classpathfilehttphttps格式路径
@RestController
@RequestMapping("/mocker")
@AgileLogger(tag = "hello")
public class MockerController {

    @Resource
    private MockService mockService;

    @Mock(condition = "(#parent.id == 1 || #parent.sub.name == lisi) && #name == zs", type = MockType.CLASS, targetClass = MyMocker.class)
    @AgileLogger
    @PostMapping("/method1")
    public R<String> method1(@RequestParam(required = false) String name, @RequestBody Parent parent) {
        return R.success(name);
    }

    @Mock(condition = "(#parent.id == 1 || #parent.sub.name == lisi) && #name == zs", type = MockType.FILE, path = "classpath:mock/mock-method2.json")
    @AgileLogger
    @PostMapping("/method2")
    public R<String> method2(@RequestParam(required = false) String name, @RequestBody Parent parent) {
        return R.success(name);
    }

    @Mock(condition = "(#parent.id == 1 || #parent.sub.name == lisi) && #name == zs", type = MockType.FILE, path = "file:/Users/wangweijun/Downloads/mock-file.json")
    @AgileLogger
    @PostMapping("/method3")
    public R<String> method3(@RequestParam(required = false) String name, @RequestBody Parent parent) {
        return R.success(name);
    }

    @Mock(condition = "(#parent.id == 1 || #parent.sub.name == lisi) && #name == zs", type = MockType.URL, path = "http://127.0.0.1:8080/mocker/test")
    @AgileLogger
    @PostMapping("/method4")
    public R<String> method4(@RequestParam(required = false) String name, @RequestBody Parent parent) {
        return R.success(name);
    }

    @AgileLogger
    @PostMapping("/method5")
    public R<String> method5(@RequestParam(required = false) String name, @RequestBody Parent parent) {
        return R.success(mockService.method5(name, parent));
    }

    @AgileLogger
    @GetMapping("/test")
    public R<String> test() {
        return R.success("test");
    }

    @Data
    public static class Parent {

        private Integer id;
        private String name;
        private Sub sub;

        @Data
        public static class Sub {
            private Integer id;
            private String name;
        }
    }
}

工具类

MapWrapper Map 转换工具类

class MapWrapperTest {
    @Data
    public static class User {
        private String name;
        private Integer age;
    }
    
    @Test
    void test() {
        Map<String, Object> map = MapWrapper.createLambda(User.class)
                .put(User::getName, "张三")
                .put(User::getAge, 18)
                .build();
        System.out.println(map);
    }
}

DataValidator 数据校验工具类

class DataValidatorTest {
    @Test
    void test() {
        List<Throwable> exceptions = DataValidator.create(DataValidator.ExceptionThrowStrategy.COLLECT)
                .validate(3 == 3, "两个数不能相等")
                .getExceptions();
        System.out.println(exceptions);
    }

}

Processor 流程执行规范工具

class ProcessorTest {
    @Test
    void test() {
        Assertions.assertThrows(RuntimeException.class, () ->
                Processor.prepare(DataValidator.ExceptionThrowStrategy.COLLECT)
                        .start(() -> "hello world")
                        .validate(s -> {
                            throw new DataValidationException("s.length() > 5");
                        })
                        .next(() -> {
                            return 1L;
                        })
                        .interim(() -> {

                        })
                        .interim((t) -> {
                            System.out.println(t);
                        })
                        .complete((s, exceptions) -> {
                            System.out.println("result = " + s);
                            System.out.println("exceptions = " + exceptions);
                            if (exceptions.get(0) instanceof DataExistsException dataExistsException) {
                                throw dataExistsException;
                            }
                            return 2L;
                        }));
    }
}

About

A powerful and flexible web toolkit plug-in

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages