new一个Service对象后@Autowired的Dao注入失败问题

1. 问题

  通过Netty写一个简单的RPC demo,服务端在收到类名、方法名、参数,用反射方法执行时报错。反射部分代码如下:

    Class<?> serviceClass = Class.forName(className);
    Method serviceMethod = serviceClass.getMethod(methodName, paramTypes);
    Object res = serviceMethod.invoke(serviceClass.newInstance(), arguments);

  第三行代码报错为InvocationTargetException。
  后面还跟着一个报错NullPointerException,该错误发生在下面代码的第四行。

@Autowired
private UserRepository userRepository;
public String login(String username, String password) {
    User user = userRepository.findByUsernameAndPassword(username, password);
    if(user == null) {
      return null;
    }
    return TokenUtils.createToken(user.getUsername(), user.getId());
}

2. 问题分析

  InvocationTargetException是反射的一个常见报错,原因很多,此处可以看出是NullPointerException导致反射出错,所以重点研究

User user = userRepository.findByUsernameAndPassword(username, password);

的NullPointerException。
  在junit单元测试中该方法运行正常。
  既然是空指针错误,这行代码就两个对象,不是user就是userRepository,debug,果然userRepository是null。原来是@Autowired注入失败。
原因
  springboot项目启动时,会构造生成必须的bean以及这些bean依赖的bean,所以你在Controller中使用Service、在Service中使用Dao,都是Spring启动时帮你初始化好了的。但是上面的反射代码中,serviceClass.newInstance()是通过new新建了一个Service对象,而这个Service对象中的Dao并没有经过Spring注入实例,所以就是null。

3. 解决办法

3.1 不用Spring依赖注入

  Service中使用Dao,直接new一个Dao对象,而不用Spring的依赖注入。但是有了Spring的依赖注入在很多地方代码写起来更简洁明了,此外Spring的等等等等优点就不赘述了。

3.2 反射不new对象,而用依赖注入

  不使用newInstance创建新的Service对象,而是通过applicationContext获取Spring给Service创建的bean。
  通过新建一个类获取applicationContext,代码如下:

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

/**
 * 获取bean,代替注入方法,用在例如线程中
 */
@Component
public class SpringBeanUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringBeanUtil.applicationContext == null) {
            SpringBeanUtil.applicationContext = applicationContext;
        }
    }

    // 获取applicationContext
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    // 通过name获取 Bean.
    public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    }

    // 通过class获取Bean.
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    // 通过name,以及Clazz返回指定的Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
}

  注意该类必须加@Component注解,即也交给Spring管理,否则不能获得applicationContext。将

serviceMethod.invoke(serviceClass.newInstance(), arguments)

修改为

serviceMethod.invoke(SpringContextUtils.getBean(className), arguments)

  这时又出现了一个新问题

 org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'com.example.RPCdemo.service.UserService' available

  原来Spring管理的UserService的bean就叫userService,而不是Class.getClassName得到的com.example.RPCdemo.service.UserService。
  补充一段代码,从className中提取bean的name:

// 获得bean名称
String[] tmp = className.split("\\.");
String simpleClassName = tmp[tmp.length-1];
// 目的是首字母改成小写
if(!Character.isLowerCase(simpleClassName.charAt(0))) {
      simpleClassName = (new StringBuilder()).append(Character.toLowerCase(simpleClassName.charAt(0))).append(simpleClassName.substring(1)).toString();
}

  以上是通过类名获取bean,也可以使用Class来获取,就没有上面的问题了。

serviceMethod.invoke(SpringContextUtils.getBean(serviceClass), arguments)

3.3 网上找到的方法,这个方法不是很方便,但是值得学习其中的想法和对java、spring的理解

  将Service代码改成如下形式(注意增加的代码):

  @Autowired
  private UserRepository userRepository;

  // 增加的代码
  public static UserServiceImpl dynamicProxy;

  @PostConstruct
  public void init() {
    dynamicProxy = this;
  }
  // 增加的代码

  @Override
  public String login(String username, String password) {
    User user = dynamicProxy.userRepository.findByUsernameAndPassword(username, password);
    System.out.println(user);
    if(user == null) {
      return null;
    }
    return TokenUtils.createToken(user.getUsername(), user.getId());
  }

  原博文是 https://blog.csdn.net/woai671/article/details/79112474 ,也没解释原因,我的理解是是把定义了一个UserServiceImpl类型的静态变量dynamicProxy,使用@PostConstruct在Spring初始化依赖注入时,将Spring构造的UserServiceImpl的bean赋值给dynamicProxy,而static变量是所有bean共享的,所以new一个UserServiceImpl时就可以通过dynamicProxy拿到UserRepository。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容