Spring @Autowired、@Resource 和 @Service

原文:Spring5:@Autowired 註解、@Resource 註解和 @Service 註解。簡單完善。

什麼是註解

傳統的 Spring 做法是使用 .xml 文件來對 bean 進行注入或者是配置 AOP、事務,這麼做有兩個缺點:

  1. 如果所有的內容都配置在 .xml 文件中,那麼 .xml 文件將會十分龐大;如果按需求分開 .xml 文件,那麼 .xml 文件又會非常多。總之這將導致配置文件的可讀性與可維護性變得很低

  2. 在開發中在 .java 文件和 .xml 文件之間不斷切換,是一件麻煩的事,同時這種思維上的不連貫也會降低開發的效率

爲了解決這兩個問題,Spring 引入了註解,通過“@XXX”的方式,讓註解與 Java Bean 緊密結合,既大大減少了配置文件的體積,又增加了 Java Bean 的可讀性與內聚性。

本篇文章,講講最重要的三個 Spring 註解,也就是 @Autowired、@Resource 和 @Service。

不使用註解

先看一個不使用註解的 Spring 示例,在這個示例的基礎上,改成註解版本的,這樣也能看出使用與不使用註解之間的區別,先定義一個老虎:

public class Tiger
{
    private String tigerName = "TigerKing";
    
    public String toString()
    {
        return "TigerName:" + tigerName;
    }
}

再定義一個猴子:

public class Monkey
{
    private String monkeyName = "MonkeyKing";
    
    public String toString()
    {
        return "MonkeyName:" + monkeyName;
    }
}

定義一個動物園:

public class Zoo
{
    private Tiger tiger;
    private Monkey monkey;
    
    public void setTiger(Tiger tiger)
    {
        this.tiger = tiger;
    }
    
    public void setMonkey(Monkey monkey)
    {
        this.monkey = monkey;
    }
    
    public Tiger getTiger()
    {
        return tiger;
    }
    
    public Monkey getMonkey()
    {
        return monkey;
    }
    
    public String toString()
    {
        return tiger + "\n" + monkey;
    }
}

Spring 的配置文件這麼寫:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
    xmlns="http://www.springframework.org/schema/beans"  
    xmlns:context="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.2.xsd"
    default-autowire="byType">
    
    <bean id="zoo" class="com.xrq.bean.Zoo" >
        <property name="tiger" ref="tiger" />
        <property name="monkey" ref="monkey" />
    </bean>
    
    <bean id="tiger" class="com.xrq.domain.Tiger" />
    <bean id="monkey" class="com.xrq.domain.Monkey" />
    
</beans>

@Autowired

@Autowired 顧名思義,就是自動裝配,其作用是爲了消除 Java 代碼裏面的 getter/setter 與 xml bean 標籤中的 property。當然,getter 看個人需求,如果私有屬性需要對外提供的話,應當予以保留。

因此,引入 @Autowired 註解,先看一下 Spring 配置文件怎麼寫:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
 3     xmlns="http://www.springframework.org/schema/beans"  
 4     xmlns:context="http://www.springframework.org/schema/context"  
 5     xsi:schemaLocation="http://www.springframework.org/schema/beans 
 6         http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
 7         http://www.springframework.org/schema/context
 8         http://www.springframework.org/schema/context/spring-context-4.2.xsd">
 9     
10     <context:component-scan base-package="com.xrq" />
11     
12     <bean id="zoo" class="com.xrq.bean.Zoo" />
13     <bean id="tiger" class="com.xrq.domain.Tiger" />
14     <bean id="monkey" class="com.xrq.domain.Monkey" />
15     
16 </beans>

注意第 10 行,使用必須告訴 Spring 一下我要使用註解了,告訴的方式有很多,<context:component-scan base-package="xxx" /> 是一種最簡單的,Spring 會自動掃描 xxx 路徑下的註解。

看到第 12 行,原來 zoo 裏面應當注入兩個屬性 tiger、monkey,現在不需要注入了。再看下,Zoo.java 也很方便,getter/setter 都可以去掉:

public class Zoo
{
    @Autowired
    private Tiger tiger;
    
    @Autowired
    private Monkey monkey;
    
    public String toString()
    {
        return tiger + "\n" + monkey;
    }
}

這裏 @Autowired 註解的意思就是,當 Spring 發現 @Autowired 註解時,將自動在代碼上下文中找到和其匹配(默認是類型匹配)的 Bean,並自動注入到相應的地方去。

有一個細節性的問題是,假如 xml bean 標籤裏面有兩個 property,Zoo.java 裏面又去掉了屬性的 getter/setter 並使用 @Autowired 註解標註這兩個屬性那會怎麼樣?答案是 Spring 會按照 xml 優先的原則去 Zoo.java 中尋找這兩個屬性的 getter/setter,導致的結果就是初始化 bean 報錯。

OK,假設此時我把 .xml 文件的 13 行、14 行兩行給去掉,再運行,會拋出異常:

Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'Zoo': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.xrq.domain.Tiger com.xrq.bean.Zoo.ttiger; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.xrq.domain.Tiger] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1214)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:305)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:301)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:196)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:835)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:537)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
    at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
    at com.xrq.test.MyTest.main(MyTest.java:13)
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.xrq.domain.Tiger com.xrq.bean.Zoo.ttiger; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.xrq.domain.Tiger] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:571)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331)
    ... 13 more
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [com.xrq.domain.Tiger] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(DefaultListableBeanFactory.java:1373)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1119)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1014)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:543)
    ... 15 more

因爲,@Autowired 註解要去尋找的是一個 Bean,Tiger 和 Monkey 的 Bean 定義都給去掉了,自然就不是一個 Bean 了,Spring 容器找不到也很好理解。那麼,如果屬性找不到我不想讓 Spring 容器拋出異常,而就是顯示 null,可以嗎?可以的,其實異常信息裏面也給出了提示了,就是將 @Autowired 註解的 required 屬性設置爲 false 即可:

public class Zoo
{
    @Autowired(required = false)
    private Tiger tiger;
    
    @Autowired(required = false)
    private Monkey monkey;
    
    public String toString()
    {
        return tiger + "\n" + monkey;
    }
}

此時,找不到 tiger、monkey 兩個屬性,Spring 容器不再拋出異而是認爲這兩個屬性爲 null。

@Autowired 接口注入

上面的比較簡單,我們只是簡單注入一個 Java 類,那麼如果有一個接口,有多個實現,Bean 裏引用的是接口名,又該怎麼做呢?比如有一個 Car 接口:

public interface Car
{
    public String carName();
}

兩個實現類 Audi 和 Benz:

@Service
public class Audi implements Car
{
    public String carName()
    {
        return "Audi car";
    }
}
@Service
public class Benz implements Car
{
    public String carName()
    {
        return "Benz car";
    }
}

寫一個 CarFactory,引用 Car:

@Service
public class CarFactory
{
    @Autowired
    private Car car;
    
    public String toString()
    {
        return car.carName();
    }
}

不用說,一定是報錯的,Car 接口有兩個實現類,Spring 並不知道應當引用哪個實現類。這種情況通常有兩個解決辦法:

  1. 刪除其中一個實現類,Spring 會自動去 base-package 下尋找 Car 接口的實現類,發現 Car 接口只有一個實現類,便會直接引用這個實現類

  2. 實現類就是有多個該怎麼辦?此時可以使用 @Qualifier 註解:

    @Service
    public class CarFactory
    {
        @Autowired
        @Qualifier("audi")
        private Car car;
    
        public String toString()
        {
            return car.carName();
        }
    }
    

    注意 @Qualifier 註解括號裏面的是 Car 接口實現類的 bean id

@Resource

把 @Resource 註解放在 @Autowired 下面說,是因爲它們作用非常相似,這個就簡單說了,例子過後點明一下 @Resource 和 @Autowired 的區別。先看一下 @Resource,直接寫 Zoo.java 了:

@Service
public class Zoo
{
    @Resource(name = "tiger")
    private Tiger tiger;
    
    @Resource(type = Monkey.class)
    private Monkey monkey;
    
    public String toString()
    {
        return tiger + "\n" + monkey;
    }
}

這是詳細一些的用法,說一下 @Resource 的裝配順序:

  1. @Resource 後面沒有任何內容,默認通過 name 屬性去匹配 bean,找不到再按 type 去匹配

  2. 指定了 name 或者 type 則根據指定的類型去匹配 bean

  3. 指定了 name 和 type 則根據指定的 name 和 type 去匹配 bean,任何一個不匹配都將報錯

然後,區分一下 @Autowired 和 @Resource 兩個註解的區別:

  1. @Autowired 默認按照 byType 方式進行 bean 匹配,@Resource 默認按照 byName 方式進行 bean 匹配

  2. @Autowired 是 Spring 的註解,@Resource 是 J2EE 的註解,這個看一下導入註解的時候這兩個註解的包名就一清二楚了

Spring 屬於第三方的,J2EE 是 Java 自己的東西,因此,建議使用 @Resource 註解,以減少代碼和 Spring 之間的耦合。

@Service

上面這個例子,還可以繼續簡化,因爲 Spring 的配置文件裏面還有 12 行~14 行三個 bean,下一步的簡化是把這三個 bean 也給去掉,使得 Spring 配置文件裏面只有一個自動掃描的標籤,增強 Java 代碼的內聚性並進一步減少配置文件。

要繼續簡化,可以使用 @Service。先看一下配置文件,當然是全部刪除了:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
    xmlns="http://www.springframework.org/schema/beans"  
    xmlns:context="http://www.springframework.org/schema/context"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-4.2.xsd">
    
    <context:component-scan base-package="com.xrq" />
    
</beans>

是不是感覺很爽?起碼我覺得是的。OK,下面以 Zoo.java 爲例,其餘的 Monkey.java 和 Tiger.java 都一樣:

@Service
public class Zoo
{
    @Autowired
    private Tiger ttiger;
    
    @Autowired
    private Monkey mmonkey;
    
    public String toString()
    {
        return ttiger + "\n" + mmonkey;
    }
}

這樣,Zoo.java 在 Spring 容器中存在的形式就是“zoo”,即可以通過 ApplicationContext 的 getBean("zoo") 方法來得到 Zoo.java。@Service 註解,其實做了兩件事情:

  1. 聲明 Zoo.java 是一個 bean,這點很重要,因爲 Zoo.java 是一個 bean,其他的類纔可以使用 @Autowired 將 Zoo 作爲一個成員變量自動注入

  2. Zoo.java 在 bean 中的 id 是“zoo”,即類名且首字母小寫

如果,我不想用這種形式怎麼辦,就想讓 Zoo.java 在 Spring 容器中的名字(id)叫做“Zoo”,可以的:

@Service("Zoo")
@Scope("prototype")
public class Zoo
{
    @Autowired
    private Monkey monkey;
    @Autowired
    private Tiger tiger;
    
    public String toString()
    {
        return "MonkeyName:" + monkey + "\nTigerName:" + tiger;
    }
}

這樣,就可以通過 ApplicationContext 的 getBean("Zoo") 方法來得到 Zoo.java 了。

這裏我還多加了一個 @Scope 註解,應該很好理解。因爲 Spring 默認產生的 bean 是單例的,假如我不想使用單例怎麼辦,xml 文件裏面可以在 bean 裏面配置 scope 屬性。註解也是一樣,配置 @Scope 即可,默認是“singleton”即單例,“prototype”表示原型即每次都會 new 一個新的出來。

補充細節

最後再補充一個我發現的細節。假如 animal 包下有 Tiger、domain 包下也有 Tiger,它們二者都加了 @Service 註解,那麼在 Zoo.java 中即使明確表示我要引用的是 domain 包下的 Tiger,程序運行的時候依然會報錯。

細想,其實這很好理解,兩個 Tiger 都使用 @Service 註解標註,意味着兩個 Bean 的名字都是“tiger”,那麼我在 Zoo.java 中自動裝配的是哪個 Tiger 呢?不明確,因此,Spring 容器會拋出 BeanDefinitionStoreException 異常,Caused by:

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'monkey' for bean class [com.xrq.domain.Monkey] 

參考:Spring 的註解 @Qualifier 用法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章