问题描述

还是为了解决上次的Hibernate拦截器问题,@Autowired不管用了。

以下是部分代码,因本文主要解决手动从容器中获取对象的问题,所以将validateWebAppMenuRoute方法的业务逻辑删除,我们只打印webAppMenuService,来判断我们的注入是否成功。

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方法获取其中某个对象。

@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时写的代码,我们手动根据我们的注解配置自己生成的上下文环境,所以我们可以任性地去操作我们的上下文。

然而,在Spring Boot项目中。

@SpringBootApplication
public class ResourceApplication {

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

clipboard.png

点开这行run方法,会发现其返回值就是我们的context,这是一种最简单的获取上下文的办法。但是一看到这么优雅的一行代码,实在不忍心破坏,永远保持main只有这一行,这太美了。

实现接口,获取上下文

这是在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);
    }
}

执行保存菜单的单元测试,结果如下:

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为我们注入了。

张喜硕
2.1k 声望423 粉丝

浅梦辄止,书墨未浓。