使用注解的方式可以減少XML的配置,注解功能更為強大,它既能實現XML的功能,也提供了自動裝配的功能,采用了自動裝配后,程序員所需要做的決斷就少了,更加有利於對程序的開發,這就是“約定優於配置”的開發原則。
在Spring中,它提供了兩種方式來讓Spring IoC容器發現Bean。
•組件掃描:通過定義資源的方式,讓Spring IoC容器掃描對應的包,從而把Bean裝配進來。
•自動裝配:通過注解定義,使得一些依賴關系可以通過注解完成。
通過掃描和自動裝配,大部分的工程都可以用Java配置完成,而不是XML,這樣可以有效減少配置和引入大量XML,它解決了在Spring 3之前的版本需要大量的XML配置的問題,這些問題曾被許多開發者詬病。由於目前注解已經成為Spring開發的主流,在之后的章節里,筆者也會以注解的方式為主介紹Spring的開發,但是請注意只是為主,而不是全部以注解的方式去實現。因為不使用XML也存在着一定的弊端,比如系統存在多個公共的配置文件(比如多個properties和XML文件),如果寫在注解里,那么那些公共資源的配置就會比較分散了,這樣不利於統一的管理,又或者一些類來自於第三方,而不是我們系統開發的配置文件,這時利用XML的方式來完成會更加明確一些,因此目前企業所流行的方式是,以注解為主,以XML為輔,本書的介紹也是如此。
使用@Component裝配Bean
@Component(value = "role") public class Role { @Value("1") private Long id; @Value("role_name_1") private String roleName; @Value("role_note_1") private String note; }
•注解@Component代表Spring IoC會把這個類掃描生成Bean實例,而其中的value屬性代表這個類在Spring中的id,這就相當於XML方式定義的Bean的id,也可以簡寫成@Component("role"),甚至直接寫成@Component,對於不寫的,Spring IoC容器就默認類名,但是以首字母小寫的形式作為id,為其生成對象,配置到容器中。
•注解@Value代表的是值的注入,這里只是簡單注入一些值,其中id是一個long型,注入的時候Spring會為其轉化類型。
現在有了這個類,但是還不能進行測試,因為Spring IoC並不知道需要去哪里掃描對象,這個時候可以使用一個Java Config來去告訴它,如代碼清單所示。
代碼清單:Java Config類
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan public class PojoConfig { }
這個類十分簡單,幾乎沒有邏輯,但是要注意兩處加粗的代碼。
•包名和代碼清單的POJO保持一致。
•@ComponentScan代表進行掃描,默認是掃描當前包的路徑,POJO的包名和它保持一致才能掃描,否則是沒有的。
可以通過Spring定義好的Spring IoC容器的實現類——AnnotationConfigApplicationContext去生成IoC容器了。它十分簡單,如代碼清單所示。
代碼清單:使用注解生成Spring IoC容器
ApplicationContext context = new AnnotationConfigApplicationContext(PojoConfig.class); Role role = context.getBean(Role.class); System.err.println(role.getId());
@ComponentScan存在着兩個配置項:第1個是basePackages,它是由base和package兩個單詞組成的,而package還使用了復數,意味着它可以配置一個Java包的數組,Spring會根據它的配置掃描對應的包和子包,將配置好的Bean裝配進來;第2個是basePackageClasses,它由base、package和class三個單詞組成的,采用復數,意味着它可以配置多個類,Spring會根據配置的類所在的包,為包和子包進行掃描裝配對應配置的Bean。
代碼清單:RoleService接口
import com.ssm.chapter10.annotation.pojo.Role; public interface RoleService { public void printRoleInfo(Role role); }
代碼清單:RoleServiceImpl類
import org.springframework.stereotype.Component; import com.ssm.chapter10.annotation.pojo.Role; import com.ssm.chapter10.annotation.service.RoleService; @Component public class RoleServiceImpl implements RoleService { // @Override public void printRoleInfo(Role role) { System.out.println("id =" + role.getId()); System.out.println("roleName =" + role.getRoleName()); System.out.println("note =" + role.getNote()); } }
代碼清單:配置@ComponentScan制定包掃描
import org.springframework.context.annotation.ComponentScan; import com.ssm.chapter10.annotation.pojo.Role; import com.ssm.chapter10.annotation.service.impl.RoleServiceImpl; @ComponentScan(basePackageClasses = {Role.class, RoleServiceImpl.class}) // @ComponentScan(basePackages = {"com.ssm.chapter10.annotation.pojo", "com.ssm.chapter10.annotation.service"}) // @ComponentScan(basePackages = {"com.ssm.chapter10.annotation.pojo", "com.ssm.chapter10.annotation.service"} //, basePackageClasses = {Role.class, RoleServiceImpl.class}) public class ApplicationConfig { }
•這是對掃描包的定義,可以采用任意一個@ComponentScan去定義,也可以取消代碼中的注釋。
•如果采用多個@ComponentScan去定義對應的包,但是每定義一個@ComponentScan,Spring就會為所定義的類生成一個新的對象,也就是所配置的Bean將會生成多個實例,這往往不是我們的需要。
•對於已定義了basePackages和basePackageClasses的@ComponentScan,Spring會進行專門的區分,也就是說在同一個@ComponentScan中即使重復定義相同的包或者存在其子包定義,也不會造成因同一個Bean的多次掃描,而導致一次配置生成多個對象。
基於上述的幾點,建議不要采用多個@ComponentScan注解進行配置,因為一旦有重復的包和子包就會產生重復的對象,這往往不是真實的需求。對於basePackages和basePackageClasses的選擇問題,basePackages的可讀性會更好一些,因此在項目中會優先選擇使用它,但是在需要大量重構的工程中,盡量不要使用basePackages定義,因為很多時候重構修改包名需要反復地配置,而IDE不會給你任何的提示。而采用basePackageClasses,當你對包移動的時候,IDE會報錯提示,並且可以輕松處理這些錯誤。
代碼清單:測試basePackages和basePackageClasses配置
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class); Role role = context.getBean(Role.class); RoleService roleService = context.getBean(RoleService.class); roleService.printRoleInfo(role); context.close();
自動裝配——@Autowired
通過學習Spring IoC容器,我們知道Spring是先完成Bean的定義和生成,然后尋找需要注入的資源。也就是當Spring生成所有的Bean后,如果發現這個注解,它就會在Bean中查找,然后找到對應的類型,將其注入進來,這樣就完成依賴注入了。所謂自動裝配技術是一種由Spring自己發現對應的Bean,自動完成裝配工作的方式,它會應用到一個十分常用的注解@Autowired上,這個時候Spring會根據類型去尋找定義的Bean然后將其注入,這里需要留意按類型(Role)的方式。
public interface RoleService2 { public void printRoleInfo(); }
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.ssm.chapter10.annotation.pojo.Role; import com.ssm.chapter10.annotation.service.RoleService2; @Component("RoleService2") public class RoleServiceImpl2 implements RoleService2 { @Autowired private Role role = null; // @Autowired // public void setRole(Role role) { // this.role = role; // } // @Override public void printRoleInfo() { System.out.println("id =" + role.getId()); System.out.println("roleName =" + role.getRoleName()); System.out.println("note =" + role.getNote()); } /**** setter and getter ****/ public Role getRole() { return role; } public void setRole(Role role) { this.role = role; } }
這里的@Autowired注解,表示在Spring IoC定位所有的Bean后,這個字段需要按類型注入,這樣IoC容器就會尋找資源,然后將其注入。比如代碼清單10-15定義的Role和代碼清單10-23定義RoleServiceImpl2的兩個Bean,假設將其定義,那么Spring IoC容器會為它們先生成對應的實例,然后依據@Au-towired注解,按照類型找到定義的實例,將其注入。
IoC容器有時候會尋找失敗,在默認的情況下尋找失敗它就會拋出異常,也就是說默認情況下,Spring IoC容器會認為一定要找到對應的Bean來注入這個字段,有些時候這並不是一個真實的需要,比如日志,有時候我們會覺得這是可有可無的,這個時候可以通過@Autowired的配置項required來改變它,比如@Autowired(required=false)。
正如之前所談到的在默認情況下是必須注入成功的,所以這里的required的默認值為true。當把配置修改為了false時,就告訴Spring IoC容器,假如在已經定義好的Bean中找不到對應的類型,允許不注入,這樣也就沒有了異常拋出,只是這樣這個字段可能為空,讀者要自行校驗,以避免發生空指針異常。在大部分的情況下,都不需要這樣修改。
@Autowired除可以配置在屬性之外,還允許方法配置,常見的Bean的setter方法也可以使用它來完成注入
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class); RoleService2 roleService2 = context.getBean(RoleService2.class); roleService2.printRoleInfo(); context.close();
自動裝配的歧義性(@Primary和@Qualifier)
@Autowired注解,它可以完成一些自動裝配的功能,並且使用方式十分簡單,但是有時候這樣的方式並不能使用。這一切的根源來自於按類型的方式,按照Spring的建議,在大部分情況下會使用接口編程,但是定義一個接口,並不一定只有與之對應的一個實現類。換句話說,一個接口可以有多個實現類
import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import com.ssm.chapter10.annotation.pojo.Role; import com.ssm.chapter10.annotation.service.RoleService; @Component("roleService3") // @Primary//方法1 public class RoleServiceImpl3 implements RoleService { // @Override public void printRoleInfo(Role role) { System.out.print("{id =" + role.getId()); System.out.print(", roleName =" + role.getRoleName()); System.out.println(", note =" + role.getNote() + "}"); } }
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import com.ssm.chapter10.annotation.pojo.Role; import com.ssm.chapter10.annotation.service.RoleService; @Component public class RoleController { @Autowired // @Qualifier("roleService3")//方法2 private RoleService roleService = null; public void printRole(Role role) { roleService.printRoleInfo(role); } }
這里的字段roleService是一個RoleService接口類型。RoleService有兩個實現類,分別是RoleServiceImpl和RoleServiceImpl3,這個時候Spring IoC容器就會犯糊塗了,它無法判斷把哪個對象注入進來,於是就會拋出異常,這樣@Autowired注入就失敗了。
通過上面的分析,可以知道產生這樣的狀況是因為它采用的是按類型來注入對象,而在Java中接口可以有多個實現類,同樣的抽象類也可以有多個實例化的類,這樣就會造成通過類型(bytype)獲取Bean的不唯一,從而導致Spring IoC類似於按類型的方法無法獲得唯一的實例化類。
為了消除歧義性,Spring提供了兩個注解@Primary和@Qualifier,這是兩個不同的注解,其消除歧義性的理念不太一樣
1. 注解@Primary
注解@Primary代表首要的,當Spring IoC通過一個接口或者抽象類注入對象的時候,由於存在多個實現類或者具體類,就會犯糊塗,不知道采用哪個類注入為好。注解@Primary則是告訴Spring IoC容器,請優先使用該類注入。
2. 注解@Qualifier正如上面所談及的歧義性,一個重要的原因是Spring在尋找依賴注入的時候采用按類型注入引起的。除了按類型查找Bean,Spring IoC容器最底層的接口BeanFactory,也定義了按名稱查找的方法,如果采用名稱查找的方法,而不是采用按類型查找的方法,那么不就可以消除歧義性了嗎?答案是肯定的,而注解@Qualifier就是這樣的一個注解。