@Autowired失效時如何獲取須要的容器對象

問題描述

仍是爲了解決上次的Hibernate攔截器問題,@Autowired無論用了。java

如下是部分代碼,因本文主要解決手動從容器中獲取對象的問題,因此將validateWebAppMenuRoute方法的業務邏輯刪除,咱們只打印webAppMenuService,來判斷咱們的注入是否成功。web

public class WebAppMenuInterceptor extends EmptyInterceptor {

    private WebAppMenuService webAppMenuService;

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        if (entity instanceof WebAppMenu) {
            this.validateWebAppMenuRoute((WebAppMenu) entity);
        }
        return super.onSave(entity, id, state, propertyNames, types);
    }

    private void validateWebAppMenuRoute(WebAppMenu webAppMenu) {
        System.out.println(webAppMenuService);
    }
}

獲取上下文

想要獲取容器中的對象,咱們想的就是先獲取容器(上下文),而後再調用容器的getBean方法獲取其中某個對象。spring

@ComponentScan("com.mengyunzhi.spring")
public class Application {

    public static void main(String []args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
        TestService testService = context.getBean(TestService.class);
    }
}

這是咱們學習Spring IOC時寫的代碼,咱們手動根據咱們的註解配置本身生成的上下文環境,因此咱們能夠任性地去操做咱們的上下文。segmentfault

然而,在Spring Boot項目中。app

@SpringBootApplication
public class ResourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }
}

clipboard.png

點開這行run方法,會發現其返回值就是咱們的context,這是一種最簡單的獲取上下文的辦法。可是一看到這麼優雅的一行代碼,實在不忍心破壞,永遠保持main只有這一行,這太美了。ide

實現接口,獲取上下文

這是在Spring揭祕一書中截下的一段話:單元測試

clipboard.png

因此,咱們只要實現上面的任一一個接口,就能獲取到注入的上下文實例。學習

package com.mengyunzhi.measurement.context;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 上下文,用於獲取本項目的上下文/容器
 * 在@Autowired失效時可以使用上下文手動獲取相關對象
 */
@Component
public class ResourceApplicationContext implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        return context;
    }
}

獲取對象

對攔截器的代碼修改以下,新建inject方法,用於注入沒法自動裝配的Bean測試

public class WebAppMenuInterceptor extends EmptyInterceptor {

    private WebAppMenuService webAppMenuService;

    private void inject() {
        if (null == this.webAppMenuService) {
            this.webAppMenuService = ResourceApplicationContext.getApplicationContext().getBean(WebAppMenuService.class);
        }
    }

    @Override
    public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
        if (entity instanceof WebAppMenu) {
            this.validateWebAppMenuRoute((WebAppMenu) entity);
        }
        return super.onSave(entity, id, state, propertyNames, types);
    }

    private void validateWebAppMenuRoute(WebAppMenu webAppMenu) {
        this.inject();
        System.out.println(webAppMenuService);
    }
}

執行保存菜單的單元測試,結果以下:this

clipboard.png

注入成功,而且是Bean默認的單例模式。

思考

爲何咱們一直使用的@Autowired註解失效了呢?

找了一夜,終於找到失效的問題了,可是本身的問題沒解決,卻順帶把歷史上遺留的一個問題解決了。

用戶認證攔截器

@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {
    
    Logger logger = Logger.getLogger(TokenInterceptor.class.getName());
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.info("在觸發action前,先觸發本方法");
        logger.info("獲取路徑中的token值");
        String tokenString = request.getParameter("token");

        UserService userService = new UserServiceImpl();
        
        logger.info("根據token獲取對應的用戶");
        User user = userService.getUserByToken(tokenString);
        if (user == null) {
            throw new SecurityException("傳入的token已過時");
        }

        logger.info("註冊spring security,進行用戶名密碼認證");
        Authentication auth = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        SecurityContextHolder.getContext().setAuthentication(auth);
        return true;
    }
}

這是同一個包下的攔截器,該攔截器用於攔截請求,使用Spring Security對用戶進行認證。這裏的UserService並非@Autowired進來的,而是new出來的,可能前輩在寫這塊的時候也遇到了注入失敗的問題。

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    static private Logger logger = Logger.getLogger(WebConfig.class.getName());

    /**
     * 配置攔截器
     * @param interceptorRegistry
     */
    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {
        logger.info("添加攔截器/**/withToken/**, 因此包含/withToken字符串的url都要被TokenInterceptor攔截");
        interceptorRegistry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**/withToken/**");
    }
}

可能第一次看這段添加攔截器的代碼可能有些摸不着頭緒,其實咱們點開源碼,可能就會理解得更深入。

clipboard.png

沒錯,這就是咱們學習過的觀察者模式

其實很簡單,調用interceptorRegistry添加攔截器的方法,添加一個咱們寫的攔截器進去。這裏是new出來的攔截器。

new出來的東西,Spring是無論的,因此,若是咱們是new的攔截器,那@Autowired註解標註的屬性就是空。因此引起@Autowired失效。

爲了驗證咱們的猜測,測試一下。

1.咱們先刪除new UserServiceImpl(),將其改成@Autowired

@Component
public class TokenInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private UserService userService;

    Logger logger = Logger.getLogger(TokenInterceptor.class.getName());
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        logger.info("在觸發action前,先觸發本方法");
        logger.info("獲取路徑中的token值");
        String tokenString = request.getParameter("token");

        logger.info("根據token獲取對應的用戶");
        User user = userService.getUserByToken(tokenString);
        if (user == null) {
            throw new SecurityException("傳入的token已過時");
        }

        logger.info("註冊spring security,進行用戶名密碼認證");
        Authentication auth = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        SecurityContextHolder.getContext().setAuthentication(auth);
        return true;
    }
}

clipboard.png

單元測試,應該出錯,由於咱們是new的攔截器,而在攔截器中@AutowiredSpring是無論裏這個注入的。而且錯誤信息就是咱們調用userService的那行代碼。

2.將new的攔截器改成@Autowired

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private TokenInterceptor tokenInterceptor;

    static private Logger logger = Logger.getLogger(WebConfig.class.getName());

    /**
     * 配置攔截器
     * @param interceptorRegistry
     */
    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {
        logger.info("添加攔截器/**/withToken/**, 因此包含/withToken字符串的url都要被TokenInterceptor攔截");
        interceptorRegistry.addInterceptor(tokenInterceptor).addPathPatterns("/**/withToken/**");
    }
}

單元測試,經過。

clipboard.png

解決了這個問題,由於本問題是由咱們在配置攔截器時一個new的失誤形成的。可是Hibernate的攔截器咱們並無本身配置,掃描到就會調用,這裏猜想問題應該出如今Hibernate在實例化我自定義的攔截器時使用的是new或某種其餘方式,反正應該不是在容器中獲取的,因此獲得的攔截器對象的@Autowired相關屬性就爲空。

總結

@Autowired描述對象之間依賴的關係,這裏的關係指的是Spring IOC容器中相關的關係。

因此,其實這個問題並非咱們的註解失效了,其實若是咱們從容器中拿一個WebAppMenuInterceptor的攔截器對象,它實際上是已經將WebAppMenuService注入進來的,只不過Hibernate調用咱們的攔截器時,採用並不是從容器中獲取的方式,只是「看起來」,註解失效了。

配置的時候能 @Autowired就不用 new,若是第三方庫中 new實例化咱們的實現,咱們須要使用手動從容器中獲取的方法,就不能讓 Spring爲咱們注入了。
相關文章
相關標籤/搜索