介绍 Spring
框架中的常见注解(NO XML
),在介绍 Spring
注解之前,简要的介绍一下 IoC
。
IoC简要介绍 IoC(Inverse of Control)
的意思是控制反转,意思就是控制权发生转换,以前我们需要某个类的对象,都是直接 new
出来的,现在有了 IoC
,对象创建的控制权交给 Spring
,如果我们需要某个对象,向 IoC
容器拿,如下
上面将 Dog
类对象放入容器的过程我们成为注入 ,容器我们一般称为 IoC
容器或者 Spring
容器。上面做的好处可以减少类与类之间的耦合性,耦合性这个词可能高大尚,我举个例子,比如 Dog
类发生了改变,高耦合性意味着如果有类依赖 Dog
类的话,那么相应的该类有可能也要发生改变,如果耦合度低的话,也就是根本察觉不到自己依赖了 Dog
类,根本就不需要改变。
我们在像容器要某个对象时,察觉不到某个类的所在(因为一般使用接口去接收从容器中拿的对象),比如
Animal animal = ioc.getObject('dog' );
假设 Dog
实现了 Animal
接口,ioc
对象表示 IoC
容器,getObject()
表示从容器中获得对象的方法,而传入的参数表示对象在容器中的 id
。如果 Dog
类发生改变(但是他在容器中的 id
是不会变的),我们获取 Dog
对象的方式还是和上面一样,不需要发生改变。假设如果是直接 new Dog()
的话,如果 Dog
的类名变了,那么所有直接依赖 Dog
类的所有类都要发生改变,这就是低耦合的好处。
注意 :要使用 Spring IoC
,记得在 pom.xml(Maven)
中导入坐标
<dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.2.0.RELEASE</version > </dependency >
@Configuration @Configuration
的定义:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface Configuration { @AliasFor( annotation = Component.class ) String value () default "" ; boolean proxyBeanMethods () default true ; }
被 @Configuration
修饰的类是 Spring
的配置类,所谓的配置类就是用来配置用的,可以配置扫描哪些包,将这些包下的类注入到 IoC
容器中,也可以配置将特定的类注入到 IoC
容器中。
被 @Configuration
修饰的类也会被注入到 IoC
容器中,@Configuration
有一个 value
值,它是用来设置该类对象在 IoC
容器中的 id
,如果不设置,那么默认是类名,不过首字母要改为小写,比如
package config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration public class SpringConfiguration {}
SpringConfiguration
是 Spring
的配置类,由于 @Configuration
没有配置id
,所以它在 IoC
容器中的 id
为 springConfiguration
,我们可以在测试类中测试,如下
import config.SpringConfiguration;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); SpringConfiguration springConfiguration = ac.getBean("springConfiguration" , SpringConfiguration.class); System.out.println(springConfiguration); } }
AnnotationConfigApplicationContext
可以看做是 IoC
容器,我们传入配置类所在的包 config
给它,它会扫描该包找到配置类,然后根据配置类为容器注入对象,在上面我们在配置类中什么都没有做,所以不会向 IoC
容器注入别的对象。
接着我们通过获得对象的方法 getBean()
获得了配置类 SpringConfiguration
的对象,该方法需要传入在 IoC
容器中的 id
,因为没有指定,默认是 springConfiguration
。
@ComponentScan @ComponentScan
注解的定义
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Repeatable(ComponentScans.class) public @interface ComponentScan { @AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class; ScopedProxyMode scopedProxy () default ScopedProxyMode.DEFAULT ; String resourcePattern () default "**
该注解的作用是扫描包或者类,将扫描到类注入到 IoC
容器中。由上面可见,该类的属性还是挺多的,这里介绍几个重要的属性。
value和basepackages 这两个属性放在一起讲是因为他们互为别名
@AliasFor("basePackages") String[] value() default {}; @AliasFor("value") String[] basePackages() default {};
它们的作用是一样的,规定要扫描哪些包,从定义看,它们的值都是字符串数组,我们在 com.xt.service
下定义 ServiceTest
类如下
package com.xt.service;public class ServiceTest { public void service () { System.out.println("service..." ); } }
Spring
的配置类如下
package config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan("com.xt.service") public class SpringConfiguration {}
我们在配置类中规定了要扫描的包为com.xt.service
,接着我们在测试类中测试
import com.xt.service.ServiceTest;import config.SpringConfiguration;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); ServiceTest serviceTest = ac.getBean("serviceTest" , ServiceTest.class); serviceTest.service(); } }
但是却发生异常,在容器中没有名为 serviceTest
的对象
这是因为即使扫描了 ServiceTest
所在的包,但并不是该包下所有的类都会被注入到容器中,而是被 @Component
注解所修饰的类才会被注入到容器中,所以我们要为 ServiceTest
加上 @Component
注解,如下
package com.xt.service;import org.springframework.stereotype.Component;@Component public class ServiceTest { public void service () { System.out.println("service..." ); } }
再次运行测试类,结果如下
注意:@Component
还有几个衍生注解,@Controller
,@Service
,@Repository
,这些注解的作用与 @Component
一毛一样,那为什么要创建这些注解,主要是他们所代表的的语义,@Controller
主要用在 Web
层的类上,@Service
主要用在 Service
层上,而 @Repository
主要用在 Dao
层上,程序员看到某类被什么注解修饰,就可以明白该类的职责是什么了。
如果 @ComponentScan
没有指定任何值,那么默认会扫描该类所在的包及其子包,如
package config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan public class SpringConfiguration {}
@ComponentScan
没有指定任何值,那么就会默认扫描 config
包下的类及其子包下的类。
basePackageClasses basePackageClasses
的定义如下:
Class<?>[] basePackageClasses() default {};
该属性的值是一个字节码数组,当设置指定字节码时,会扫描指定字节码所在包及其子包,假设有如下结构
UserServiceImpl
实现了 UserService
接口,二者内容如下
package com.xt.service;public interface UserService { void doService () ; }
package com.xt.service.impl;import com.xt.service.UserService;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { public void doService () { System.out.println("do service..." ); } }
Spring
的配置类如下
package config;import com.xt.service.UserService;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan(basePackageClasses = UserService.class) public class SpringConfiguration {}
我们配置 basePackageClasses
为 UserService.class
,所以会扫描 UserService
所在的包及其子包,所以 UserServiceImpl
会被注入到容器中,我们在测试类中测试如下
import com.xt.service.UserService;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); UserService userService = ac.getBean("userServiceImpl" , UserService.class); userService.doService(); } }
结果如下
nameGenerator 该属性的作用是设置注入到容器中的对象(我们一般称这个对象为 bean
)的 id
名称的生成规则,如下
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
在上面我们可以看到,默认使用了 BeanNameGenerator
这个类去生成 id
的名称,而生成的规则在上面介绍过,即 id
为类名,不过首字母要小写, Spring
中的有关默认 id
名称生成的部分源码如下
public static String decapitalize (String name) { if (name == null || name.length() == 0 ) { return name; } if (name.length() > 1 && Character.isUpperCase(name.charAt(1 )) && Character.isUpperCase(name.charAt(0 ))){ return name; } char chars[] = name.toCharArray(); chars[0 ] = Character.toLowerCase(chars[0 ]); return new String(chars); }
我们可以看到,如果对于第一个字母和第二个字母都为大写的这种特殊的类名是不会将首字母变为小写的。
除了可以使用默认的生成规则,我们还可以自己自定义 id
的生成规则,在 custom
包下新建 CustomBeanNameGenerator
类,该类的作用就是 id
名称的生成规则,该类需要实现 BeanNameGenerator
接口,详细如下
package custom;import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.beans.factory.support.BeanNameGenerator;import org.springframework.core.annotation.AnnotationAttributes;import org.springframework.core.type.AnnotationMetadata;import org.springframework.lang.Nullable;import org.springframework.util.Assert;import org.springframework.util.ClassUtils;import org.springframework.util.StringUtils;import java.beans.Introspector;import java.util.Map;import java.util.Set;public class CustomBeanNameGenerator implements BeanNameGenerator { public String generateBeanName (BeanDefinition beanDefinition, BeanDefinitionRegistry beanDefinitionRegistry) { String beanName = null ; if (beanDefinition instanceof AnnotatedBeanDefinition) { AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition; AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata(); Set<String> types = annotationMetadata.getAnnotationTypes(); for (String type: types) { AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(annotationMetadata.getAnnotationAttributes(type, false )); if (annotationAttributes != null && isStereotypeWithNameValue(type, annotationMetadata.getMetaAnnotationTypes(type), annotationAttributes)) { Object value = annotationAttributes.get("value" ); if (value instanceof String) { String strValue = (String) value; if (StringUtils.hasLength(strValue)) { if (beanName != null && !strValue.equals(beanName)) { throw new IllegalArgumentException("多个注解设置了value,产生了冲突" ); } else { beanName = strValue; } } } } } } return beanName != null ? beanName : buildDefaultBeanName(beanDefinition); } private boolean isStereotypeWithNameValue (String annotationType, Set<String> metaAnnotationTypes, @Nullable Map<String, Object> attributes) { boolean isStereotype = annotationType.equals("org.springframework.stereotype.Component" ) || metaAnnotationTypes.contains("org.springframework.stereotype.Component" ) || annotationType.equals("javax.annotation.ManagedBean" ) || annotationType.equals("javax.inject.Named" ); return isStereotype && attributes != null && attributes.containsKey("value" ); } private String buildDefaultBeanName (BeanDefinition beanDefinition) { String beanClassName = beanDefinition.getBeanClassName(); String shortClassName = ClassUtils.getShortName(beanClassName); return "my" + shortClassName; } }
上面的 id
生成规则为如果 @Component
及其衍生注解设置了 id
名称,则使用设置的名称,否则默认的 id
名称为 my +
类名。现在将 SpringConfiguration
的 @ComponentScan
的 nameGenerator
设置为 CustomBeanNameGenerator.class
,如下
package config;import com.xt.service.UserService;import custom.CustomBeanNameGenerator;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan(nameGenerator = CustomBeanNameGenerator.class, basePackages = "com.xt.service.impl") public class SpringConfiguration {}
UserServiceImpl
的内容如下
package com.xt.service.impl;import com.xt.service.UserService;import org.springframework.stereotype.Service;@Service public class UserServiceImpl implements UserService { public void doService () { System.out.println("do service..." ); } }
可见使用 @Service
修饰了,但是没用设置 value
值,所以默认生成的 id
是 myUserServiceImpl
,在测试类中测试如下
import com.xt.service.UserService; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); UserService userService = ac.getBean("myUserServiceImpl" , UserService.class); userService.doService(); } }
输出如下
可见我们的自定义 id
生成规则生效了,现在修改 UserServiceImpl
如下,这次设置 id
值
package com.xt.service.impl;import com.xt.service.UserService;import org.springframework.stereotype.Service;@Service("userService") public class UserServiceImpl implements UserService { public void doService () { System.out.println("do service..." ); } }
根据我们的规则,这时的 id
名为设定的值,即 userService
,修改测试类
UserService userService = ac.getBean("userService" , UserService.class);
在此修改 UserServiceImpl
,这次使用多个注解修饰,如下
package com.xt.service.impl;import com.xt.service.UserService;import org.springframework.stereotype.Component;import org.springframework.stereotype.Service;@Service("userService") @Component("userServiceImpl") public class UserServiceImpl implements UserService { public void doService () { System.out.println("do service..." ); } }
运行测试类,这时会抛出异常,如下
useDefaultFilters 在 @ComponentScan
中有一个属性 useDefaultFilters
boolean useDefaultFilters () default true ;
它的值默认为 true
,即如果被扫描的类被 @Component
,@Controller
,@Service
,@Repository
这四个注解修饰时,那么将该类对象注入到容器中,如果为 false
,那么被这四个注解修饰的类不会被添加到容器中(但是有可能通过其他注解或属性将该类添加到容器中,如 includeFilters
,@Import
)。
includeFilters includeFilters
属性的作用是允许符合过滤规则的类对象注入到容器中,它的值是一个 Filter
注解
@Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Filter { FilterType type () default FilterType.ANNOTATION ; @AliasFor("classes") Class<?>[] value() default {}; @AliasFor("value") Class<?>[] classes() default {}; String[] pattern() default {}; }
该注解有一个 type
属性,它的默认取值为 FilterType.ANNOTATION
,表示过滤的规则为注解,如果我们设置 value
属性为某注解的字节码对象,那么被该注解修饰的类对象可以注入到容器中,比如允许自定义 @MyAnno
注解所修饰的类注入到容器中
@ComponentScan(includeFilters = @ComponentScan.Filter(MyAnno.class))
在扫描的类中,如果该类被自定义注解 @MyAnno
修饰,那么该类对象会被注入到容器中。
excludeFilters 与 includeFilters
的作用相反,对于符合过滤规则的类对象不能被注入到容器中,如
@ComponentScan(excludeFilters = @ComponentScan.Filter(Service.class))
上面的注解的作用是,扫描到的类如果被 @Service
修饰,那么该类对象不能被加入到容器中。
在上面我们知道 Filter
注解中,它的 type
属性为 FilterType.ANNOTATION
,即被某注解修饰,他会(includeFilters
)/不可以(excludeFilters
)加入到容器中,其实 type
还可以有多种取值,如下
package org.springframework.context.annotation;public enum FilterType { ANNOTATION, ASSIGNABLE_TYPE, ASPECTJ, REGEX, CUSTOM; private FilterType () { } }
在这里我们在介绍 FilterType.CUSTOM
,这表示我们可以自定义过滤规则,定义过滤规则的类需要实现 TypeFilter
接口。在 custom
中新建 CustomFilter
类
package custom;import org.springframework.core.type.classreading.MetadataReader;import org.springframework.core.type.classreading.MetadataReaderFactory;import org.springframework.core.type.filter.TypeFilter;import java.io.IOException;public class CustomFilter implements TypeFilter { public boolean match (MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return true ; } }
修改配置类如下
package config;import custom.CustomFilter;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.FilterType;import org.springframework.stereotype.Service;@Configuration @ComponentScan( basePackages = "com.xt.service", includeFilters = @ComponentScan.Filter( type = FilterType.CUSTOM, classes = CustomFilter.class ) ) public class SpringConfiguration {}
因为 CustomFilter
实现的 match
始终返回 true
,所以被扫描到的类对象会被无条件的注入到容器中,我们修改 com.xt.service.impl.UserServiceImpl
如下
package com.xt.service.impl;import com.xt.service.UserService;public class UserServiceImpl implements UserService { public void doService () { System.out.println("do service..." ); } }
该类没有被任何注解修饰,就是一个普通的类,但是它的对象还是会注入到容器中,测试类如下
import com.xt.service.UserService;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); UserService userService = ac.getBean("userServiceImpl" , UserService.class); userService.doService(); } }
输出为
@Bean 对于我们自己写的类,我们可以通过 @Component
及其衍生注解,使得被扫描到时被注入到容器中,但是对于第三方的类库,由于已经被编译为了字节码,我们已经无法修改,即不能再别人的源码上加上注解,那么我们如果想将第三方类库对象注入到容器,我们该怎么办呢,使用 @Bean
可以解决这个问题。@Bean
的定义为
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Bean { @AliasFor("name") String[] value() default {}; @AliasFor("value") String[] name() default {}; @Deprecated Autowire autowire () default Autowire.NO ; boolean autowireCandidate () default true ; String initMethod () default "" ; String destroyMethod () default "(inferred) " ; }
以数据源对象 DataSource
对象为例,首先导入坐标
<dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.2.0.RELEASE</version > </dependency >
配置类如下
package config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.datasource.DriverManagerDataSource;import javax.sql.DataSource;@Configuration public class SpringConfiguration { @Bean public DataSource createDataSource () { return new DriverManagerDataSource(); } }
在扫描配置类时,会扫描配置类中被 @Bean
注解的方法,会将该方法返回的对象注入到容器中,这是默认的 id
名称为方法名 。测试类如下
import org.springframework.context.annotation.AnnotationConfigApplicationContext;import javax.sql.DataSource;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); DataSource dataSource = ac.getBean("createDataSource" , DataSource.class); System.out.println(dataSource); } }
输出为
可见容器中有 DataSource
对象了。
如果被 @Bean
注解的方法重载了的话,那么会将重载的方法返回的对象注入容器,如
package config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.datasource.DriverManagerDataSource;import javax.sql.DataSource;@Configuration public class SpringConfiguration { @Autowired private DataSource dataSource; @Bean public DataSource createDataSource () { return new DriverManagerDataSource(); } @Bean public JdbcTemplate createJdbcTemplate () { System.out.println("无参函数" ); return new JdbcTemplate(dataSource); } @Bean public JdbcTemplate createJdbcTemplate (DataSource dataSource) { System.out.println("有参函数" ); return new JdbcTemplate(dataSource); } }
有两个 createJdbcTemplate
方法,根据上面所说,重载的方法,即下面那个有参数的函数返回的对象会被注入到容器中,可以在测试类中测试如下
import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.jdbc.core.JdbcTemplate;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); JdbcTemplate jdbcTemplate = ac.getBean("createJdbcTemplate" , JdbcTemplate.class); System.out.println(jdbcTemplate); } }
name和value @AliasFor("name") String[] value() default {}; @AliasFor("value") String[] name() default {};
按照上面的定义,name
和 value
互为别名,它们的作用就是设置注入到容器对象的 id
名称。
autowireCandidate boolean autowireCandidate () default true ;
该属性的作用与 @Autowired
有关,当我们使用 @Autowired
自动注入时,如
@Autowired private DataSource dataSource;
容器会将其中的 dataSource
对象注入到成员变量中,但是如果 autowireCandidate
设置为 false
,那么该对象就不能使用 @Autowired
自动注入,如
@Autowired private DataSource dataSource;@Bean(autowireCandidate = false) public DataSource createDataSource () { return new DriverManagerDataSource(); }
这时就不能将 dataSource
对象通过 @Autowired
注解自动注入到成员变量 dataSource
中,再次运行测试代码,会抛出异常。
@Import 在实际的开发中,可能有多个配置类,比如数据库的配置类,那么主配置类就要导入数据库配置类的配置,这时就需要用到 @Import
注解。@Import
注解的定义如下
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Import { Class<?>[] value(); }
@Import
注解的作用是导入 value
属性所指明的类,将这些类对象注入到容器中,如有下面的数据库配置类
package config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.datasource.DriverManagerDataSource;import javax.sql.DataSource;public class JdbcConfiguration { @Bean("dataSource") public DataSource createDataSource () { return new DriverManagerDataSource(); } @Bean("jdbcTemplate") public JdbcTemplate createJdbcTemplate (@Autowired DataSource dataSource) { return new JdbcTemplate(dataSource); } }
我们在主配置类中导入该配置类
package config;import org.springframework.context.annotation.Import;@Configuration @Import(JdbcConfiguration.class) public class SpringConfiguration {}
我们在导入 JdbcConfiguration
时,会扫描 JdbcConfiguration
中的方法,将被 @Bean
注解的方法返回的对象注入到容器中,所以这时 DataSource
对象和 JdbcTemplate
对象会被注入到容器中,在测试类测试如下
import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.jdbc.core.JdbcTemplate;import javax.sql.DataSource;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); DataSource dataSource = ac.getBean("dataSource" , DataSource.class); JdbcTemplate jdbcTemplate = ac.getBean("jdbcTemplate" , JdbcTemplate.class); assert dataSource != null ; assert jdbcTemplate != null ; } }
运行测试类顺利运行,没有报错
JdbcConfiguration
它也会被注入容器中,这时它的 id
为它的全限定类名,而不是类名首字母小写,即config.JdbcConfiguration
ImportSelector 当我们需要动态的决定导入哪些类时,或者需要大量导入类时,我们可以为 @Import
传入自定义导入类,该类需要实现 ImportSelector
接口。该接口中有一个方法 String[] selectImports(AnnotationMetadata annotationMetadata)
,该方法返回一个字符串数组,这个数组包含的是要添加到容器中的类名。
因为我们过滤规则使用 AspectJ
表达式,所以需要导入相关坐标
<dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.8.13</version > </dependency >
现在我们完成这么一个功能,读取配置文件中的过滤规则和要扫描的包,将扫描包中符合过滤规则的类对象添加到容器中,在 resources
下新建 customImport.properties
如下
custom.expression =com.xt.service.impl.* custom.basePackage =com.xt
过滤规则使用 ASPECTJ
格式的过滤规则,扫描的包为 com.xt
,新建类 custom.CustomImport
如下
package custom;import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;import org.springframework.context.annotation.ImportSelector;import org.springframework.core.io.support.PropertiesLoaderSupport;import org.springframework.core.io.support.PropertiesLoaderUtils;import org.springframework.core.type.AnnotationMetadata;import org.springframework.core.type.filter.AspectJTypeFilter;import org.springframework.core.type.filter.TypeFilter;import java.util.HashSet;import java.util.Properties;import java.util.Set;public class CustomImport implements ImportSelector { private String expression; private String basePackage; public CustomImport () { try { Properties properties = PropertiesLoaderUtils.loadAllProperties("customImport.properties" ); expression = properties.getProperty("custom.expression" ); basePackage = properties.getProperty("custom.basePackage" ); } catch (Exception e) { e.printStackTrace(); } } public String[] selectImports(AnnotationMetadata annotationMetadata) { ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false ); TypeFilter typeFilter = new AspectJTypeFilter(expression, CustomImport.class.getClassLoader()); scanner.addIncludeFilter(typeFilter); Set<String> classes = new HashSet<String>(); scanner.findCandidateComponents(basePackage).forEach(beanDefinition -> classes.add(beanDefinition.getBeanClassName())); return classes.toArray(new String[classes.size()]); } }
根据上面的过滤规则,只有 UserServiceImpl
会被添加到容器中(除配置类),在测试类中测试一番
import config.SpringConfiguration;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class); String[] names = ac.getBeanDefinitionNames(); for (String name: names) { System.out.println(name); } } }
测试类是获得容器所有对象的 id
,并打印出来,如下
除了前面五个做初始化工作的对象以及配置类对象,只有 UserServiceImpl
被添加到容器中,这次我们修改过滤规则为
custom.expression=com.xt..* custom.basePackage=com.xt
Aspectj
表达式为将 com.xt
包及其子孙包下的所有类都添加到容器中,再次运算测试类,输出为
这时 com.xt.utils.Logger
对象也被添加到容器中了。
注意:上面的 UserServiceImpl
和 Logger
都没有使用任何注解进行修饰,就是一个普通的 Java
类。
ImportBeanDefinitionRegistrar ImportBeanDefinitionRegistrar
的功能同 ImportSelector
,不过二者的返回值不同,ImportSelector
返回一个要添加到容器中类名名称组成的数组,而 ImportBeanDefinitionRegistrar
什么都不返回,说明在 ImportBeanDefinitionRegistrar
内部已经将扫描包下符合规则的类添加到容器中去了。现在我们要实现与上面相同的功能,新建 custom.CustomImportBeanDefinitionRegistrar
,如下
package custom;import org.springframework.beans.factory.support.BeanDefinitionRegistry;import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;import org.springframework.core.io.support.PropertiesLoaderUtils;import org.springframework.core.type.AnnotationMetadata;import org.springframework.core.type.filter.AspectJTypeFilter;import org.springframework.core.type.filter.TypeFilter;import java.util.Properties;public class CustomImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { private String expression; private String basePackage; public CustomImportBeanDefinitionRegistrar () { try { Properties properties = PropertiesLoaderUtils.loadAllProperties("customImport.properties" ); expression = properties.getProperty("custom.expression" ); basePackage = properties.getProperty("custom.basePackage" ); } catch (Exception e) { e.printStackTrace(); } } @Override public void registerBeanDefinitions (AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false ); TypeFilter typeFilter = new AspectJTypeFilter(expression, CustomImportBeanDefinitionRegistrar.class.getClassLoader()); scanner.addIncludeFilter(typeFilter); scanner.scan(basePackage); }
该类的大部分工作与 CustomImport
是一样的,就是最后使用了 ClassPathBeanDefinitionScanner
对象直接扫描包,将符合过滤规则的类对象注入到容器中。配置文件和配置类如下
custom.expression =com.xt..* custom.basePackage =com.xt
package config;import custom.CustomImportBeanDefinitionRegistrar;import org.springframework.context.annotation.Import;@Import(CustomImportBeanDefinitionRegistrar.class) public class SpringConfiguration {}
测试类与上面相同,运行测试类,结果如下
与 ImportSelector
实现的效果相同,不过有一点不同的是,ImportSelector
的 id
生成规则为全限定类名 ,而 ImportBeanDefinitionRegistrar
的 id
为类名首字母小写 。
@PropertySource @PropertySource
注解的作用是用来读取资源文件,该注解的定义如下
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(PropertySources.class) public @interface PropertySource { String name () default "" ; String[] value(); boolean ignoreResourceNotFound () default false ; String encoding () default "" ; Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class; }
他不仅可以读取 properties
文件,还可以读取 xml
文件,甚至可以通过自定义 yml
文件解析器读取 yml
文件。
主配置类如下
package config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import org.springframework.context.annotation.PropertySource;@Configuration @PropertySource("jdbc.properties") @Import({JdbcConfig.class}) public class SpringConfiguration {}
在主配置类中读取了配置文件 jdbc.properties
,配置文件和 JDBC
配置类 JdbcConfig
如下所示
jdbc.driver =com.mysql.jdbc.Driver jdbc.url =jdbc:mysql://localhost:3306/sb jdbc.username =root jdbc.password =root
package config;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.jdbc.datasource.DriverManagerDataSource;import javax.sql.DataSource;public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean("dataSource") public DataSource createDataSource () { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource; } @Bean("jdbcTemplate") public JdbcTemplate createJdbcTemplate (@Autowired DataSource dataSource) { return new JdbcTemplate(dataSource); } }
我们将通过 PropertySource
读取到的资源,通过 @Value
注解,以 Spring EL
表达式的形式注入到了成员变量中。同时也可以读取 xml
文件,我们新建 jdbc.xml
, 内容如下
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd" > <properties> <entry key="jdbc.driver" >com.mysql.jdbc.Driver</entry> <entry key="jdbc.url" >jdbc:mysql: <entry key="jdbc.username" >root</entry> <entry key="jdbc.password" >root</entry> </properties>
修改 @PropertySource
的值
@PropertySource("jdbc.xml")
由上面可以看出,xml
文件相对于 properties
有更加明显的层级关系,结构比较清楚,但是这种优点的代价就是冗余性性很高,为了表达真正有用的信息,加入很多无用的内容。为了整合 properties
和 xml
的优点,人们提出了一个新的格式的文件 YAML
(文件后缀名为.yml
),它不仅书写简单,并且可以表达层级关系,新建 jdbc.yml
如下
jdbc: driver: com.mysql.jdbc.driver url: jdbc:mysql://localhost:3306/sb username: root password: root
但是 @PropertySource
默认只支持 propeties
和 xml
文件的读取,如果要支持 yml
文件的读取,就需要自己定义解析类,我们可以借助第三方的类库来解析 yml
文件,在 pom.xml
中导入解析 yml
类库的坐标
<dependency > <groupId > org.yaml</groupId > <artifactId > snakeyaml</artifactId > <version > 1.23</version > </dependency >
然后在配置类中修改如下
@PropertySource(value = "jdbc.yml", factory = CustomYAMLPropertySourceFactory.class)
新建 custom.CustomYAMLPropertySourceFactory
实现 PropertySourceFactory
接口,如下
package custom;import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;import org.springframework.core.env.PropertiesPropertySource;import org.springframework.core.env.PropertySource;import org.springframework.core.io.support.EncodedResource;import org.springframework.core.io.support.PropertySourceFactory;import java.io.IOException;import java.util.Properties;public class CustomYAMLPropertySourceFactory implements PropertySourceFactory { public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource) throws IOException { YamlPropertiesFactoryBean factoryBean = new YamlPropertiesFactoryBean(); factoryBean.setResources(encodedResource.getResource()); Properties properties = factoryBean.getObject(); return name != null ? new PropertiesPropertySource(name, properties) : new PropertiesPropertySource(encodedResource.getResource().getFilename() ,properties); } }
@DependsOn 有的时候一个 Bean
对象需要在另一个 Bean
对象注入到容器之后才能注入到容器,比如 One
类对 Two
类有依赖关系,在逻辑上需要先注入 Two
对象,One, Two
的定义如下
package example;import org.springframework.stereotype.Component;@Component public class One { public One () { System.out.println("one被创建了" ); } }
package example;import org.springframework.stereotype.Component;@Component public class Two { public Two () { System.out.println("Two被创建了" ); } }
主配置类如下
package config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan("example") public class SpringConfiguration {}
测试类如下
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); } }
运行输出为
One
先于 Two
先注入容器中,如果需要 One
在 Two
后,可以加上 @DependsOn
,如下
package example;import org.springframework.context.annotation.DependsOn;import org.springframework.stereotype.Component;@Component @DependsOn("two") public class One { public One () { System.out.println("one被创建了" ); } }
再次运行测试类
这时 Two
类对象先被创建。
@Lazy @Lazy
是用来延迟加载时机的,一般我们在初始化容器时,就会将扫描到的类对象注入到容器中,如果对于一个大型的项目,可能会有成千上万个类,如果在一开始就将全部的对象注入到容器,会大大的延缓项目的启动时间,所以为了提高效率,我们可以在需要该类对象时才把对象注入到容器中,还是在上例中,新建 Three
package example;import org.springframework.context.annotation.Lazy;import org.springframework.stereotype.Component;@Component public class Three { public Three () { System.out.println("three被创建了" ); } }
运行测试类
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); } }
输出为
我们只是在测试类中初始化了容器,但是 Three
类对象已经被注入到容器中了。这时我们对 Three
使用 @Lazy
注解,再次运行测试类
这时 Three
类对象并没有被创建,只有我们第一次向容器获取所需对象时,才会被注入到容器中,修改测试类如下
import example.Three;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); System.out.println("===========================" ); Three three = ac.getBean("three" , Three.class); } }
结果为
注意: @Lazy
注解对于范围为单例的类有效。
@Conditional @Conditional
注解是用来设定注入条件的,当扫描到某类或者方法时,是否将该类对象或返回对象注入到容器,根据 @Conditional
指定的条件决定。@Conditional
注解的定义如下
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { Class<? extends Condition>[] value(); }
该注解只有一个 value
属性,该属性的值是一个继承了 Condition
接口的字节码对象,Condition
接口有一个 match
方法,该方法返回一个布尔值,当返回 true
时,可以将对象注入到容器中,否则不行。
现在我们有两个数据源,一个是 Windows
下的数据源,一个是 Linux
下的数据源,现在我们的任务根据操作系统来决定使用哪个数据源注入到容器中,JdbcConfig
类定义如下
package config;import condition.LinuxCondition;import condition.WindowsCondition;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Conditional;import org.springframework.jdbc.datasource.DriverManagerDataSource;import javax.sql.DataSource;public class JdbcConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean("dataSource") @Conditional(WindowsCondition.class) public DataSource createWindowsDataSource () { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); System.out.println("Windows Env" ); return dataSource; } @Bean("dataSource") @Conditional(LinuxCondition.class) public DataSource createLinuxDataSource () { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(driver); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); System.out.println("Linux Env" ); return dataSource; } }
在上面我们对 Windows
和 Linux
的数据源都使用了 Conditional
注解,分别使用 WindowsCondition
和 LinuxCondition
类来决定是否注入到容器中,二类的内容如下
package condition;import org.springframework.context.annotation.Condition;import org.springframework.context.annotation.ConditionContext;import org.springframework.core.env.Environment;import org.springframework.core.type.AnnotatedTypeMetadata;public class WindowsCondition implements Condition { public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { Environment environment = context.getEnvironment(); String osName = environment.getProperty("os.name" ); System.out.println(osName); if (osName.contains("Windows" )) { return true ; } return false ; } }
package condition;import org.springframework.context.annotation.Condition;import org.springframework.context.annotation.ConditionContext;import org.springframework.core.env.Environment;import org.springframework.core.type.AnnotatedTypeMetadata;public class LinuxCondition implements Condition { public boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata) { Environment environment = context.getEnvironment(); String osName = environment.getProperty("os.name" ); System.out.println(osName); if (osName.contains("Linux" )) { return true ; } return false ; } }
配置类和测试类如下所示
package config;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Import;import org.springframework.context.annotation.PropertySource;@Configuration @PropertySource("classpath:jdbc.properties") @Import(JdbcConfig.class) public class SpringConfiguration {}
import org.springframework.context.annotation.AnnotationConfigApplicationContext;import javax.sql.DataSource;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); DataSource dataSource = ac.getBean("dataSource" , DataSource.class); } }
运行测试类,输出为
@Profile 有的时候我们在不同的环境下注入到容器中的类是不同的,比如开发环境、测试环境和生产环境下三者注入的类是不同的,要实现这样的效果,我们可以使用上面提及的 @Conditional
注解,但是考虑到上述问题比较常见,所以 Spring
为我们提供了 @Profile
注解来实现这样的功能, @Profile
的定义如下
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(ProfileCondition.class) public @interface Profile { String[] value(); }
可以观察到 @Profile
的底层也是使用了 @Conditional
注解,@Profile
注解只有一个属性值 value
,它是用来定义环境名称的,比如我们一般使用 dev
代表开发环境,test
代表测试环境,prod
代表生产环境。
在配置好类属于哪个环境后,可以通过容器对象中环境对象中的setActiveProfiles()
方法来激活对应的环境,这样只有对应环境的类会被注入到容器中,我们新建三个测试类
package example;import org.springframework.context.annotation.Profile;import org.springframework.stereotype.Component;@Profile("dev") @Component public class Dev { public Dev () { System.out.println("开发环境" ); } }
package example;import org.springframework.context.annotation.Profile;import org.springframework.stereotype.Component;@Profile("test") @Component public class Test { public Test () { System.out.println("测试环境" ); } }
package example;import org.springframework.context.annotation.Profile;import org.springframework.stereotype.Component;@Profile("prod") @Component public class Prod { public Prod () { System.out.println("生产环境" ); } }
配置类如下
package config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan("example") public class SpringConfiguration {}
测试类如下
import config.SpringConfiguration;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(); ac.getEnvironment().setActiveProfiles("dev" ); ac.register(SpringConfiguration.class); ac.refresh(); } }
输出为
自动注入 我们一般在 Service
层中会用到 Dao
层中的对象,所以在 Service
中一般会有一个 Dao
层的成员变量,这个对象一般是在容器中的,所以 Service
层需要向容器索取对象,我们可以使用注解使得容器中的对象自动注入到对应的成员变量中,下面就介绍几个常用的自动注入注解。
@Autowired和@Qualifier @Autowired
注解首先会根据成员变量的类型去容器中找对应类型的注解,如果有多个相同类型的对象,那么会使用成员变量的名称作为 id
去容器中寻找对应的对象。
JdbcConfig
类如下
package config;import org.springframework.context.annotation.Bean;import org.springframework.jdbc.datasource.DriverManagerDataSource;import javax.sql.DataSource;public class JdbcConfig { @Bean("dataSource1") public DataSource createDataSource1 () { return new DriverManagerDataSource(); } @Bean("dataSource2") public DataSource createDataSource2 () { return new DriverManagerDataSource(); } }
我们向容器中注入了两个 DataSource
对象,这时我们在 TestAutowired
中使用 @AutoWired
注解,如下
package test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import javax.sql.DataSource;@Component public class TestAutowired { @Autowired DataSource dataSource; }
由于在容器中有两个 DataSource
类型的对象,所以 @Autowired
无法根据类型自动注入,所以 @Autowired
会根据变量名 dataSource
作为 id
去容器中寻找对象注入,但是在容器中并没有 id
为 dataSource
的对象,所以不能够成功注入。
为了解决上面 @Autowired
注解存在的问题,我们可以通过 @Qualifier
注解指定自动注入的 id
名称,如
@Autowired @Qualifier("dataSource1") DataSource dataSource;
注意:
@Qualifier
注解不能单独使用,必须配合 @Autowired
注解或后面提到的 @Inject
注解使用
@Autowired
有一个属性为 required
,该属性是规定是否要求一定要注入成功,默认为 true
,即必须注入成功,否则抛出异常。
@Inject和@Named 使用 @Inject
和 @Named
需要导入对象的坐标
<dependency > <groupId > javax.inject</groupId > <artifactId > javax.inject</artifactId > <version > 1</version > </dependency >
@Inject
注解是 JSR330
规范规定的注解,它的作用也是实现自动注入注解,它的规则是根据类型自动注入,如果有多个相同类型的对象,那么会保错,与 @Autowired
注解不同,并不会以变量名作为 id
继续寻找。
@Named
注解也是 JSR330
规范规定的注解,它可以和 @Inject
注解配合使用,它是用来设置在容器中的 id
名称的,与 @Qualifier
注解一样,@Named
注解不能单独使用,必须配合 @Autowired
注解或 @Inject
使用
@Inject @Named("dataSource1") DataSource dataSource;
@Resource @Resource
注解是 JSR250
规范规定的注解,它有一个 name
属性,该属性是用来指定自动注入对象在容器中的 id
,同 @Named
和 @Qualifier
不同,他可以单独使用,如
@Resource(name = "dataSource1") DataSource dataSource;
@Primary 当我们仅仅使用 Autowired
时,如果容器中有多个相同类型的对象且容器没有与成员变量名相同的对象,那么注入是会失败的,如
package config;import org.springframework.context.annotation.Bean;import org.springframework.jdbc.datasource.DriverManagerDataSource;import javax.sql.DataSource;public class JdbcConfig { @Bean public DataSource createDataSource1 () { return new DriverManagerDataSource(); } @Bean public DataSource createDataSource2 () { return new DriverManagerDataSource(); } }
@Autowired DataSource dataSource;
这时自动注入会失败,这时我们可以使用 @Primary
来表明那个对象时主要的,当有多个类型相同的对象时,优先注入该对象,如
@Bean @Primary public DataSource createDataSource1 () { return new DriverManagerDataSource(); }
这时使用 @Autowired
注解,会默认注入这个 DataSource
对象。
@PostConstruct和@PreDestroy 在创建 Bean
后,可能需要做一些初始化的工作,这时我们可以使用 @PostConstruct
注解,在对象被销毁前,可能需要做一些资源的回收工作,这时我们可以使用 @PreDestroy
,这两个注解只能放在方法上,新建一个 utils.Logger
类如下
package utils;import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;@Component public class Logger { public Logger () { System.out.println("对象创建" ); } @PostConstruct public void init () { System.out.println("初始化工作..." ); } @PreDestroy public void destroy () { System.out.println("资源回收工作..." ); } }
我们使用 @PostContruct
注解了 init
方法和使用 PreDestroy
注解了 destroy
方法,这意味着当创建Logger
对象后会执行 init
方法进行初始化,在 Logger
对象销毁前,会执行 destroy
执行资源回收工作。现进行验证,配置类如下
package config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration @ComponentScan("utils") public class SpringConfiguration {}
测试类如下
import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringTest { public static void main (String[] args) { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("config" ); ac.close(); } }
结果如下
参考资料