Backend Development 10 min read

Dynamic Hot‑Pluggable AOP in Spring: Managing Advice at Runtime

This article explains how to implement a hot‑pluggable AOP mechanism in Spring by dynamically adding or removing Advice through custom endpoints and event listeners, covering core concepts, implementation code, a demonstration, and best‑practice considerations for backend developers.

Architect
Architect
Architect
Dynamic Hot‑Pluggable AOP in Spring: Managing Advice at Runtime

This article demonstrates how to implement hot‑pluggable AOP in Spring, allowing users to enable or disable logging advice at runtime instead of hard‑coding it.

Pre‑knowledge : It introduces the key Spring AOP concepts – Advice (the action taken at a join point), Advisor (holds an Advice), Advised (the proxy factory configuration) and Pointcut .

Core hot‑plug logic : A custom Spring Boot endpoint registers or deregisters an AspectJExpressionPointcutAdvisor in the bean factory, while an event listener reacts to add/delete events to install or uninstall the corresponding plugin.

@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";
    }

}
@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;
        }
    }
}
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){
            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());
            }
        }
    }
}

Demo service and controller

@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

@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;
    }
}

The article then walks through two test scenarios: accessing the /hello/{msg} endpoint before adding any advice (no logging) and after dynamically adding the logging advice via the custom endpoint (log output appears). It also shows removing the advice and confirming that logging stops.

In summary, the implementation relies on a clear understanding of Advice, Advisor, Advised and Pointcut, dynamic registration of AspectJExpressionPointcutAdvisor beans, and optional use of custom class loaders for loading plugin JARs.

JavaAOPbackend developmentSpringAdviceDynamic Plugin
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

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.