Spring学习笔记

不定期更新到个人网站:https://www.upheart.top/

Spring5

Null-Safety注解

从spring5开始新增了null-safety注解@NonNull@Nullable@NonNullFields@NonNullApi,来防止出现运行时的空指针异常

@NonNull

使用在字段,方法参数或方法的返回值。表示不能为空

@NonNullFields

使用在包级别,并且是该包下类的字段不能为空

当一个类中的字段使用了太多的NonNull时可以考虑使用@NonNullFields注解,使用该注解必须先定义一个名为package-info.java的文件,例如:

@NonNullApi
@NonNullFields
package org.springframework.mail;

import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

@Nullable

使用在字段,方法参数或方法的返回值。表示可以为空

当一个类的包被@NonNullFields或@NonNullApi注解,而我们想要从包级别指定的非null约束中免除某些字段,方法,返回值时可以使用@Nullable

@NonNullApi

@NonNullFields一样使用在包级别,但是区别是它作用是该包下的类的方法参数和返回值不能为空

当一个类中的方法参数和返回值使用了太多的NonNull时可以考虑使用@NonNullFields注解,使用该注解必须先定义一个名为package-info.java的文件,形式同上

Bean的创建

在容器内部,每个bean的定义可以被表示为BeanDefinition,通过BeanDefinition可以获得bean的很多信息包括:包名,bean的作用域,生命周期,bean的引用和依赖等

通过ApplicationContext的getBeanFactory()方法,能够获得DefaultListableBeanFactory的实现。实现类有两个方法可以将用户自定义的bean注册到Spring容器中。两个方法是:

  • registerSingleton(String beanName, Object singletonObject)
  • registerBeanDefinition(String beanName, BeanDefinition beanDefinition)

其中,registerSingleton通过bean名字,和bean实例在注册。registerBeanDefinition则通过bean名字和beanDefinition来注册

如果想自己构建beanDefinition比较麻烦,他需要实现的方法比较多

使用方式如下:

// create and configure beans
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

//从ApplicationContext 中获取 DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory=(DefaultListableBeanFactory)context.getBeanFactory();

BeanDefinition beanDefinition=beanFactory.getBeanDefinition("daoA");

System.out.println(beanDefinition.getBeanClassName());

    DaoC daoC=new DaoC();
 //将daoC注册到spring容器中
   beanFactory.registerSingleton("daoC", daoC);

 //也可以注册beanDefinition
// beanFactory.registerBeanDefinition();

//从Spring容器中获取到刚刚手动注册的bean
System.out.println(context.getBean("daoC"));

Bean的命名

Bean可以通过id和name属性来命名,id是全局唯一的,name可以有多个,可以通过逗号,分号或者空格来区分多个name

当然id和name不是必须的,如果你不指定bean的标志符,SPring容器会为你自动分配一个。这种没有名字的Bean一般用在内部bean或者自动装载的情况

bean命名一般采用开头小写的驼峰模式。如:accountManager, accountService, userDao, loginController等

使用也可以为bean定义别名,一般用在大型系统中,将引入的外部bean和自身的系统命名保持一致

<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>

如果你使用Java配置,那么可以使用@Bean来为Bean命名

Bean的实例化

实例化bean一般有3种方式,通过构造函数实例化,通过静态工程方法实例化,通过实例的工厂方法实例化

一般来说我们使用构造函数在Spring容器中创建bean。这个和用new创建bean并将其注入到Spring容器中在本质上是一样的

工厂方法用的比较少,如果我们需要每次生成一个新的对象时候,就可以考虑使用工厂方法了

构造函数实例化

在Spring中,bean的构造函数跟普通构造函数没有什么区别,最常见的就是无参构造函数:

<bean id="exampleBean" class="examples.ExampleBean"/>

当然也可以创建带参数的构造函数:

    
        
    

静态工厂方法

静态工厂方法通过class属性指定包含静态工厂方法的类,使用名为factory-method的属性指定工厂方法本身的名称

这个指定的方法用来返回需要的实例

    
    <bean id="clientService"
          class="com.flydean.services.ClientService"
          factory-method="createInstance"/>

ClientService的代码如下:

public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    /**
     * 工厂方法,必须是静态方法
     * @return
     */
    public static ClientService createInstance() {
        return clientService;
    }
}

大家注意,这里的createInstance方法必须是静态的

实例工厂方法

和静态工厂方法一样,实例工厂方法只不过是使用实例中的方法来创建相应的bean对象

这样在factory-bean定义工厂bean实例,在factory-method中定义需要创建bean的方法:

    
    <bean id="serviceLocator" class="com.flydean.services.DefaultServiceLocator">
    bean>

    <bean id="serviceA"
          factory-bean="serviceLocator"
          factory-method="createServiceAInstance"/>

    <bean id="serviceB"
          factory-bean="serviceLocator"
          factory-method="createServiceBInstance"/>

DefaultServiceLocator代码如下:

public class DefaultServiceLocator {

    private static ServiceA serviceA = new ServiceA();

    private static ServiceB serviceB = new ServiceB();

    public ServiceA createServiceAInstance() {
        return serviceA;
    }

    public ServiceB createServiceBInstance() {
        return serviceB;
    }
}

Bean作用域

在Spring中,有六个作用域。分别是singleton,prototype,request,session,application,websocket

除了这六个比较通用的作用域外,Spring3.0开始,添加了thread作用域,spring4.2开始,添加了transaction作用域

Singleton作用域

设计模式大家应该学过,设计模式里面有一个很经典的模式就是Singleton模式。Spring里面的Singleton作用域表明这个定义的bean在整个Spring容器中只有一个实例。任何对这个bean的请求都会返回这个唯一的实例

Singleton作用域是Spring中bean的默认作用域,所以,Singleton的bean可以如下定义:

    <bean id="beanA" class="com.flydean.beans.BeanA" scope="singleton"/>

    <bean id="beanB" class="com.flydean.beans.BeanB" />

上面两个bean都是singleton作用域

Prototype作用域

Prototype也是设计模式中一个很经典的模式。Prototype也被很多人也叫他多例模式,就是说可以创建出很多个类的实例

在Spring容器中,如果一个bean被定义为Prototype,那么,每次通过getBean()方法来获取这个bean都会返回一个新的bean实例

下面展示了如何定义一个prototype的bean:

<bean id="BeanC" class="com.flydean.beans.BeanB" scope="prototype"/>

因为prototype是多例的模式,所以Spring不负责该bean的整个生命周期,一旦bean被创建,交给client使用,Spring就不会再负责维护该bean实例

如果在Prototype bean上面配置了生命周期回调方法,那么该方法是不会起作用的。客户端需要自己释放该bean中的资源

要让Spring容器释放原型作用域bean所拥有的资源,可以使用自定义bean post-processor,用来处理bean的资源清理

某种意义上Spring的Prototype相当于java中的new方法

web 作用域

Request, Session, Application, 和WebSocket作用域仅在使用web的Spring ApplicationContext实现中,如果将这些作用域同Spring正常的IOC容器一起使用,则会报错:IllegalstateException

配置web作用域的方式和普通的应用程序稍有不同。Web程序需要运行在相应的Web容器中,通常我们需要将程序入口配置在web.xml中

如果你使用了Spring MVC的DispatcherServlet,那么不需要做额外的配置,因为DispatcherServlet已经包含了相关的状态

  • servlet 2.5 web容器中,如果是DispatcherServlet之外的的请求,那么需要注册org.springframework.web.context.request.RequestContextListener。
  • servlet 3.0+web容器中,可以使用WebApplicationInitializer接口以编程的方式来添加

下面是注册RequestContextListener的例子:

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        listener-class>
    listener>
    ...
web-app>

如果Listener不能注册,那么可以注册RequestContextFilter,如下所示:

<web-app>
    ...
    <filter>
        <filter-name>requestContextFilterfilter-name>
        <filter-class>org.springframework.web.filter.RequestContextFilterfilter-class>
    filter>
    <filter-mapping>
        <filter-name>requestContextFilterfilter-name>
        <url-pattern>/*url-pattern>
    filter-mapping>
    ...
web-app>

通过配置DispatcherServlet, RequestContextListener, 和 RequestContextFilter ,我们就可以在相应的请求服务中调用相应范围的bean

Bean的属性赋值

属性赋值:Bean的属性上使用@Value注解:必须是由容器调用构造器创建的bean这些@Value中的属性值才会被注入,自己显示写了new方法的@Value属性就不起作用了,因为自己写了new方法则实例不是由容器创建

public class Person {
 
	@Value("张三丰")
	private String name;
	@Value("#{20-2}")
	private Integer age;
	@Value("${property.address}")
	private String address;
 
	public String getName() {
		return name;
	}
 
	public void setName(String name) {
		this.name = name;
	}
 
	public Integer getAge() {
		return age;
	}
 
	public void setAge(Integer age) {
		this.age = age;
	}
 
	public String getAddress() {
		return address;
	}
 
	public void setAddress(String address) {
		this.address = address;
	}
 
}

可以通过@Value注入基本数据类型、字符串、SpEL表达式(#{})、也可获取运行环境变量(${}),即properties文件中的变量值,在之前采用配置的方式的时候,需要使用标签指明配置文件的位置,在容器启动的时候会将这些文件中的键值对存储在运行环境中,使用注解的方式则需要在容器Bean上使用@PropertySource,@PropertySource的value属性是一个数组,可以同时指定多个配置文件,可以使类路径下的(classpath:),也可以是文件路径下的(file:)

@Configuration
@PropertySource("classpath:config.properties")
public class MainConfig {
 
	@Bean
	public Person person() {
		return new Person();
	}
}

配置文件中的值也可以通过applicationContext来获取:因为容器加载时会将这些配置的键值对存放在环境变量中

public class MainTest {
 
	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
 
	@Test
	public void test1() throws Exception {
		ConfigurableEnvironment environment = context.getEnvironment();
		String property = environment.getProperty("property.address");
		System.out.println(property);
		context.close();
	}
 
}

设置Bean属性

@Scope设置组件作用域

@Configuration
public class MainConfigA {
    @Bean("person")
    @Scope("singleton")
    public Person person(){
        return new Person("张三丰",25);
    }
}

@Scope的值:

singleton:单例,默认值,每次都返回同一个对象,该对象是在容器启动的时候创建的,在使用的时候直接从容器中获取
prototype:多实例,每次都新创建一个对象,对象不会在容器启动时创建,而是在获取对象时创建,每次获取都创建一个新对象
request:一个请求范围内只创建一个对象
session:一次会话范围内只创建一个对象

@Scope注解也可以在组件的类名上标注:

@Component
@Scope("prototype")
public class Person {
 
	public Person() {
		System.out.println("Person Constructor");
	}
 
}

@Lazy注解实现组件Bean的懒加载

单实例的Bean对象默认在容器启动的时候创建,可以设置懒加载使Bean的创建时机延后至第一次获取的时候创建和初始化,懒加载仅作用于单实例(singleton)的Bean

@Configuration
@ComponentScan(value="com.bdm",useDefaultFilters = false,includeFilters = {@ComponentScan.Filter(type = FilterType.CUSTOM,classes = {BdmTypeFilter.class})})
public class MainConfig {
 
    @Bean
    @Lazy
    public Person person() {
        return new Person("lisi", 12);
    }
}

@Conditional:按照条件动态注册Bean

使用@Conditional注解需自定义Condition:实现Condition接口

public class MyCondition implements Condition {
 
	// context:判断条件使用的上下文
	// metaData:使用该注解的类的注解信息
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		// 运行时的信息以及环境变量、虚拟机的变量
		Environment environment = context.getEnvironment();
		String property = environment.getProperty("os.name");
		if(property.toLowerCase().contains("Windows"))
			return true;
		return false;
	}
 
}

容器Bean中使用@Conditional注解:使person类仅会在windows系统中才会创建

@Configuration
public class MainConfig {
 
	@Bean
	@Conditional({MyCondition.class})
	public Person person(){
		return new Person("张三",12);
	}
}

@Conditional注解可以标注在配置类上也可以标注在@Bean注解的方法上,作用的范围不同,标注在类上时影响整个容器Bean

@Conditional的值是一个Condition类型的数组,Condition是一个接口,有一个matches方法,该方法中有两个参数:

ConditionContext:通过此类型的参数可以获取运行环境、Bean工厂等信息
AnnotatedTypeMetadata:通过此类型的变量可获取到使用@Conditional注解的类的注解信息

Bean扫描

@Configuration
@ComponentScan(value = "com.bdm", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})
public class MainConfig {
 
    @Bean
    public Person person() {
        return new Person("lisi", 12);
    }
}

@ComponentScan注解的value属性指定了扫描的包,表示此包中使用了@Controller、@Service、@Repository、@Component等注解的类会被纳入Spring容器的管理;excludeFilters属性表示该包中哪些类别的类不会被纳入Spring容器管理,它的属性值是一个数组;includeFilters属性表示指定的包中哪些类别的类会被纳入到Spring容器管理,注意要同时设置useDefaultFilters的属性为false

JDK1.8之后一个容器类可以使用多次@ComponentScan注解,JDK1.8之前则可以使用@ComponentScans实现这个需求

@Configuration
@ComponentScans(value = {@ComponentScan(value = "com.bdm", useDefaultFilters = false, includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})}), @ComponentScan(value = "com.bdm", excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})})
public class MainConfig {
 
    @Bean
    public Person person() {
        return new Person("lisi", 12);
    }
}

如果通过@ComponentScan设置的包有包含关系时,同一个被纳入Spring容器的Class会创建多少个bean实例呢?答案是1个,因为通过包扫描的方式创建的bean实例的id是类名的首字母小写,spring在创建bean时会先判断该id的bean有无创建,已创建则不再创建,否则才创建;那如果除了包扫描中有该类,同时@Bean注解的方法中也有该Class的实例,这个时候会创建几个bean实例呢?这个要看@Bean注解的方法的方法名或者通过@Bean的name属性为该实例指定的id是否和类名的首字母小写后相同,相同则仅创建一个,否则创建多个,说明spring容器在创建bean时会先检查该id的bean是否已经存在,不存在则创建,否则不创建

FilterType的值:

FilterType.ANNOTATION:指定注解注解的类
FilterType.ASSIGNABLE_TYPE:指定类型的类
FilterType.ASPECTJ:使用AspectJ表达式过滤
FilterType.REGEX:使用正则表达式过滤
FilterType.CUSTOM:自定义过滤规则

自定义过滤规则

自定义规则类需实现TypeFilter接口:

public class BdmTypeFilter implements TypeFilter{
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        //注解
        AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
        //路径
        Resource resource = metadataReader.getResource();
        //类名
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        String className = classMetadata.getClassName();
        if(className.contains("action"))
            return true;
        MetadataReader metadataReader1 = metadataReaderFactory.getMetadataReader("");
        return false;
    }
}

MetadataReader:获取正被扫描的类的信息,包括该类的注解信息、路径信息、类名信息等

MetadataReaderFactory:获取MetadataReader

使用自定义过滤规则:type=FilterType.CUSTOM

Bean的后置处理器

BeanPostProcessor:是一个接口,bean的后置处理器(实际是在bean的初始化前后执行操作)

public class MyBeanPostProcessor implements BeanPostProcessor{
 
	/**
	 * bean是容器调用构造器创建的实例
	 * beanName是实例的id
	 * 在所有初始化方法之前调用
	 */
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
 
	
	/**
	 * bean是容器调用构造器创建的实例
	 * beanName是实例的id
	 * 在所有初始化方法之后调用
	 */
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
}

将MyBeanPostProcessor纳入到容器中:

@Configuration
@Import(MyBeanPostProcessor.class)
public class LifeCycleConfig {
 
	@Bean(initMethod="init0",destroyMethod="destroy0")
	public Car car(){
		return new Car();
	}
}

这样该容器中的所有bean在初始化前后都会执行该后置处理器中的逻辑,即使未定义初始化方法这些逻辑也会执行的,由此可以猜测:BeanPostProcessor可能是面向切面的

通过查看源码,整个逻辑是这样的:

在调用构造器之后,会为对象的所有属性赋值,赋值完成后会调用初始化方法,但是在调用初始化方法时会进入一个切面,完成在初始化的前后执行BeanPostProcessor的逻辑:容器会遍历所有的BeanPostProcessor并依次执行每个后置处理器的前置方法(postProcessBeforeInitialization),直至全部执行完或者其中一个该类方法返回null,然后完成Bean的初始化,初始化完成后,容器会继续遍历执行所有的BeanPostProcessor的后置方法(postProcessAfterInitialization),直至全部执行完或者其中一个该方法返回null

JDK1.8之后interface也可以有方法体了,子类也可以不实现接口中已有方法体的方法:

public interface BeanPostProcessor {
 
	@Nullable
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
 
	@Nullable
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
 
}

BeanPostProcessor在Spring底层的使用:

1.组件通过实现ApplicationContextAware接口获取IOC容器,其实是ApplicationContextAwareProcessor在起作用

@Component
public class ContextBean implements ApplicationContextAware {
 
	private ApplicationContext context;
 
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		context = applicationContext;
	}
}

怎么获取到的IOC容器呢?容器在调用Bean的构造器并给所有属性赋值之后,在执行所有初始化方法之前和之后,会执行一些BeanPostProcessor接口的子类中的逻辑,比如ApplicationContextAwareProcessor,在执行ApplicationContextAwareProcessor的postProcessBeforeInitialization()方法时,会判断Bean是否实现了ApplicationContextAware接口,然后注入相应的ApplicationContext容器给Bean使用

2.对组件的属性进行校验的BeanValidationPostProcessor

3.InitDestroyAnnotationBeanPostProcessor:扫描Bean中标注了@PostConstruct和@PreDestroy注解的方法并执行,这也就是为什么@PostConstruct标注的初始化方法在其他初始化方法之前执行

4.AutowiredAnnotationBeanPostProcessor:为bean中标注了@Autowired注解的属性赋值,这就是为什么可以在init()方法中使用@Autowired注解的属性的原因,因为在调用init方法之前会将这些属性的值注入

5.AsyncAnnotationBeanPostProcessor:使标注了@Async的方法异步执行

Bean的生命周期

Bean的生命周期:bean创建->初始化->销毁的过程,由容器来管理,容器会在bean进行到当前生命周期时调用我们自定义的初始化和销毁方法

1、通过@Bean的属性指定初始化和销毁方法:这两个方法必须是无入参的

public class Car {
 
	public Car(){
		System.out.println("构造");
	}
	
	public void init(){
		System.out.println("初始化");
	}
	
	public void destroy(){
		System.out.println("销毁");
	}
}
@Configuration
public class LifeCycleConfig {
 
	@Bean(initMethod="init",destroyMethod="destroy")
	public Car car(){
		return new Car();
	}
}

对于单实例的Bean来说:容器启动时就会调用该Bean的构造器和初始化方法,销毁方法会在容器关闭的时候调用

对于其他类型的Bean来说:容器会在获取Bean时调用完构造器之后调用其初始化方法,但是容器不会销毁多实例Bean

public class MainTest {
	
	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
	
	@Test
	public void test1(){
		context.close();
	}
 
}

注意上面的context.close()方法并不是在ApplicationContext接口中定义的,而是在其子类中定义的

这样我们就可以在Bean的初始化和销毁时执行一些逻辑,比如数据源的开启和关闭

2、让Bean实现InitializingBean和DisposableBean两个接口

public class Car implements InitializingBean,DisposableBean{
 
	public Car(){
		System.out.println("构造");
	}
 
	public void afterPropertiesSet() throws Exception {
		System.out.println("初始化");
	}
 
	public void destroy() throws Exception {
		System.out.println("销毁");
	}
	
}

这样容器在调用完构造器之后就会接着调用该Bean的afterPropertiesSet方法完成初始化,如果为单例Bean,在容器关闭时会调用destroy方法执行销毁逻辑

3、使用@PostConstruct和@PreDestroy注解

public class Car {
 
	public Car() {
 
	}
 
	@PostConstruct
	public void init() {
 
	}
 
	@PreDestroy
	public void destroy() {
 
	}
 
}

@PostConstruct:在执行完构造器并且对象的所有属性被赋值之后调用

@PreDestroy:在对象销毁之前调用,相当于销毁前的通知方法

在Bean创建和初始化时的执行顺序:构造器->@PostConstruct注解的方法->InitializingBean接口的afterPropertiesSet->@Bean的initMethod属性指定的方法

在Bean销毁时的调用顺序:@PreDestroy注解的方法->DisposableBean接口的destroy方法->@Bean的destroyMethod属性指定的方法

FactoryBean接口

FactoryBean是一个接口,子类需实现其三个方法:

getObject():获取对象,必须重写

getObjectType():获取对象类型,必须重写

isSingleton():是否单例,可以重写

public class MyPersonFactoryBean implements FactoryBean<Person> {
 
	public Person getObject() throws Exception {
		return new Person("小吴",23);
	}
 
	public Class<?> getObjectType() {
		return Person.class;
	}
 
	/**
	 * true:单例
	 * false:每次获取都重新创建新实例
	 */
	public boolean isSingleton() {
		return true;
	}
 
}

容器中注册工厂Bean:

@Configuration
public class MainConfig {
	
	@Bean
	public MyPersonFactoryBean myPersonFactoryBean(){
		return new MyPersonFactoryBean();
	}
}

测试会发现在容器中获取id为myPersonFactoryBean的Bean的实例的类型是Person:说明将工厂Bean注册后获取的对象其实是工厂Bean的getObject()方法返回的实例,那如果想获取MyPersonFactoryBean类型的实例呢?在id的前面加个&即可

Aware接口

在Spring中有个Aware接口

Spring中很多接口继承于该接口,该接口的子接口可以为Spring容器中的组件提供Spring自带的一些组件,比如ApplicationContext、BeanFactory等,只需要实现XxxAware接口即可,比如容器中的组件通过实现ApplicationContextAware接口来获取ApplicationContext对象

public interface ApplicationContextAware extends Aware {
	void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
@Component
public class ContextBean implements ApplicationContextAware{
	
	private ApplicationContext context;
 
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = applicationContext;
	}
 
}

组件Bean仅仅是实现了接口,接口中有个setXxx(Xxx xxx)的方法,组件Bean实现的接口并不会为相应的Xxx赋值,那么这些值是怎么注入到组件Bean中的呢?是Spring通过回调的方式注入的,也就是说Spring在发现组件Bean实现了XxxAware接口时,会调用其setXxx(Xxx xxx)方法将xxx(ApplicationContext、BeanFactory等)注入到相应的组件bean中

常用的Aware子接口:BeanNameAware(查找当前bean的名字)、ApplicationContextAware(获取当前IOC容器)、BeanFactoryAware(获取beanFactory)、EmbeddedValueResolverAware(Spring内置的值解析器,可以解析出占位符、SpEL表达式等)

@Component
public class ContextBean implements EmbeddedValueResolverAware {
 
	private StringValueResolver resolver;
 
	public void setEmbeddedValueResolver(StringValueResolver resolver) {
		String resolveStringValue = resolver.resolveStringValue("你好:${os.name},我今年#{2*15}岁了");
		System.out.println(resolveStringValue);//你好:Windows 10,我今年30岁了
		this.resolver = resolver;
	}
}

通过${}获取系统环境变量,#{}则可以动态计算一些值

之所以能通过实现ApplicationContextAware等接口的方式获取到ApplicationContext等Spring的底层组件,是因为一些Bean的后置处理器在起作用,比如ApplicationContextAwareProcessor,在这些bean的后置处理器中会回调容器组件bean中的setXxx(Xxx xxx)方法将相应的Spring组件注入,每个XxxAware都有对应的XxxAwareProcessor后置处理器,这些后置处理器会在创建完bean之后再为bean的属性赋值或者注入相应的组件

导入组件

@Import快速导入一个组件

@Import:值是一个Class[],容器会注册被导入的类,可用来导入第三方的类对象,或者一个包下仅有极少数的类需要被纳入IOC容器管理时,导入对象的id默认为组件的全类名

@Configuration
@Import({Student.class,Teacher.class})
public class MainConfig {
 
	@Bean
	@Conditional({MyCondition.class})
	public Person person(){
		return new Person("张三",12);
	}
	
	@Bean
	public Person person1(){
		return new Person("李四",13);
	}
}

ImportSelector:是一个接口,子类需实现其selectImports方法,该方法的入参是AnnotationMetadata(可获取当前标注@Import注解的类的所有注解信息),返回需要导入的组件的全类名数组,不能返回null,否则会报空指针异常

public class MyImportSelector implements ImportSelector{
 
	public String[] selectImports(AnnotationMetadata metadata) {
		return new String[]{"com.bdm.modle.Teacher"};
	}
 
}

使用ImportSelector:可实现有条件的导入

@Configuration
@Import({Student.class,MyImportSelector.class})
public class MainConfig {
 
	@Bean
	@Conditional({MyCondition.class})
	public Person person(){
		return new Person("张三",12);
	}
}

此时容器中导入的类除了Student之外,还包括MyImportSelector的selectImports方法中返回的数组

ImportBeanDefinitionRegistrar:也是一个接口,子类需实现其registerBeanDefinitions方法,该方法的入参是AnnotationMetadata(获取类的注解信息)和BeanDefinitionRegistry(调用其registerBeanDefinition方法注册Bean),通过实现此接口可以使容器手工注册组件

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar{
 
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		boolean flag = registry.containsBeanDefinition("person");//如果容器中含有person
		if(flag) {
			RootBeanDefinition beanDefinition = new RootBeanDefinition(Teacher.class);
			//指定bean的id
			registry.registerBeanDefinition("teacher", beanDefinition);
		}
	}
}
@Configuration
@Import({Student.class,MyImportBeanDefinitionRegistrar.class})
public class MainConfig {
 
	@Bean
	@Conditional({MyCondition.class})
	public Person person(){
		return new Person("张三",12);
	}
}

自动装配

@Autowired:

默认先按照类型去容器中找相应的组件(applicationContext.getBean(Xxx.class)),若只找到一个则进行装配,若找到多个则再按照属性的名称作为id找对应的组件(applicationContext.getBean(“beanId”));

另外可使用@Qualifier指定属性Bean的id进行装配,如果没有找到指定的属性bean,则会报NoSuchBeanDefinitionException的异常

如果想在找不到属性bean的时候不让程序出错,则可以设置@Autowired的required属性为false:@Autowired(required=false),此时则在容器中有该属性bean时装配,无则不装配

@Service
public class BookService {
	
	@Autowired
	@Qualifier("bookDao")
	private BookDao bookDao;
 
}
@Configuration
@ComponentScan({"com.bdm.service","com.bdm.dao"})
public class MyAutowireConfig {
	@Bean
	public BookDao bookDao2(){
		return new BookDao();
	}
 
}

此时该配置类容器中会有两个BookDao实例,一个是通过自动扫描创建的,一个是通过@Bean注解创建的,通过自动扫描创建的bean的id默认为类名的首字母小写,通过@Bean注解创建的id则为方法名

使用Spring提供的@Primary注解指定首选装配的Bean:在容器中有多个该类型的bean时,优先装配该bean

@Configuration
@ComponentScan({"com.bdm.service","com.bdm.dao"})
public class MyAutowireConfig {
	
	@Bean
	@Primary
	public BookDao bookDao2(){
		return new BookDao();
	}
}

此时在容器中有多个BookDao类型的bean时会优先装配这个由@Primary注解的bean,除非使用@Qualifier明确制定了要装配的bean的id

@Autowired不止可以标注在属性上,也可以标注在构造器、方法、参数上

@Component
public class Student {
 
	private Bike bike;
 
	public Bike getBike() {
		return bike;
	}
 
	@Autowired
	public void setBike(Bike bike) {
		this.bike = bike;
	}
	
}

一般标注在Setter方法上,其实可以标注在任何方法上,但是方法一定要有入参

此时,Spring容器在初始化Student对象后就会调用其标注@Autowired的方法,将属性注入进去,注入的方式依然是先根据类型查找,查到多个时再根据参数名匹配。那么问题来了,标注有@Autowired注解的方法是何时调用的呢?如果在被注入的对象创建之前调用肯定是不行的,在从容器中获取Student的实例时该实例就已经有了bike属性值,说明该属性的注入并非在第一次使用时,而是在容器创建bean之后通过后置处理的方式注入的该属性,难道是在容器中所有的Bean都创建完之后才会调用这些@Autowired注解的方法吗?是的,其实是AutowiredAnnotationBeanPostProcessor后置处理器的功劳,在创建完Bean之后会后置处理这些标注有@Autowired注解的方法、属性、入参等

标注在构造器上:注意必须是有参构造器

@Component
public class Student {
 
	private Bike bike;
 
	@Autowired
	public Student(Bike bike) {
		this.bike = bike;
	}
 
	public Bike getBike() {
		return bike;
	}
 
	public void setBike(Bike bike) {
		this.bike = bike;
	}
 
}

默认情况下,Spring在实例化bean时会调用类的无参构造器,但是在类提供了有参构造器时,如果有参构造器上有@Autowired注解,Spring则会调用该有参构造器创建实例,如果没有,则还是调无参构造器(无参构造器存在)。那么问题来了,如果类中既有无参构造,又有@Autowired注解的有参构造,或者有多个@Autowired注解的有参构造时Spring会怎么创建实例呢?在有多个@Autowired标注的构造器时会报错,在仅有一个@Autowired标注的构造器时,会调用@Autowired标注的构造器,如果只有一个有参构造器时,@Autowired可以省略

那么还有一个问题假如容器中的bean没有无参构造器,但有一个标注了@Autowired注解的有参构造器(在只有一个有参构造器时该构造器上的@Autowired可以省略,就是说隐藏带有@Autowired),此时在创建bean的时候就需要注入属性,那就不是AutowiredAnnotationBeanPostProcessor能解决的了,这时会怎么样呢?这时Spring会将这个Bean的创建时机置后,待其他的Bean都创建完之后再创建该Bean,如果两个类中都有@Autowired注解的构造器,且彼此的构造器中的入参相互引用的话则会报错,因为这两个对象都无法创建,报UnsatisfiedDependencyException异常,此时采用@Autowired(required=false)也不可以,原因也很简单,因为在创建对象时都需要检查彼此有没有创建好,而返回的都是false,所以不可以

标注在参数上:作用和标注在方法上是一样的,只不过标注在参数上可以更精确的指明哪些参数需要由Spring注入,哪些不必由Spring注入

@Component
public class Student {
 
	private Bike bike;
 
	private Car car;
 
	public Student(@Autowired Bike bike, Car car) {
		this.bike = bike;
		this.car = car;
		System.out.println("两个参数...");
	}
 
	public Bike getBike() {
		return bike;
	}
 
	public void setBike(Bike bike) {
		this.bike = bike;
	}
 
	public Car getCar() {
		return car;
	}
 
	public void setCar(Car car) {
		this.car = car;
	}
 
}

容器类中@Bean注解的方法的入参的实例也是从容器中获取的:相当于入参的位置加了@Autowired,此时入参位置的bean必须能在容器中找到,否则会报错,如果不想使其报错,可以使用required=false指明其并非必须的

在IOC容器中的bean的方法的入参中如果有Spring的底层类型,则会自动注入进去:

@Component
public class Car {
	public Car(ApplicationContext applicationContext) {
		String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
		for (String string : beanDefinitionNames) {
			System.out.println(string);
		}
	}
}

此处构造器中的ApplicationContext会由容器注入进去

@Resource(JSR250规范)和@Inject(JSR330规范)

这两个是Java规范的注解,而@Autowired是Spring自己的注解

@Resource默认是按照组件的名称(属性名作为id)装配,而不像@Autowired是默认先按照类型装配,使用@Resource时@Primary注解会失效,也可以使用@Resource注解的name属性指定组件,类似于@Autowired结合@Qualifier,但是@Resource不支持required=false,也就是说使用@Resource时必须装配成功,否则会报错:NoSuchBeanDefinitionException

@Service
public class BookService {
	
	//@Autowired(required=false)
	//@Qualifier("bookDao3")
	@Resource(name="bookDao2")
	private BookDao bookDao;
 
}

使用@Inject时需要导入依赖:javax.inject

<dependency>
	<groupId>javax.injectgroupId>
	<artifactId>javax.injectartifactId>
	<version>1version>
dependency>

@Inject的功能和@Autowired的功能一样,也支持@Primary注解,只是少了required=false的属性

@Service
public class BookService {
	
	//@Autowired(required=false)
	//@Qualifier("bookDao3")
	//@Resource(name="bookDao2")
	@Inject
	private BookDao bookDao;
 
}

这三个注解的区别在于:@Autowired是Spring的注解,脱离不了Spring的支持,@Resource和@Inject是java的规范,其他的框架也会支持,但是@Autowired的功能稍微强大一些

这些注解之所以能完成自动装配的功能,是因为Spring提供的后置处理器AutowiredAnnotationBeanPostProcessor,该后置处理器通过解析@Autowired、@Resource、@Inject等注解完成自动装配

AOP面向切面

AOP:在程序运行期间,动态的将某段代码切入到指定方法运行时的指定时机运行,其实就是动态代理

Spring提供了对AOP很好的支持,使用时需要导入spring-aspects包

切面类:类上需要注解@Aspect,切面类中的方法需要动态感知业务方法的执行情况

@Aspect 
public class AopAspect {
	
	@Before("public int com.bdm.aop.MathCalculator.div(int, int)")
	public void logStart() {
		System.out.println("计算开始");
	}
 
	@After("public int com.bdm.aop.MathCalculator.*(int, int)")
	public void logEnd() {
		System.out.println("计算结束");
	}
 
	@AfterReturning("public int com.bdm.aop.MathCalculator.*(..)")
	public void logReturn() {
		System.out.println("计算结果");
	}
 
	@AfterThrowing("public int com.bdm.aop.MathCalculator.div(..)")
	public void logException() {
		System.out.println("计算异常");
	}
}

切面类中的方法也称为通知方法:

前置通知(@Before):在目标方法运行之前运行

后置通知(@After):在目标方法运行之后运行,即使出现异常也会运行

返回通知(@AfterReturning):在目标方法正常返回之后运行

异常通知(@AfterThrowing):在目标方法运行出现异常之后运行

环绕通知(@Around):动态代理,手动推进目标方法的运行

在使用这些注解的时候必须配合切入点表达式,来指明在哪些方法执行之前、之后、返回时、异常时执行,在写切入点表达式时可以使用通配符*表示所有方法或者类,入参可以使用..表示所有参数类型

如果切入点表达式一样的话,则可以使用一个方法抽取切入点表达式,注意该方法的名称可以任意,而且方法体内无需写任何内容,只需要使用@PointCut注解并将切入点表达式标明即可:

@Aspect public class AopAspect {
	
	@Pointcut("execution(public int com.bdm.aop.MathCalculator.*(..))")
	public void pointCut(){}
	
	@Before("pointCut()")
	public void logStart() {
		System.out.println("计算开始");
	}
 
	@After("com.bdm.aop.AopAspect.pointCut()")
	public void logEnd() {
		System.out.println("计算结束");
	}
 
	@AfterReturning("pointCut()")
	public void logReturn() {
		System.out.println("计算结果");
	}
 
	@AfterThrowing("com.bdm.aop.AopAspect.pointCut()")
	public void logException() {
		System.out.println("计算异常");
	}
}

本类中使用时可直接写方法名,其他类也想使用此切入点表达式时则需要使用该方法的全类名

容器会将标注有@Aspect注解的类认为是切面类

使用Spring的切面需要开启Spring的切面自动代理,只需要在配置类中加注解@EnableAspectJAutoProxy,Spring中有很多@EnableXxx注解,用来开启一些功能

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
 
	@Bean
	public MathCalculator cal(){
		return new MathCalculator();
	}
	@Bean
	public AopAspect aspect(){
		return new AopAspect();
	}
 
}

配置bean怎么区分哪个bean是切面类呢,它会看哪个类上有@Aspect注解,另外切面方法的执行仅对Spring容器中的bean起作用,对于我们自己new出来的对象是不起作用的,原因也很简单,我们自己创建的bean并没有被spring管理,也就没有为其设置切面方法等

通过JoinPoint对象获取调用目标方法时的信息,比如方法名、参数等,使用returning指定用通知方法的哪个入参接收返回值,使用throwing指定用哪个入参接收异常,另外如果使用JoinPoint,则必须将其放在切面方法入参的第一个位置,否则会报错:

@Aspect
public class AopAspect {
	
	@Pointcut("execution(public int com.bdm.aop.MathCalculator.*(..))")
	public void pointCut(){}
	
	@Before("pointCut()")
	public void logStart(JoinPoint point) {
		String name = point.getSignature().getName();
		Object[] args = point.getArgs();
		System.out.println(name + "方法计算开始,入参:" + Arrays.asList(args));
	}
 
	@After("com.bdm.aop.AopAspect.pointCut()")
	public void logEnd(JoinPoint point) {
		String name = point.getSignature().getName();
		System.out.println(name + "方法计算结束");
	}
 
	@AfterReturning(value="pointCut()",returning="obj")
	public void logReturn(JoinPoint point,Object obj) {
		String name = point.getSignature().getName();
		System.out.println(name +"方法的计算结果:" + obj.toString());
	}
 
	@AfterThrowing(value="com.bdm.aop.AopAspect.pointCut()",throwing="exception")
	public void logException(Exception exception) {
		System.out.println("计算异常:"+ exception.getMessage());
	}
}

Quartz

Quartz是个开源的作业调度框架

Quartz核心的概念:scheduler任务调度、Job任务、JobDetail任务细节、Trigger触发器

  • Scheduler:调度器,调度器接受一组JobDetail+Trigger即可安排一个任务,其中一个JobDetail可以关联多个Trigger
  • Job:Job是任务执行的流程,是一个类
  • JobDetail:JobDetail是Job是实例,是一个对象,包含了该实例的执行计划和所需要的数据
  • Trigger:Trigger是定时器,决定任务何时执行

使用Quartz调度系统的思路就是,首先写一个具体的任务(job),配置任务的触发时间(Trigger),Scheduler很根据JobDetail+Trigger安排去执行此任务

Quartz 定时器的时间设置

时间的配置如下:0 30 16 * * ?

时间大小由小到大排列,从秒开始,顺序为 秒,分,时,天,月,年 *为任意 ?为无限制。由此上面所配置的内容就是,在每天的16点30分启动

Profile环境切换

Profile:Spring提供的可以根据当前环境(开发、测试、生产),动态的激活和切换一系列的组件的功能,可以使用@Profile注解实现,比如数据源根据环境的切换。@Profile注解用于指定组件在哪个环境下会注册到IOC容器中,若不加该注解则在所有环境下都会注册到容器中:

这里使用了三种不同的方式为属性赋值(@Value或者SpringValueResolver都可以)

@PropertySource("classpath:/config.properties")
@Configuration
public class MyProfileConfig implements EmbeddedValueResolverAware {
 
	@Value("${db.user}")
	private String user;
 
	private String driverClass;
 
	@Bean
	@Profile("dev")
	public DataSource devDataSource(@Value("${db.password}") String password) throws PropertyVetoException {
		ComboPooledDataSource dataSource = new ComboPooledDataSource();
		dataSource.setUser(user);
		dataSource.setPassword(password);
		dataSource.setDriverClass(driverClass);
		dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/dev");
		return dataSource;
	}
 
	@Bean
	@Profile("test")
	public DataSource testDataSource(@Value("${db.password}") String password) throws PropertyVetoException {
		ComboPooledDataSource dataSource = new ComboPooledDataSource();
		dataSource.setUser(user);
		dataSource.setPassword(password);
		dataSource.setDriverClass(driverClass);
		dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
		return dataSource;
	}
 
	@Bean
	@Profile("pro")
	public DataSource proDataSource(@Value("${db.password}") String password) throws PropertyVetoException {
		ComboPooledDataSource dataSource = new ComboPooledDataSource();
		dataSource.setUser(user);
		dataSource.setPassword(password);
		dataSource.setDriverClass(driverClass);
		dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/pro");
		return dataSource;
	}
 
	public void setEmbeddedValueResolver(StringValueResolver resolver) {
		this.driverClass = resolver.resolveStringValue("${db.driverClass}");
	}
}

加了环境标识(@Profile)的bean,只有在这个环境被激活时该bean才会被注册到IOC容器中,容器的默认环境是default,即在不设置容器的环境的情况下所有的@Profile(“default”)和不加@Profile注解的bean都会注册到容器中,那么怎么改变容器的环境呢?

1、使用命令行参数在Run Configuration的VM arguments中设置:-Dspring.profiles.active的值

-Dspring.profiles.active=test

此时则容器中只有testDataSource

2、使用代码的方式为容器设置启用环境:

public void test1() throws Exception {
	// 1、创建容器对象
	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
	// 2、设置容器的环境:此操作要在注册配置bean之前,可以设置多个环境
	context.getEnvironment().setActiveProfiles("dev", "test");
	// 3、注册配置bean
	context.register(MyProfileConfig.class);
	// 4、刷新容器
	context.refresh();
	context.close();
}

注意此时在创建IOC容器的时候必须使用无参构造器创建,因为有参构造器在创建对象时就会注册配置bean并刷新容器,此时即使再设置其容器环境也已经迟了,因为bean已经创建了

也可以将@Profile注解标注在配置类上,此时该配置类只有在与IOC容器环境一致时才会被注册到IOC容器中

@Profile({"test","dev"})
@Configuration
@PropertySource("classpath:/config.properties")
public class MyProfileConfig implements EmbeddedValueResolverAware {

}

你可能感兴趣的:(Spring)