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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Dynamic Hot‑Pluggable AOP in Spring: Runtime Management of Advice

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaaopspringAdvicedynamic
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

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.