需求
原各個業(yè)務模塊有對應的批量導入、批量修改、批量導出功能,但是批量操作屬于大量耗費系統(tǒng)資源的操作,并且沒有統(tǒng)一的流程,不便于管理,于是考慮將其加入到定時任務中,上傳文件時只是創(chuàng)建任務信息,在系統(tǒng)空閑時進行文件解析、驗證、入庫操作。
由于定時任務需要根據(jù)任務去調(diào)用不同模塊的文件處理細節(jié)并記錄處理結(jié)果,考慮使用動態(tài)代理的方式,將文件解析、數(shù)據(jù)入庫等業(yè)務細節(jié)分發(fā)到具體業(yè)務模塊,定時任務只負責調(diào)用業(yè)務處理方法及記錄處理結(jié)果。
流程:
- 自定義注解標記業(yè)務處理方法
- 依賴springboot掃描功能,將注解標記的方法加載到內(nèi)存中
- 定時任務執(zhí)行時,使用反射調(diào)用具體方法
開始
一、自定義注解
- 準備準備知識
1.1. 元注解
元注解是由java提供的,所有java中的注解都依賴于元注解。有如下四個注解:
- @Target: 描述注解的使用范圍,可多選
public enum ElementType {
TYPE, // 類、接口、枚舉類
FIELD, // 成員變量(包括:枚舉常量)
METHOD, // 成員方法
PARAMETER, // 方法參數(shù)
CONSTRUCTOR, // 構造方法
LOCAL_VARIABLE, // 局部變量
ANNOTATION_TYPE, // 注解類
PACKAGE, // 可用于修飾:包
TYPE_PARAMETER, // 類型參數(shù),JDK 1.8 新增
TYPE_USE // 使用類型的任何地方,JDK 1.8 新增
}
使用示例:
// 該注解可標記在方法和類上
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface SysTaskExecuteMethod {}
- @Retention: 描述注解保留的時間范圍
public enum RetentionPolicy {
SOURCE, // 源文件保留:僅在源文件中保留,編譯時將被忽略
CLASS, // 編譯期保留(默認值):編譯階段保留,運行期不可見
RUNTIME // 運行期保留,可通過反射去獲取注解信息
}
- @Documented: 描述在使用 javadoc 工具為類生成幫助文檔時是否要保留其注解信息
- @Inherited: 描述使被它修飾的注解具有繼承性:被他修飾的類,其子類自動繼承該注解
1.2. 創(chuàng)建自定義注解
創(chuàng)建自定義注解時,使用 @interface標識注解類即可
public @interface CustomerAnnotation {
// coding
}
- 定義注解
2.1. 定義類標識注解
因為使用的是springboot的自動掃描及裝配實現(xiàn)實例的注冊及持有,springboot提供的API只能掃描到類級注解,故此處需要定義一個類注解參與,該注解僅僅作為標記該類有參與細節(jié)處理
package com.customer.common.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassAnnotation{
}
2.2. 定義業(yè)務處理注解
因為使用反射調(diào)用具體的方法,所以使用范圍為ElementType.METHOD、保留時間為RetentionPolicy.RUNTIME
為了便于區(qū)分標識的方法對應的任務分類增加了注解參數(shù)
package com.customer.common.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DongingSomething {
/**
* 任務名稱
*/
String taskName();// 不加detault的參數(shù)表示為必填參數(shù)
/**
* 任務所屬分類
*/
String type();
}
二、注解掃描
使用springboot提供的API實現(xiàn)自定義注解掃描:增加自定義配置,實現(xiàn)org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor接口
@Configuration // springboot啟動時會自動識別其為配置項
public class CustomAnnotationScanner implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(beanDefinitionRegistry);
scanner.setBeanNameGenerator(new AnnotationBeanNameGenerator());
// 定義需要掃描的注解 -- 自定義注解
scanner.addIncludeFilter(new AnnotationTypeFilter(ClassAnnotation.class));
// 定義掃描的包 若不在該包及其子包中,將無法被掃描到
scanner.scan("com.customer");
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
// 使用springboot上下文獲取注解實例,并添加到上下文中,后期使用直接注入即可使用
@Bean("customerExecuteInstance")
public List<CustomerExecuteInstance> getAllCustomerExecuteInstance(ApplicationContext applicationContext) {
List<CustomerExecuteInstance> result = new ArrayList<>();
// 使用springboot上下文方式獲取注解標識的實例
// springbootAPI未提供直接掃描方法上的注解,故自定義注解有兩個,此處使用的是類上的注解
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(ClassAnnotation.class);
for (Map.Entry<String, Object> item : beansWithAnnotation.entrySet()) {
// 一般方式使用此方法即可得到方法的注解,比如單元測試中能正常獲取到
Method[] methods = item.getValue().getClass().getMethods();
// 運行環(huán)境下,方法是在service實現(xiàn)類中的,中上下文中的實例其實都是被代理的,此時將無法通過此方法得到方法上的注解
// 處理被代理的類
if (AopUtils.isJdkDynamicProxy(item.getValue())){
Object singletonTarget = AopProxyUtils.getSingletonTarget(item.getValue());
if (singletonTarget != null) {
methods = singletonTarget.getClass().getMethods();
}
} else if (AopUtils.isCglibProxy(item.getValue())) {
methods = item.getValue().getClass().getSuperclass().getMethods();
}
for (Method method : methods) {
SysTaskExecuteMethod annotation = method.getAnnotation(DongingSomething.class);
if (annotation != null) {
// 檢查方法返回值 只能是CustomerResult及其子類
// 因為需要做后置處理,且后置處理依賴業(yè)務處理結(jié)果,故此處增加返回參數(shù)驗證
if (!CustomerResult.class.isAssignableFrom(method.getReturnType())) {
log.error("\n\t\t*******************" + item.getValue().getClass().getName() + "." + method.getName() + "方法返回值類型不符合規(guī)范!*************************");
continue;
}
result.add(
new CustomerExecuteInstance()
.setTaskName(annotation.taskName())
.setTaskType(annotation.type())
.setMethod(method)
.setInstance(item.getValue()));
}
}
}
return result;
}
}
CustomerExecuteInstance類
@Data // 使用lombok自動增加getter、setter方法
@Accessors(chain = true)// getter、setter方法返回當前對象,方便使用鏈式編程
public class CustomerExecuteInstance {
private String taskName;
private String taskType;
private Method method;
private Object instance;
public boolean equals(String name, String type) {
if (name == null || type == null) {
return false;
}
return name.equals(this.taskName) && type.equals(this.taskType);
}
}
三、注解使用
在需要的類及方法上添加注解
示例:
- 使用在service類中
@Service // service實現(xiàn)類標識
@ClassAnnotation
public class ModelDoingServiceImpl implements ModelDoingService {
// Method[] methods = item.getValue().getClass().getMethods()將無法掃描到該注解
@DongingSomething(taskName = "name1", type = "type1")
public CustomerResult importMeterInfos(TaskInfo task) {
// coding.....
return null;
}
}
- 使用在普通類中
@ClassAnnotation
class A1{
// Method[] methods = item.getValue().getClass().getMethods()可掃描到該注解
@DongingSomething(taskName = "test1", type = "type1")
public CustomerResult testxxx(Long projectId, String fileBusinessId, JSONObject paramsJson) {
System.out.println("execute method:"+projectId + fileBusinessId+ paramsJson);
return "execute success";
}
}
四、定時任務中調(diào)用細節(jié)處理
定時任務中使用如下:
@Slf4j // 使用日志框架,方便日志輸出
public class TaskExecution {
@Resource // 直接注入bean,springboot上下文中已有他們的實例及相關信息
private List<CustomerExecuteInstance> executeInstanceList;
public void executeTask() {
//查詢待執(zhí)行任務
List<Task> taskList = new ArrayList();
// 查詢?nèi)蝿疹惐? for (Task task : taskList) {
CustomerExecuteInstance currInst = executeInstanceList.stream()
// 使用實體類型中的自定義equles方法
.filter(ins -> ins.equals(task.getName(), task.getType()))
.findFirst()
.orElse(null);
if (currInst == null) {
log.warn(task.getType() + " 類型下的【" + task.getName() + "】任務無對應任務執(zhí)行邏輯。");
continue;
}
try {
CustomerResult result;
// 使用反射,直接調(diào)用方法處理對應邏輯
// 此處需要提前約定方法參數(shù)只能為 Task及其子類的對象,也可根據(jù)需求自行拓展
result = (CustomerResult) currInst.getMethod().invoke(currInst.getInstance(), task);
// 記錄執(zhí)行結(jié)果
// doing...
} catch (Exception e) {
// 記錄執(zhí)行失敗結(jié)果
// doing ...
}
// 任務執(zhí)行結(jié)束后的后續(xù)操作
// doing...
}
}
}
定時任務如何實現(xiàn)。。。。
這個問題請關注后期補充上 [手動狗頭]
五、寫在最后
至此,整個業(yè)務貫穿打通,實現(xiàn)了業(yè)務的分離解耦