本文内容: 1、起因 2、static 3、@Autowired 4、解决方案 |
先说结论:不能。
|| 起因
从某系统交接过来一部分功能的代码,其中包含了一个工具类,该工具类中用到了一个Component的方法。 原系统的实现是:
public class CityUtil {
public static List<City> getCitiesByProvinceId(Integer provinceId){
return SpringBeanFactory.getBean(LocalComponent.class).getCities(201);
}
}
@Component
public class LocalComponent {
public List<City> getCities(Integer provinceId){
// 实现逻辑略
return null;
}
}
新工程里没有类似SpringBeanFactory.getBean(Class class)的工具方法,也不能在Spring初始化的时候setApplicationContext(新工程使用的是自研的web框架,和Spring的整合是在框架内部实现的), 所以也就没细想,直接使用 @Autowired 注入了LocalComponent。
public class CityUtil {
@Autowired
private static LocalComponent localComponent;
public static List<City> getCitiesByProvinceId(Integer provinceId){
return localComponent.getCities(201);
}
}
细心的朋友可能已经看出问题了,上面第5行在运行时会报空指针,因为 @Autowired 注入根本没生效。localComponent为空无非有2种原因,要么注入的就是空,要么没注入。所以要先了解一下 static 和 @Autowired 各自的特性,看看到底哪里用错了。
|| static
| static 是什么?
static 是java的一个关键字,很多书里也叫修饰符,可以用来修饰属性、方法、代码块、内部类。Java关键字可以在这里查看:
https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html
然而我在官网并没有找到专门讲static的章节,但是发现在类设计中的4个小节里,都涉及到了static关键字的使用,日常的使用场景也都涵盖了。
https://docs.oracle.com/javase/tutorial/extra/certification/javase-8-programmer2.html
1.6章节。
文档中提到,它修饰的内容都是属于class的(要注意class、object、instance的区别)。这些在class加载的时候就会分配内存空间,就是JVM规范里的方法区。所以一个项目中不应该有大量带有static的类,或者用static修饰占用很大空间的变量,即便Java 8 对应的HotSpot 采用元空间转移到堆空间的形式实现方法区。
看过文档之后才发现,原来所学的Java入门教程,都源自这里啊。从Hello World! 到数据类型,从运算符到语法结构,从IO到Socket这里都能够找到。 |
| Java不是号称一切皆对象吗,为什么又有static?
Java本身是一门面向对象的语言,日常使用最多的也是new一个object,然后通过object. 来使用它的属性和方法。那为什么又有static修饰的属性和方法呢。
我们都知道JVM有自己的常量池,供Java自己的一些class使用,这些在JVM启动的时候就创建好了。那我们用户自己也有类似的需求怎么办?
官网Bicycle的例子中设定了一个场景,希望有一个变量numberOfBicycles能够记录创建了多少个bicycle对象,所以这个变量属于class,不属于任何object,所有的object共享这个变量,class的每一个instance会共享同一内存空间的这个类变量。
对此,我总结了3点:
职责划分:这个计数不属于某一个object,所有的object是独立的,相互是不通信的,所以这个只能归属于类,就好比网站的计数器,它不属于某个session,所有session共享它。
节省空间:资源复用,相同的内容没必要重复的开辟内存空间。
节省时间:以时间换时间,前面提到static修饰的内容在class加载时就分配好了内存空间,所以它是以牺牲JVM启动耗时来降低使用时初始化带来的耗时。毕竟JVM启动的次数相对于对象使用的次数是可以忽略不计的。很多软件设计中用写耗时来换取读省时也体现了这一理念。
| 没有static可以吗?
如果不考虑上面的问题,没有static好像程序也能运行,无非就是浪费点空间,耗费点时间。
不过回想我们写个第一个Java程序Hello World! ,这是我们第一次接触static,它修饰的是main()方法。如果没有static,启动类要new出来?new出来之后如何调用呢?如果启动类里依赖了其他的类呢?一直new下去?
底层的实现已经不得而知,有兴趣的朋友可以继续深究。
|| @Autowired
| @Autowired是什么?
官方文档:
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-autowired-annotation
这一节的开篇就注明了可以使用JSR330标准里的@Inject来替换Spring的@Autowired。也就是说,@Autowired和@Inject一样,都是对JSR330标准注解(依赖注入)的一种实现。
@Autowired的用法在文档里已经讲的非常清楚了,但是整个章节里也没提到能不能用在static修饰的属性或方法上。所以只能在源码里找找答案了,本章的最后说到,这类注解都是被 Spring的BeanPostProcessor处理的。
| 底层对static修饰属性、方法是如何处理的?
顺着上面的思路,在源码中找到对应的BeanPostProcessor -- AutowiredAnnotationBeanPostProcessor。
AutowiredAnnotationBeanPostProcessor实现了MergedBeanDefinitionPostProcessor接口,而这个接口有好几个实现类,有的从名字上就能看出是哪个注解的Processor。这里顺便说一下CommonAnnotationBeanPostProcessor,因为@Resource就是被它处理的,日后再谈论@Autowired和@Resource区别的时候就可以加上这一点,它们是被不同的BeanPostProcessor处理的。 |
AutowiredAnnotationBeanPostProcessor里面重点看一下buildAutowiringMetadata方法的这几行代码就可以了
if (Modifier.isStatic(field.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static fields: " + field);
}
return;
}
...
if (Modifier.isStatic(method.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static methods: " + method);
}
return;
}
代码在构建带有@Autowired修饰的Element的时候会过滤掉static修饰的属性和方法,所以到后面赋值的阶段就和这些static修饰的属性、方法毫无关系了。
而且这种情况还打印了日志,启动的时候怎么没发现呢?再次启动项目,还真找到了这一行日志...
Autowired annotation is not supported on static fields: private static com.test.LocalComponent com.test.CityUtil.localComponent
事后再仔细想一下,这样处理也是合理的:
1、类加载肯定是在Spring初始化bean之前,前面刚赋好的值,后面被Spring偷偷改了,也不像话
2、类属性的值被改了,就不是影响一个对象的事情了,所有的都会受影响
|| 解决方案
| 能不能在XML中注入?
继续在官方文档中寻找答案,文档中给出了<bean>标签的使用说明,但仍然没有提到能不能注入static修饰的属性,所以只能参考例子试一把,看看底层的处理逻辑就知道了。
修改applicationContext.xml
<bean class="com.test.CityUtil">
<property name="localComponent" ref="localComponent"></property>
</bean>
文档1.3中提到,容器内的bean被表示为BeanDefinition对象,由于是从xml中解析初始化的,所以顺着这个思路debug到org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader,继续找到解析<bean>的代码
顺着这个方法继续debug,走到下面这个方法:
org.springframework.beans.factory.xml.BeanDefinitionParserDelegate#parsePropertyElement(Element ele, BeanDefinition bd)
这里面会把要赋值的 propertyName和value封装成PropertyValue对象放到BeanDefinition中的propertyValueList中。
到此没有发现对static做特殊处理的逻辑,中间的逻辑快速跳过,直接去看实例化的环节。org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
到这一步已经可以拿到CityUtil的实例对象,也没有发现对static做特殊处理的逻辑,继续看赋值环节:
org.springframework.beans.AbstractPropertyAccessor#setPropertyValues(org.springframework.beans.PropertyValues, boolean, boolean)
org.springframework.beans.BeanWrapperImpl.BeanPropertyHandler#setValue(final Object object, Object valueToApply)
方法的最后,拿到要赋值的方法和值,invoke之后就会赋值成功,也就是调用了setter方法。到这里还是没看到对static做特殊处理的逻辑,invoke之后其实已经能看到localComponent有值了,但心里还是不踏实,赋的值是否有效?
执行了单测,通过,说明这种方式是行得通的。
不能自动注入,但可以通过XML注入,说白了就是你可以显示的set,自己随便搞,但是我不会帮你自动set。static属性值被修改时一定是你人为操作,有源可溯。 |
| 不注入了,用的时候从容器中获取
这种就是最常用的方式了。首先,需要先能拿到容器,看一下BeanFactory和ApplicationContext的类图关系:
所以得先拿到BeanFactory 或者 ApplicationContext,然后就能通过getBean( )方法拿到我们想要的bean了。
方案一:拿到BeanFactory
@Component
public class SpringBeanFactory implements BeanFactoryAware {
private static BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
SpringBeanFactory.beanFactory = beanFactory;
}
public static <T>T getBean(Class<T> clazz){
return beanFactory.getBean(clazz);
}
}
方案二:拿到ApplicationContext
@Component
public class ApplicationContextFactory implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextFactory.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
}
两种方案都可以达到目的,但这种写法不符合编码规范,sonar会有警告。这就再次说明static修饰的属性是不希望被再次赋值的。
XML注入已经很少使用了,需要注入多个属性的话,配置会变得臃肿,如果不看配置文件,可能就不知道从哪里赋的值,所以就有了@Autowired吧; 从容器中获取,比较灵活,而且更直观。 具体采用哪种形式,看个人喜好吧。 |
|| 总结
1、有问题可以先试着官方文档中寻找答案
2、看源码一定要debug起来,否则多态会搞死你