SpringBoot 實(shí)現(xiàn)動(dòng)態(tài)插拔的 AOP,太實(shí)用了!
現(xiàn)在有這么一個(gè)需求:就是我們?nèi)罩镜拈_與關(guān)是交給使用人員來控制的,而不是由我們開發(fā)人員固定寫死的。大家都知道可以用aop來實(shí)現(xiàn)日志管理,但是如何動(dòng)態(tài)的來實(shí)現(xiàn)日志管理呢?
aop源碼中的實(shí)現(xiàn)邏輯中有這么一個(gè)步驟,就是會依次掃描Advice的實(shí)現(xiàn)類,然后執(zhí)行。我們要做的就是自定義一個(gè)advice的實(shí)現(xiàn)類然后,在用戶想要開啟日志的時(shí)候就把a(bǔ)dvice加到項(xiàng)目中來,關(guān)閉日志的時(shí)候就把a(bǔ)dvice剔除就行了。
前置知識
(1) Advice:
org.aopalliance.aop.Advice
“通知”,表示 Aspect 在特定的 Join point 采取的操作。包括 “around”, “before” and “after 等 Advice,大體上分為了三類:BeforeAdvice、MethodInterceptor、AfterAdvice
(2) Advisor:
org.springframework.aop.Advisor
“通知者”,它持有 Advice,是 Spring AOP 的一個(gè)基礎(chǔ)接口。它的子接口 PointcutAdvisor 是一個(gè)功能完善接口,它涵蓋了絕大部分的 Advisor。
(3) Advised:
org.springframework.aop.framework.Advised
AOP 代理工廠配置類接口。提供了操作和管理 Advice 和 Advisor 的能力。它的實(shí)現(xiàn)類 ProxyFactory 是 Spring AOP 主要用于創(chuàng)建 AOP 代理類的核心類。
熱插拔AOP執(zhí)行核心邏輯
核心實(shí)現(xiàn)代碼
(1) 動(dòng)態(tài)管理advice端點(diǎn)實(shí)現(xiàn)
@RestControllerEndpoint(id = "proxy")
@RequiredArgsConstructor
public class ProxyMetaDefinitionControllerEndPoint {
private final ProxyMetaDefinitionRepository proxyMetaDefinitionRepository;
@GetMapping("listMeta")
public List<ProxyMetaDefinition> getProxyMetaDefinitions(){
return proxyMetaDefinitionRepository.getProxyMetaDefinitions();
}
@GetMapping("{id}")
public ProxyMetaDefinition getProxyMetaDefinition(@PathVariable("id") String proxyMetaDefinitionId){
return proxyMetaDefinitionRepository.getProxyMetaDefinition(proxyMetaDefinitionId);
}
@PostMapping("save")
public String save(@RequestBody ProxyMetaDefinition definition){
try {
proxyMetaDefinitionRepository.save(definition);
return "success";
} catch (Exception e) {
}
return "fail";
}
@PostMapping("delete/{id}")
public String delete(@PathVariable("id")String proxyMetaDefinitionId){
try {
proxyMetaDefinitionRepository.delete(proxyMetaDefinitionId);
return "success";
} catch (Exception e) {
}
return "fail";
}
}
(2) 利用事件監(jiān)聽機(jī)制捕獲安裝或者卸載插件
@RequiredArgsConstructor
public class ProxyMetaDefinitionChangeListener {
private final AopPluginFactory aopPluginFactory;
@EventListener
public void listener(ProxyMetaDefinitionChangeEvent proxyMetaDefinitionChangeEvent){
ProxyMetaInfo proxyMetaInfo = aopPluginFactory.getProxyMetaInfo(proxyMetaDefinitionChangeEvent.getProxyMetaDefinition());
switch (proxyMetaDefinitionChangeEvent.getOperateEventEnum()){
case ADD:
aopPluginFactory.installPlugin(proxyMetaInfo);
break;
case DEL:
aopPluginFactory.uninstallPlugin(proxyMetaInfo.getId());
break;
}
}
}
(3) 安裝插件
public void installPlugin(ProxyMetaInfo proxyMetaInfo){
if(StringUtils.isEmpty(proxyMetaInfo.getId())){
proxyMetaInfo.setId(proxyMetaInfo.getProxyUrl() + SPIILT + proxyMetaInfo.getProxyClassName());
}
AopUtil.registerProxy(defaultListableBeanFactory,proxyMetaInfo);
}
(4) 安裝插件核心實(shí)現(xiàn)
public static void registerProxy(DefaultListableBeanFactory beanFactory,ProxyMetaInfo proxyMetaInfo){
AspectJExpressionPointcutAdvisor advisor = getAspectJExpressionPointcutAdvisor(beanFactory, proxyMetaInfo);
addOrDelAdvice(beanFactory,OperateEventEnum.ADD,advisor);
}
private static AspectJExpressionPointcutAdvisor getAspectJExpressionPointcutAdvisor(DefaultListableBeanFactory beanFactory, ProxyMetaInfo proxyMetaInfo) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
beanDefinition.setBeanClass(AspectJExpressionPointcutAdvisor.class);
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression(proxyMetaInfo.getPointcut());
advisor.setAdvice(Objects.requireNonNull(getMethodInterceptor(proxyMetaInfo.getProxyUrl(), proxyMetaInfo.getProxyClassName())));
beanDefinition.setInstanceSupplier((Supplier<AspectJExpressionPointcutAdvisor>) () -> advisor);
beanFactory.registerBeanDefinition(PROXY_PLUGIN_PREFIX + proxyMetaInfo.getId(),beanDefinition);
return advisor;
}
(5) 卸載插件
public void uninstallPlugin(String id){
String beanName = PROXY_PLUGIN_PREFIX + id;
if(defaultListableBeanFactory.containsBean(beanName)){
AopUtil.destoryProxy(defaultListableBeanFactory,id);
}else{
throw new NoSuchElementException("Plugin not found: " + id);
}
}
(6) 卸載插件核心實(shí)現(xiàn)
public static void destoryProxy(DefaultListableBeanFactory beanFactory,String id){
String beanName = PROXY_PLUGIN_PREFIX + id;
if(beanFactory.containsBean(beanName)){
AspectJExpressionPointcutAdvisor advisor = beanFactory.getBean(beanName,AspectJExpressionPointcutAdvisor.class);
addOrDelAdvice(beanFactory,OperateEventEnum.DEL,advisor);
beanFactory.destroyBean(beanFactory.getBean(beanName));
}
}
(7) 操作advice實(shí)現(xiàn)
public static void addOrDelAdvice(DefaultListableBeanFactory beanFactory, OperateEventEnum operateEventEnum,AspectJExpressionPointcutAdvisor advisor){
AspectJExpressionPointcut pointcut = (AspectJExpressionPointcut) advisor.getPointcut();
for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
Object bean = beanFactory.getBean(beanDefinitionName);
if(!(bean instanceof Advised)){
if(operateEventEnum == OperateEventEnum.ADD){
buildCandidateAdvised(beanFactory,advisor,bean,beanDefinitionName);
}
continue;
}
Advised advisedBean = (Advised) bean;
boolean isFindMatchAdvised = findMatchAdvised(advisedBean.getClass(),pointcut);
if(operateEventEnum == OperateEventEnum.DEL){
if(isFindMatchAdvised){
advisedBean.removeAdvice(advisor.getAdvice());
log.info("########################################## Remove Advice -->【{}】 For Bean -->【{}】 SUCCESS !",advisor.getAdvice().getClass().getName(),bean.getClass().getName());
}
}else if(operateEventEnum == OperateEventEnum.ADD){
if(isFindMatchAdvised){
advisedBean.addAdvice(advisor.getAdvice());
log.info("########################################## Add Advice -->【{}】 For Bean -->【{}】 SUCCESS !",advisor.getAdvice().getClass().getName(),bean.getClass().getName());
}
}
}
}
熱插拔AOP演示示例
(1) 創(chuàng)建一個(gè)service
@Service
@Slf4j
public class HelloService implements BeanNameAware, BeanFactoryAware {
private BeanFactory beanFactory;
private String beanName;
@SneakyThrows
public String sayHello(String message) {
Object bean = beanFactory.getBean(beanName);
log.info("============================ {} is Advised : {}",bean, bean instanceof Advised);
TimeUnit.SECONDS.sleep(new Random().nextInt(3));
log.info("============================ hello:{}",message);
return "hello:" + message;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
}
(2) 創(chuàng)建一個(gè)controller
@RestController
@RequestMapping("hello")
@RequiredArgsConstructor
public class HelloController {
private final HelloService helloService;
@GetMapping("{message}")
public String sayHello(@PathVariable("message")String message){
return helloService.sayHello(message);
}
}
(3) 準(zhǔn)備一個(gè)日志切面jar
切面內(nèi)容為
@Slf4j
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object result;
try {
result = invocation.proceed();
} finally {
log.info(">>>>>>>>>>>>>>>>>>>>>>>>TargetClass:【{}】,method:【{}】,args:【{}】",invocation.getThis().getClass().getName(),invocation.getMethod().getName(), Arrays.toString(invocation.getArguments()));
}
return result;
}
}
(4) 測試
場景一:未添加切面時(shí) 瀏覽器訪問:http://localhost:8080/hello/zhangsan 觀察控制臺
場景二:通過postman動(dòng)態(tài)操作代理
① 新增代理
觀察控制臺:
########################################## BuildCandidateAdvised -->【com.github.lybgeek.aop.test.hello.service.HelloService】 With Advice -->【com.github.lybgeek.interceptor.LogMethodInterceptor】 SUCCESS !
此時(shí)瀏覽器訪問:http://localhost:8080/hello/zhangsan
再次觀察控制臺:
出現(xiàn)了切面日志信息,說明代理生效。
② 刪除代理
觀察控制臺:
########################################## Remove Advice -->【com.github.lybgeek.interceptor.LogMethodInterceptor】 For Bean -->【com.github.lybgeek.aop.test.hello.service.HelloService$$EnhancerBySpringCGLIB$$7bc75aa3】 SUCCESS !
此時(shí)瀏覽器訪問:http://localhost:8080/hello/zhangsan
再次觀察控制臺:
此時(shí)沒有出現(xiàn)切面日志信息,說明代理刪除成功
總結(jié)
本文實(shí)現(xiàn)熱插拔AOP就在于對advice、advised、advisor、pointcut概念的理解,這是實(shí)現(xiàn)熱插拔AOP的前提,其次就是對自定義classloader也需要有一定的了解,因?yàn)槲覀僯ar不一定從classpath底下加載,也有可能來源其他地方,比如遠(yuǎn)程鏈接啥的,最后就是把原先spring自動(dòng)幫我們實(shí)現(xiàn)aop,我們利用相關(guān)的api,自己手動(dòng)實(shí)現(xiàn)一遍,示例代碼的api只是利用spring api其中一種實(shí)現(xiàn)方式,它還有多種實(shí)現(xiàn)方式,比如可以利用TargetSource,感興趣的朋友,也可以自己實(shí)現(xiàn)一把。
至于那個(gè)代理增刪改查端點(diǎn)contoller,是我之前看springcloud gateway的路由定位器端點(diǎn)源碼,一直沒找到機(jī)會實(shí)現(xiàn)一下,就把他搬來這個(gè)示例實(shí)現(xiàn)一把,加深一下印象。