使用注解开发
在Spring中,基于注解的配置是基于xml文件配置的一种替代方案,官方对于使用注解开发和使用xml配置文件开发的比较结果是:“it depends.”即各有好处,使用注解开发能够简化xml文件的配置,避免xml配置文件过于复杂难以维护,且能够更加方便的实现配置。而使用xml文件配置开发的优点是不需要介入字节码数据,且配置信息统一管理。
但不管怎么说,使用注解配置进行开发已经是十分常见的了。需要注意的是,注解注入是在xml配置注入之前的,这意味着如果存在重复配置,xml配置将会覆盖注解配置。
默认情况下,Spring是不开启注解支持的,开启方法是在xml配置文件中引入context
名字空间以及加入<context:annotation-config/>
标签:
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd" > <context:annotation-config /> </beans >
基于注解的配置
@Required
@Required
注解作用在bean的setter方法上,用于说明:该属性必须在配置时得到注入。
1 2 3 4 5 6 7 8 9 10 11 public class SimpleMovieLister { private MovieFinder movieFinder; @Required public void setMovieFinder (MovieFinder movieFinder) { this .movieFinder = movieFinder; } }
需要注意的是,@Required
注解已经在Spring 5.1以后被正式弃用(deprecated)了。官方建议使用构造器自动装配注解中的required
属性用于替代(见后文)。
@Autowired
@Autowired
注解用于实现依赖的自动注入,具有很大的灵活性。
@Autowired
注解默认的注入方式为byType,即根据依赖的类型进行匹配,当容器中存在多个相同类型的bean时,通过byName的方式进行匹配。也可以结合@Qualifier
注解来指定依赖。(见后文)
用于构造器。
1 2 3 4 5 6 7 8 9 10 11 public class MovieRecommender { private final CustomerPreferenceDao customerPreferenceDao; @Autowired public MovieRecommender (CustomerPreferenceDao customerPreferenceDao) { this .customerPreferenceDao = customerPreferenceDao; } }
从Spring Framework 4.3版本之后,对于只有一个构造器的bean,无需使用@Autowired
注解指定了。对于有多个构造器且没有默认构造器的bean,需要使用@Autowired
注解至少指定其中一个构造器。
用于setter方法。
1 2 3 4 5 6 7 8 9 10 11 public class SimpleMovieLister { private MovieFinder movieFinder; @Autowired public void setMovieFinder (MovieFinder movieFinder) { this .movieFinder = movieFinder; } }
甚至可以用于具有多个参数的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MovieRecommender { private MovieCatalog movieCatalog; private CustomerPreferenceDao customerPreferenceDao; @Autowired public void prepare (MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) { this .movieCatalog = movieCatalog; this .customerPreferenceDao = customerPreferenceDao; } }
用于字段。
1 2 3 4 5 6 public class MovieRecommender { @Autowired private MovieCatalog movieCatalog; }
需要保证自动装配所需的依赖被提前声明,否则会报“no type match found”错误。
数组和集合
@Autowired
注解还可以对数组和集合实现自动装配。
1 2 3 4 5 6 7 public class MovieRecommender { @Autowired private MovieCatalog[] movieCatalogs; }
1 2 3 4 5 6 7 8 9 10 11 public class MovieRecommender { private Set<MovieCatalog> movieCatalogs; @Autowired public void setMovieCatalogs (Set<MovieCatalog> movieCatalogs) { this .movieCatalogs = movieCatalogs; } }
当Map的key类型为String时,其也能被自动装配,自动装配bean的key为相关的bean name,value为相关的bean。
1 2 3 4 5 6 7 8 9 10 11 public class MovieRecommender { private Map<String, MovieCatalog> movieCatalogs; @Autowired public void setMovieCatalogs (Map<String, MovieCatalog> movieCatalogs) { this .movieCatalogs = movieCatalogs; } }
自动装配数组和集合类型的字段时,会将容器中所有符合类型的bean push进数组或容器中,默认的顺序为容器中声明的顺序。也可以通过让相关的bean实现org.springframework.core.Ordered
接口或使用@Order
注解来决定在数组或容器中的顺序。
此外,可以设置@Autowired
注解的required
属性为false
来指定属性是否是必须的,当依赖不可用时,不会被注入。
1 2 3 4 5 6 7 8 9 10 11 12 public class SimpleMovieLister { private MovieFinder movieFinder; @Autowired(required = false) public void setMovieFinder (MovieFinder movieFinder) { this .movieFinder = movieFinder; } }
因为@Autowired
注解的required
属性默认为true
,所以只有一个构造器能够被声明为@Autowired
。
若想对多个构造器指定@Autowired
,只能将其中一个的required
属性设置为true
,其它的设置为false
。
@Autowired
注解的required
属性是被弃用的@Required
注解的较好替代。
对于Spring Framework 5.0,可以使用@Nullable
注解来表达某个依赖非必须:
1 2 3 4 5 6 7 public class SimpleMovieLister { @Autowired public void setMovieFinder (@Nullable MovieFinder movieFinder) { ... } }
@Primary
使用@Autowired
注解通过bean类型来进行自动装配,可能会出现多种匹配结果,即容器中存在多个相同类型的bean。@Primary
注解是解决冲突的其中一种方法。
@Primary
注解指定的bean作为首要装配选项,即当有多个bean符合类型要求时,将@Primary
注解指定的bean注入。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class MovieConfiguration { @Bean @Primary public MovieCatalog firstMovieCatalog () { ... } @Bean public MovieCatalog secondMovieCatalog () { ... } }
1 2 3 4 5 6 7 public class MovieRecommender { @Autowired private MovieCatalog movieCatalog; }
@Primary
注解等同于在xml配置文件中将bean标签的primary属性设置为true。
1 2 3 <bean class ="example.SimpleMovieCatalog" primary ="true" > </bean >
@Qualifier
使用@Primary
注解并不是一个很有力的解决依赖匹配冲突问题的方法。可以使用Spring提供的@Qualifier
注解来更精确的指定依赖。
1 2 3 4 5 6 7 8 public class MovieRecommender { @Autowired @Qualifier("main") private MovieCatalog movieCatalog; }
1 2 3 4 5 6 7 8 <bean id ="prim" class ="example.SimpleMovieCatalog" > <qualifier value ="main" /> </bean > <bean class ="example.SimpleMovieCatalog" > <qualifier value ="action" /> </bean >
若没有给bean配置qualifier
属性,@Qualifier
注解使用bean的name
属性或id
属性来作为备用标识,也就是说,可以不用在bean中配置qualifier
属性。即在上述例子中,可以使用@Qualifier("prim")
达到同样的效果。
此外,需要说明qualifier
属性并不是bean的唯一标识,也就是说可以多个bean指定相同的qualifier
属性,若要对容器或数组使用@Qualifier
注解,会将所有具有指定qualifier
属性的bean注入容器中:
1 2 3 4 5 6 7 8 public class MovieRecommender { @Autowired @Qualifier("main") private Set<MovieCatalog> movieCatalog; }
1 2 3 4 5 6 7 8 9 <bean class ="example.SimpleMovieCatalog" > <qualifier value ="main" /> </bean > <bean class ="example.SimpleMovieCatalog" > <qualifier value ="main" /> </bean >
从官方手册说明来看,并不推荐使用@Qualifier
注解将bean的name
属性或id
属性作为标识,如果想要通过name
属性来选择bean,更推荐使用JSR-250的@Resource
注解而不是@Autowired
注解,@Resource
注解在语义上的定义就是通过bean的唯一的name
属性作为标识来选择bean。而@Autowired
注解有不同的语义:通过bean的类型进行依赖注入,当同时使用@Qualifier
注解时,在这些选中类型的bean中再选择有特定qualifier
属性的bean。
实际上,JSR-250的@Resource
注解是优先使用byName的方式进行依赖匹配,当然也可以指定byType的方式进行匹配,这一点和@Autowired
注解恰好相反。
@Resource
Spring支持使用JSR-250中的@Resource
注解来实现依赖注入,该注解作用于字段或setter方法上。@Resource
注解优先使用byName的方式进行依赖匹配,也就是上面所说的byName的语义。
1 2 3 4 5 6 7 8 9 public class SimpleMovieLister { private MovieFinder movieFinder; @Resource(name="myMovieFinder") public void setMovieFinder (MovieFinder movieFinder) { this .movieFinder = movieFinder; } }
如果没有指定@Resource
注解中的name值,则会从注解的字段或方法上获取默认名称,对于字段而言,将会取字段名作为name值;对于setter方法而言,取其作用的属性名作为name值。
1 2 3 4 5 6 7 8 9 10 11 public class SimpleMovieLister { private MovieFinder movieFinder; @Resource public void setMovieFinder (MovieFinder movieFinder) { this .movieFinder = movieFinder; } }
当不指定name值,且在容器中找不到默认name值的依赖时,@Resource
注解将会退而通过byType的方式再去容器中寻找匹配,如果再次匹配失败,则依赖注入报错。
需要说明的是,@Resource
注解同样可以解析一些公有的类实例,如BeanFactory
,ApplicationContext
等,这些可以直接作为依赖注入。
@Value
@Value
注解常用于注入外部的一些配置,它可以作用在字段、方法以及参数上。
1 2 3 4 5 6 7 8 9 @Component public class MovieRecommender { private final String catalog; public MovieRecommender (@Value("${catalog.name}") String catalog) { this .catalog = catalog; } }
需要以下配置:
1 2 3 @Configuration @PropertySource("classpath:application.properties") public class AppConfig { }
且在application.properties
文件存在该配置:
1 catalog.name=MovieCatalog
当${catalog.name}
配置找不到时,就会将字面注入,即注入"${catalog.name}"
,可以通过配置PropertySourcesPlaceholderConfigurer
bean来控制配置不存在的情况,在Spring boot中是默认配置的。
此外,@Value
注解中还可以使用SpEL表达式,详见手册。
@PostConstruct和@PreDestroy
@PostConstruct
和@PreDestroy
用于注解bean的声明周期钩子函数。
1 2 3 4 5 6 7 8 9 10 11 12 public class CachingMovieLister { @PostConstruct public void populateMovieCache () { } @PreDestroy public void clearMovieCache () { } }
类路径扫描及管理组件
@Component及其衍生注解
@Component
注解作用于某个类或接口上,将其标注为被Spring管理的一个组件,即该类的实例将被注入Spring容器中。
@Component
注解有三个具有更具体意义的衍生注解:@Controller
,@Service
,@Repository
,用于标注类作为控制层、服务层或持久层的一个组件。请使用三个具体的注解来替代@Component
注解,这样程序具有更清晰的含义,且衍生注解将来可能会被赋予更多额外的意义。
1 2 3 4 5 6 7 8 9 10 @Service public class SimpleMovieLister { private MovieFinder movieFinder; public SimpleMovieLister (MovieFinder movieFinder) { this .movieFinder = movieFinder; } }
1 2 3 4 @Repository public class JpaMovieFinder implements MovieFinder { }
需要注意的是,想要Spring自动识别这些被注解修饰的组件注入容器,还需要开启包扫描(ComponentScan),开启包扫描可以在xml文件中使用context:component-scan
标签开启,只有指定包下的组件才被识别并注入容器中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd" > <context:component-scan base-package ="org.example" /> </beans >
若使用基于Java的容器配置(见后文)的话,可以使用@ComponentScan
注解开启:
1 2 3 4 5 @Configuration @ComponentScan(basePackages = "org.example") public class AppConfig { }
元注解和复合注解
Spring提供了很多元注解(meta-annotations),元注解是可以应用在另一个注解身上的注解。例如,上面提到的@Controller
,@Service
,@Repository
注解,它们都以@Component
注解作为元注解修饰:
1 2 3 4 5 6 7 8 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Service { }
一个注解可以使用多个元注解标注,形成一个复合注解,这样能够获得所有元注解的效果。例如Spring MVC中的@RestController
注解就是@Controller
注解和@ResponseBody
注解复合而成的。
1 2 3 4 5 6 7 8 9 10 11 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Controller @ResponseBody public @interface RestController { @AliasFor( annotation = Controller.class ) String value () default "" ; }
组件相关配置
首先,一般我们在@Configure
注解类内部使用@Bean
注解向Spring容器中注入bean(这在下文基于Java的容器配置中会详细说明),但在组件(Component)内部也可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Component public class FactoryMethodComponent { @Bean @Qualifier("public") public TestBean publicInstance () { return new TestBean("publicInstance" ); } public void doWork () { } }
以上给一个工厂方法注解了@Bean
来向容器注入,同样可以在这个方法上使用@Qualifier
,@Scope
等注解来配置,就和基于Java配置一样。
手册上说明,在@Component
注解中的@Bean
和在@Configure
注解类内部使用@Bean
向容器中注入的bean是有区别的,前者没有被CGLIB代理,而后者是被CGLIB代理的,也就是说后者的@Bean
方法调用并不是java语义上的调用,而是被代理了。
在使用@Component
注解或其三个衍生注解时,可以通过指定value属性的值来指定组件被注入容器中的名字。若没有指定,则默认名字为首字母小写的类名。
1 2 3 4 5 @Service("myMovieLister") public class SimpleMovieLister { }
一般组件默认也是最常用的scope是singleton,若需要指定其它的scope也可以通过@Scope
注解实现:
1 2 3 4 5 @Scope("prototype") @Repository public class MovieFinderImpl implements MovieFinder { }
还可以通过@Qualifier
注解来指定组件的qualifier
属性,用于标识:
1 2 3 4 5 @Component @Qualifier("Action") public class ActionMovieCatalog implements MovieCatalog { }
基于Java的容器配置
Spring支持完全使用Java代码来配置Spring容器,这意味着不再需要.xml
配置文件了,这也是Spring boot中所实现的。
@Bean和@Configuration
可以使用被@Configuration
注解标识的类来进行容器配置,对类中的方法使用@Bean
注解来向容器中注入被管理的对象实例,相当于.xml
配置文件中的<bean/>
标签。我们上面也提到了,@Bean
注解能够用在任何@Component
注解标识的类的方法中,但最常用的还是用在被@Configuration
注解标识的类中。
实际上,从源码可以看出,@Component
注解是@Configuration
注解的元注解:
1 2 3 4 5 6 7 8 9 10 11 12 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor( annotation = Component.class ) String value () default "" ; boolean proxyBeanMethods () default true ; }
通过上述注解,一个最简单的配置类如下:
1 2 3 4 5 6 7 8 @Configuration public class AppConfig { @Bean public MyService myService () { return new MyServiceImpl(); } }
它相当于以下xml
配置文件中的标签:
1 2 3 <beans > <bean id ="myService" class ="com.acme.services.MyServiceImpl" /> </beans >
需要说明的是,将@Bean
注解放在@Configuration
注解类的内部的情况下,是开启了「full模式」的,也就是说配置类这个组件是被CGLIB代理的,这样调用配置类中的标注了@Bean
的方法时,并不是Java语义上的调用,该调用会被重定向至容器的生命周期管理中,也就是说能够保证每次调用这个@Bean
方法时都会从容器中取出同一个对象。
而将@Bean
注解放在@Component
注解类或普通类中,则是不存在代理,也就是「lite模式」的,调用@Bean
方法就是Java语义上的调用。
使用AnnotationConfigApplicationContext实例化容器
类似于使用xml
配置的容器使用ClassPathXmlApplicationContext
实例化,使用Java配置的容器可以使用AnnotationConfigApplicationContext
实例化。
1 2 3 4 5 public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); }
实际上,AnnotationConfigApplicationContext
构造参数并不一定要是@Configuration
注解的类(通常是这样),使用@Component
注解的类也可以作为参数用于实例化容器。
当然也可以使用其无参构造器,然后使用register
方法用于实例化容器:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AppConfig.class, OtherConfig.class); ctx.register(AdditionalConfig.class); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); }
除此之外,甚至可以调用scan
方法来指定扫描包,包内的被扫描到的组件被注入容器:
1 2 3 4 5 6 public static void main (String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.scan("com.acme" ); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); }
@Bean注解的一些细节
可以将@Bean
注解放在返回类型为接口的方法上,返回的是其实现实例:
1 2 3 4 5 6 7 8 @Configuration public class AppConfig { @Bean public TransferService transferService () { return new TransferServiceImpl(); } }
被@Bean
标注的方法可以有任意数量的参数,这些参数指定了该bean的依赖,即容器会将相关的依赖通过参数提供给bean:
1 2 3 4 5 6 7 8 @Configuration public class AppConfig { @Bean public TransferService transferService (AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } }
这和通过构造器实现依赖注入是完全一样的。
首先,任何被@Bean
注解定义的类的生命周期钩子都是可用的,如在bean中使用的@PostConstruct
和@PreDestroy
注解,或者这个bean实现了InitializingBean
、DisposableBean
等接口,当然也可以通过@Bean
注解的相关属性设置用于指定任意生命周期钩子函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class BeanOne { public void init () { } } public class BeanTwo { public void cleanup () { } } @Configuration public class AppConfig { @Bean(initMethod = "init") public BeanOne beanOne () { return new BeanOne(); } @Bean(destroyMethod = "cleanup") public BeanTwo beanTwo () { return new BeanTwo(); } }
实际上,因为是基于Java代码配置的,完全可以不指定初始化方法是谁,而是直接在bean返回前调用:
1 2 3 4 5 6 @Bean public BeanOne beanOne () { BeanOne beanOne = new BeanOne(); beanOne.init(); return beanOne; }
其它
可以给@Bean
提供value属性,用于给bean指定名字:
1 2 3 4 5 6 7 8 9 10 @Configuration public class AppConfig { @Bean("myDataSource") public DataSource dataSource () { } }
还可以通过@Scope
指定bean作用域以及使用@Description
指定bean描述。
@Configuration注解的一些细节
关于内部依赖以及手动调用@Bean
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Configuration public class AppConfig { @Bean public ClientService clientService1 () { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientService clientService2 () { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientDao clientDao () { return new ClientDaoImpl(); } }
注意到,在一个配置类内部,一个@Bean
方法能够调用另一个@Bean
方法返回的实例作为依赖。在上述例子中,clientDao()
方法被调用两次,按照Java语义调用方法的话这将不符合该bean的singleton作用域。
实际上,这在前文也稍微解释过,被标注为@Configuration
的类将会被CGLIB代理,并注入到容器中。因此容器中的配置类并不是原始类,调用其内部的@Bean
方法时不会按照原始的Java语义执行,容器首先会检查容器中是否已经存在该bean,若存在则直接将其返回。由此保证了内部依赖的正确配置。实际上也就是上面说的full模式。
@Import注解
类似于xml
配置中使用import
标签进行多个配置文件的合并,Spring允许使用@Import
注解来引入其它配置类中的bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class ConfigA { @Bean public A a () { return new A(); } } @Configuration @Import(ConfigA.class) public class ConfigB { @Bean public B b () { return new B(); } }
这种情况下,处理依赖问题有两种方式,第一种是前面所说的@Bean
方法能够指定任意数量的参数,可以利用参数来解决依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Configuration public class ServiceConfig { @Bean public TransferService transferService (AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { @Bean public AccountRepository accountRepository (DataSource dataSource) { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { @Bean public DataSource dataSource () { } } public static void main (String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00 , "A123" , "C456" ); }
因为@Configuration
类最终还是作为一个bean被注入至容器中,故还可以通过在内部使用自动装配注解@Autowired
实现解决依赖问题,不过手册说明因为配置类的处理发生在上下文初始化的早期,尽可能不采用这种方式。还是推荐使用方法参数注入的方式。
总结
本文介绍了Spring对注解开发的支持,内容和示例基本都来源于官方手册(见参考文献)。
参考文献
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html