Backend Development 9 min read

Hot‑Pluggable AOP: Dynamic Advice Management in Spring

The article shows how to implement a hot‑plug AOP system in Spring that lets users enable or disable logging at runtime by scanning, registering, and deregistering Advice/Advisor/Pointcut beans through the bean factory, demonstrated with a REST endpoint that adds or removes a logging interceptor.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
Hot‑Pluggable AOP: Dynamic Advice Management in Spring

This article describes how to let end‑users enable or disable logging at runtime by dynamically adding or removing AOP advice instead of hard‑coding it.

Key concepts introduced are:

Advice : the action taken at a join point (e.g., before, after, around).

Advisor : holds an Advice and is the basic Spring AOP interface.

Advised : an object that can have Advice/Advisor applied; implemented by Spring's ProxyFactory .

The core hot‑plug logic scans the Advice implementations and registers or deregisters them based on user requests.

Core Implementation Code

@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((Supplier
) () -> advisor);
    beanFactory.registerBeanDefinition(PROXY_PLUGIN_PREFIX + proxyMetaInfo.getId(),beanDefinition);
    return advisor;
}
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 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);
    }
}
@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;
    }
}

Two test scenarios are shown: (1) calling the service without any advice (no log output) and (2) adding the advice via a POST request to the plugin endpoint, then calling the service again to see the logging information. Deleting the advice removes the logging.

In summary, the article demonstrates a hot‑plug AOP mechanism by dynamically managing Advice , Advisor , and Pointcut objects through Spring’s bean factory APIs. Understanding these concepts and the ability to load custom JARs at runtime are prerequisites for this technique.

backendJavaadvice managementDynamic Pluginhot-pluggableSpring AOP
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

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.