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。