Spring框架学习总结

Spring框架学习总结

一、什么是spring?

1.1 spring的简单理解

官网:官网
官方下载地址:下载地址
GitHub地址:源码

Spring是一个简化java企业级开发的一个轻量级的开源框架。内部包含了很多技术,比如:控制反转&依赖注入,AOP,Spring事务管理,还可以集成其他框架。

1.2 spring容器

也叫IoC容器,是具有依赖注入功能的容器。主要负责容器中:对象的初始化对象的实例化对象与对象之间依赖关系的配置对象的销毁对外提供对象的查找等操作,对象的整个生命周期都是由容器来控制。

1.3 spring中IoC容器的配置方式

支持xml方式java注解方式,在配置文件中列出需要让IoC管理的对象,在spring容器启动时会去加载这个配置文件,然后将这些对象组装好供外部访问者使用。

1.4 Bean概念

由spring管理的对象统称为Bean对象。就是普通的java对象,和使用者new的对象是一样的,只是这些对象由spring来创建和管理。
需要先在配置文件中定义好需要创建的Bean对象,这些配置统称为Bean定义配置元数据信息,spring容器通过读取这些配置元数据信息来构建和组装我们需要的对象。

1.5 常用的spring容器接口和对象

1.5.1 BeanFactory 接口

spring容器的顶层接口,提供了容器最基本的功能。用BeanFactory加载配置文件时,不会创建容器里的对象,在获取(使用)对象的时候才会创建

org.springframework.beans.factory.BeanFactory

常用的几个方法:

//按bean的id或者别名查找容器中的bean
Object getBean(String name) throws BeansException

//这个是一个泛型方法,按照bean的id或者别名查找指定类型的bean,返回指定类型的bean对象
<T> T getBean(String name, Class<T> requiredType)throws BeansException;

//返回容器中指定类型的bean对象
<T> T getBean(Class<T> requiredType) throws BeansException;

//获取指定类型bean对象的获取器,这个方法比较特别,以后会专门来讲
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);

1.5.2 ApplicationContext 接口

继承了BeanFactory接口,包含BeanFactory的所有功能,并且进行了扩展,增加了许多企业级功能,如:AOP。国际化,事件支持等。加载xml的时候就会创建对象。

org.springframework.context.ApplicationContext

1.5.3 ClassPathXmlApplicationContext 类

实现了ApplicationContext接口,该实现类可以从classpath中加载beans.xml的配置,然后创建xml中需要的bean对象

org.springframework.context.support.ClassPathXmlApplicationContext

1.5.4 AnnotationConfigApplicationContext 类

实现了ApplicationContext接口,该实现类可以加载用注解方式配置的Bean对象

org.springframework.context.annotation.AnnotationConfigApplicationContext

二、bean.xml详解和spring创建bean实例的方式

2.1 beans.xml详解

2.1.1 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-4.3.xsd">
    <import resource="引入其他beans.xml配置文件" />
    <bean id="bean标识" class="类型名称"/>
    <alias name="bean标识" alias="别名" />
</beans>
(1) beans标签

beans是根元素,希迈纳可以包含任意数量的bean,import,alias元素
bean标签定义一个bean对象,格式如下:

<bean id="bean唯一标识" name="bean名称" class="完整类型名称" factory-bean="工厂bean名称" factory-method="工厂方法"/>

每个bean都有一个名称,叫做bean名称。bean名称在spring中必须唯一,否则会报错,可以通过bean名称从spring容器中获取对应的对象。

(2) bean名称和别名定义规则

名称和别名可以通过bean元素中的id和name来定义,具体定义规则如下:

  • 当id存在时,不管有没有name,取id为bean的名称。
  • 当id不存在时,此时看name,name的值可以通过**,;**或者空格分割,最后会按照分隔符得到一个String数组,数组的第一个元素作为bean的名称,其他的作为bean的别名
  • 当id和name都存在的时候,id为bean名称,name用来定义多个别名
  • 当id和name都不指定的时候,bean名称自动生成;生成规则是:bean名称为完整类名#编号,别名为完整的类名。
(3) alias元素

alias元素也可以用来给某个bean定义别名,语法:

(4) import元素

当我们的系统比较大的时候,会分成很多模块,每个模块会对应一个bean xml文件,我们可以在一个总的bean xml中对其他bean xml进行汇总,相当于把多个bean xml的内容合并到一个里面了,可以通过import元素引入其他bean配置文件。

2.2 bean标签中的属性

2.2.1 scope(作用域)属性

<bean id="" class="" scope="作用域" /> 
  1. singleton:表示这个bean是单例的。当scope为singleton时,spring容器在启动加载spring配置文件的时候,会将bean创建好放在容器中。整个spring容器中只存在一个bean实例,通过容器多次查找bean的时候(调用BeanFactory的getBean方法或者bean之间注入依赖的bean对象的时候),返回的都是同一个bean对象。有个特殊的情况,当bean的lazy被设置为true的时候,表示懒加载,那么使用的时候才会创建。
  2. prototype:表示这个bean是多例的,通过容器每次获取的bean都是不同的实例,每次获取时都会重新创建一个bean实例对象
  3. request:在spring容器的web环境中,每个http请求都会创建一个bean实例,request结束的时候,这个bean也就结束了
  4. session:在spring容器的web环境中,每个会话会对应一个bean实例,不同的session对应不同的bean实例
  5. application:一个web应用程序对应一个bean实例。但是,一个应用程序中可以创建多个spring容器,不同的容器中bean的名称不能相同。
  6. 自定义作用域:自定义scope 3个步骤,实现Scope接口,将实现类注册到spring容器,使用自定义的sope
    缺点
  7. 单例bean是整个应用共享的,所以需要考虑到线程安全问题
  8. 多例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,会影响系统的性能

2.2.2 通过depend-on干预bean创建和销毁顺序

<bean id="bean1" class="" depend-on="bean2,bean3; bean4" />

depend-on:设置当前bean依赖的bean名称,可以指定多个,多个之间可以用”,;空格“进行分割。
不管bean2,bean2,bean4在任何地方定义,会先将bean2,bean3,bean4在bean1创建之前创建好,表示bean1依赖于这3个bean;销毁的时候也会先销毁当前bean,再去销毁被依赖的bean,即先销毁bean1,再去销毁depend-on指定的bean2,bean3,bean4。

2.2.2 通过primary解决根据条件有多个匹配的bean

<!--primary默认值都是false-->
<bean id="bean1" class="" primary="true" />

spring中可以通过bean元素的primary属性的primary="true"来解决这个问题,可以通过这个属性来指定当前bean为主要候选者,当容器查询一个bean的时候,如果容器中有多个候选者匹配的时候,此时spring会返回主要的候选者。

public class PrimartBean {
    public interface IService {} //@1
    public static class ServiceA implements IService {} //@2
    public static class ServiceB implements IService {} //@3
    private IService service;
    public void setService(IService service) { this.service = service; }
    @Override
    public String toString() {
        return "SetterBean{" + "service=" + service + '}';
    }
}
//测试类
@Test
public void test(){
      ApplicationContext context = new ClassPathXmlApplicationContext("primaryBean.xml");
      System.out.println(context.getBean("primaryBean", PrimartBean.class));
      System.out.println(context.getBean(PrimartBean.IService.class));
}

xml

<bean id="serviceA" class="com.zjhc.autoDi.primaryBean.PrimartBean.ServiceA" primary="true"/>
<bean id="serviceB" class="com.zjhc.autoDi.primaryBean.PrimartBean.ServiceB"/>
<bean id="primaryBean" class="com.zjhc.autoDi.primaryBean.PrimartBean" autowire="byType"/>

2.2.3 通过autowire-candidate解决根据条件有多个匹配的bean

  1. default:这个是默认值,autowire-candidate如果不设置,其值就是default
  2. true:作为候选者
  3. false:不作为候选者
<!--autowire-candidate默认值都是true-->
<bean id="bean1" class="" autowire-candidate="false" />
public class PrimartBean {
    public interface IService {} //@1
    public static class ServiceA implements IService {} //@2
    public static class ServiceB implements IService {} //@3
    private IService service;
    public void setService(IService service) { this.service = service; }
    @Override
    public String toString() {
        return "SetterBean{" + "service=" + service + '}';
    }
}
//测试类
@Test
public void test(){
      ApplicationContext context = new ClassPathXmlApplicationContext("primaryBean.xml");
      System.out.println(context.getBean("primaryBean", PrimartBean.class));
      System.out.println(context.getBean(PrimartBean.IService.class));
}

xml

<bean id="serviceA" class="com.zjhc.autoDi.primaryBean.PrimartBean.ServiceA" autowire-candidate="false"/>
<bean id="serviceB" class="com.zjhc.autoDi.primaryBean.PrimartBean.ServiceB"/>
<bean id="primaryBean" class="com.zjhc.autoDi.primaryBean.PrimartBean" autowire="byType"/>

上面两种情形的分析
容器在创建PrimartBean 的时候,发现其autowire为byType,即按类型自动注入,此时会在PrimartBean 类中查找所有setter方法列表,其中就包含了setService方法,setService方法参数类型是IService,然后就会去容器中按照IService类型查找所有符合条件的bean列表,此时容器中会返回满足IService这种类型并且autowire-candidate="true"的bean,刚才有说过bean元素的autowire-candidate的默认值是true,所以容器中符合条件的候选bean有2个:serviceA和serviceB,setService方法只需要一个满足条件的bean,此时会再去看这个列表中是否只有一个主要的bean,即bean元素的primary=“ture”的bean(而bean元素的primary默认值都是false),所以没有primary为true的bean,此时spring不知道选哪个,所以抛出NoUniqueBeanDefinitionException异常。
问题解决
从上面过程中可以看出将某个候选bean的primary置为true或者只保留一个bean的autowire-candidate为true,将其余的满足条件的bean的autowire-candidate置为false就可以解决问题了。

2.2.4 bean延迟初始化和实时初始化(lazy-init)

<bean id="bean1" class="" lazy-init="是否是延迟初始化" />

在bean定义的时候通过lazy-init属性来配置bean是否是延迟加载,true:延迟初始化,false:实时初始化

2.2.4.1 实时初始化

在容器启动过程中被创建组装好的bean,称为实时初始化的bean,spring中默认都是实时初始化的bean,这些bean默认都是单例的,在容器启动过程中会被创建好,然后放在spring容器中以供使用
优点
(1) 实时初始化的bean如果定义有问题,会在容器启动过程中会抛出异常,让开发者快速发现问题
(2) 容器启动完毕之后,实时初始化的bean已经完全创建好了,此时被缓存在spring容器中,当我们需要使用的时候,容器直接返回就可以了,速度是非常快的

2.2.4.2 延迟初始化

延迟初始化的bean在容器启动过程中不会创建,而是需要使用的时候才会去创建。
什么时候bean会被使用:
(1)被其他bean作为依赖进行注入的时候,比如通过property元素的ref属性进行引用,通过构造器注入、通过set注入、通过自动注入,这些都会导致被依赖bean的创建。
(2)调用容器的getBean方法获取bean

2.2.5 使用继承简化bean配置(abstract & parent)

当多个类中的属性配置完全一样是,可以进行简化。

    <bean id="serviceA" class="com.javacode2018.demo12.ServiceA"/>

    <bean id="serviceB" class="com.javacode2018.demo12.ServiceB">
        <property name="name" value="路人甲Java"/>
        <property name="serviceA" ref="serviceA"/>
    </bean>

    <bean id="serviceC" class="com.javacode2018.demo12.ServiceB">
        <property name="name" value="路人甲Java"/>
        <property name="serviceA" ref="serviceA"/>
    </bean>
</beans>

通过继承优化代码:

  1. 将公共的代码提取出来,定义一个abstract="true"的属性,没有class,表示这个bean是抽象的,在spring容器中不会被创建,只是会将其当做bean定义的模板。从容器中查找abstract=true的bean的时候,会报错BeanIsAbstractException异常。
  2. bean的parent属性可以指定当前bean的父bean,子bean可以继承父bean中配置信息。
  3. 子bean中也可自定义父bean中已经定义的配置,这样子会覆盖父bean中的配置信息。
    <bean id="serviceA" class="com.javacode2018.demo12.ServiceA"/>

    <bean id="baseService" abstract="true">
        <property name="name" value="路人甲Java"/>
        <property name="serviceA" ref="serviceA"/>
    </bean>
	
    <bean id="serviceB" class="com.javacode2018.demo12.ServiceB"
     parent="baseService"/>
    <bean id="serviceC" class="com.javacode2018.demo12.ServiceC"
     parent="baseService"/>
</beans>

2.2.6 单例bean中使用多例bean(lookup-method)

lookup-method:方法查找,可以对指定的bean的方法进行拦截,然后从容器中查找指定的bean作为被拦截方法的返回值

package com.lookupmethod;
public class ServiceA {}
package com.lookupmethod;
public class ServiceB {
    public void say() {
        ServiceA serviceA = this.getServiceA();
        System.out.println("this:" + this + ",serviceA:" + serviceA);
    }
    public ServiceA getServiceA() { //@1
        return null;
    }
}
    <bean id="serviceA" class="com.lookupmethod.ServiceA" scope="prototype"/>

    <bean id="serviceB" class="com.lookupmethod.ServiceB">
        <lookup-method name="getServiceA" bean="serviceA"/>
    </bean>
@Test
public void lookupmethod() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("lookupmethod.xml");

   System.out.println(context.getBean(ServiceA.class)); //@1
   System.out.println(context.getBean(ServiceA.class)); //@2
   System.out.println("serviceB中的serviceA");
   ServiceB serviceB = context.getBean(ServiceB.class); //@3
   serviceB.say();
   serviceB.say();
}
com.lookupmethod.ServiceA@619713e5
com.lookupmethod.ServiceA@708f5957
serviceB中的serviceA
this:com.lookupmethod.ServiceB$$EnhancerBySpringCGLIB$$aca8be5a@68999068,serviceA:com.lookupmethod.ServiceA@7722c3c3
this:com.lookupmethod.ServiceB$$EnhancerBySpringCGLIB$$aca8be5a@68999068,serviceA:com.lookupmethod.ServiceA@2ef3eef9

2.3 spring创建bean实例的方式

详见博客:Spring创建bean实例的常用四种方式

2.4 bean的生命周期

  1. 通过无参构造器创建bean实例
  2. 通过set方法给bean的属性设置值和其他bean的引用
  3. 将bean时实例传递给bean后置处理器postProcessBeforeInitialization
  4. 调用配置好的bean的初始化方法
  5. 将bean时实例传递给bean后置处理器postProcessAfterInitialization
  6. bean已创建
  7. 调用配置好的bean的销毁方法
//普通类
public class BeanLife {

    private String name;
    public BeanLife(){
        System.out.println("第一步,通过无参构造创建bean实例");
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("第二步,通过set方法设置属性值");
    }

    public void initMethod(){
        System.out.println("第四步,调用bean的初始化方法");
    }

    public void destroyMethod(){
        System.out.println("第七步,调用bean的销毁方法");
    }
}
//bean后置处理器
public class MyBeanPostProcess implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第三步,在bean初始化之前执行");
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第五步,在bean初始化之后执行");
        return bean;
    }
}
//spring配置文件
<bean id="beanlife" class="com.zjhc.beanlife.BeanLife" init-method="initMethod" destroy-method="destroyMethod">
   <property name="name" value="zzzz"/>
</bean>

<bean id="myBeanPostProcess" class="com.zjhc.beanlife.MyBeanPostProcess"/>
//测试方法
 @Test
    public void test(){
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beanlife.xml");

        BeanLife beanlife = context.getBean("beanlife", BeanLife.class);

        System.out.println("第六步,创建好的bean对象为:"+beanlife);

        //手动销毁bean
        context.close();
    }

三、控制反转&依赖注入

3.1 控制反转IoC

使用Spring前:所有对象的创建和组装都是使用者自己控制的。如果依赖多个对象的话,代码量较大,不方便维护,耦合度较高(依赖有调整时,改动比较大),不利于扩展
在这里插入图片描述
使用spring后:将对象创建和组装的主动控制权交给spring容器,使用者只需要去容器查找需要的对象就可以了。对象的构建过程被反转了,所以叫做控制反转。
IoC优点:IoC是面向对象编程中的一种设计原则,主要是为了降低系统代码的耦合度,让系统有利于扩展和维护。

3.2 依赖注入DI

指spring容器在创建对象时给其依赖对象设置值的方式。

3.2.1 手动注入(xml方式)

3.2.1.1 构造器注入
  1. 根据构造器参数索引注入
<bean id="diByConstructorParamIndex" class="com.javacode2018.lesson001.demo5.UserModel">
    <constructor-arg index="0" value="第一个参数值"/>
    <constructor-arg index="1" value="第二个参数值"/>
</bean>

注意:通过参数索引注入对参数顺序有很强的依赖性,若构造函数参数位置被调整过,会导致注入出错;可以通过新增构造函数来解决
2. 根据构造器参数类型注入

<bean id="diByConstructorParamType" class="com.javacode2018.lesson001.demo5.UserModel">
    <constructor-arg type="参数类型" value="参数值"/>
    <constructor-arg type="参数类型" value="参数值"/>
</bean>
  1. 根据构造器参数名称注入
<bean id="diByConstructorParamName" class="com.javacode2018.lesson001.demo5.UserModel">
    <constructor-arg name="参数类型" value="参数值"/>
    <constructor-arg name="参数类型" value="参数值"/>
</bean>

注意:关于参数名称可能不稳定的问题,spring提供了解决方案,通过ConstructorProperties注解来定义参数的名称,将这个注解加在构造方法上面,如下:

@ConstructorProperties({"第一个参数名称", "第二个参数的名称",..."第n个参数的名称"})
public 类名(String p1, String p2...,参数n) {
}
3.2.1.2 set方法注入
<bean id="" class="">
    <property name="属性名称" value="属性值" />
</bean>

注意:setter注入相对于构造函数注入要灵活一些,构造函数需要指定对应构造函数中所有参数的值,而setter注入的方式没有这种限制,不需要对所有属性都进行注入,可以按需进行注入

3.2.1.3 注入容器中的bean
3.2.1.3.1 ref方式
<constructor-arg ref="需要注入的bean的名称"/>
<property name="属性名称" ref="需要注入的bean的名称" />
3.2.1.3.2 内置bean方式
<!--构造器方式 -->
<constructor-arg>
    <bean class=""/>
</constructor-arg>
<!--set方法 -->
<property name="属性名称">
    <bean class=""/>
</property>

案例
PersonModel

public class PersonModel {

    private UserModel userModel;
    private CarModel carModel;

    public PersonModel(){}
    public PersonModel(UserModel userModel,CarModel carModel){
        this.userModel = userModel;
        this.carModel = carModel;
    }
    public UserModel getUserModel() {return userModel;}
    public void setUserModel(UserModel userModel) {this.userModel = userModel;}
    public CarModel getCarModel() {return carModel; }
    public void setCarModel(CarModel carModel) { this.carModel = carModel; }
    @Override
    public String toString() {
        return "PersonModel{" +
                "userModel=" + userModel +
                ", carModel=" + carModel +
                '}';
    }
}

UserModel

public class UserModel {
    private String uName ;
    public void setuName(String uName) {this.uName = uName;}
    public String getuName() { return uName; }
    @Override
    public String toString() {
        return "UserModel{" +
                "uName='" + uName + '\'' +
                '}';
    }
}

CarModel

public class CarModel {
    private String cname;
    private String ctype;
    public CarModel(){}
    public CarModel(String cname,String ctype){
        this.cname = cname;
        this.ctype = ctype;
 }
    public String getCname() { return cname; }
    public void setCname(String cname) { this.cname = cname;}
    public String getCtype() { return ctype;}
    public void setCtype(String ctype) {this.ctype = ctype;}
    @Override
    public String toString() {
        return "CarModel{" +
                "cname='" + cname + '\'' +
                ", ctype='" + ctype + '\'' +
                '}';
    }
}

beans.xml

 <!-- 通过构造器方式注入容器中的bean -->
    <bean id="userModel" class="com.zjhc.innerbean.UserModel">
        <property name="uName" value="111"/>
    </bean>
     <bean id="personModel" class="com.zjhc.innerbean.PersonModel">
         <!--通过ref引用容器中定义的其他bean-->
         <constructor-arg name="userModel" ref="userModel"/>
         <constructor-arg index="1">
             <bean class="com.zjhc.innerbean.CarModel">
                 <constructor-arg index="0" value="宾利"/>
                 <constructor-arg index="1" value="ssl"/>
             </bean>
         </constructor-arg>
      </bean>
      
    <!-- 通过setter方式注入容器中的bean -->
     <bean id="personModel2" class="com.zjhc.innerbean.PersonModel">
         <property name="userModel" ref="userModel"/>
         <property name="carModel">
             <bean class="com.zjhc.innerbean.CarModel">
                 <property name="cname" value="保时捷"/>
                 <property name="ctype" value="X++"/>
             </bean>
         </property>
     </bean>
3.2.1.4 其他类型注入
  1. 注入java.util.List(list元素)
<property name="">
<list>
    <value>Spring</value><ref bean="bean名称"/><list></list><bean></bean><array></array><map></map>
</list>
</property>
  1. 注入java.util.Set(set元素)
<property name="">
<set>
    <value>Spring</value><ref bean="bean名称"/><list></list><bean></bean><array></array><map></map>
</set>
</property>
  1. 注入java.util.Map(map元素)
<property name="">
<map>
    <entry key="jaja" value="30"/><entry key-ref="key引用的bean名称" value-ref="value引用的bean名称"/>
</map>
</property>
  1. 注入数组(array元素)
<property name="">
<array>
    <value>数组中元素</value>
</array>
</property>
  1. 注入java.util.Properties(props元素)
<property name="">
   <props>
        <prop key="username">zksc</prop>
        <prop key="password">123456</prop>
        <prop key="port">8888</prop>
    </props>
</property>
  1. 注入null值或者空串
<property name="">
  <null/>
</property>
<property name="cname" value=""/>

3.2.2 自动注入(xml方式和注解方式)

3.2.2.1 自动注入之xml方式
<bean id="" class="" autowire="byType|byName|constructor|default" />
  1. byName:会自动在容器上下文查找和自己对象里的属性的set方法后面的值相同的bean的id,需要保证IOC容器里bean的id要全局唯一
  2. byType:会自动在容器上下文查找和自己对象里的属性的set方法参数类型相同的bean,需要保证IOC容器里bean的类型要全局唯一
  3. constructor:判断当前构造器所有参数是否在容器中都可以找到匹配的bean对象,如果可以找到就使用这个构造器进行注入。
  4. default:根元素beans下有个default-autowire属性,这个属性可以批量设置当前文件中所有bean的自动注入的方式。
3.2.3.2 自动注入之注解方式

注意:spring全注解开发只需要在beans.xml中配置包扫描

3.2.3.2.1 @Autowired

@Autowired为spring的注解,默认通过ByType注入,当通过byType无法唯一确定bean的时候再通过byName查找.
当容器中有多个该对象时,通过ByType和byName都无法确定时,需要**@Qualifier**的value属性指定唯一的bean对象注入

3.2.3.2.2 @Resource

@Resource为java的注解,先通过ByName去容器中找,找不到再通过ByType类型去找;
当容器中有多个该对象时,通过ByType和byName都无法确定时,通过@Resource的name属性指定唯一的bean对象注入。

3.2.3.2.2 @Value

四、 Spring中常用的注解

4.1 Spring创建bean对象的注解(需要xml)

注意:spring4之后使用注解需要先导入aop依赖。

4.1.1 @Compent,@Repository,@Service,@Controller

    @Component
    public class ComponentUser {
      @Value("张三")
      private String name; 
      //@Value("张三")
      public void setName(String name) {this.name = name;}
    }

@Component 意思是组件,加在类上说明这个类被spring管理了,相当于一个bean.
@Component 等价于 <bean id="user" class="com.zjhc.ComponentUser"></bean>
@Value 等价于<property name="name" value="张三"/> ,也可以加在set方法上
@Component的衍生注解:在web开发中,会按照mvc三层架构分层

  • dao:@Repository
  • service:@Service
  • controller:@Controller

注意:这四个注解的功能是一样的,都是将某个类注册到spring容器中,创建并装配bean

4.1.2 配置包扫描

4.1.2.1在 beans.xml配置注解扫描
<!--指定要扫描的组件包,这个包下的注解就会生效-->
<context:component-scan base-package="com.zjhc.component"/>
<!--开启注解支持-->
<context:annotation-config/>
4.1.2.2 包扫描的两种细节配置方式:
<!--
use-default-filters="false" 表示不使用默认filter,自己配置filter不全部扫描
context:include-filter 设置扫描哪些内容
比如:只扫描@Controller
-->
<context:component-scan base-package="com.zjhc" use-default-filters="false">
   <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--
context:exclude-filter 设置不扫描哪些内容
比如:除过@Controller,其他的都扫描
-->
<context:component-scan base-package="com.zjhc">
   <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

4.2 使用java创建bean对象的注解(不需要xml,bean批量注册)

4.2.1 @Configuration和@Bean

JavaConfig是spring的一个子项目,在spring4之后,它成为了核心功能

@Configuration
public class MyConfig {
   @Bean
   public User getUser(){
       return new User();
   }
}

等价于

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启注解支持-->
    <context:annotation-config/>
</beans>
  • @Configuration :代表这是一个配置类,和spring的beans.xml配置文件是等价的,这个注解配置的类会被spring托管,注册到容器中,也是一个@Component
  • @Bean :注册一个bean,相当于beans.xml里面的,@Bean方法的名字相当于的id属性,方法的返回值相当于的class属性
  • @Import :当有多个config配置类是。也可以通过@Import(类.class)进行组合
  • @ComponentScan: 指定要扫描的包,该包下的注解就会生效
  • 通过AnnotationConfigApplicationContext来加载@Configuration注解修饰的类

4.2.2 spring中,类上加不加@Configuration注解,有什么区别?

  1. @Configuration注解修饰的类,会被spring通过cglib做增强处理,通过cglib会生成一个代理对象,代理会拦截所有被@Bean注解修饰的方法,可以确保创建的bean是单例的,多次调用时时同一个bean。不加该注解则生成的生成的时多例的bean。
  2. 不管@Bean所在的类上是否有@Configuration注解,都可以将@Bean修饰的方法作为一个bean注册到spring容器中
  3. 只有当该配置类作为获取容器的方法AnnotationConfigApplicationContext的参数时,配置类上的@Configuration可以不写。如果配置类上都不加@Configuration注解,那么可以把这些配置类作为参数传到AnnotationConfigApplicationContext方法里。

案例:bean之间是有依赖关系
ServiceA

public class ServiceA {}

ServiceB

public class ServiceB {
    private ServiceA serviceA;
    public ServiceB(ServiceA serviceA) { this.serviceA = serviceA;}
    @Override
    public String toString() {
        return "ServiceB{" + "serviceA=" + serviceA + '}';
        }
}

MyConfig

@Configuration
public class MyConfig {

    @Bean
    public ServiceA serviceA(){
        System.out.println("调用ServiceA方法");
        return new ServiceA();
    }
    @Bean
    public ServiceB serviceB1(){
        System.out.println("调用ServiceB1方法");
        ServiceA serviceA = this.serviceA();
        return new ServiceB(serviceA);
    }
    @Bean
    public ServiceB serviceB2(){
        System.out.println("调用ServiceB2方法");
        ServiceA serviceA = this.serviceA();
        return new ServiceB(serviceA);
    }
}

测试

 @Test
    public void test(){
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        for (String beanDefinitionName : context.getBeanDefinitionNames()) {
            String[] aliases = context.getAliases(beanDefinitionName);
            System.out.println(String.format("bean名称:%s,别名:%s,bean对象:%s",
                    beanDefinitionName, 
                    Arrays.asList(aliases),
                    context.getBean(beanDefinitionName)));
        }
    }

结果:
1.有@Configuration
在这里插入图片描述
2.没有@Configuration
在这里插入图片描述

4.3 其他常用注解

4.3.1 @ComponentScan和ComponentScans(bean批量注册)

  1. @ComponentScan用于批量注册bean,spring会按照这个注解的配置,递归扫描指定包中的所有类,将满足条件的类批量注册到spring容器中
  2. 可以通过value、basePackages、basePackageClasses 这几个参数来配置包的扫描范围
  3. 可以通过useDefaultFilters、includeFilters、excludeFilters这几个参数来配置类的过滤器,被过滤器处理之后剩下的类会被注册到容器中
  4. 指定包名的方式配置扫描范围存在隐患,包名被重命名之后,会导致扫描实现,所以一般我们在需要扫描的包中可以创建一个标记的接口或者类,作为basePackageClasses的值,通过这个来控制包的扫描范围
  5. @CompontScan注解会被ConfigurationClassPostProcessor类递归处理,最终得到所有需要注册的类
4.3.1.1 源码定义
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class) //@1
public @interface ComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
    String resourcePattern() default "**/*.class";
    boolean useDefaultFilters() default true;
    Filter[] includeFilters() default {};
    Filter[] excludeFilters() default {};
    boolean lazyInit() default false;
}

常用参数:

  1. value:指定需要扫描的包,如:com.javacode2018
  2. basePackages:作用同value;value和basePackages不能同时存在设置,可二选一
  3. basePackageClasses:指定一些类,spring容器会扫描这些类所在的包及其子包中的类
  4. nameGenerator:自定义bean名称生成器
  5. resourcePattern:需要扫描包中的那些资源,默认是:**/*.class,即会扫描指定包中所有的class文件
  6. useDefaultFilters:对扫描的类是否启用默认过滤器,默认为true
  7. includeFilters:过滤器:用来配置被扫描出来的那些类会被作为组件注册到容器中
  8. excludeFilters:过滤器,和includeFilters作用刚好相反,用来对扫描的类进行排除的,被排除的类不会被注册到容器中
  9. lazyInit:是否延迟初始化被注册的bean
  10. @Repeatable(ComponentScans.class),这个注解可以同时使用多个
4.3.1.2 工作过程:
  1. Spring会扫描指定的包,且会递归下面子包,得到一批类的数组
  2. 然后这些类会经过上面的各种过滤器,最后剩下的类会被注册到容器中
4.3.1.3 关键问题:
  1. 需要扫描哪些包?
    通过value、backPackages、basePackageClasses这3个参数来控制
  2. 过滤器有哪些?
    通过useDefaultFilters、includeFilters、excludeFilters这3个参数来控制过滤器
4.3.1.4 扫描规则:

默认情况下,任何参数都不设置的情况下会将@ComponentScan修饰的类所在的包作为扫描包。
默认情况下,useDefaultFilters=true,spring容器内部会使用默认过滤器,规则是:凡是类上有@Repository、@Service、@Controller、@Component这几个注解中的任何一个的,那么这个类就会被作为bean注册到spring容器中,所以默认情况下,只需在类上加上这几个注解中的任何一个,这些类就会自动交给spring容器来管理了。

4.3.1.5 案例1:任何参数未设置

分别在dao,controller,service包下创建类,用@Service,@Controller,@Repository注解标注
UserService

package com.zjhc.componentSacn.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {}

UserController

package com.zjhc.componentSacn.controller;
import org.springframework.stereotype.Controller;
@Controller
public class UserController {}

UserDao

package com.zjhc.componentSacn.dao;
import org.springframework.stereotype.Repository;
@Repository
public class UserDao {}

UserModel

package com.zjhc.componentSacn;
import org.springframework.stereotype.Component;
@Component
public class UserModel {}

ScanBean

package com.zjhc.componentSacn;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class ScanBean {}

测试

@Test
public void test2(){
   ApplicationContext context = new AnnotationConfigApplicationContext(ScanBean.class);
   for (String beanName : context.getBeanDefinitionNames()) {
       System.out.println(beanName+"---->"+context.getBean(beanName));
   }
}

使用AnnotationConfigApplicationContext作为ioc容器,将ScanBean.class作为参数传入,默认会扫描ScanBean类所在的包中的所有类,类上有@Component、@Repository、@Service、@Controller任何一个注解的都会被注册到容器中
在这里插入图片描述

4.3.1.6 案例2:指定需要扫描的包

指定需要扫毛哪些包,可以通过value或者basePackage来配置,二者选其一,都配置运行会报错,下面我们通过value来配置

package com.zjhc.componentSacn;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan({"com.zjhc.componentSacn.controller,
                 com.zjhc.componentSacn.dao"})
public class ScanBean {}

测试结果
在这里插入图片描述

4.3.1.7 案例:basePackageClasses指定扫描范围

指定包名的方式扫描存在的一个隐患,若包被重名了,会导致扫描会失效,我们可以在需要扫描的包中定义一个标记的接口或者类,他们的唯一的作用是作为basePackageClasses的值,其他没有任何用途。
定义一个类或者接口

package com.zjhc.componentSacn.controller;
public class a {}
import com.zjhc.componentSacn.controller.a;
import org.springframework.context.annotation.ComponentScan;

//@ComponentScan({"com.zjhc.componentSacn.controller,com.zjhc.componentSacn.dao"})
@ComponentScan(basePackageClasses = a.class)
public class ScanBean {
}
scanBean---->com.zjhc.componentSacn.ScanBean@67e2d983
userController---->com.zjhc.componentSacn.controller.UserController@5d47c63f
4.3.1.8 includeFilters和excludeFilters的使用

是一个Filter类型的数组,多个Filter之间为或者关系,即满足任意一个就可以了,看一下Filter的代码:

@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {

    FilterType type() default FilterType.ANNOTATION;

    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    String[] pattern() default {};
}

主要参数:
type:过滤器的类型,是个枚举类型,5种类型
ANNOTATION:通过注解的方式来筛选候选者,即判断候选者是否有指定的注解
ASSIGNABLE_TYPE:通过指定的类型来筛选候选者,即判断候选者是否是指定的类型
ASPECTJ:ASPECTJ表达式方式,即判断候选者是否匹配ASPECTJ表达式
REGEX:正则表达式方式,即判断候选者的完整名称是否和正则表达式匹配
CUSTOM:用户自定义过滤器来筛选候选者,对候选者的筛选交给用户自己来判断
value:和参数classes效果一样,二选一
classes:3种情况如下
当type=FilterType.ANNOTATION时,通过classes参数可以指定一些注解,用来判断被扫描的类上是否有classes参数指定的注解
当type=FilterType.ASSIGNABLE_TYPE时,通过classes参数可以指定一些类型,用来判断被扫描的类是否是classes参数指定的类型
当type=FilterType.CUSTOM时,表示这个过滤器是用户自定义的,classes参数就是用来指定用户自定义的过滤器,自定义的过滤器需要实现org.springframework.core.type.filter.TypeFilter接口
pattern:2种情况如下
当type=FilterType.ASPECTJ时,通过pattern来指定需要匹配的ASPECTJ表达式的值
当type=FilterType.REGEX时,通过pattern来自正则表达式的值

4.3.1.8.1 扫描包含注解的类

我们自定义一个注解,让标注有这些注解的类自动注册到容器中

package com.zjhc.componentSacn.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnno {
}

创建一个类,使用这个注解标注

package com.zjhc.componentSacn.annotation;
@MyAnno
public class Service1 {}

再来一个类,使用spring中的@Compontent标注

package com.zjhc.componentSacn.annotation;
import org.springframework.stereotype.Component;
@Component
public class Service2 {}

再来一个类,使用@CompontentScan标注

package com.zjhc.componentSacn;
@ComponentScan(includeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION,classes = MyAnno.class)})
public class ScanBean2 {}

测试用例

 @Test
public void test3(){
    ApplicationContext context = new AnnotationConfigApplicationContext(ScanBean2.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(beanName+"---->"+context.getBean(beanName));
    }
}

结果
在这里插入图片描述
问题:Service1上标注了@MyBean注解,被注册到容器了,但是没有标注@MyBean啊,怎么也被注册到容器了?
回答:@CompontentScan注解中的useDefaultFilters默认是true,表示会启用默认的过滤器,默认的过滤器会将标注有@Component、@Repository、@Service、@Controller这几个注解的类也注册到容器中。

修改扫描代码:
如果我们只想将标注有@MyBean注解的bean注册到容器,需要将默认过滤器关闭,即:useDefaultFilters=false

package com.zjhc.componentSacn;
@ComponentScan(useDefaultFilters = false,includeFilters = {@ComponentScan.Filter(type= FilterType.ANNOTATION,classes = MyAnno.class)})
public class ScanBean2 {}

再输出:
在这里插入图片描述

4.3.1.8.2 包含指定类型的类

被扫描的类满足IService.class.isAssignableFrom(被扫描的类)条件的都会被注册到spring容器中
@ComponentScan(
useDefaultFilters = false,
includeFilters = {
@ComponentScan.Filter(type= FilterType.ASSIGNABLE_TYPE,classes = IService.class)}
)
接口

package com.zjhc.componentSacn.annotation.componentBytype;
public interface IService {}

实现类

package com.zjhc.componentSacn.annotation.componentBytype;
public class ServiceA implements IService {}

package com.zjhc.componentSacn.annotation.componentBytype;
public class ServiceB implements IService {}

@CompontentScan标注的类

package com.zjhc.componentSacn.annotation.componentBytype;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
@ComponentScan(useDefaultFilters = false,
includeFilters = {@ComponentScan.Filter(type= FilterType.ASSIGNABLE_TYPE,
classes = IService.class)})
public class ScanBean3 {}

测试:

@Test
public void test4(){
   ApplicationContext context = new AnnotationConfigApplicationContext(ScanBean3.class);
   for (String beanName : context.getBeanDefinitionNames()) {
       System.out.println(beanName+"---->"+context.getBean(beanName));
   }
}

在这里插入图片描述

4.3.1.8.3 自定义Filter

步骤:
1.设置@Filter中type的类型为:FilterType.CUSTOM
2.自定义过滤器类,需要实现接口:org.springframework.core.type.filter.TypeFilter
3.设置@Filter中的classses为自定义的过滤器类型

TypeFilter这个接口的定义:
是一个函数式接口,包含一个match方法,方法返回boolean类型,有2个参数,都是接口类型的,下面介绍一下这2个接口

@FunctionalInterface
public interface TypeFilter {
    boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException;
}

MetadataReader接口:
类元数据读取器,可以读取一个类上的任意信息,如类上面的注解信息、类的磁盘路径信息、类的class对象的各种信息,spring进行了封装,提供了各种方便使用的方法。
MetadataReader接口的定义:

public interface MetadataReader {
    /**
     * 返回类文件的资源引用
     */
    Resource getResource();

    /**
     * 返回一个ClassMetadata对象,可以通过这个读想获取类的一些元数据信息,
     * 如类的class对象、是否是接口、是否有注解、是否是抽象类、父类名称、接口名称、
     * 内部包含的之类列表等等,可以去看一下源码
     */
    ClassMetadata getClassMetadata();

    /**
     * 获取类上所有的注解信息
     */
    AnnotationMetadata getAnnotationMetadata();
}

MetadataReaderFactory接口:
类元数据读取器工厂,可以通过这个类获取任意一个类的MetadataReader对象
MetadataReaderFactory接口定义:

public interface MetadataReaderFactory {
    /**
     * 返回给定类名的MetadataReader对象
     */
    MetadataReader getMetadataReader(String className) throws IOException;

    /**
     * 返回指定资源的MetadataReader对象
     */
    MetadataReader getMetadataReader(Resource resource) throws IOException;
}

案例:
需求:我们来个自定义的Filter,判断被扫描的类如果是IService接口类型的,就让其注册到容器中。
接口

package com.zjhc.componentSacn.annotation.componentBytype;
public interface IService {}

实现类

package com.zjhc.componentSacn.annotation.componentBytype;
public class ServiceA implements IService {}

package com.zjhc.componentSacn.annotation.componentBytype;
public class ServiceB implements IService {}

自定义的TypeFilter类:

package com.zjhc.componentSacn.annotation.componentByfilter;

import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import java.io.IOException;
public class MyTypeFilter implements TypeFilter {
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        Class clzz = null;
        try {
            //获取当前被扫描的类
            clzz = Class.forName(metadataReader.getClassMetadata().getClassName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        //判断clzz是否为IService类型
        boolean result = IService.class.isAssignableFrom(clzz);
        return result;
    }
}

@CompontentScan标注的类

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
@ComponentScan(useDefaultFilters = false,
includeFilters = {@ComponentScan.Filter(type= FilterType.CUSTOM,
classes = MyTypeFilter.class)})
public class ScanBean4 {
}
4.3.1.9 @ComponentScan重复使用
@ComponentScans({
        @ComponentScan(basePackageClasses = a.class),
        @ComponentScan(
                useDefaultFilters = false, //不启用默认过滤器
                includeFilters = {
                        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IService.class)
                })})
public class ScanBean3 {
}

4.3.2 @Import使用:bean的批量注册

@Import可以用来批量导入需要注册的各种类,如普通的类、配置类,然后完成普通类和配置类中所有bean的注册。
@Import可以使用在任何类型上,通常情况下,类和注解上用的比较多。
value:一个Class数组,设置需要导入的类,可以是@Configuration标注的列,可以是ImportSelector接口或者ImportBeanDefinitionRegistrar接口类型的,或者需要导入的普通组件类。
当使用AnnotationConfigApplicationContext(MyConfig.class)加载配置类创建容器时,方法参数为某个主配置类的字节码文件,此时主配置类上可以不用加@Configuration注解。当有多个创建bean的配置类时,通过**@Import**注解在主配置类上进行导入其他配置类,此时其他配置类上也可以不加@Configuration注解。

源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
    /**
     * {@link Configuration @Configuration}, {@link ImportSelector},
     * {@link ImportBeanDefinitionRegistrar}, 
     * or regular component classes to import.
     */
    Class<?>[] value();
}

@Import的value常见的有5种用法

  1. value为普通的类
@Import({Service1.class, Service2.class})
public class MainConfig1 {}
com.demo24.test1.Service1->com..demo24.test1.Service1@7e0b85f9
com.demo24.test1.Service2->com.demo24.test1.Service2@63355449

通过@Import导入的2个类,bean名称为完整的类名,可以使用@Compontent注解指定被导入类的bean名称

@Component("service1")
public class Service1 {}
service1->com.demo24.test1.Service1@45efd90f
  1. value为@Configuration标注的类
package com.javacode2018.lesson001.demo24.test2;
//模块1配置类
@Configuration
public class ConfigModule1 {
    @Bean
    public String module1() { return "我是模块1配置类!"; }
    }

package com.javacode2018.lesson001.demo24.test2;
//模块2配置类
@Configuration
public class ConfigModule2 {
    @Bean
    public String module2() { return "我是模块2配置类!"; }
}

package com.javacode2018.lesson001.demo24.test2;
//通过Import来汇总多个@Configuration标注的配置类
@Import({ConfigModule1.class, ConfigModule2.class}) //@1
public class MainConfig2 {}
mainConfig2->com.javacode2018.lesson001.demo24.test2.MainConfig2@ba2f4ec
com.javacode2018.lesson001.demo24.test2.ConfigModule1->com.javacode2018.lesson001.demo24.test2.ConfigModule1$$EnhancerBySpringCGLIB$$700e65cd@1c1bbc4e
module1->我是模块1配置类!
com.javacode2018.lesson001.demo24.test2.ConfigModule2->com.javacode2018.lesson001.demo24.test2.ConfigModule2$$EnhancerBySpringCGLIB$$a87108ee@55fe41ea
module2->我是模块2配置类!
  1. value为@CompontentScan标注的类

项目中分多个模块,每个模块有各自独立的包,我们在每个模块所在的包中配置一个@CompontentScan类,然后通过@Import来导入需要启用的模块
第一个组件的包及里面的类:

package com.javacode2018.lesson001.demo24.test3.module1;
@Component
public class Module1Service1 {}

package com.javacode2018.lesson001.demo24.test3.module1;
@Component
public class Module1Service2 {}

package com.javacode2018.lesson001.demo24.test3.module1;
//模块1的主键扫描
@ComponentScan
public class CompontentScanModule1 {}

第二个组件的包及里面的类:

package com.javacode2018.lesson001.demo24.test3.module2;
@Component
public class Module2Service1 {}

package com.javacode2018.lesson001.demo24.test3.module2;
@Component
public class Module2Service2 {}

package com.javacode2018.lesson001.demo24.test3.module2;
// 模块2的组件扫描
@ComponentScan
public class CompontentScanModule2 {}

总配置类:通过@Import导入每个模块中的组件扫描类

/**
 * 通过@Import导入多个@CompontentScan标注的配置类
 */
@Import({CompontentScanModule1.class, CompontentScanModule2.class}) //@1
public class MainConfig3 {}
  1. value为ImportBeanDefinitionRegistrar接口类型
1. 定义ImportBeanDefinitionRegistrar接口实现类,在registerBeanDefinitions方法中使用registry来注册bean
2. 使用@Import来导入步骤1中定义的类
3. 使用步骤2@Import标注的类作为AnnotationConfigApplicationContext构造参数创建spring容器
4. 使用AnnotationConfigApplicationContext操作bean

这个接口提供了通过spring容器api的方式直接向容器中注册bean

public interface ImportBeanDefinitionRegistrar {
 default void registerBeanDefinitions(
         AnnotationMetadata importingClassMetadata, 
         BeanDefinitionRegistry registry,
         BeanNameGenerator importBeanNameGenerator) {

     registerBeanDefinitions(importingClassMetadata, registry);
 }
 default void registerBeanDefinitions(
          AnnotationMetadata importingClassMetadata, 
          BeanDefinitionRegistry registry) {
 }
}

importingClassMetadata:AnnotationMetadata类型的,通过这个可以获取被@Import注解标注的类所有注解的信息。
registry:BeanDefinitionRegistry类型,是一个接口,内部提供了注册bean的各种方法。
importBeanNameGenerator:BeanNameGenerator类型,是一个接口,内部有一个方法,用来生成bean的名称。

BeanDefinitionRegistry接口:bean定义注册器,提供了bean注册的各种方法,基本上所有bean工厂都实现了这个接口,让bean工厂拥有bean注册的各种能力,AnnotationConfigApplicationContext类也实现了这个接口

public interface BeanDefinitionRegistry extends AliasRegistry {
    /**
     * 注册一个新的bean定义
     * beanName:bean的名称
     * beanDefinition:bean定义信息
     */
    void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException;

    /**
     * 通过bean名称移除已注册的bean
     * beanName:bean名称
     */
    void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

    /**
     * 通过名称获取bean的定义信息
     * beanName:bean名称
     */
    BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

    /**
     * 查看beanName是否注册过
     */
    boolean containsBeanDefinition(String beanName);
    /**
     * 获取已经定义(注册)的bean名称列表
     */
    String[] getBeanDefinitionNames();
    /**
     * 返回注册器中已注册的bean数量
     */
    int getBeanDefinitionCount();
    /**
     * 确定给定的bean名称或者别名是否已在此注册表中使用
     * beanName:可以是bean名称或者bean的别名
     */
    boolean isBeanNameInUse(String beanName);
}

BeanNameGenerator接口:bean名称生成器

public interface BeanNameGenerator {
    String generateBeanName(
    BeanDefinition definition,
     BeanDefinitionRegistry registry);
}

这个接口spring有三个内置的实现:
DefaultBeanNameGenerator:默认bean名称生成器,xml中bean未指定名称的时候,默认就会使用这个生成器,默认为:完整的类名#bean编号
AnnotationBeanNameGenerator:注解方式的bean名称生成器,比如通过@Component(bean名称)的方式指定bean名称,如果没有通过注解方式指定名称,默认会将完整的类名作为bean名称。
FullyQualifiedAnnotationBeanNameGenerator:将完整的类名作为bean的名称

BeanDefinition接口:bean定义信息,用来表示bean定义信息的接口,我们向容器中注册bean之前,会通过xml或者其他方式定义bean的各种配置信息,bean的所有配置信息都会被转换为一个BeanDefinition对象,然后通过容器中BeanDefinitionRegistry接口中的方法,将BeanDefinition注册到spring容器中,完成bean的注册操作。
案例:
Service1:

package com.javacode2018.lesson001.demo24.test4;
public class Service1 {}

Service2:需要注入Service1

package com.javacode2018.lesson001.demo24.test4;
public class Service2 {
    private Service1 service1;

    public Service1 getService1() { return service1;}
    public void setService1(Service1 service1) { this.service1 = service1; }
    @Override
    public String toString() {
        return "Service2{" + "service1=" + service1 +'}'; }
}

定义一个类实现ImportBeanDefinitionRegistrar接口,然后在里面实现上面2个类的注册

package com.javacode2018.lesson001.demo24.test4;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
 @Override
 public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
     //定义一个bean:Service1
     BeanDefinition service1BeanDinition = BeanDefinitionBuilder.genericBeanDefinition(Service1.class).getBeanDefinition();
     //注册bean
     registry.registerBeanDefinition("service1", service1BeanDinition);

     //定义一个bean:Service2,通过addPropertyReference注入service1
     BeanDefinition service2BeanDinition = BeanDefinitionBuilder.genericBeanDefinition(Service2.class).
             addPropertyReference("service1", "service1").
             getBeanDefinition();
     //注册bean
     registry.registerBeanDefinition("service2", service2BeanDinition);
 }
}

定义配置类导入实现类

@Import({MyImportBeanDefinitionRegistrar.class})
public class ImportBean{}

测试:

@Test
public void test4() {
    //1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ImportBean.class);
    //2.输出容器中定义的所有bean信息
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
    }
}
  1. value为ImportSelector接口类型
7. 定义ImportSelector接口实现类,在selectImports返回需要导入的类的名称数组
8. 使用@Import来导入步骤1中定义的类
9. 使用步骤2@Import标注的类作为AnnotationConfigApplicationContext构造参数创建spring容器
10. 使用AnnotationConfigApplicationContext操作bean

ImportSelector接口:

public interface ImportSelector {
    /**
     * 返回需要导入的类名的数组,可以是任何普通类,配置类(@Configuration、@Bean、@CompontentScan等标注的类)
     * @importingClassMetadata:用来获取被@Import标注的类上面所有的注解信息
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);
}

案例:
普通类:Service1

package com.javacode2018.lesson001.demo24.test5;
public class Service1 {}

@Configuration标注的配置类:Module1Config

package com.javacode2018.lesson001.demo24.test5;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Module1Config {
    @Bean
    public String name() {
        return "公众号:路人甲java";
    }
    @Bean
    public String address() {
        return "上海市";
    }
}

自定义一个ImportSelector,然后返回上面2个类的名称

package com.javacode2018.lesson001.demo24.test5;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
public class MyImportSelector implements ImportSelector {
    @Override
   public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{
                Service1.class.getName(),
                Module1Config.class.getName()
        };
    }
}

@Import标注的类,导入MyImportSelector

package com.javacode2018.lesson001.demo24.test5;
import com.lesson001.demo24.test4.MyImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.Import;
/**
 * 通过@Import导入MyImportSelector接口实现类
 */
@Import({MyImportSelector.class})
public class MainConfig5 {}

测试

@Test
public void test5() {
 //1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类
 AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig5.class);
 //2.输出容器中定义的所有bean信息
 for (String beanName : context.getBeanDefinitionNames()) {
     System.out.println(String.format("%s->%s", beanName, context.getBean(beanName)));
 }
}
  1. value为DeferredImportSelector接口类型

DeferredImportSelector是ImportSelector的子接口,既然是ImportSelector的子接口,所以也可以通过@Import进行导入。和ImportSelector不同地方有两点:延迟导入和指定导入的类的处理顺序

延迟导入
@Import的value包含了多个普通类、多个@Configuration标注的配置类、多个ImportSelector接口的实现类,多个ImportBeanDefinitionRegistrar接口的实现类,还有DeferredImportSelector接口实现类,此时spring处理这些被导入的类的时候,会将DeferredImportSelector类型的放在最后处理,会先处理其他被导入的类,其他类会按照value所在的前后顺序进行处理
案例:
来3个配置类,每个配置类中都通过@Bean定一个string类型的bean,内部输出一句文字。
Configuration1:

package com.javacode2018.lesson001.demo24.test7;
@Configuration
public class Configuration1 {
    @Bean
    public String name1() {
        System.out.println("name1");
        return "name1";
    }
}

Configuration2:

package com.javacode2018.lesson001.demo24.test7;
@Configuration
public class Configuration2 {
    @Bean
    public String name2() {
        System.out.println("name2");
        return "name2";
    }
}

Configuration3:

package com.javacode2018.lesson001.demo24.test7;
@Configuration
public class Configuration3 {
    @Bean
    public String name3() {
        System.out.println("name3");
        return "name3";
    }
}

一个ImportSelector实现类,导入Configuration1

package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class ImportSelector1 implements ImportSelector {
   @Override
   public String[] selectImports(AnnotationMetadata importingClassMetadata) {
       return new String[]{ Configuration1.class.getName()};
    }
}

一个DeferredImportSelector实现类,导入Configuration2

package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class DeferredImportSelector1 implements DeferredImportSelector {
   @Override
   public String[] selectImports(AnnotationMetadata importingClassMetadata) {
       return new String[]{Configuration2.class.getName()};
    }
}

一个总的配置类

package com.javacode2018.lesson001.demo24.test7;
import org.springframework.context.annotation.Import;
@Import({
        DeferredImportSelector1.class,
        Configuration3.class,
        ImportSelector1.class,
})
public class MainConfig7 {}

测试及输出:

@Test
public void test7() {
    //1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig7.class);
}
name3
name1
name2

输出的结果结合一下@Import中被导入的3个类的顺序,可以看出DeferredImportSelector1是被最后处理的,其他2个是按照value中所在的先后顺序处理的。

指定导入的类的处理顺序
@Import中有多个DeferredImportSelector接口的实现类时候,可以指定他们的顺序,指定顺序常见2种方式:
实现Ordered接口的方式:

org.springframework.core.Ordered
public interface Ordered {
    int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
    int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
    int getOrder();
}

实现Order注解的方式:

org.springframework.core.annotation.Order

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
    int value() default Ordered.LOWEST_PRECEDENCE;
}

案例:
2个配置类,内部都有一个@Bean标注的方法,用来注册一个bean
Configuration1:

package com.javacode2018.lesson001.demo24.test8;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Configuration1 {
    @Bean
    public String name1() {
        System.out.println("name1");
        return "name1";
    }
}

Configuration2:

package com.javacode2018.lesson001.demo24.test8;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class Configuration2 {
    @Bean
    public String name2() {
        System.out.println("name2");
        return "name2";
    }
}

来2个DeferredImportSelector实现类,分别来导入上面2个配置文件,顺便通过Ordered接口指定一下顺序,DeferredImportSelector1的order为2,DeferredImportSelector2的order为1,order值越小优先级越高
DeferredImportSelector1:

package com.javacode2018.lesson001.demo24.test8;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;

public class DeferredImportSelector1 implements DeferredImportSelector, Ordered {
   @Override
   public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{Configuration1.class.getName()};
    }
    @Override
    public int getOrder() {
        return 2;
    }
}

DeferredImportSelector2:

package com.javacode2018.lesson001.demo24.test8;
import com.javacode2018.lesson001.demo24.test7.Configuration2;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotationMetadata;

public class DeferredImportSelector2 implements DeferredImportSelector, Ordered {
   @Override
   public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{Configuration2.class.getName()};
    }
    @Override
    public int getOrder() {
        return 1;
    }
}

来个总的配置类,引入上面两个ImportSelector
MainConfig8:

package com.javacode2018.lesson001.demo24.test8
import org.springframework.context.annotation.Import;
@Import({
        DeferredImportSelector1.class,
        DeferredImportSelector2.class,
})
public class MainConfig8 {}

测试输出:

@Test
public void test8() {
    //1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig8.class);
}
name2
name1
4.3.2.1 案列

需求
凡是类名中包含service的,调用他们内部任何方法,我们希望调用之后能够输出这些方法的耗时
实现分析
我们可以通过代理来实现,bean实例创建的过程中,我们可以给这些bean生成一个代理,在代理中统计方法的耗时,这里面有2点:

  1. 创建一个代理类,通过代理来间接访问需要统计耗时的bean对象
  2. 拦截bean的创建,给bean实例生成代理生成代理

具体实现
Service1

package com.javacode2018.lesson001.demo24.test6;
import org.springframework.stereotype.Component;
@Component
public class Service1 {
    public void m1() {
        System.out.println(this.getClass() + ".m1()");
    }
}

Service2

package com.javacode2018.lesson001.demo24.test6;
import org.springframework.stereotype.Component;
@Component
public class Service2 {
    public void m1() {
        System.out.println(this.getClass() + ".m1()");
    }
}

创建统计耗时的代理类:用cglib来实现一个代理类

package com.javacode2018.lesson001.demo24.test6;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CostTimeProxy implements MethodInterceptor {
    //目标对象
    private Object target;

    public CostTimeProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        long starTime = System.nanoTime();
        //调用被代理对象(即target)的方法,获取结果
        Object result = method.invoke(target, objects); //@1
        long endTime = System.nanoTime();
        System.out.println(method + ",耗时(纳秒):" + (endTime - starTime));
        return result;
    }

    /**
     * 创建任意类的代理对象
     *
     * @param target
     * @param <T>
     * @return
     */
    public static <T> T createProxy(T target) {
        CostTimeProxy costTimeProxy = new CostTimeProxy(target);
        Enhancer enhancer = new Enhancer();
        enhancer.setCallback(costTimeProxy);
        enhancer.setSuperclass(target.getClass());
        return (T) enhancer.create();
    }
}

拦截bean实例的创建,返回代理对象

package com.javacode2018.lesson001.demo24.test6;
import com.javacode2018.lesson001.demo23.test4.CostTimeProxy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.Nullable;

public class MethodCostTimeProxyBeanPostProcessor implements BeanPostProcessor {
    @Nullable
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean.getClass().getName().toLowerCase().contains("service")) {
            return CostTimeProxy.createProxy(bean); //@1
        } else {
            return bean;
        }
    }
}

需要将MethodCostTimeProxyBeanPostProcessor注册到容器中才会起作用,下面我们通过@Import结合ImportSelector的方式来导入这个类,将其注册到容器中

package com.javacode2018.lesson001.demo24.test6;
import com.javacode2018.lesson001.demo23.test4.MethodCostTimeProxyBeanPostProcessor;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MethodCostTimeImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{MethodCostTimeProxyBeanPostProcessor.class.getName()};
    }
}

来一个@Import来导入MethodCostTimeImportSelector,下面我们使用注解的方式,在注解上使用@Import

package com.javacode2018.lesson001.demo24.test6;
import com.javacode2018.lesson001.demo23.test4.MethodCostTimeImportSelector;
import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MethodCostTimeImportSelector.class)
public @interface EnableMethodCostTime {
}

来一个总的配置类

package com.javacode2018.lesson001.demo24.test6;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableMethodCostTime //@1
public class MainConfig6 {
}

上面使用了@CompontentScan注解,此时会将Servce1和Service2这两个类注册到容器中。
@1:此处使用了@EnableMethodCostTime注解,而@EnableMethodCostTime注解上使用了@Import(MethodCostTimeImportSelector.class),此时MethodCostTimeImportSelector类中的MethodCostTimeProxyBeanPostProcessor会被注册到容器,会拦截bean的创建,创建耗时代理对象。

测试:

@Test
public void test6() {
    //1.通过AnnotationConfigApplicationContext创建spring容器,参数为@Import标注的类
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig6.class);
    Service1 service1 = context.getBean(Service1.class);
    Service2 service2 = context.getBean(Service2.class);
    service1.m1();
    service2.m1();
}

结果:

class com.javacode2018.lesson001.demo24.test6.Service1.m1()
public void com.javacode2018.lesson001.demo24.test6.Service1.m1(),耗时(纳秒)74200
class com.javacode2018.lesson001.demo24.test6.Service2.m1()
public void com.javacode2018.lesson001.demo24.test6.Service2.m1(),耗时(纳秒)33800

如果我们不想开启方法耗时统计,只需要将MainConfig6上的@EnableMethodCostTime去掉就可以了
spring中有很多类似的注解,以@EnableXXX开头的注解,基本上都是通过上面这种方式实现的,如:

@EnableAspectJAutoProxy
@EnableCaching
@EnableAsync

4.3.3 @Conditional 通过条件控制Bean的注册

请浏览另一篇博客:@Conditional注解的详解和应用

4.3.3 @Import使用:bean的批量注册

4.4 java注解的定义和使用

关于注解定义的详解,浏览博客:Java注解的定义和使用以及spring对注解的增强

五、AOP面向切面编程

5.1 代理模式

5.1.1 什么是代理模式?

在这里插入图片描述
为其它对象提供一个代理以控制对这个对象的访问,在某些情况下,一个对象不适合或不能直接访问另一个对象,而代理可以在客户类与目标对象之间起到中介的作用。 具有这种访问关系呈现出来的模式称之为代理模式

5.1.2 代理的作用

  • 功能增强:在原有功能的基础上增加了额外的功能
  • 控制访问:代理类不让客户能直接访问目标对象

5.2 代理的实现

5.2.1 静态代理

(1) 含义:

静态代理类是手工创建的一个java类,所要代理的目标是确定的。代理类包含了目标对象,在对目标对象的方法进行调用时可以进行功能增强。

(2)图示

在这里插入图片描述
代理类B、C和目标类D、E均实现了接口F中抽象方法出售U盘的功能。但厂家D、E不向个人客户A直接出售,只向代理商家B、C出售。某一天,F新增了U盘的某功能,类B、C、D、E均要修改

(3)角色分析
  • 抽象角色(租房):一般是接口或者抽象类
  • 真实角色(房东):被代理的角色
  • 代理角色(中介):代理真实角色,可以做扩展操作,比如看房子,收中介费等
  • 客户:访问代理对象的人
(4)优缺点
  • 静态代理模式好处:
    • 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务
    • 公共业务交给代理角色,实现业务的分工
    • 公共业务发生扩展的时候,方便集中管理
  • 静态代理模式缺点:
    • 为了个性化增强,每一个真实的角色就会产生一个代理角色,代码量会翻倍,开发效率会变低
    • 当接口变化时,代理类和被代理类都要相应变化。
(5)demo展示
  • 接口类
//接口类
public interface UserService{
 void add();
} 
  • 真实角色类 实现 接口
public class UserServiceImpl implements UserService{
	@Override
    public void add() {
       System.out.println("新增了一个用户");
    }
}
  • 代理角色类 代理 真实角色类
    如果UserServiceITwompl也实现了UserService,那么也要编写UserServiceTwoImplProxy
public class UserServiceImplProxy implements UserService{
   private UserServiceImpl userServiceImpl;
   public void setUserServiceimpl(UserServiceImpl userServiceimpl) {
        this.userServiceimpl = userServiceimpl;
    }
   @Override
    public void add() {
    log("add");
    userServiceimpl.add();
    }
    //增加日志功能
    public void log(String msg){
       System.out.println("执行了"+msg+"方法");
    }
}
  • 客户类
public class Client {
    public static void main(String[] args) {
        UserServiceImpl userService = new UserServiceImpl();
      //UserServiceTwoImpl userServiceT = new UserServiceTwoImpl();
        UserServiceProxy proxy = new UserServiceProxy();
        proxy.setUserServiceimpl(userService);
      //proxy.setUserServiceimpl(userServiceT);
        proxy.del();
    }
}

5.2.2 动态代理

(1)含义

在程序执行过程中,JDK利用反射机制创建对象的能力创建代理对象,不用创建代理类。代理目标不是确定的,而是可活动的,变化的。
动态代理支持在系统运行期给类动态添加代理,然后通过操控代理类完成对目标类的调用

(2)分类
  • 动态代理分为两大类:
  • 基于接口的动态代理:JDK动态代理 :
    • 使用java反射包java.lang.reflect中的类和接口实现动态代理的功能
    • 类:ProxyMethod
    • 接口:InvocationHandler
  • 基于类的动态代理:cglib动态代理:
    • 第三方的开源项目,高效的code生成类库,可以在运行期间扩展java类,实现java接口,被广泛的AOP框架使用

使用JDK动态代理,要求目标类与代理类实现相同接口。若目标类没实现接口,则不能使用该方法实现。此时可以考虑使用cgLib。cgLib代理的生成原理是生成目标类的子类,子类对象即代理对象。因此目标类不能是final的。

(3)优缺点
  • 动态代理模式好处:
    • 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务
    • 公共业务交给代理角色,实现业务的分工
    • 公共业务发生扩展的时候,方便集中管理
    • 一个动态代理类,可以代理实现了同一个接口的多个类
    • 当在接口中修改方法时,不用修改代理类
    • JDK动态代理要求代理目标必须实现接口

在实际开发中,将日志、事务等与业务方法无关、但很多业务方法都要执行的代码放在InvocationHandler类invoke方法的功能增强中,这样一来:

  • 解耦合,使业务代码与非业务代码分离,业务类专注于业务的实现
  • 减少代码复用
(5)demo
  • 接口类
    • 目标接口,定义目标类实现的功能
//接口类
public interface UserService{
 void add();
} 
  • 目标类
    • JDK动态代理要求代理目标必须实现接口
public class UserServiceImpl implements UserService{
	@Override
    public void add() {
        System.out.println("UserService的add方法执行:新增了一个用户");
    }
}
  • 动态代理类
    创建InvocationHandler的实现类,在invoke方法中实现代理功能
    主要步骤:

    • 创建动态代理的目标对象
    • 通过构造方法或者set方法注入目标对象,首选构造方法注入
    • 生成动态代理类对象,并将返回值转为接口类型
    • 调用应用处理程序的invoke方法处理代理实例,如有返回值,接收返回结果,可进行强制类型转换;
    • 在invoke实现功能增强(日志、事务等)

    注意:
    invoke方法是java.lang.reflect.InvocationHandler中的方法,此方法实现了对原有目标对象的业务方法的调用,即目标类的方法的调用,并在此基础上可以选择进行一定的功能增强。此方法的参数均为JDK管理,不需要人工传入:

    • Object proxy:目标类的代理对象
    • Method method:目标类的业务方法对象
    • Object[] args:目标类的业务方法所需的参数(例如本例业务方法没有参数,args为空)
  • 创建动态代理类Proxy对象的参数:

    • 第一个参数是目标类的类加载器,可看作固定写法
    • 第二个参数是目标类的接口对象,可看作固定写法,正是因为这个参数决定了JDK动态代理必须实现接口
    • 第三个参数是包含目标类的MyInvocationHandle 对象
public class MyInvocationHandle implements InvocationHandle{
	//1.动态代理的目标对象
	private object target;
	//2.构造方法注入 / set方法注入  
	public void setTarget(Object target){
		this.target = target;
	}
	public MyInvocationHandle (Object target){
		this.target = target;
	}
	//3.生成代理类对象
	public Object getProxy(){
		return Proxy.getInstance(target.getClass().getClassLoader(),
					target.getClass().getInterfaces(),
					this);
	}

	//4.处理代理实例,返回结果
	@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //动态代理的本质,就是通过反射实现
        //1.调用目标方法
        Object result = method.invoke(target, args);
        //实现功能增强
        System.out.println("myInvocationHandler类的invoke方法执行," +
                "在原有业务方法结束后增强打印日志功能");
        log(method.getName());
        return result;
    }
	//4. 加入要增加一个输出日志的功能
    public void log(String msg){
        System.out.println("执行了"+msg+"方法");
    }
}
  • 客户类调用步骤:
    • 创建目标对象
    • 创建InvocationHandle对象,构造器参数决定了代理目标,传谁代理谁
    • 创建代理对象,并将返回值强转为接口类型,强转成功的原因是目标对象target实现了 UserService接口
    • 实现代理的业务功能
public class Client{
	public static void main(String[] args){
	    //创建目标对象
		UserService userService = new UserServiceImpl();
		//创建InvocationHandle对象,构造器参数决定了代理目标,传谁代理谁
		InvocationHandle Handle = new MyInvocationHandle(userService);
		//创建代理对象,并将返回值转为接口类型
		//强转成功的原因在于目标对象target实现了ISomeService接口
		UserService proxy= (UserService)handle.getProxy();
		//实现代理的业务功能
		proxy.add();
	}
}

5.3 AOP(Aspect Oriented Programming)面向切面编程

5.3.1 概述

在面向切面编程的思想里面,把功能分为核心业务功能,和周边功能:

  • 所谓的核心业务,比如登陆,增加数据,删除数据都叫核心业务
  • 所谓的周边功能,比如性能统计,日志,事务管理等等
    周边功能在 Spring 的面向切面编程AOP思想里,即被定义为切面

定义:AOP(Aspect Oriented Programming 面向切面编程),通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
目的:是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中,从而减少系统的重复代码量,并且能够降低模块间的耦合度,有利于程序代码的扩展和维护,降低了维护成本,提高了开发的效率。
交叉业务逻辑:是指通用的、与主业务逻辑无关,却被各个业务模块大量调用代码,如安全检查、 事务、日志、缓存等。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.3.2 AOP的相关术语

  • 横切关注点:跨越应用程序多个模块的方法或者功能。如:日志记录、缓存、安全检查、事务处理等。
  • 切面(Aspect):横切关注点被模块化的对象,指封装好的用于插入业务逻辑功能的类,是对业务逻辑的一种增强
  • 连接点(Joinpoint):指被切面织入的具体方法。通常业务接口中所有可以增强的方法均为连接点。Spring中一个连接点表示一个方法的调用
  • 切入点(Pointcut):一个或者多个连接点的集合,定义了切面在何处执行,切点也是一个连接点,也指满足匹配规则的一组类或者方法
  • 通知/增强(Advice):在切面类中定义的方法,也是在切点处要执行的代码,通知定义了通知代码切入到目标代码的时间点,是在目标代码之前执行还是之后执行,通知类型不同切入时间不同
  • 目标(Target):将要被增强的对象。即包含主业务逻辑的类的对象
  • 代理(Proxy):目标对象增强后创建的代理对象;
  • 织入(Wearving):把切面应用到目标对象并创建新的代理对象的过程,切面在指
    定的连接点被织入到目标对象中。
    • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的
    • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-timeweaving,LTW)就支持以这种方式织入切面
    • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织
      入切面时,AOP容器会为目标对象动态地创建一个代理对象。
      Spring AOP就是以这种方式织入切面的
  • 引入(Introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
    在这里插入图片描述

5.3.3 AOP实现方式

5.3.3.1 AOP的技术实现框架:
  • Spring:Spring的API实现较为笨重,一般用在事务处理
  • aspectJ: 一个开源,专门做AOP的框架,隶属于Eclipse基金会
5.3.3.2 AspectJ 对 AOP 的实现
  • aspectJ框架实现AOP有两种方式:
    • 使用xml配置文件,一般用于配置全局事务
    • 使用注解,一般在项目开发中使用这种方式
(1) AspectJ 的通知类型

AspectJ 中常用的通知有五种类型,体现在五个不同的添加在切面的注解:

  1. 前置通知:@Before
  2. 后置通知:@AfterReturning、
  3. 环绕通知:@Around、
  4. 异常通知:@AfterThrowing、
  5. 最终通知:@After
(2) AspectJ 的切入点表达式
execution ( [modifiers-pattern]  访问权限类型
	 ret-type-pattern 返回值类型
	  [declaring-type-pattern]  全限定性类名 
	  name-pattern(param-pattern) 方法名(参数类型和参数个数) 
	 [throws-pattern]  抛出异常类型 ) 

① execution (* com.sample.service.impl…*. *(…))

  1. execution(): 表达式主体。
  2. 第一个星号:表示返回类型, 星号表示所有的类型。
  3. 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包。
  4. 第二个*号:表示类名,*号表示所有的类。
  5. *(…):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
  6. ② 通过方法签名定义切点
    execution(public * *(…)) 匹配所有目标类的public方法

③ 通过类定义切点
execution(* com.baobaotao.Waiter.(…)) 匹配Waiter包下所有类的所有方法,参数不限
execution(
com.baobaotao.Waiter+.(…)) 匹配Waiter接口及其所有实现类的方法
④ 通过类包定义切点
在类名模式串中,“.”表示包下的所有类,而“…”表示包、子孙包下的所有类
execution(
com.baobaotao.(…)) 匹配com.baobaotao包下所有类的所有方法
execution(
com.baobaotao…(…)) 匹 配com.baobaotao包、子孙包下所有类的所有方法,如com.baobaotao.dao,com.baobaotao.servier以及 com.baobaotao.dao.user包下的所有类的所有方法都匹配。“…”出现在类名中时,后面必须跟“”,表示包、子孙包下的所有类

切入点表达式 要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。注意,表达式中加[ ]的部分表示可省略部分,各部分间用空格分开。
在其中可以使用以下符号:
*:0至多个任意字符
… : 用在方法参数中,表示任意多个参数;用在包名后,表示当前包及其子包
+:用在类名后,表示当前类及其子类;用在接口名后,表示当前接口及其实现类

<aop:pointcut id="pointcut" expression="execution(* com.zjhc.aop.service.UserServiceImpl.*(..))"/>
(3) AspectJ 的开发环境
<!--spring-->
 <dependency>
        <groupId>org.springframework</groupId>
         <artifactId>spring-webmvc</artifactId>
         <version>5.3.9</version>
     </dependency>
     <!--junit-->
     <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>4.12</version>
         <scope>test</scope>
     </dependency>
     <!--aspect织入-->
     <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjweaver</artifactId>
         <version>1.9.6</version>
 </dependency>
(4) beans.xml加入aop的约束
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:aop="http://www.springframework.org/schema/aop"
   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">
</beans>
(5) AspectJ 的开启注解支持
  • 注解支持的参数:
    • proxy-target-class=“false” JDK动态代理
    • proxy-target-class=“true” cglib动态代理
<aop:aspectj-autoproxy />
(6) AspectJ 的通知注解

切面类:@Aspect 注解的类叫做切面类
通知注解:在切面类中修饰方法的注解,这些注解体现了通知类型。通知注解修饰的方法称之为通知方法,所有的通知方法都可包含JoinPoint类型参数

  1. @Before前置通知
    在目标方法执行之前执行。被注解为前置通知的方法,JoinPoint类型的参数的对象本身就是切入点表达式。通过该参数,可获取切入点表达式、方法签名、 目标对象等
@Aspect
public class AnnotationLog {
    @Before(value = "execution(* com.zjhc.aop3.service.UserServiceImpl.*(..))")
    public void before(JoinPoint jp){
        //在方法中实现功能的增强,例如日志的代码
        System.out.println("==========方法之前执行");
        //获取方法的定义
        System.out.println("连接点的方法定义为:"+jp.getSignature());
        System.out.println("连接点的方法名称为:"+jp.getSignature().getName());
        //获取方法执行时的参数
        Object[] args = jp.getArgs();
        for (Object arg : args) {
            System.out.println("方法执行时参数为:"+arg);
        }
    }
  1. @AfterReturning后置通知-注解有 returning 参数
    在目标方法执行之后执行,可以获取到目标方法的返回 值。该注解的 returning 属性就是用于指定接收方法返回值的变量名的。被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
 @AfterReturning(value = "execution(* com.zjhc.aop3.service.UserServiceImpl.insert(..))",returning = "result")
    public void afterReturning(JoinPoint jp,Object result){
        //修改目标方法的返回值
        if (result != null) {
            String rs = (String) result;
        }
        System.out.println("【后置通知】,在目标方法之后执行的。能够获取到目标方法的执行结果:"+result);
    }
  1. @Around 环绕通知-增强方法有 ProceedingJoinPoint 参数
    在目标方法执行之前之后执行。被注解为环绕增强的方法要有返回,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数。接口 ProceedingJoinPoint 继承于JoinPoint,因此可以根据它获取方法的信息。其有一个 proceed()方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。最后,环绕增强方法将其返回值返回。该增强方法实际是拦截了目标方法的执行;
    ProceedingJoinPoint能获取连接点方法的定义,参数等信息
@Around(value = "execution(* com.zjhc.aop3.service.UserServiceImpl.update(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //ProceedingJoinPoint能获取连接点方法的定义,参数等信息
        String name = "";
        int age =0;
        Object[] args = pjp.getArgs();
        if (args != null && args.length>0) {
            age = (int) args[0];
            name = (String) args[1];
        }
        //获取方法的定义
        System.out.println("-----------------连接点的方法定义为:"+pjp.getSignature());
        System.out.println("-----------------连接点的方法名称为:"+pjp.getSignature().getName());
        System.out.println("-----------------方法执行时参数为:"+age+":"+name);

        Object result = null;
        System.out.println("【环绕通知】,在目标方法之前加入日志。");
        //控制目标方法是否执行
        if("zkc".equals(name) && age == 18){
            result = pjp.proceed();
        }
        System.out.println("【环绕通知】,在目标方法之后加入事务管理。");

        //可以直接返回result,或者返回修改后的结果
        if (result != null) {
            result = "操作成功";
        }
        return result;
    }
  1. @AfterThrowing 异常通知-注解中有 throwing 属性
    在目标方法抛出异常后执行。该注解的 throwing 属性用于指定所发生的异常类对象。 当然,被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象;在效果上相当于一个try…catch语句。目标方法的方法体在try语句块中,而切面方法的方法体放在了catch子句中
  2. @After最终通知
    无论目标方法是否抛出异常,该增强均会被执行。在执行效果上,相当于将切面方法的方法体放在了try…catch…finally…语句发finally子句中。
  3. @Pointcut 定义切入点
    当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。 AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。
    其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均 可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。 方法体内部也无需添加代码。
 @Pointcut(value = "execution(* com.zjhc.aop3.service.UserServiceImpl.update(..))")
    private void pointcut(){}
    
    @Around(value = "pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
    }
  1. 有多个增强类对同一个方法进行增强时,设置增强类优先级@Order(数值类型),数值越小优先级越高。

5.3.4 AOP具体实现方式

5.3.4.1 使用spring的API接口实现【主要是Spring API】
  • 接口
public interface UserService {
    String insert();
    void delete();
    void update();
    void select();
}
  • 创建目标类
public class UserServiceImpl implements UserService{
    public String insert() {
        System.out.println("执行了insert方法");
        return "新增成功";
    }
    public void delete() {
        System.out.println("执行了delete方法");
    }
    public void update() {
        System.out.println("执行了update方法");
    }
    public void select() {
        System.out.println("执行了select方法");
    }
}
  • 通过spring API 创建要切入的通知
public class BeforeLog implements MethodBeforeAdvice {
    /**
     * @param method 要执行的目标对象的方法
     * @param args 要执行的目标对象的方法的参数
     * @param target 目标对象
     * @throws Throwable
     */
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("目标对象"+target.getClass().getName()+"的"+method.getName()+"将被执行");
    }
}
public class AfterLog implements AfterReturningAdvice {
    /**
     * @param returnValue 返回值
     * @param method  执行的目标对象的方法
     * @param args  执行的目标对象的方法的参数
     * @param target 目标对象
     * @throws Throwable
     */
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("目标对象"+target.getClass().getName()+"的"+method.getName()+"方法的返回值:"+returnValue);
    }
}
  • 创建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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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 id="userService" class="com.zjhc.aop.service.UserServiceImpl"/>
    <bean id="afterLog" class="com.zjhc.aop.log.AfterLog"/>
    <bean id="beforeLog" class="com.zjhc.aop.log.BeforeLog"/>

    <aop:config>
        <aop:pointcut id="pointcut" expression="execution(* com.zjhc.aop.service.UserServiceImpl.*(..))"/>
        <!--执行环绕增加-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>
</beans>
  • 测试
public class AopTest {
    @Test
    public void tes(){
        ApplicationContext context = new ClassPathXmlApplicationContext("aopBeans.xml");
        UserService userService = (UserService) context.getBean("userService");
        userService.insert();
    }
}
  • 测试结果
    在这里插入图片描述
5.3.4.2 使用自定义方式实现AOP【主要是切面】
  • 接口
public interface UserService {
    String insert();
}
  • 被切入的目标类
public class UserServiceImpl implements UserService {
    @Override
    public String insert() {
        System.out.println("执行了insert方法");
        return "执行成功";
    }
}
  • 自定义切面
public class Log {
    public void before(JoinPoint jp){
        System.out.println("=========方法执行之前执行"+jp.getSignature());
    }
    public void after(JoinPoint jp){
        System.out.println("=========方法执行之后执行"+jp.getSignature());
    }
    public void afterReturn(JoinPoint jp){
        System.out.println("=========方法执行之后有返回值时执行"+jp.getTarget());
    }
}
  • beans.xml
   <bean id="userService1" class="com.zjhc.aop2.service.UserServiceImpl"/>
    <bean id="log" class="com.zjhc.aop2.log.Log"/>

    <!--自定义aop-->
    <aop:config>
        <aop:pointcut id="point" expression="execution(* com.zjhc.aop2.service.UserServiceImpl.*(..))"/>
        <aop:aspect ref="log">
            <aop:before method="before" pointcut-ref="point"/>
            <aop:after method="after" pointcut-ref="point"/>
            <aop:after-returning method="afterReturn" pointcut-ref="point"/>
        </aop:aspect>
    </aop:config>
  • 测试
@Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("aopBeans2.xml");
        UserService userService = (UserService) context.getBean("userService1");
        userService.insert();
    }
  • 测试结果
    在这里插入图片描述
5.3.4.3 使用注解的AOP实现
  • 接口
public interface UserService {
    String insert(String name);
    String update(int age,String name);
}
  • 目标类
public class UserServiceImpl implements UserService {
    @Override
    public String insert(String name) {
        System.out.println("【目标方法】执行了insert方法");
        return "新增成功";
    }
    @Override
    public String update(int age,String name) {
        System.out.println("【目标方法】执行了update方法");
        return "更新成功";
    }
}
  • 切面
@Aspect
public class AnnotationLog {

    @Pointcut(value = "execution(* com.zjhc.aop3.service.UserServiceImpl.update(..))")
    private void pointcut(){}

    @Before(value = "execution(* com.zjhc.aop3.service.UserServiceImpl.*(..))")
    public void before(JoinPoint jp){
        //在方法中实现功能的增强,例如日志的代码
        System.out.println("【前置通知】,在目标方法之前执行的");
        //获取方法的定义
        System.out.println("-----------------连接点的方法定义为:"+jp.getSignature());
        System.out.println("-----------------连接点的方法名称为:"+jp.getSignature().getName());

        //获取方法执行时的参数
        Object[] args = jp.getArgs();
        for (Object arg : args) {
            System.out.println("-----------------方法执行时参数为:"+arg);
        }
    }

    @AfterReturning(value = "execution(* com.zjhc.aop3.service.UserServiceImpl.insert(..))",returning = "result")
    public void afterReturning(JoinPoint jp,Object result){
        //修改目标方法的返回值
        if (result != null) {
            String rs = (String) result;
        }
        System.out.println("【后置通知】,在目标方法之后执行的。能够获取到目标方法的执行结果:"+result);
    }

    @Around(value = "pointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //ProceedingJoinPoint能获取连接点方法的定义,参数等信息
        String name = "";
        int age =0;
        Object[] args = pjp.getArgs();
        if (args != null && args.length>0) {
            age = (int) args[0];
            name = (String) args[1];
        }
        //获取方法的定义
        System.out.println("-----------------连接点的方法定义为:"+pjp.getSignature());
        System.out.println("-----------------连接点的方法名称为:"+pjp.getSignature().getName());
        System.out.println("-----------------方法执行时参数为:"+age+":"+name);

        Object result = null;
        System.out.println("【环绕通知】,在目标方法之前加入日志。");
        //控制目标方法是否执行
        if("zkc".equals(name) && age == 18){
            result = pjp.proceed();
        }

        System.out.println("【环绕通知】,在目标方法之后加入事务管理。");

        //可以直接返回result,或者返回修改后的结果
        if (result != null) {
            result = "操作成功";
        }
        return result;
    }
}
  • beans.xml
 <bean id="userService2" class="com.zjhc.aop3.service.UserServiceImpl"/>
    <bean id="logg" class="com.zjhc.aop3.log.AnnotationLog"/>
    <!--开启注解支持-->
    <!--
    proxy-target-class="false" JDK动态代理
    proxy-target-class="true"  cglib动态代理
    -->
    <aop:aspectj-autoproxy />
  • 测试
 @Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("aop3Bean.xml");
        UserService userService = (UserService) context.getBean("userService2");
        //userService.insert("zkc");
        userService.update(18,"zkc");
    }
  • 测试结果
    在这里插入图片描述

六、Spring集成myBatis

6.1 添加依赖

<!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.7</version>
        </dependency>
        <!--mybatis整合spring-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
        </dependency>
        <!--spring jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.3.9</version>
        </dependency>
        <!--spring-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.9</version>
        </dependency>
        <dependency>
           <groupId>com.alibaba</groupId>
           <artifactId>druid</artifactId>
           <version>1.1.23</version>
        </dependency>

6.2 新建mybatis主配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <typeAliases>
        <package name="com.zjhc.entity"/>
    </typeAliases>
    
</configuration>

6.3 新建spring的配置文件(重要)

<?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">
      
    <context:property-placeholder location="classpath:druid.properties" />
    
    <!--datasource-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
      <!--<property name="driverClassName" value="${jdbc.driver}"/>-->
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <!--<property name="url" value="${jdbc.url}"/>-->
        <property name="url" value="jdbc:mysql://121.41.106.140:43306/eblog?useUnicode=true&amp;useSSL=false&amp;userCharacterEncoding=utf8"/>
        <!--<property name="username" value="${jdbc.username}"/>-->
        <property name="username" value="root"/>
        <!--<property name="password" value="${jdbc.password}"/>-->
        <property name="password" value="ronny@123456"/>
    </bean>

    <!--sqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--绑定数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--绑定mybatis配置-->
        <property name="mapperLocations" value="classpath:com/zjhc/mapper/*.xml"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>

    <!--SqlSessionTemplate-->
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>
    
   <bean id="userMapper" class="com.zjhc.mapper.UserMapperImpl"/>
</beans>

6.4 数据源配置文件druid.properties

jdbc.url=jdbc:mysql://localhost:3306/ejb?useUnicode=true&amp;useSSL=false&amp;characterEncoding=utf8
jdbc.username=root
jdbc.password=123456
jdbc.driver=com.jdbc.cj.Driver

七、Spring与事务

Spring事务相关:Spring事务的实现和面试常问情形

7.1 理论知识

事务是数据库操作的最基本单元,逻辑上的一组操作,要么都成功,如果有一个失败,所有操作都失败。
事务的四个特性:
原子性:事务是一个不可再分割的工作单位,事务中的操作要么都发生,要么都不发生
一致性:事务开始之前和事务结束以后,数据库的完整性约束没有被破坏
持久性:一个事务一旦提交,它对数据库中的数据的改变就是永久性的
隔离性:数据库存在多个事务同时操作时,保证各事务之间互不干扰,操作互不影响

7.1.1 事务管理器接口

事务管理器是 PlatformTransactionManager 接口对象。其主要用于完成事务的提交、回滚,及获取事务的状态信息。PlatformTransactionManager 接口有两个常用的实现类:
➢ DataSourceTransactionManager:JdbcTemplate 或 MyBatis 进行数据库操作时使用。
➢ HibernateTransactionManager: Hibernate 进行持久化数据时使用。

7.1.2 Spring 的回滚方式

Spring 事务的默认回滚方式是:发生运行时异常和 error 时回滚,发生编译时异常时提交.

运行时异常:是 RuntimeException 类或其子类,即只有在运行时才出现的异常。如, NullPointerException、ArrayIndexOutOfBoundsException、IllegalArgumentException 等均属于运行时异常。这些异常由 JVM 抛出,在编译时不要求必须处理。只要代码编写足够仔细,程序足够健壮,运行时异常是可以避免的。

编译时异常:即在代码编写时要求必须捕获或抛出的异常,若不处理, 则无法通过编译。如 SQLException,ClassNotFoundException,IOException 等都属于编译时异常。 RuntimeException 及其子类以外的异常,均属于编译时异常。

7.1.3 事务定义接口

事务定义接口TransactionDefinition中定义了事务描述相关的三类常量:事务隔离级别、 事务传播行为、事务默认超时时限,及对它们的操作。

1.定义了五个事务隔离级别常量

事务的隔离级别:定义一个事务可能受其他并发事务活动影响的程度,事务的隔离级别可以想象为这个事务处理数据的自私程度

如果不考虑事务的隔离性,在事务的并发操作中可能会出现脏读,不可重复读,幻读:
脏读(Dirty read):一个事务读取了被另一个事务修改但未提交的数据时。如果这些数据稍后被回滚了,那么第一个事务读取的数据就会是无效的
不可重复读(Nonrepeatable read):一个事务多次执行相同的查询,但每次查询结果都不相同。这通常是因为另一个并发事务在两次查询之间更新了该条数据并提交。不可重复读重点在修改
幻读(Phantom reads):当一个事务读取几行记录后,另一个并发事务插入或者删除了一些记录。在后来的查询中,第一个事务看到原来没有的记录。幻读重点在新增或删除
区别:
脏读和不可重复读的区别:脏读是某一事务读取了另一个事务未提交的脏数据;而不可重复读则是在同一个事务范围内多次查询同一条数据却返回了不同的数据,这是由于在查询间隔期间,该条数据被另一个事务修改并提交了。
幻读和不可重复读区别:幻读和不可重复读都是读取了另一个事务中已经提交的数据,不同的是不可重复读多次查询的都是同一个数据项,针对的是对同一行数据的修改;而幻读针对的是一个数据整体(如:数据条数),主要是新增或者删除

在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常涉及到锁定在数据库中的记录(甚至有时是锁表)。完全隔离要求事务相互等待来完成工作,会阻碍并发。因此,可以根据业务场景选择不同的隔离级别:

隔离级别含义
ISOLATION_DEFAULT采用 DB 默认的事务隔离级别
ISOLATION_READ_UNCOMMITTED允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读
ISOLATION_READ_COMMITTED(Oracle 默认级别)允许从已经提交的并发事务读取,保证了一个事务不会读到另一个并行事务已修改但未提交的数据。可防止脏读,但幻读和不可重复读仍可能会发生。
ISOLATION_REPEATABLE_READ(MYSQL默认级别)可重复读,保证一个事务不能读取另一个事务已修改但未提交的数据并且在开始读取数据时,不再允许其他事务进行修改操作。可防止脏读和不可重复读,但幻读仍可能发生
ISOLATION_SERIALIZABLE完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是性能最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。

并发性能:Read Uncommitted > Read Committed > Repeatable Read > Serializable
隔离级别:Read Uncommitted < Read Committed < Repeatable Read < Serializable
隔离级别越低,并发性能越高

2.定义了七个事务传播行为常量

所谓事务传播特性就是多个事务方法相互调用时,事务如何在这些方法间传播。一般用在事务嵌套的场景。
如:A类中的事务方法 add()调用B类中的事务方法update(),在调用执行期间事务的维护情况,就称为事务传播行为。

class ServiceA {           
     void methodA() {  
         ServiceB.methodB();  
     }  }      
class ServiceB {           
     void methodB() {  }           
} 
传播特性含义
PROPAGATION_REQUIRED如果外层有事务,则当前事务加入到外层事务,内外部方法均属于同一个事务,一块提交,一块回滚。如果外层没有事务,内部方法为自己新开启一个事务,且内部方法的事务之间互不干扰。出现异常时只回滚自己,外层方法不会回滚
PROPAGATION_REQUIRES_NEW如果外层有事务,内部方法每次都会单独新开启一个事务,同时把外层事务挂起,当内部事务执行完毕,恢复外围事务的执行,内部事务抛出异常会被外部事务感知,自己捕获后只回滚自己则不会被外围事务感知。内外部事务之间,内部事务和方法之间互相独立互不干扰。如果外层没有开启事务,执行当前内部方法新开启的事务,且内部的事物之间互不干扰,出现异常时只回滚自己
PROPAGATION_SUPPORTS如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务
PROPAGATION_NOT_SUPPORTED该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
PROPAGATION_NEVER该传播机制不支持外层事务,即如果外层有事务就抛出异常
PROPAGATION_MANDATORY如果外层没有事务,则抛出异常
PROPAGATION_NESTED外围事务未开启时,作用与REQUIRED相同,内部方法都会新开启自己的事务,并且互不干扰。外围开启事务时,内部事务是外部事务的子事务,外围事务回滚,子事务一定回滚;同时内部方法抛出异常回滚,外围事务感知异常导致整体事务回滚。还有一点,内部方法事务是外围的一个子事务,有单独的保存点,所以方法抛出异常捕获后可以单独回滚,不影响外围主事务和其他子事务

注意:什么是事务挂起操作?
可以这样理解,对当前存在事务的现场生成一个快照,然后将事务现场清理干净,然后重新开启一个新事物,新事物执行完毕之后,将事务现场清理干净,在根据前面快照恢复旧事务

7.2 使用 Spring 的事务注解管理事务

1.开启注解驱动

<tx:annotation-driven transaction-manager="transactionManager" />

2.声明事务管理器

 <!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <constructor-arg ref="dataSource"/>
</bean>

3.业务层 public 方法加入事务属性的注解

//    @Transactional(
//     propagation = Propagation.REQUIRED,
//     isolation = Isolation.DEFAULT, 
//     timeout = 20,
//     rollbackFor ={Exception.class})
    @Transactional

@Transactional 的所有可选属性如下所示:
➢ propagation:用于设置事务传播属性。该属性类型为 Propagation 枚举,默认值为 Propagation.REQUIRED。
➢ isolation:用于设置事务的隔离级别。该属性类型为 Isolation 枚举,默认值为 Isolation.DEFAULT。
➢ readOnly:用于设置该方法对数据库的操作是否是只读的。该属性为 boolean,默认值 为 false。
➢ timeout:用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为 -1,即没有时限。在实际业务开发中一般不设置。
➢ rollbackFor:指定需要回滚的异常类。类型为 Class[],默认值为空数组。若只有 一个异常类时,可以不使用数组。
➢ rollbackForClassName:指定需要回滚的异常类类名。类型为 String[],默认值为空数组。 当然,若只有一个异常类时,可以不使用数组。
➢ noRollbackFor:指定不需要回滚的异常类。类型为 Class[],默认值为空数组。当然,若 只有一个异常类时,可以不使用数组。
➢ noRollbackForClassName:指定不需要回滚的异常类类名。类型为 String[],默认值为空 数组。若只有一个异常类时,可以不使用数组。

需要注意的是,@Transactional 若用在方法上,只能用于 public 方法上。对于其他非 public 方法,如果加上了注解@Transactional,虽然 Spring 不会报错,但不会将指定事务织入到该 方法中。因为 Spring 会忽略掉所有非 public 方法上的@Transaction 注解。 若@Transaction 注解在类上,则表示该类上所有的方法均将在执行时织入事务

7.3 使用 AspectJ 的 AOP 配置xml管理事务

1.添加Maven依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>    
    <version>4.3.16.RELEASE</version>
</dependency>   
<dependency> 
   <groupId>org.springframework</groupId>
   <artifactId>spring-aspects</artifactId>
    <version>4.3.16.RELEASE</version>   
 </dependency> 

2.声明事务管理器

 <!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <constructor-arg ref="dataSource"/>
</bean>

3.配置事务通知

<!--声明事务的通知
 指定业务方法的事务属性(传播行为,隔离级别,超时,回滚等)
-->
<tx:advice id="buyAdvice" transaction-manager="transactionManager">
 <tx:attributes>
     <tx:method name="buyGoods" propagation="REQUIRED" isolation="DEFAULT"
                rollback-for="java.lang.NullPointerException,com.bjpowernode.excep.NotEnoughException"/>
     <!--设置addXXX方法的事务-->
     <tx:method name="add*" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.Exception"/>
     <!--设置updateXXX方法的事务-->
     <tx:method name="update*" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.Exception"/>
     <!--设置removeXXX方法的事务-->
     <tx:method name="remove*" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.Exception"/>
     <!--其他方法的事务-->
     <tx:method name="*" propagation="SUPPORTS" read-only="true" />
 </tx:attributes>
</tx:advice>

4.配置增强器

<aop:config>
<!--声明切入点表达式:指定一些类和方法要加入切面的功能-->
<aop:pointcut id="servicePt" expression="execution(* *..service..*.*(..))" />
<aop:advisor advice-ref="buyAdvice" pointcut-ref="servicePt"/>
</aop:config>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一位不知名民工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值