Dynamic Hot‑Pluggable AOP in Spring: Runtime Management of Advice
This article explains how to implement a hot‑plugable AOP mechanism in Spring that lets end users enable or disable logging advice at runtime by dynamically adding or removing Advice, Advisor, and Proxy definitions through custom endpoints and event listeners.
The requirement is to let users control the on/off state of logging without hard‑coding it in the source code; instead of a static AOP configuration, a dynamic approach is needed where an Advice can be added to or removed from the Spring container at runtime.
Prerequisite concepts – Advice (org.aopalliance.aop.Advice) represents the operation to be performed at a join point; Advisor (org.springframework.aop.Advisor) holds an Advice and optionally a Pointcut; Advised (org.springframework.aop.framework.Advised) is the proxy factory interface that manages Advice and Advisor instances.
The core hot‑plug logic consists of scanning Advice implementations, registering them as beans, and using Spring’s DefaultListableBeanFactory to add or remove them on demand.
@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"; } }
Event listener that reacts to add/remove plugin events:
@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; } } }
Plugin installation utilities:
public void installPlugin(ProxyMetaInfo proxyMetaInfo){ if(StringUtils.isEmpty(proxyMetaInfo.getId())){ proxyMetaInfo.setId(proxyMetaInfo.getProxyUrl() + SPIILT + proxyMetaInfo.getProxyClassName()); } AopUtil.registerProxy(defaultListableBeanFactory, proxyMetaInfo); }
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(() -> advisor); beanFactory.registerBeanDefinition(PROXY_PLUGIN_PREFIX + proxyMetaInfo.getId(), beanDefinition); return advisor; }
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); } }
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)); } }
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 && isFindMatchAdvised){ advisedBean.removeAdvice(advisor.getAdvice()); log.info("Remove Advice -->[{}] For Bean -->[{}] SUCCESS!", advisor.getAdvice().getClass().getName(), bean.getClass().getName()); } else if(operateEventEnum == OperateEventEnum.ADD && isFindMatchAdvised){ advisedBean.addAdvice(advisor.getAdvice()); log.info("Add Advice -->[{}] For Bean -->[{}] SUCCESS!", advisor.getAdvice().getClass().getName(), bean.getClass().getName()); } } }
Demo application:
@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; } }
@RestController @RequestMapping("hello") @RequiredArgsConstructor public class HelloController { private final HelloService helloService; @GetMapping("{message}") public String sayHello(@PathVariable("message") String message){ return helloService.sayHello(message); } }
Logging interceptor used as the dynamic advice:
@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; } }
Testing steps: (1) call http://localhost:8080/hello/zhangsan without any advice – only the service log appears; (2) use Postman to POST to the custom endpoint to add a proxy – console shows a "BuildCandidateAdvised" message and subsequent requests produce the logging interceptor output; (3) POST to delete the proxy – console shows a "Remove Advice" message and the interceptor output disappears, confirming successful removal.
In summary, implementing hot‑plug AOP requires a solid grasp of Advice, Advisor, Advised, and Pointcut concepts, as well as optional class‑loader tricks for loading JARs from non‑classpath locations; the example demonstrates one practical way using Spring’s AOP API, though alternatives such as TargetSource also exist.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.