[踩雷紀錄] 各種 Annotation 在同個 class 中調用失效

[踩雷紀錄] 各種 Annotation 在同個 class 中調用失效

2022, Oct 05    

在 spring boot 的框架加護之下,很多的功能都可以透過簡單的 annotation 啟用,像是 @Transactional 或是 @Cacheable 都是常會用到的,不過這些功能卻會在同個 class 中互相調用的時候失效,下面提供幾個解決方案

原因

由於 Spring boot 在做依賴注入的時候其實會做一層代理(Proxy),以便進行像是 AOP 的切片操作

所以如果是從不同的類別透過 Spring boot 注入進來的 class 才會套用到這層代理,內部互相調用是用不到的

也可以得知不只是 Annotation 會失效,就連 AOP 都不會起作用

解決方案

  • 範例程式:
@Sl4j
class UserService{
    @Autowired
    private UserDao userDao;
    @Autowired
    private PostDao postDao;

    public void getUserPosts(Long userId) throws Exception {
       List<Post> posts = findPostByUserFromCache(userId);
       log.info("posts: {}", posts);
    }

    @Cacheable("post")
    public List<Post> findPostByUserFromCache(Long userId) throws Exception {
        return postDao.findByUserId(userId);
    }
}

獨立 Service

最簡單也最推薦的其實就是獨立寫另一個 class 來調用方法

這也可以幫助撰寫程式架構的時候思考每個 service 跟 method 分工的合理性

AopContext.currentProxy()

另一個做法是透過直接取得被代理過的實例來呼叫方法可以透過 AopContext.currentProxy() 來辦到

參考以下程式碼

...

public void getUserPosts(Long userId) throws Exception {
    List<Post> posts = ((UserService)AopContext.currentProxy()).findPostByUserFromCache(userId);
    log.info("posts: {}", posts);
}

...

這樣可以拿到該類別被 AOP 代理的實例然後才去調用方法,所以能夠正確套用 AOP 的功能

不過要使用這個方法需要加一個 @EnableAspectJAutoProxy(exposeProxy = true) 的 annotation 到 Spring boot 的起始類別,把 AOP 的 Proxy 實例曝露出來

ApplicationContext.getBean()

這個方法跟上面有點像,一樣是取得正確的實例來調用方法

不過這次我們直接拿 Spring boot 幫我們創建好的 Bean 來使用,看到下面程式

...

@Autowired
private ApplicationContext applicationContext;

public void getUserPosts(Long userId) throws Exception {
       List<Post> posts = applicationContext.getBean(UserService.class).findPostByUserFromCache(userId);
       log.info("posts: {}", posts);
}

...

簡單解決,也不需要額外開功能,效果基本上一樣,我是比較偏好這樣寫的

我自己會將這部分在包成一個工具來使用

@Component
public class ContextUtils {
    private static ApplicationContextUtils instance;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void registerInstance() {
        instance = this;
    }

    public static <T> T getBean(Class<T> clazz) {
        return instance.applicationContext.getBean(clazz);
    }
}

包成 static method 可以方便我隨時隨地使用,也不需要再額外注入