spring基础知识-容器和AOP

简介

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用来创建单个类的实例,它提供了一种更加灵活的方式来创建实例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值