简介
spring:一个轻量级开源的框架,它的核心功能是对于bean的生命周期进行管理,实现各个组件之间的解耦,bean就是一个类的实例。框架是软件开发中的一套解决问题的方法,不同的框架解决不同的问题,使用框架的好处是框架封装了细节,使开发者可以更简单的实现功能。
spring的核心功能:
- 容器:负责管理bean的生命周期,通过它来实现程序中各个类的解耦。spring通过配置文件或注解来配置容器,然后用户在代码中通过容器来获取bean
- AOP:提供了对于面向切面编程的支持,面向切面编程是指,把各个模块之间公共的部分提取出来,统一管理
spring的核心概念:
- 控制反转:IOC,inversion of control,对象的创建由容器来完成,而不是由用户来完成。控制反转是从用户的角度来讲
- 依赖注入:DI,dependency injection,如果一个实例依赖另一个实例,代码上讲就是一个类中某个字段的数据类型是另一个类,容器会主动在创建实例的时候,将被依赖的实例注入进来,称为依赖注入,原始的方式是在类中通过new的方式来创建依赖的实例。依赖注入是从spring容器的角度来讲
控制反转和依赖注入,它们可以实现各个模块之间的解耦,各个模块之间不再彼此依赖,而是统一依赖spring容器
spring的特点:
- 轻量:从大小和开销两方面而言spring都是轻量的
- 非侵入式的:spring应用中的实例不依赖于spring中的特定类
- 声明式事务的支持:只需要通过配置就可以完成对事务的管理,而无需手动编程
- 方便集成各种优秀框架:spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架的直接支持
入门案例:第一个spring程序
第一步:构建普通maven项目,添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
第二步:编写spring项目的配置文件:applicationContext.xml,这是默认的叫法,也可以叫做beans.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>
第三步:编写一个类Person,有属性有方法,用作测试
第四步:在beans.xml文件中配置Person的实例:<bean id="person" class="org.wyj.beans.Person" />
第五步:编写测试类
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext
= new ClassPathXmlApplicationContext("applicationContext.xml");
System.out.println("applicationContext = " + applicationContext);
applicationContext.close();
}
代码讲解:这些代码用于创建spring容器并且从spring容器中获取Person类的实例,用户在需要一个实例的时候,不是直接new,而是从spring容器中获取,这就是控制反转。
spring容器
spring容器是spring的核心功能,它负责创建和管理bean。
bean
bean就是一个类的实例,spring中把一个类的实例称作bean,spring容器的核心功能,就是对bean进行管理。
入门案例中演示了通过配置文件来配置一个bean,然后通过spring容器来获取这个bean,这里学习bean的更多特性。
作用域
作用域决定了bean的生命周期,不同的作用域,有不同的生命周期。
bean有哪些作用域:
- singleton:默认作用域,单例bean,采用单例模式的bean,和spring容器的生命周期相同
- prototype:原型模式,用户每从spring容器中获取一次bean,就创建一个bean的实例,spring容器不负责管理它的生命周期
- 只可以在web开发中使用的作用域:
- request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
- session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。
- global-session:全局作用域,所有会话共享一个实例
配置案例:
<!--通过scope属性来设置bean的作用域,singleton是默认作用域-->
<bean id="person" class="org.wyj.beans.Person" scope="singleton" />
创建时机和生命周期
singleton作用域的bean:
- 创建时机:bean会在spring容器被创建时创建
- 生命周期:singleton作用域下的bean,它的生命周期和容器的生命周期是一致的,随着容器的创建而创建,随着容器的销毁而销毁
prototype作用域的Bean:
- 创建时机:bean会在用户获取bean时被创建,在用户不使用bean时被销毁
- 生命周期:对于prototype作用域的Bean,spring只负责创建,当容器创建了Bean的实例后,bean的实例就交给客户端代码管理,spring容器将不再跟踪其生命周期。prototype作用域的bean不会被缓存到spring容器中
创建方式
创建一个bean,就是创建一个类的实例,bean的创建又称bean的实例化
构造器实例化
最普通的、也是使用最多的方式,普通的配置就是使用构造器来实例化bean的
配置方式:<bean id="id名称" class="类的全限定名" />
bean工厂 使用工厂模式来实例化bean
由用户提供的工厂类来创建bean,一个工厂类只能实例化一种类型的bean,如果bean的实例化过程比较复杂,可以使用这种方式。
bean工厂的使用比较少,但是有些应用场景下需要这种方式,比如JDBC,由于不清楚具体使用哪个实例,所以需要由用户提供的工厂方法来决定。
静态工厂实例化
提供一个工厂类,类中用来实例化bean的方法是静态的。
这种方式的配置:
<!--配置用于实例化的方法即可-->
<bean id="person5" class="org.wyj.factory.StaticFactory" factory-method="getPerson5"/>
工厂类:
public class StaticFactory {
public static Person5 getPerson5() {
// 这里可以执行其它业务逻辑,决定具体要实例化哪个类
return new Person5();
}
}
实例工厂实例化
提供一个工厂类,类中用来实例化bean的方法是实例方法,
这种方式的配置:
<!--实例化工厂类-->
<bean id="dynamicFactory" class="org.wyj.factory.DynamicFactory" lazy-init="true"/>
<!--配置工厂类中用于实例化bean的方法-->
<bean id="person6" factory-bean="dynamicFactory" factory-method="getPerson6" lazy-init="true"/>
工厂类:
public class DynamicFactory {
public Person6 getPerson6() {
return new Person6();
}
}
依赖注入
bean的依赖:一个实例依赖另一个实例,代码层面就是一个类中某个成员变量的数据类型是另一个类
依赖注入:又称为bean的装配,spring容器会在创建bean的时候,主动将它依赖的bean注入进来,称为依赖注入,原始的方式是在类中通过new的方式来创建依赖的实例。依赖注入是从spring容器的角度来讲
依赖注入的具体方式
setter注入
通过property标签,指定属性名,spring容器会调用属性的setter方法为该属性赋值
配置案例:
<bean id="userService" class="org.wyj.service.impl.UserServiceImpl" lazy-init="true">
<!--setter注入-->
<property name="userDao" ref="userDao"/>
</bean>
构造器注入
通过constructor-arg标签,指定要向构造方法中传递哪些参数,spring容器会寻找合适的构造方法
配置案例:
<bean id="userService2" class="org.wyj.service.impl.UserServiceImpl" lazy-init="true">
<!--用户在这里指定构造器需要的参数,spring会根据参数寻找合适的构造器-->
<constructor-arg name="userDao" ref="userDao"/>
</bean>
依赖注入的数据类型
可以向bean中注入普通数据、对象、集合等。
配置案例:
<bean id="person3" class="org.wyj.beans.Person3" lazy-init="true">
<!--1、注入普通数据,使用value属性-->
<constructor-arg name="name" value="ls"/>
<constructor-arg name="age" value="19"/>
</bean>
<bean id="userService2" class="org.wyj.service.impl.UserServiceImpl" lazy-init="true">
<!--2、注入对象,也就是其它bean,使用ref属性,引用配置文件中的其它bean-->
<constructor-arg name="userDao" ref="userDao"/>
</bean>
<!-- 3、注入集合类型的数据 -->
<bean id="student" class="org.wyj.beans.Student" lazy-init="true">
<property name="name" value="a"/>
<property name="age" value="18"/>
<property name="friendsName">
<!--3.1、注入list-->
<list>
<value>zs</value>
<value>ls</value>
<value>ww</value>
</list>
</property>
<property name="friendsAge">
<!--3.2、注入map-->
<map>
<entry key="zs" value="11"/>
<entry key="ls" value="12"/>
<entry key="ww" value="13"/>
</map>
</property>
<property name="properties">
<!--3.1、注入properties-->
<props>
<prop key="zs">man</prop>
<prop key="ls">woman</prop>
<prop key="ww">man</prop>
</props>
</property>
</bean>
总结:
-
普通数据类型:注入普通类型的数据,例如字符串、数字,使用value属性。
-
引用数据类型:注入对象,使用ref属性,表示引用其它bean。
-
集合数据类型:注入集合,如list、map、array。
- 注入list集合:在property标签内使用list标签,如果集合是普通数据类型,在list标签内使用value标签,如果集合是引用数据类型,在list标签中使用ref标签
- 注入map集合:在property标签内使用map标签
- 注入Properties集合:在property标签内使用props标签
懒加载
默认情况下,singleton作用域的bean,会在创建spring容器时被创建,但是,如果用户觉得这么做比较耗费内存,也可以配置在获取bean时才创建bean。
配置方式:
<!--lazy-init属性,值为true,表示开启懒加载,默认值是false-->
<bean id="person" class="org.wyj.beans.Person" scope="singleton" lazy-init="true"/>
初始化方法和销毁方法
相当于两个扩展点,指定bean在创建时和销毁时要做的事情,例如,销毁bean时,可以在销毁方法中执行释放资源的逻辑。
配置案例:
<bean id="person4" class="org.wyj.beans.Person4"
init-method="init"
destroy-method="destroy"/>
代码:
public class Person4 {
private String name;
private int age;
// 在类中定义初始化方法和销毁方法
public void init() {
}
public void destroy() {
}
}
spring容器的分类
Spring 提供了两种容器,分别为BeanFactory和ApplicationContext
- BeanFactory:是一个管理bean的工厂,它主要负责初始化各种bean,并调用它们的生命周期方法,是基础类型的spring容器,
- ApplicationContext: BeanFactory间接的子接口,也被称为应用上下文。它不仅提供了BeanFactory的所有功能,还添加了对资源访问、事件传播等方面的支持。
ApplicationContext 接口有两个常用的实现类:
- ClassPathXmlApplicationContext:从类路径中寻找指定的xml配置文件
- FileSystemXmlApplicationContext:从文件系统路径中寻找指定的xml配置文件
这两个类只是寻找配置文件的方式不同,其它是一样的。
通过配置文件来使用spring
spring以xml文件作为配置文件,通过它注册并管理bean,配置文件的默认名称是applicationContext.xml。
在之前介绍bean的过程中,接触到了bean的不同特性和配置方式,这里对用到的配置做一个总结。
配置文件中关于bean的标签和属性
配置文件中关于bean的标签:
<beans>
:配置文件的根标签<bean>
:配置一个类的实例<property>
:setter注入<constructor-arg>
:构造器注入,属性和property标签的属性一样
bean标签的属性:
- id:一个bean的唯一标识符
- name:bean的名称,name属性中可以为Bean指定多个名称,每个名称之间用逗号或分号隔开
- class:bean的实现类,它的值是类的全限定名
- scope:用于设定bean的作用域,其属性值有 singleton、prototype、request、session、global Session
- init-method:指定初始化bean时执行的方法
- destroy-method:指定销毁bean时执行的方法
- lazy-init:默认值是false,表示在容器被创建时就初始化所有的单例模式的bean,把值改为true,表示在用户获取bean时在创建。作用于单例模式的bean.
property标签的属性:
- name:字段的名称
- value:当注入的不是依赖对象,而是基本数据类型时,用value
- ref:值是另一个bean标签的id,代表引用另一个bean标签
依据模块来拆分配置文件
如果bean太多,一个spring配置文件会太大,通常会依据模块或层次,将不同的bean放到不同的配置文件中,在将所有的配置文件都引入到applicationContext.xml文件中,
引入方式:<import resource="其它配置文件的相对路径">
,由于注解的使用,这种方式已经很少使用了
在配置文件中引用properties文件
在spring的配置文件中,可以引用properties文件中的键值对,这种方式可以支持用户把某个单元下的配置单独放到一个properties文件中。
具体方式:
第一步:在配置文件中引入context命名空间
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
</beans>
第二步:在spring的配置文件中添加标签,引入properties配置文件
<context:property-placeholder location="classpath:jdbc.properties" local-override="true"/>。
标签内容讲解:
- location属性:指定properties文件的路径,classpath表示配置文件在类路径下
- local-override属性:spring默认会优先加载系统环境变量,如果properties文件中的key和系统变量有冲突,会优先使用系统变量,所以,需要设置local-override属性的值为true,表示优先使用spring中的变量。
第三步:使用 “${key}” 的形式来引用properties中的键值对
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" >
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</bean>
通过注解来使用spring
spring是一款轻代码、重配置的框架,使用注解代替xml文件进行配置,可以简化配置,提高开发效率。
推荐使用纯注解的方式来使用spring,注意,注解不能标注在接口上,只能标注在实现类上
使用配置文件加注解的方式进行配置
这里注解主要代替了配置文件中配置bean的工作。
使用方式:
第一步:在配置文件中配置扫描注解
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--扫描指定包下的所有注解,component-scan的两个子标签 context:include-filter、context:exclude-filter,
可以更加细致的控制扫描动作-->
<context:component-scan base-package="org.wyj"/>
</beans>
第二步:在类上使用注解配置bean,它代替了配置文件中原先的bean标签
@Component("userService") // 配置bean,相当于原先的bean标签
public class UserServiceImpl implements UserService {
@Autowired // 依赖注入,相当于原先的property标签,但是有些许不同,随后详细讲解
@Qualifier("userDao")
private UserDao userDao;
}
第三步:和使用配置文件的方式相同,读取配置文件,创建spring容器
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext
= new ClassPathXmlApplicationContext("applicationContext.xml");
UserService useService = (UserService) applicationContext.getBean("userService");
applicationContext.close();
}
配置bean的注解:
- @Component:使用在类上用于实例化bean
- @Controller、@Service、@Repository:这三个注解的功能和@Component的功能是相同的,但是它们有自己各自的语义。使用这三个注解来代替@Component,可以让注解的语义更清晰
- @Controller:使用在web层
- @Service:使用在service层
- @Repository:使用在dao层
- @Scope(“prototype”):作用于类上,和bean标签中scope属性的使用是一样的
依赖注入用到的注解:
- @Autowired:可以用于构造函数、成员变量、Setter方法,通常使用在字段上,用于根据类型进行依赖注入。它是直接利用反射获取字段,然后赋值,不走setter方法或构造器。它的工作机制:首先根据类型进行注入,如果找到多个相同类型的bean,则把字段名视为id值,依据id值进行查找,如果id值不同但是类型相同,也会注入。
- @Qualifier:要结合@Autowired一起使用,不按照类型查找,按照id名称从容器中进行匹配,然后注入。
Java中提供的注解:spring对这些注解做了支持
- @PostConstruct:初始化方法,在构造方法执行完成之后调用
- @PreDestroy:销毁方法
- @Resource(name=“${bean_id}”):相当于@Autowired加@Qualified
使用纯注解的方式进行配置
比使用配置文件加注解的方式更进一步,在这里,使用一个配置类代替配置文件。
第一步:编写配置类
@Configuration // 表示当前类是一个配置类
@ComponentScan("org.wyj") // 配置扫描注解的路径,相当于 <context:component-scan> 标签
public class SpringConfig {
// 相当于bean标签,方法名是bean的名称,返回值是bean的实例
@Bean("person")
public Person person() {
return new Person();
}
}
第二步:获取spring容器,这里使用的实现类和基于xml配置使用的实现类是不同的
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
配置类中使用到的注解:
- @Configuration:作用于类上,用于指定当前类是一个spring配置类,当创建容器时会从该类上加载注解。
- @ComponentScan(“包名”):作用于类上,用于组件扫描,类似于:
<context:component-scan base-package="org.wyj" />
- @Bean(“beanId”):被@Bean标注的方法返回的对象由spring容器管理,通常将这个注解用在配置类中,实例方法和静态方法都可以,但通常是实例方法。这个注解相当于bean标签,基于注解配置第三方类库中的bean时,就是使用@Bean注解。
使用注解进行依赖注入
在需要被注入依赖的字段上,使用@Autowired注解或@Resource注解,spring容器就会自动根据字段的类型和名称,注入相应的实例。
@Autowired和@Resource之间的区别
@Autowired:spring提供的。默认是按照类型装配注入的,它要求依赖对象必须存在,可以设置它required属性为false,如果相同类型的bean有多个,@Autowired注解会选择一个和字段名称相同的bean,如果没有和字段名称相同的bean,但是有其它类型相同的bean,则会注入类型相同的bean。
@Resource:Java提供的。默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
@Autowired注入为null的原因
原因一:没有开启注解扫描
原因二:使用new关键字创建的对象不受spring容器管理,无法注入
原因三:注入静态变量,spring是基于实例层面上的依赖注入
在主配置类中加载其它配置类
之前在介绍基于xml的配置时提到过,可以把bean放到不同的配置文件中,然后使用import标签引入其它配置文件,基于注解的配置也提供了类似的功能,使用 @Import(配置类.class, …) 的方式来完成。
案例:
@Configuration
@ComponentScan("org.wyj")
@Import({DataSourceConfig.class}) // 引入其它配置类
public class SpringConfig { }
@Import({类对象, …}):作用于类上,加载其它配置类
使用注解加载properties文件
案例:
@PropertySource("classpath:jdbc.properties")
public class DataSourceConfig {
@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 getDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
}
使用方式:@PropertySource注解配置@Value注解,解析并获取配置文件中的键值对
- @PropertySource(“classpath:jdbc.properties”):作用于类上,用于加载properties配置文件。使用注解加载properties文件的时候,用户变量可能会和环境变量冲突,所以尽量为用户变量起一个独特的名称。
- @Value(“${str}”):作用于字段,可以引用配置文件中的变量
- 在配置类上,使用 @Import({DataSourceConfig.class}) 导入当前配置
@ComponentScan排除某些类
案例:
@ComponentScan(value = "org.wyj", excludeFilters =
@ComponentScan.Filter(type = FilterType.REGEX,
pattern = "org.wyj.config.AppConfig[0-9]+"))
自动装配
自动装配是指spring会在容器中自动查找,并自动给bean装配与其相关的属性
基于xml配置的自动装配方式
使用bean标签的autowire属性设置自动装配效果
autowire属性的值:
- no:默认的方式是不进行自动装配,通过手工设置ref属性来进行装配
- byName:通过bean的名称进行自动装配,如果一个bean的property与另一bean的name相同,就进行自动装配。
- byType:通过参数的数据类型进行自动装配。
- constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
- autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
基于注解的自动装配方式
使用@Autowired、@Resource注解来自动装配指定的bean。当容器扫描到这些注解时,就会在容器自动查找需要的bean,并装配给该对象的属性。
特殊的bean
FactoryBean
bean工厂,用来创建一类bean,它本身spring中的一个接口,用户实现此接口,可以自定义某个对象的创建过程。FactoryBean本身也是一个bean,也需要被注册到spring容器中。FactoryBean的设计主要是为了扩展容器中bean的创建方式
使用案例:
第一步:创建FactoryBean
@Component
public class Student implements FactoryBean<Student> {
// 获取bean
@Override
public Student getObject() throws Exception {
return new Student();
}
// 返回bean的类对象
@Override
public Class<?> getObjectType() {
return Student.class;
}
// bean的作用域是否是singleton
@Override
public boolean isSingleton() {
return false;
}
}
第二步:使用FactoryBean
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext
= new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println("---容器初始化完成---");
// 1、根据FactoryBean的ID来获取FactoryBean中创建的对象
Object student = applicationContext.getBean("student");
System.out.println("student = " + student);
// 2、获取factoryBean本身,要在id前加上 ”&“
Object factoryBean = applicationContext.getBean("&student");
System.out.println("factoryBean = " + factoryBean);
applicationContext.close();
}
使用时的注意事项:
- 根据FactoryBean的ID从容器中获取到的对象,是FactoryBean的getObject方法返回的对象,而不是FactoryBean本身,
- 要获取FactoryBean本身,需要在ID前加一个“&”符
SmartFactoryBean
FactoryBean的子类,指定了懒加载的逻辑
后置处理器
后置处理器是spring提供的扩展点,用于对原有的功能进行扩展
spring有两种后置处理器:
-
BeanFactoryPostProcessor:bean工厂的后置处理器,作用是对bean的定义信息进行修改
-
BeanPostProcessor:bean的后置处理器,在bean实例化完成后,可以对bean进行一些特殊的设置。它会在bean初始化方法的前后调用,每创建一个bean,就会被调用一次。
spring本身提供了多种后置处理器,spring正是通过这些后置处理器来处理@Autowired等注解。
BeanFactoryPostProcessor
案例:用户可以自定义bean工厂后置处理器,只需要实现BeanFactoryPostProcessor,在处理器中,获取bean工厂,然后获取bean工厂中的bean定义,可以修改bean的定义信息
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
}
}
bean工厂后置处理器在spring容器实例化好后、bean实例化之前执行。
BeanPostProcessor
在每个bean的初始化前后都会执行
案例:
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
/**
* 初始化之前执行
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
// 初始化之后执行
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
}
bean的生命周期
bean的生命周期有四个阶段:实例化、依赖注入、初始化(扩展点)、销毁。
bean生命周期的四个阶段:
-
实例化:根据配置信息,调用bean的无参构造,实例化一个bean对象
-
依赖注入:根据配置信息,注入当前bean依赖的其它bean
-
初始化:spring在这里提供了一些扩展点,可以做一些初始化操作。
-
销毁:容器关闭时销毁bean
初始化过程中的扩展点:
- Aware类型的接口:通过Aware类型的接口,可以让当前容器获取到spring容器的某些资源
- BeanNameAware接口:获取bean的名称
- BeanClassLoaderAware接口:获取bean的类加载器
- BeanFactoryAware接口:获取bean工厂
- ApplicationContextAware接口:获取spring容器
- InitializingBean接口:初始化时的扩展点
- BeanPostProcessor接口:在初始化的前后执行
销毁过程中的扩展点:
- DisposableBean接口:销毁bean时会调用bean实现的destroy方法。单例bean的生命周期和spring容器的生命周期是相同的,所以在spring容器关闭时销毁bean
案例:一个实现所有扩展点的bean,查看它的生命周期过程中执行了哪些方法。
// 一个普通的bean,实现了初始化过程中所有的扩展点
@Component
public class Person10 implements BeanNameAware, BeanClassLoaderAware
, BeanFactoryAware, ApplicationContextAware
, InitializingBean, DisposableBean {
private Person11 person11;
public Person10() {
System.out.println("----实例化")
}
@Autowired
public void setPerson11(Person11 person11) {
System.out.println("----依赖注入 setter方法 @Autowired");
this.person11 = person11;
}
@Override
public void setBeanName(String name) {
System.out.println("----BeanNameAware接口");
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("----ClassLoaderAware接口");
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("----BeanFactoryAware接口");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
System.out.println("----ApplicationContextAware接口");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("----InitializingBean接口");
}
@Override
public void destroy() throws Exception {
System.out.println("----DisposableBean接口");
}
}
后置处理器:
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
// 初始化前执行
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
// BeanPostProcessor在每个bean初始化前后都会执行,这里只查看指定的bean
if (beanName.equals("person10")) {
System.out.println("----MyBeanPostProcessor postProcessBeforeInitialization");
}
return bean;
}
// 初始化后执行
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (beanName.equals("person10")) {
System.out.println("----MyBeanPostProcessor postProcessAfterInitialization");
}
return bean;
}
}
测试:
public class BeanLifeCycleOrderTest {
private static AnnotationConfigApplicationContext applicationContext;
@BeforeClass
public static void before() {
applicationContext
= new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println("-----开始执行测试方法-----");
}
@AfterClass
public static void after() {
System.out.println("-----开始关闭容器-----");
applicationContext.close();
System.out.println("-----结束执行测试方法-----");
}
/**
* 测试bean的生命周期中各种方法的执行顺序
*/
@Test
public void test1() {
Person10 person10 = applicationContext.getBean("person10", Person10.class);
System.out.println("person10 = " + person10);
Assert.assertNotNull(person10);
}
}
执行结果:
----实例化
----依赖注入 setter方法 @Autowired
----BeanNameAware接口
----ClassLoaderAware接口
----BeanFactoryAware接口
----ApplicationContextAware接口
----MyBeanPostProcessor postProcessBeforeInitialization
----InitializingBean接口 afterPropertiesSet
----MyBeanPostProcessor postProcessAfterInitialization
-----开始执行测试方法-----
person10 = org.wyj.dao.Person10@7c0c77c7
-----开始关闭容器-----
----DisposableBean接口 destroy
-----结束执行测试方法-----
从结果中,可以看出bean的生命周期:实例化、依赖注入、初始化、销毁,以及阶段之间的执行顺序
注意:InitializingBean、DisposableBean和init-method、destroy-method,它们的功能是相同的,一个使用在配置文件中,一个使用在注解中,如果同一个bean上同时应用了这两种配置,那么接口对应的方法会优先执行,无论是初始化时,还是销毁时
AOP
AOP,面向切面编程,Aspect Oriented Programming,它是使用代理模式,在不修改源码的情况下增强某个组件原有的功能。面向切面编程是面向对象编程的延续,它可以分离应用的业务逻辑与系统级服务,让代码更加易于维护。
代理模式
通过代理对象访问目标对象,这样可以在不改变目标对象基础上增强额外的功能,如添加权限、访问控制等。
代理模式内部的角色:
- 目标类:实现业务功能,要被代理的类
- 代理类:用户通过代理类来访问目标类
- 目标接口:代理类和目标类都需要实现目标接口
- 增强类:目标类 + 增强类 = 代理类,代理类需要执行增强类中的方法和目标类中的方法,通过这种方式,在不改变目标类的情况下增强目标类的功能
代理模式的分类:依据创建代理类的时间点,代理模式分为静态代理和动态代理。
静态代理
由程序员编写代理类,在编译时,接口、目标类、代理类已经全部确定,在程序运行之前,代理类的类文件就已经生成
案例:
1、目标接口
public interface SellTickets {
void sell();
}
2、目标类:售票功能
public class TrainStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
3、增强类:售票的时候额外收取服务费
public class ProxyPoint implements SellTickets {
private final TrainStation trainStation;
public ProxyPoint(TrainStation trainStation) {
this.trainStation = trainStation;
}
@Override
public void sell() {
System.out.println("代理点收取服务费");
trainStation.sell();
}
}
4、创建代理类,测试
public static void main(String[] args) {
// 创建代理类,传入目标类的实例,通过代理类来访问目标类
ProxyPoint proxyPoint = new ProxyPoint(new TrainStation());
proxyPoint.sell();
}
静态代理的实现:
- 编写目标接口和目标类:编写目标类需要实现的接口,然后编写目标类,实现业务功能。
- 编写代理类:代理类实现目标类的接口,同时代理类持有目标类的实例,在代理类中,使用目标类的实例访问目标类中的方法,同时添加其它功能的代码
- 访问代理类:在访问时,首先获取代理类的实例,然后获取目标类的实例,并且在代理类的实例中传入目标类的实例,然后访问代理类中的方法,由代理类来访问目标类中的方法
静态代理的特点是,代理类写死在代码中,而动态代理,代理类则是在运行时动态生成,它是基于动态字节码技术。
动态代理
代理类在程序运行时动态创建的代理方式被称为动态代理。
常用的动态代理技术:
- jdk代理:基于接口的动态代理技术
- cglib代理:基于父类的动态代理技术
基于jdk的动态代理
使用jdk提供的API来创建动态代理类,底层使用了动态字节码技术,运行时在内存中动态地创建一个类,然后创建这个类的实例。
案例:
创建代理类的代码:
public class ProxyFactory {
// 创建火车站的代理类
public static SellTickets getTrainStationProxyObj(TrainStation trainStation) {
Object proxyObj = Proxy.newProxyInstance(
trainStation.getClass().getClassLoader(), // 目标类的类加载器
trainStation.getClass().getInterfaces(), // 目标类的接口
(proxy, method, args) -> {
System.out.println("收取代理费"); // 在这里写增强目标类功能的方法
return method.invoke(trainStation, args); // 调用目标类中的方法
}
);
return (SellTickets) proxyObj;
}
}
测试:
public class Client {
public static void main(String[] args) {
// 代理类持有目标类的实例,所以创建代理类时要传入目标类的实例
SellTickets proxyObj = ProxyFactory.getTrainStationProxyObj(new TrainStation());
proxyObj.sell();
System.out.println("proxyObj.getClass() = " + proxyObj.getClass()); // class com.sun.proxy.$Proxy0
}
}
和之前静态代理的不同之处在于,代理类是运行时使用字节码技术动态生成的
基于cglib提供的动态代理
由于jdk提供的动态代理技术要求目标类必须要实现目标接口,所以它的使用场景会有所限制,cglib提供了一种动态代理技术,它是基于继承关系,目标类不需要实现接口。
使用cglib实现动态代理的步骤:
第一步:引用依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
第二步:编写生成代理类的代码
public class ProxyFactory {
public static TrainStation getProxyObj(TrainStation trainStation) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TrainStation.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method
, Object[] objects, MethodProxy methodProxy)
throws Throwable {
System.out.println("收取代理费");
return method.invoke(trainStation, objects);
}
});
return (TrainStation) enhancer.create();
}
}
测试:
public class Client {
public static void main(String[] args) {
TrainStation proxyObj = ProxyFactory.getProxyObj(new TrainStation());
proxyObj.sell();
// class org.wyj.proxy.cg_proxy.TrainStation$$EnhancerByCGLIB$$37abd3d3
System.out.println("proxyObj.getClass() = " + proxyObj.getClass());
}
}
两种方式的比较
功能:cglib更强,因为jdk提供的代理类需要和目标类实现相同的接口,但是cglib不需要,它只需要继承目标类,不过要注意,如果目标类中的方法是private的、静态的、final的,cglib无法基于该方法进行代理,因为它无法被继承,cglib无法重写。
性能:在jdk1.8,jdk代理效率高于cglib,因为jdk生成的字节码更少
AOP相关概念
AOP中的角色:
- target:目标对象
- proxy:代理对象
- advice:通知,增强类中的方法被称为通知,用于通知的方法不可以有参数
- joinpoint:连接点,被拦截到的方法,spring只支持方法类型的连接点
- pointcut:切入点,定义了要对哪些连接点进行拦截,切入点支持泛型,一个切入点可能对应多个连接点。
- aspect:切面,切入点和通知的结合。用于通知的方法通常写在切面类中
- weaving:织入,把切入点和通知结合起来创建代理对象的过程
切入点和连接点的区别:连接点是一个具体的方法,表示要拦截这个方法,切入点定义了要拦截哪些方法。
入门案例:基于xml配置的AOP
第一步:导入AOP的相关依赖spring-context和aspectjweaver。aop是一种编程思想,spring和aspectjweaver都实现了这种思想,但是aspectjweaver的实现更好一点,spring官方也推荐使用aspectjweaver来配置AOP。在spring5中spring本身就引入了aspectjweaver,所以用户可以不用自己引入了。在这里引入aspectjweaver的依赖,是为了方便以注解的形式配置AOP,随后会用到
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
第二步:创建目标类
package org.wyj.dao;
public class TargetObj {
public void executeWork(){
System.out.println("目标对象开始执行功能");
}
}
第三步:创建增强类,编写通知方法
package org.wyj.aspect;
public class EnhanceObj {
public void before() {
System.out.println("前置通知1");
}
}
public class EnhanceObj2 {
public void before() {
System.out.println("前置通知2");
}
}
第四步:编写spring的配置文件,在spring中配置目标类和增强类的bean,
<?xml version="1.0" encoding="UTF-8" ?>
<!--引入aop命名空间-->
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置目标类、增强类的bean-->
<bean id="targetObj" class="org.wyj.dao.TargetObj"/>
<bean id="enhanceObj" class="org.wyj.aspect.EnhanceObj"/>
<bean id="enhanceObj2" class="org.wyj.aspect.EnhanceObj2"/>
<!--配置切面-->
<aop:config>
<!--配置切入点,这个切入点可被多个切面使用-->
<aop:pointcut id="p1"
expression="execution(public void org.wyj.dao.TargetObj.executeWork())"/>
<!--声明切面1-->
<aop:aspect ref="enhanceObj"> <!--配置切面使用哪个增强类-->
<!--before类型的通知,method属性配置增强方法,pointcut配置切入点,
表示具体要对哪个方法进行什么样的增强-->
<aop:before method="before" pointcut-ref="p1" />
</aop:aspect>
<!--声明切面2-->
<aop:aspect ref="enhanceObj2">
<aop:before method="before" pointcut-ref="p1" />
</aop:aspect>
<!--总结:同一个切入点,多个切面,切面按照声明的顺序执行-->
</aop:config>
</beans>
第五步:测试,从容器中获取目标类,执行方法,查看aop能否正确执行
@Test
public void test() {
ClassPathXmlApplicationContext applicationContext
= new ClassPathXmlApplicationContext("applicationContext.xml");
// 获取目标类的实例
TargetObj targetObj = applicationContext.getBean("targetObj", TargetObj.class);
// 查看配置的代理类是否执行
targetObj.executeWork();
applicationContext.close();
}
执行结果:
前置通知1
前置通知2
目标对象开始执行功能
切入点 execution表达式的语法
格式:execution([修饰符] 返回值类型 包名.类名.方法名(参数的数据类型的全限定名, …))异常类型,
语法中的通配符:
- 方法全限定名中的“*”:可以用“*”代表任意返回值类型、包名、类名、方法名
- 包名与类名之间的 . 和 … :一个点代表当前包下的类,两个点代表当前包及其子包下的类
- 参数列表中的 … :参数列表可以使用两个点表示任意个数、任意类型的参数列表
案例:
- 拦截一个固定的方法:execution(public void org.wyj.aop.Target.method()):
- 拦截Target类中返回值是void类型的任意方法:execution(void com.wyj.aop.Target.*(…))
- 拦截aop下所有类中的所有方法:execution(* com.wyj.aop..(…))
- 拦截aop下及其子包下的所有类中的所有方法:execution(* com.wyj.aop….(…)):
- 拦截以set开头的任意方法:execution(* set*(…))
通知的类型
AOP中通知的类型:通知,advice,其实就是对目标切入点进行增强的内容,有5种类型的通知:
- before:前置通知,在方法之前自动执行的通知,可应用于权限管理等功能
- around:环绕通知,在方法前后自动执行的通知,可应用于日志、事务管理等功能。
- after:后置通知,无论程序发生任何事情,都将在方法结束之后执行
- after-returning:返回后通知,在方法之后自动执行的通知,可应用于关闭资源等功能
- after-throwing:异常通知,在方法抛出异常时自动执行的通知
AOP中通知的执行顺序:
- 没有发生异常的情况下:around、before、目标方法、around、after、afterReturning
- 发生异常的情况下:around、before、目标方法、after、afterThrowing
环绕通知的写法:
// ProceedingJoinPoint类中的proceed方法中会调用目标类的方法,通过这种形式实现环绕通知
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("before");
Object result = pjp.proceed();
System.out.println("after");
return result;
}
入门案例2:基于注解实现的AOP
第一步:导入AOP的相关依赖
第二步:创建目标类
@Service // 切面存在的话就会返回代理对象
public class HelloService {
public HelloService() {
System.out.println("HelloService 构造方法");
}
public void sayHello(String peopleName) throws IOException {
String result = "你好:" + peopleName;
System.out.println(result);
}
/**
* 抛异常的情况下,通知的执行顺序
*/
public void sayHello2(String peopleName) throws IOException {
System.out.println("sayHello2:" + peopleName);
throw new RemoteException("aaa");
}
}
第三步:创建切面类,在切面类中创建切入点和增强方法,切入点和增强方法必须写在切面类中
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
@Aspect // 标明当前类是一个切面类
public class LogAspect {
public LogAspect() { }
// 配置切入点
@Pointcut("execution(* org.wyj.service.HelloService.sayHello*(..))")
public void pointcut() { }
//前置通知 增强方法/增强器
@Before("pointcut()") // 方法名后要加括号
public void logStart(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println("前置通知:logStart()==>" + name + "....【args: "
+ Arrays.asList(joinPoint.getArgs()) + "】");
}
//后置通知
@After("pointcut()")
public void logEnd(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println("后置通知:logEnd()==>" + name
+ "....【args: "
+ Arrays.asList(joinPoint.getArgs()) + "】");
}
// 环绕通知
@Around("pointcut()")
public void logAround(ProceedingJoinPoint joinPoint) throws Throwable {
String name = joinPoint.getSignature().getName();
System.out.println("环绕通知,前:logAround()==>" + name
+ "....【args: "
+ Arrays.asList(joinPoint.getArgs()) + "】");
joinPoint.proceed();
System.out.println("环绕通知,后:logAround()==>" + name
+ "....【args: "
+ Arrays.asList(joinPoint.getArgs()) + "】");
}
//返回后通知
@AfterReturning(value = "pointcut()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
String name = joinPoint.getSignature().getName();
System.out.println("返回通知:logReturn()==>" + name
+ "....【args: " + Arrays.asList(joinPoint.getArgs())
+ "】【result: " + result + "】");
}
// 异常通知
@AfterThrowing(value = "pointcut()", throwing = "e")
public void logError(JoinPoint joinPoint, Exception e) {
String name = joinPoint.getSignature().getName();
System.out.println("异常通知:logError()==>" + name
+ "....【args: " + Arrays.asList(joinPoint.getArgs())
+ "】【exception: " + e + "】");
}
}
第四步:配置目标类
第五步:配置类,在配置类上添加注解:@Configuration、@ComponentScan(“org.wyj”)、@EnableAspectJAutoProxy
@Configuration
@ComponentScan("org.wyj")
@EnableAspectJAutoProxy // 开启对于切面注解的支持
public class AppConfig { }
第七步:测试,从容器中获取目标类,调用目标类中的方法,查看AOP的执行
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext
= new AnnotationConfigApplicationContext(AppConfig.class);
HelloService helloService = applicationContext.getBean("helloService", HelloService.class);
helloService.sayHello("Bob");
applicationContext.close();
}
基于注解的AOP涉及到的API
1、aspectjweaver中的API
ProceedingJoinPoint:public interface ProceedingJoinPoint extends JoinPoint
,暴露proceed方法,用于支持环绕通知
配置切面时用到的注解:
- @Aspect:声明一个切面
- @Pointcut:声明一个切入点表达式
- @Before:前置通知
- @After:后置通知
- @Around:环绕通知
- @AfterReturning:返回后通知
- @AfterThrowing:异常后通知
2、spring中的API
@EnableAspectJAutoProxy:开启对于@Aspect等标签的支持
AOP的执行机制
AOP的底层是通过spring提供的动态代理技术实现的,而且使用纯Java实现,不需要专门的编译过程和类加载器。在spring容器启动时,会根据配置,通过动态代理技术生成代理实例生成目标类的代理类,从spring容器中获取目标类时,返回的实际上是代理类,通过代理类来访问目标类,同时织入要增强的功能,实现在不修改原代码的前提下,增强功能
切面的执行顺序
默认执行顺序:Spring AOP 将按照它们在类路径中的顺序执行切面
可以使用 @Order
注解或 Ordered
接口来指定切面的优先级。优先级值越小,优先级越高。
获取当前的代理对象
AopContext.currentProxy(),作用是获取当前的代理对象,在目标类的a方法中调用b方法时,想让代理对b方法生效,就使用这种方式
spring框架的单元测试
spring提供了spring-test,可以使用它来配合junit编写单测
案例:使用spring集成junit进行测试的步骤
第一步:导入依赖 spring-test
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
第二步:编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class SpringJunitCombineTest {
@Resource(name = "dataSource")
private DataSource dataSource;
@Autowired
private Person person;
@Test
public void springJunitTest() {
userService.save();
System.out.println("dataSource = " + dataSource);
}
}
案例讲解:在测试类上加上两个注解
- @RunWith(SpringJUnit4ClassRunner.class):表示使用某个特定的类来运行spring,这个类是由spring提供的
- 加载配置:下面两个操作二选一
- 如果是配置类:@ContextConfiguration(classes = {配置类的类对象, …})
- 如果是配置文件:@ContextConfiguration(“classpath:applicationContext.xml”)
spring框架中每个模块的作用
spring-beans.jar:定义了spring框架对bean的管理,beanFactory是核心接口,负责根据配置文件加载bean
spring-core.jar:提供了核心功能,包括环境、资源等
spring-context.jar:构建与核心模块之上,扩展了BeanFactory,ApplicationContext是核心接口
Q&A
spring框架中的bean是线程安全的么?如果线程不安全,那么如何处理?
spring只是按照用户指定的方式来创建bean,用户需要自己负责bean的线程安全
FactoryBean和BeanFactory的区别
BeanFactory是IOC容器的根接口,FactoryBean用来创建单个类的实例,它提供了一种更加灵活的方式来创建实例