Agile Framework for building web applications easily
@EnableAgile
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class AgileTestApplication {
public static void main(String[] args) {
SpringApplication.run(AgileTestApplication.class, args);
}
}<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>- 使用方法参数的幂等信息
@GetMapping("/id1")
@Idempotent(timeout = 1000)
public R<String> id1(@IdempotentKey String name, @IdempotentKey Integer age) {
return R.success(name + "-" + age);
}- 使用对象内的幂等信息
@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.DEFAULT和RateLimitType.IP
@PostMapping("/limit")
@RateLimiter(timeout = 10, count = 10)
public R<Id2Vo> limit(@RequestBody Id2Vo id2Vo) {
return R.success(id2Vo);
}<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: updateclass 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: 123456789class 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 加载类型:需要重写
AgileSensitiveFilter的loadOtherTypeSensitiveWords方法
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,并重写AgileSensitiveFilter的loadOtherTypeSensitiveWords方法
@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
主要作用:测试时返回模拟数据,减少测试代码量
<dependency>
<groupId>io.github.thebesteric.framework.agile.plugins</groupId>
<artifactId>mocker-plugin</artifactId>
<version>${latest.version}</version>
</dependency>- condition: 表达式,使用
#开头,表示引用当前方法的参数 - type: 模拟数据类型,支持
CLASS、FILE、URL - targetClass: 模拟数据类,当 type 为 CLASS 时,必须指定,要继承
Mocker接口 - path: 模拟数据文件路径,当 type 为 FILE 或 URL 时,必须指定,支持
classpath、file、http、https格式路径
@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;
}
}
}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);
}
}class DataValidatorTest {
@Test
void test() {
List<Throwable> exceptions = DataValidator.create(DataValidator.ExceptionThrowStrategy.COLLECT)
.validate(3 == 3, "两个数不能相等")
.getExceptions();
System.out.println(exceptions);
}
}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;
}));
}
}