Backend Development 11 min read

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.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Dynamic Hot‑Pluggable AOP Logging with Spring: Design, Implementation and Demo

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.

backendJavaaopSpringLoggingDynamic
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.