Dynamic Hot‑Pluggable AOP Logging with Spring: Design, Implementation and Demo
This article explains how to make Spring AOP logging configurable at runtime by dynamically adding or removing Advice objects through custom endpoints, event listeners, and plugin management code, and demonstrates the approach with a complete example including service, controller, and aspect jar.
The requirement is to let end users turn logging on or off instead of hard‑coding it in the source code. While AOP can handle logging, the challenge is to make the AOP configuration dynamic.
In Spring AOP the framework scans Advice implementations and registers them. By creating a custom Advice and adding or removing it at runtime, we can control whether logging is active.
Prerequisite Knowledge
Advice
org.aopalliance.aop.Advice – the operation that an aspect performs at a join point. Types include BeforeAdvice , MethodInterceptor , and AfterAdvice .
Advisor
org.springframework.aop.Advisor – holds an Advice . Its sub‑interface PointcutAdvisor is the most commonly used.
Advised
org.springframework.aop.framework.Advised – the AOP proxy factory configuration interface. Its implementation ProxyFactory creates the proxy classes.
Hot‑Pluggable AOP Core Logic
The diagram (omitted) shows the flow of adding or removing an Advice based on user actions.
Core Implementation Code
1. Dynamic management of advice endpoint
@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. Event listener to capture plugin install/uninstall
@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. Install plugin
public void installPlugin(ProxyMetaInfo proxyMetaInfo) {
if (StringUtils.isEmpty(proxyMetaInfo.getId())) {
proxyMetaInfo.setId(proxyMetaInfo.getProxyUrl() + SPIILT + proxyMetaInfo.getProxyClassName());
}
AopUtil.registerProxy(defaultListableBeanFactory, proxyMetaInfo);
}4. Core registration logic
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;
}5. Uninstall plugin
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. Core un‑registration
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. Add or remove advice from existing beans
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 isMatch = findMatchAdvised(advisedBean.getClass(), pointcut);
if (operateEventEnum == OperateEventEnum.DEL && isMatch) {
advisedBean.removeAdvice(advisor.getAdvice());
log.info("Remove Advice --> {} For Bean --> {} SUCCESS", advisor.getAdvice().getClass().getName(), bean.getClass().getName());
} else if (operateEventEnum == OperateEventEnum.ADD && isMatch) {
advisedBean.addAdvice(advisor.getAdvice());
log.info("Add Advice --> {} For Bean --> {} SUCCESS", advisor.getAdvice().getClass().getName(), bean.getClass().getName());
}
}
}Hot‑Pluggable AOP Demo
1. Create a 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. Create a 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. Prepare a logging aspect jar
@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. Test Steps
Scenario 1 – No aspect added : Access http://localhost:8080/hello/zhangsan and observe that no logging appears in the console.
Scenario 2 – Dynamically add the proxy via the POST endpoint. The console prints a message like:
########################################## BuildCandidateAdvised --> com.github.lybgeek.aop.test.hello.service.HelloService With Advice --> com.github.lybgeek.interceptor.LogMethodInterceptor SUCCESS !Now the same URL produces logging output, confirming the proxy is active.
Scenario 3 – Dynamically remove the proxy . The console shows a removal message and subsequent requests no longer produce logging, proving the advice was successfully detached.
Summary
The hot‑pluggable AOP solution hinges on understanding the concepts of Advice , Advisor , Advised , and Pointcut , as well as handling class loading when plugins may come from outside the standard classpath. By manually managing Spring’s AOP APIs—registering or destroying AspectJExpressionPointcutAdvisor beans and adding/removing advice from existing beans—we achieve runtime control over logging without restarting the application.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.