Spring

发布于:2025-05-26 ⋅ 阅读:(39) ⋅ 点赞:(0)

Spring 框架两大核心机制(IoC、AOP)

  • IoC(控制反转)/ DI(依赖注入)
  • AOP(面向切面编程)

Spring 是一个企业级开发框架,是软件设计层面的框架,优势在于可以将应用程序进行分层,开发者可以自主选择组件。

如何使用 IoC

  • 创建 Maven 工程,pom.xml 添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.southwind</groupId>
    <artifactId>aispringioc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.11.RELEASE</version>
        </dependency>
    </dependencies>

</project>
  • 创建实体类 Student
package com.southwind.entity;

import lombok.Data;

@Data
public class Student {
    private long id;
    private String name;
    private int age;
}
  • 传统的开发方式,手动 new Student
Student student = new Student();
student.setId(1L);
student.setName("张三");
student.setAge(22);
System.out.println(student);
  • 通过 IoC 创建对象,在配置文件中添加需要管理的对象,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:context="http://www.springframework.org/schema/context"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
">

    <bean id="student" class="com.southwind.entity.Student">
        <property name="id" value="1"></property>
        <property name="name" value="张三"></property>
        <property name="age" value="22"></property>
    </bean>

</beans>
  • 从 IoC 中获取对象,通过 id 获取。
//加载配置文件
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Student student = (Student) applicationContext.getBean("student");
System.out.println(student);

配置文件

  • 通过配置 bean 标签来完成对象的管理。

    • id:对象名。

    • class:对象的模版类(所有交给 IoC 容器来管理的类必须有无参构造函数,因为 Spring 底层是通过反射机制来创建对象,调用的是无参构造

  • 对象的成员变量通过 property 标签完成赋值。

    • name:成员变量名。
    • value:成员变量值(基本数据类型,String 可以直接赋值,如果是其他引用类型,不能通过 value 赋值)
    • ref:将 IoC 中的另外一个 bean 赋给当前的成员变量(DI)
    <bean id="student" class="com.southwind.entity.Student">
        <property name="id" value="1"></property>
        <property name="name" value="张三"></property>
        <property name="age" value="22"></property>
        <property name="address" ref="address"></property>
    </bean>
    
    <bean id="address" class="com.southwind.entity.Address">
        <property name="id" value="1"></property>
        <property name="name" value="科技路"></property>
    </bean>
    

IoC 底层原理

  • 读取配置文件,解析 XML。
  • 通过反射机制实例化配置文件中所配置所有的 bean。
package com.southwind.ioc;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class ClassPathXmlApplicationContext implements ApplicationContext {
    private Map<String,Object> ioc = new HashMap<String, Object>();
    public ClassPathXmlApplicationContext(String path){
        try {
            SAXReader reader = new SAXReader();
            Document document = reader.read("./src/main/resources/"+path);
            Element root = document.getRootElement();
            Iterator<Element> iterator = root.elementIterator();
            while(iterator.hasNext()){
                Element element = iterator.next();
                String id = element.attributeValue("id");
                String className = element.attributeValue("class");
                //通过反射机制创建对象
                Class clazz = Class.forName(className);
                //获取无参构造函数,创建目标对象
                Constructor constructor = clazz.getConstructor();
                Object object = constructor.newInstance();
                //给目标对象赋值
                Iterator<Element> beanIter = element.elementIterator();
                while(beanIter.hasNext()){
                    Element property = beanIter.next();
                    String name = property.attributeValue("name");
                    String valueStr = property.attributeValue("value");
                    String ref = property.attributeValue("ref");
                    if(ref == null){
                        String methodName = "set"+name.substring(0,1).toUpperCase()+name.substring(1);
                        Field field = clazz.getDeclaredField(name);
                        Method method = clazz.getDeclaredMethod(methodName,field.getType());
                        //根据成员变量的数据类型将 value 进行转换
                        Object value = null;
                        if(field.getType().getName() == "long"){
                            value = Long.parseLong(valueStr);
                        }
                        if(field.getType().getName() == "java.lang.String"){
                            value = valueStr;
                        }
                        if(field.getType().getName() == "int"){
                            value = Integer.parseInt(valueStr);
                        }
                        method.invoke(object,value);
                    }
                    ioc.put(id,object);
                }
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e){
            e.printStackTrace();
        } catch (NoSuchMethodException e){
            e.printStackTrace();
        } catch (InstantiationException e){
            e.printStackTrace();
        } catch (IllegalAccessException e){
            e.printStackTrace();
        } catch (InvocationTargetException e){
            e.printStackTrace();
        } catch (NoSuchFieldException e){
            e.printStackTrace();
        }
    }

    public Object getBean(String id) {
        return ioc.get(id);
    }
}

通过运行时类获取 bean

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Student student = (Student) applicationContext.getBean(Student.class);
System.out.println(student);

这种方式存在一个问题,配置文件中一个数据类型的对象只能有一个实例,否则会抛出异常,因为没有唯一的 bean。

通过有参构造创建 bean

  • 在实体类中创建对应的有参构造函数。
  • 配置文件
<bean id="student3" class="com.southwind.entity.Student">
    <constructor-arg name="id" value="3"></constructor-arg>
    <constructor-arg name="name" value="小明"></constructor-arg>
    <constructor-arg name="age" value="18"></constructor-arg>
    <constructor-arg name="address" ref="address"></constructor-arg>
</bean>
<bean id="student3" class="com.southwind.entity.Student">
    <constructor-arg index="0" value="3"></constructor-arg>
    <constructor-arg index="2" value="18"></constructor-arg>
    <constructor-arg index="1" value="小明"></constructor-arg>
    <constructor-arg index="3" ref="address"></constructor-arg>
</bean>

给 bean 注入集合

<bean id="student" class="com.southwind.entity.Student">
    <property name="id" value="2"></property>
    <property name="name" value="李四"></property>
    <property name="age" value="33"></property>
    <property name="addresses">
        <list>
            <ref bean="address"></ref>
            <ref bean="address2"></ref>
        </list>
    </property>
</bean>

<bean id="address" class="com.southwind.entity.Address">
    <property name="id" value="1"></property>
    <property name="name" value="科技路"></property>
</bean>

<bean id="address2" class="com.southwind.entity.Address">
    <property name="id" value="2"></property>
    <property name="name" value="高新区"></property>
</bean>

scope 作用域

Spring 管理的 bean 是根据 scope 来生成的,表示 bean 的作用域,共4种,默认值是 singleton。

  • singleton:单例,表示通过 IoC 容器获取的 bean 是唯一的。
  • prototype:原型,表示通过 IoC 容器获取的 bean 是不同的。
  • request:请求,表示在一次 HTTP 请求内有效。
  • session:会话,表示在一个用户会话内有效。

request 和 session 只适用于 Web 项目,大多数情况下,使用单例和原型较多。

prototype 模式当业务代码获取 IoC 容器中的 bean 时,Spring 才去调用无参构造创建对应的 bean。

singleton 模式无论业务代码是否获取 IoC 容器中的 bean,Spring 在加载 spring.xml 时就会创建 bean。

Spring 的继承

与 Java 的继承不同,Java 是类层面的继承,子类可以继承父类的内部结构信息;Spring 是对象层面的继承,子对象可以继承父对象的属性值。

<bean id="student2" class="com.southwind.entity.Student">
    <property name="id" value="1"></property>
    <property name="name" value="张三"></property>
    <property name="age" value="22"></property>
    <property name="addresses">
        <list>
            <ref bean="address"></ref>
            <ref bean="address2"></ref>
        </list>
    </property>
</bean>

<bean id="address" class="com.southwind.entity.Address">
    <property name="id" value="1"></property>
    <property name="name" value="科技路"></property>
</bean>

<bean id="address2" class="com.southwind.entity.Address">
    <property name="id" value="2"></property>
    <property name="name" value="高新区"></property>
</bean>

<bean id="stu" class="com.southwind.entity.Student" parent="student2">
    <property name="name" value="李四"></property>
</bean>

Spring 的继承关注点在于具体的对象,而不在于类,即不同的两个类的实例化对象可以完成继承,前提是子对象必须包含父对象的所有属性,同时可以在此基础上添加其他的属性。

Spring 的依赖

与继承类似,依赖也是描述 bean 和 bean 之间的一种关系,配置依赖之后,被依赖的 bean 一定先创建,再创建依赖的 bean,A 依赖于 B,先创建 B,再创建 A。

<?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-3.2.xsd
                           ">

    <bean id="student" class="com.southwind.entity.Student" depends-on="user"></bean>

    <bean id="user" class="com.southwind.entity.User"></bean>

</beans>

Spring 的 p 命名空间

p 命名空间是对 IoC / DI 的简化操作,使用 p 命名空间可以更加方便的完成 bean 的配置以及 bean 之间的依赖注入。

<?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"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
">

    <bean id="student" class="com.southwind.entity.Student" p:id="1" p:name="张三" p:age="22" p:address-ref="address"></bean>

    <bean id="address" class="com.southwind.entity.Address" p:id="2" p:name="科技路"></bean>

</beans>

Spring 的工厂方法

IoC 通过工厂模式创建 bean 的方式有两种:

  • 静态工厂方法
  • 实例工厂方法

静态工厂方法

package com.southwind.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Car {
    private long id;
    private String name;
}
package com.southwind.factory;

import com.southwind.entity.Car;

import java.util.HashMap;
import java.util.Map;

public class StaticCarFactory {
    private static Map<Long, Car> carMap;
    static{
        carMap = new HashMap<Long, Car>();
        carMap.put(1L,new Car(1L,"宝马"));
        carMap.put(2L,new Car(2L,"奔驰"));
    }

    public static Car getCar(long id){
        return carMap.get(id);
    }
}
<!-- 配置静态工厂创建 Car -->
<bean id="car" class="com.southwind.factory.StaticCarFactory" factory-method="getCar">
    <constructor-arg value="2"></constructor-arg>
</bean>

实例工厂方法

package com.southwind.factory;

import com.southwind.entity.Car;

import java.util.HashMap;
import java.util.Map;

public class InstanceCarFactory {
    private Map<Long, Car> carMap;
    public InstanceCarFactory(){
        carMap = new HashMap<Long, Car>();
        carMap.put(1L,new Car(1L,"宝马"));
        carMap.put(2L,new Car(2L,"奔驰"));
    }

    public Car getCar(long id){
        return carMap.get(id);
    }
}
<!-- 配置实例工厂 bean -->
<bean id="carFactory" class="com.southwind.factory.InstanceCarFactory"></bean>

<!-- 赔偿实例工厂创建 Car -->
<bean id="car2" factory-bean="carFactory" factory-method="getCar">
    <constructor-arg value="1"></constructor-arg>
</bean>

IoC 自动装载(Autowire)

IoC 负责创建对象,DI 负责完成对象的依赖注入,通过配置 property 标签的 ref 属性来完成,同时 Spring 提供了另外一种更加简便的依赖注入方式:自动装载,不需要手动配置 property,IoC 容器会自动选择 bean 完成注入。

自动装载有两种方式:

  • byName:通过属性名自动装载
  • byType:通过属性的数据类型自动装载

byName

<bean id="cars" class="com.southwind.entity.Car">
    <property name="id" value="1"></property>
    <property name="name" value="宝马"></property>
</bean>

<bean id="person" class="com.southwind.entity.Person" autowire="byName">
    <property name="id" value="11"></property>
    <property name="name" value="张三"></property>
</bean>

byType

<bean id="car" class="com.southwind.entity.Car">
    <property name="id" value="2"></property>
    <property name="name" value="奔驰"></property>
</bean>

<bean id="person" class="com.southwind.entity.Person" autowire="byType">
    <property name="id" value="11"></property>
    <property name="name" value="张三"></property>
</bean>

byType 需要注意,如果同时存在两个及以上的符合条件的 bean 时,自动装载会抛出异常。

AOP

AOP:Aspect Oriented Programming 面向切面编程。

AOP 的优点:

  • 降低模块之间的耦合度。
  • 使系统更容易扩展。
  • 更好的代码复用。
  • 非业务代码更加集中,不分散,便于统一管理。
  • 业务代码更加简洁存粹,不参杂其他代码的影响。

AOP 是对面向对象编程的一个补充,在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面编程。将不同方法的同一个位置抽象成一个切面对象,对该切面对象进行编程就是 AOP。

如何使用?

  • 创建 Maven 工程,pom.xml 添加
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.11.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>5.0.11.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.0.11.RELEASE</version>
    </dependency>
</dependencies>
  • 创建一个计算器接口 Cal,定义4个方法。
package com.southwind.utils;

public interface Cal {
    public int add(int num1,int num2);
    public int sub(int num1,int num2);
    public int mul(int num1,int num2);
    public int div(int num1,int num2);
}
  • 创建接口的实现类 CalImpl。
package com.southwind.utils.impl;

import com.southwind.utils.Cal;

public class CalImpl implements Cal {
    public int add(int num1, int num2) {
        System.out.println("add方法的参数是["+num1+","+num2+"]");
        int result = num1+num2;
        System.out.println("add方法的结果是"+result);
        return result;
    }

    public int sub(int num1, int num2) {
        System.out.println("sub方法的参数是["+num1+","+num2+"]");
        int result = num1-num2;
        System.out.println("sub方法的结果是"+result);
        return result;
    }

    public int mul(int num1, int num2) {
        System.out.println("mul方法的参数是["+num1+","+num2+"]");
        int result = num1*num2;
        System.out.println("mul方法的结果是"+result);
        return result;
    }

    public int div(int num1, int num2) {
        System.out.println("div方法的参数是["+num1+","+num2+"]");
        int result = num1/num2;
        System.out.println("div方法的结果是"+result);
        return result;
    }
}

上述代码中,日志信息和业务逻辑的耦合性很高,不利于系统的维护,使用 AOP 可以进行优化,如何来实现 AOP?使用动态代理的方式来实现。

给业务代码找一个代理,打印日志信息的工作交个代理来做,这样的话业务代码就只需要关注自身的业务即可。

package com.southwind.utils;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

public class MyInvocationHandler implements InvocationHandler {
    //接收委托对象
    private Object object = null;

    //返回代理对象
    public Object bind(Object object){
        this.object = object;
        //获取object的类加载器、所有接口(方法),this表示当前的类MyInvocationHandler
        return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName()+"方法的参数是:"+ Arrays.toString(args));
        Object result = method.invoke(this.object,args);
        System.out.println(method.getName()+"的结果是"+result);
        return result;
    }
}

以上是通过动态代理实现 AOP 的过程,比较复杂,不好理解,Spring 框架对 AOP 进行了封装,使用 Spring 框架可以用面向对象的思想来实现 AOP。

Spring 框架中不需要创建 InvocationHandler,只需要创建一个切面对象,将所有的非业务代码在切面对象中完成即可,Spring 框架底层会自动根据切面类以及目标类生成一个代理对象。
LoggerAspect

package com.southwind.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Aspect
@Component
public class LoggerAspect {

    @Before(value = "execution(public int com.southwind.utils.impl.CalImpl.*(..))")
    public void before(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取参数
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println(name+"方法的参数是:"+ args);
    }

    @After(value = "execution(public int com.southwind.utils.impl.CalImpl.*(..))")
    public void after(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法执行完毕");
    }

    @AfterReturning(value = "execution(public int com.southwind.utils.impl.CalImpl.*(..))",returning = "result")
    public void afterReturning(JoinPoint joinPoint,Object result){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法的结果是"+result);
    }

    @AfterThrowing(value = "execution(public int com.southwind.utils.impl.CalImpl.*(..))",throwing = "exception")
    public void afterThrowing(JoinPoint joinPoint,Exception exception){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法抛出异常:"+exception);
    }

}

LoggerAspect 类定义处添加的两个注解:

  • @Aspect:表示该类是切面类。
  • @Component:将该类的对象注入到 IoC 容器。

具体方法处添加的注解:

@Before:表示方法执行的具体位置和时机。

CalImpl 也需要添加 @Component,交给 IoC 容器来管理。

package com.southwind.utils.impl;

import com.southwind.utils.Cal;
import org.springframework.stereotype.Component;

@Component
public class CalImpl implements Cal {
    public int add(int num1, int num2) {
        int result = num1+num2;
        return result;
    }

    public int sub(int num1, int num2) {
        int result = num1-num2;
        return result;
    }

    public int mul(int num1, int num2) {
        int result = num1*num2;
        return result;
    }

    public int div(int num1, int num2) {
        int result = num1/num2;
        return result;
    }
}

spring.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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
">

    <!-- 自动扫描 -->
    <context:component-scan base-package="com.southwind"></context:component-scan>

    <!-- 是Aspect注解生效,为目标类自动生成代理对象 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

context:component-scancom.southwind 包中的所有类进行扫描,如果该类同时添加了 @Component,则将该类扫描到 IoC 容器中,即 IoC 管理它的对象。即我们要使用注解,同时指定包扫描的路径,这样就交给IOC容器了。这两个要结合起来使用。

aop:aspectj-autoproxy 让 Spring 框架结合切面类和目标类自动生成动态代理对象。

  • 切面Aspect:横切关注点被模块化的抽象对象,通知+切入点就是切面。
  • 通知Advice:切面对象完成的工作。指哪些重复的内容,也就是共性功能。
  • 目标对象Target:被通知的对象,即被横切的对象。
  • 代理:切面、通知、目标混合之后的对象。
  • 连接点JoinPoint:通知要插入业务代码的具体位置。
  • 切点PointCut:AOP 通过切点定位到连接点,通知仅在切入点方法执行时被应用。
    在这里插入图片描述
    我们要统计DeptServiceImpl下的每个方法的执行时间。那么DeptServiceImpl这个类就是目标对象,它下面的方法就是连接点,表示要执行公共的AOP逻辑。而公共的AOP逻辑我们成为切面,它是由通知和切入点构成。通知就是公共逻辑,而切入点就是execution切入点表达式,这里指定哪些类要执行我们的公共方法。我们把这个AOP类称为切面类。

统计各个业务方法的执行耗时

  • 导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 编写AOP程序
@Component
@Aspect
public class TimeAspect {
    @Around("execution(* com.southwind.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        Long begin = System.currentTimeMillis();

        Object result = joinPoint.proceed();

        Long end  = System.currentTimeMillis();
        System.out.println(joinPoint.getSignature()+"方法耗时:"+(end - begin) + "ms");
        return result;
    }
}

输出结果:

String com.southwind.controller.HelloController.hello()方法耗时:4ms

AOP使用场景

  • 记录操作日志
  • 权限控制
  • 事务管理等

AOP进阶

通知类型:

  • @Around:环绕通知,此注解标注的通知方法在目标方法前后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都执行
  • @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行。

a、bean依赖的注入方式

  • set注入
  • 构造方法注入

set注入

UserServiceImpl.java

public class UserServiceImpl implements UserService{

    public void setUserDao(UserDao userDao){
        System.out.println("注入userDao");
    }
}

UserDaoIml.java

public class UserDaoIml implements UserDao{
}

beans.xml配置

<bean id="userDao" class="com.southwind.UserDaoIml"></bean>

<bean id="userService" class="com.southwind.UserServiceImpl">
    <property name="userDao" ref="userDao"/>
</bean>

解释:我要在UserServiceImpl中注入UserDaoIml,这里提供set方法。然后在beans.xml配置UserServiceImpl和UserDaoIml,注入是通过set进行注入,userService的name属性对应的是setUserDao的UserDao首字母小写,ref引用的是userDao这个bean

构造方法注入

UserServiceImpl.java

public class UserServiceImpl implements UserService{

    public UserServiceImpl(UserDao userDao){
        System.out.println("有参构造创建对象");
    }
}

beans.xml配置

<bean id="userDao" class="com.southwind.UserDaoIml"></bean>

<bean id="userService" class="com.southwind.UserServiceImpl">
    <constructor-arg name="userDao" ref="userDao"/>
</bean>

a、bean的配置详情

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
上面既有构造方法,又有初始化方法,执行顺序是,先执行构造方法创建对象,然后再执行初始化方法对对象进行初始化。

在这里插入图片描述
实现initializingBean接口,重写afterPropertiesSet()方法,这个方法就会被自动调用。

initializingBean的afterPropertiesSet方法调用,要先于init-method方法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
静态工厂方法实例化bean

静态工厂BeanFactory1.java

public class BeanFactory1 {

    public static UserDao userDao(){
        return new UserDaoIml();
    }
}

beans.xml

<bean id="userDao" class="com.southwind.factory.BeanFactory1" factory-method="userDao"></bean>

分析:我们在beans.xml中配置的bean的id是userDao,class却是BeanFactory1,最后返回的类型不是BeanFactory1,是因为我们指定了factory-method,在创建对象时会自动调用userDao方法

实例工厂方法实例化bean

实例工厂BeanFactory2.java

public class BeanFactory2 {

    public UserDao userDao(){
        return new UserDaoIml();
    }
}

beans.xml

<bean id="beanFactory" class="com.southwind.factory.BeanFactory2"/>

<bean id="userDao" factory-bean="beanFactory" factory-method="userDao"/>

分析:我们要通过实例工厂BeanFactory2创造userDao,实例工厂要调用userDao()方法,首先要创建工厂对象,所以我们创建了beanFactory的配置。创建userDao对象时,指定factory-bean是beanFactory,factory-method是userDao

有参静态工厂、有参实例工厂方法实例化bean

这里以有参实例工厂方法实例化bean为例,有参静态工厂的代码类似

实例工厂BeanFactory2.java

public class BeanFactory2 {

    public UserDao userDao(String name){
        return new UserDaoIml();
    }
}

beans.xml

<bean id="beanFactory" class="com.southwind.factory.BeanFactory2"/>

<bean id="userDao" factory-bean="beanFactory" factory-method="userDao">
    <constructor-arg name="name" value="hihi"></constructor-arg>
</bean>

只需要在原来基础上,加上constructor-arg为形参name赋值即可

通过FactoryBean创建bean

FactoryBean是一个接口,翻译为工厂bean,任何一个类实现了这个接口,就可以成为一个bean工厂,然后就会自动调用getObject方法返回bean。

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

BeanFactory.java,自定义类实现FactoryBean

public class BeanFactory implements FactoryBean<UserDao> {

    @Override
    public UserDao getObject() throws Exception {
        return new UserDaoIml();
    }

    @Override
    public Class<?> getObjectType() {
        return UserDao.class;
    }
}

beans.xml

<bean id="userDao" class="com.southwind.factory.BeanFactory"></bean>

在这里插入图片描述

b、spring配置非自定义(第三方jar包中)的bean

  • 配置DruidDataSource,这里的value值根据自已需要指定
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value=""/>
    <property name="url" value=""/>
    <property name="username" value=""/>
    <property name="password" value=""/>
</bean>
  • 配置数据库连接Connection。Connection是通过DriverManager类的getConnection方法调用来获取的,集成到spring中,就需要通过静态工厂方法来获取
<!--加载MySQL驱动-->
<bean id="clazz" class="java.lang.Class" factory-method="forName">
    <constructor-arg name="className" value="com.mysql.cj.jdbc.Driver"/>
</bean>

<!-- 获取connection -->
<bean id="connection" class="java.sql.DriverManager" factory-method="getConnection">
    <constructor-arg name="url" value="jdbc:mysql://localhost:3306/db01"/>
    <constructor-arg name="user" value="root"/>
    <constructor-arg name="password" value="root"/>
</bean>

1、BeanFactory与ApplicationContext的关系

  • BeanFactory是Spring的早期接口,称为Spring的Bean工厂,ApplicationContext是后期更高级接口,称之为Spring 容器
  • ApplicationContext在BeanFactory基础上对功能进行了扩展,例如:监听功能、国际化功能等。BeanFactory的API更偏向底层,ApplicationContext的API大多数是对这些底层API的封装;
  • Bean创建的主要逻辑和功能都被封装在BeanFactory中,ApplicationContext不仅继承了BeanFactory,而且ApplicationContext内部还维护着BeanFactory的引用,所以,ApplicationContext与BeanFactory既有继承关系(ApplicationContext继承了BeanFactory),又有融合关系(ApplicationContext内部维护着BeanFactory属性)。
  • Bean的初始化时机不同,原始BeanFactory是在首次调用getBean时才进行Bean的创建,而ApplicationContext则是配置文件加载,容器一创建就将Bean都实例化并初始化好。

ApplicationContext 接口,是 BeanFactory 的子接口。它扩展了 BeanFactory 接口的功能,如:

  • 国际化
  • 通配符方式获取一组 Resource 资源
  • 整合 Environment 环境(能通过它获取各种来源的配置信息)
  • 事件发布与监听,实现组件之间的解耦

国际化

系统需要支持多种语言,一个key,如果语言是英文就翻译为英文;如果语言是中文就翻译为中文

public class TestMessageSource {
    public static void main(String[] args) {
        GenericApplicationContext context = new GenericApplicationContext();

        context.registerBean("messageSource", MessageSource.class, () -> {
            ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
            ms.setDefaultEncoding("utf-8");
            ms.setBasename("messages");
            return ms;
        });

        context.refresh();

        System.out.println(context.getMessage("hi", null, Locale.ENGLISH));
        System.out.println(context.getMessage("hi", null, Locale.CHINESE));
        System.out.println(context.getMessage("hi", null, Locale.JAPANESE));
    }
}

国际化文件均在 src/resources 目录下

messages.properties(空)

messages_en.properties

hi=Hello

messages_ja.properties

hi=こんにちは

messages_zh.properties

hi=你好

注意

  • ApplicationContext 中 MessageSource bean 的名字固定为 messageSource
  • 使用 SpringBoot 时,国际化文件名固定为 messages
  • 空的 messages.properties 也必须存在

发布事件

1、定义事件。public UserRegisteredEvent(Object source),这里是source表示由谁来发布事件

public class UserRegisteredEvent extends ApplicationEvent{
    public UserRegisteredEvent(Object source){
        log.debug("用户注册");
        super(source);
    }
}

2、发布事件

context.publishEvent(new UserRegisteredEvent(context));

3、接收事件。spring中任何一个类都可以作为监听器接收事件

@Component
public class MyComponent2{
    @EventListener
    public void test(UserRegisteredEvent event){
        log.debug("{}", event);
        log.debug("发送短信");
    }
}

事件是一种重要的解耦方式。比如我现在要实现用户注册,注册完成后发送短信或者发送邮件。但是发送短信还是发送邮件,功能会变化。那就不能把代码写死。解决方法就是发事件

如果一个属性上面,即加了@Autowired和@Resource注解,那么@Autowired注解先生效,因为@Autowired对应的bean后处理器顺序靠前

2、对象创建完成后,如果想要初始化怎么做

  • 写一个初始化方法,在xml中配置该bean的init-method属性为该初始化方法
// 初始化方法
public void init(){
    System.out.println("初始化...");
}
<bean id="student" class="com.southwind.entity.Student" init-method="init">
</bean>
  • 实现InitializingBean接口,重写afterPropertiesSet()方法
public class UserServiceImpl implements UserService, InitializingBean{
    @Override
    public void afterPropertiesSet() throw Exception{
        
    }
}

3、Bean的实例化的基本流程

Spring容器在进行初始化时,会将xml配置的<bean>的信息封装成一个BeanDefinition对象,所有的BeanDefinition存储到一个名为beanDefinitionMap的Map集合中去,Spring框架在对该Map进行遍历,使用反射创建Bean实例对象,创建好的Bean对象存储在一个名为singletonObjects的Map集合中,当调用getBean方法时则最终从该Map集合中取出Bean实例对象返回。

4、Spring的后处理器(特别重要)

Spring的很多功能都是通过BeanFactoryPostProcessor和BeanPostProcessor进行实现的

Spring的后处理器是Spring对外开发的重要扩展点,==允许我们介入到Bean的整个实例化流程中来,以达到动态注册BeanDefinition,动态修改BeanDefinition,以及动态修改Bean的作用。==Spring主要有两种后处理器:

  • BeanFactoryPostProcessor:Bean工厂后处理器,在BeanDefinitionMap填充完毕,Bean实例化之前执行;(只执行一次,因为是在BeanDefinitionMap填充完毕执行
  • BeanPostProcessor:Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行。(每个Bean创建完成之后都会执行一次

4.1 BeanFactoryPostProcessor

BeanFactoryPostProcessor是一个接口规范,实现了该接口的类只要交由Spring容器管理的话,那么Spring就会回调该接口的方法,用于对BeanDefinition注册和修改的功能。
在这里插入图片描述
应用:

1、动态将一个类的实现类由类A修改为类B

public class UserServiceImpl implements UserService {
}

public class UserDaoImpl implements UserDao {
}

xml配置文件

<beans>
	<bean id="userDao" class="com.southwind.dao.impl.UserDaoImpl"></bean>

    <bean id="userService" class="com.southwind.service.impl.UserServiceImpl"></bean>

    <!-- 注册BeanFactoryPostProcessor -->
    <bean class="com.southwind.processor.MyBeanFactoryPostProcessor"></bean>
</beans>

自定义MyBeanFactoryPostProcessor,在上面的xml文件中,我们配置userService这个bean对应的是UserServiceImpl这个类。然后在MyBeanFactoryPostProcessor中将它的类名修改为UserDaoImpl

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        System.out.println("MyBeanFactoryPostProcessor");

        // 修改beanDefinition
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("userService");
        beanDefinition.setBeanClassName("com.southwind.dao.impl.UserDaoImpl");
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Object userService = context.getBean("userService");
        System.out.println(userService);
    }
}

输出结果:发现MyBeanFactoryPostProcessor会调用。同时通过名称userService获取到的对象竟然是UserDaoImpl

MyBeanFactoryPostProcessor
com.southwind.dao.impl.UserDaoImpl@56ef9176

2、动态将一个bean注册到Spring容器

我们新增一个PersonDaoImpl,但是不在beans.xml中给它配置,而是将它在MyBeanFactoryPostProcessor进行配置,达到注册到Spring容器的效果。具体原理就是,创建PersonDaoImpl对应的BeanDefinition,然后将它放到BeanDefinitionMap中。Spring就会扫描BeanDefinitionMap,然后创建对象

PersonDaoImpl类

public class PersonDaoImpl implements PersonDao {
}

beans.xml

<beans>
	<bean id="userDao" class="com.southwind.dao.impl.UserDaoImpl"></bean>

    <bean id="userService" class="com.southwind.service.impl.UserServiceImpl"></bean>

    <bean class="com.southwind.processor.MyBeanFactoryPostProcessor"></bean>
</beans>

MyBeanFactoryPostProcessor类,在该类中注册PersonDaoImpl

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 注册。先创建beanDefinition,然后调用registerBeanDefinition方法进行注册
        BeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClassName("com.southwind.dao.impl.PersonDaoImpl");
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
        defaultListableBeanFactory.registerBeanDefinition("personDao", beanDefinition);
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Object personDao = context.getBean(PersonDao.class);
        System.out.println(personDao);
    }
}

输出结果:

com.southwind.dao.impl.PersonDaoImpl@69ea3742

4.2 BeanDefinitionRegistryPostProcessor

BeanFactoryPostProcessor可以实现注册功能,但是需要进行强制类型转换,比较麻烦。

Spring 提供了一个BeanFactoryPostProcessor的子接口BeanDefinitionRegistryPostProcessor专门用于注册BeanDefinition操作,一步到位

MyBeanDefinitionRegistryPostProcessor类

public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        // 注册
        BeanDefinition beanDefinition = new RootBeanDefinition();
        beanDefinition.setBeanClassName("com.southwind.dao.impl.PersonDaoImpl");
        beanDefinitionRegistry.registerBeanDefinition("personDao", beanDefinition);
    }

    // 这个是从父接口BeanFactoryPostProcessor继承过来的
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }
}

beans.xml

<bean class="com.southwind.processor.MyBeanDefinitionRegistryPostProcessor"></bean>

测试类:

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Object personDao = context.getBean(PersonDao.class);
        System.out.println(personDao);
    }
}

输出结果:

com.southwind.dao.impl.PersonDaoImpl@3159c4b8

重点:使用Spring的BeanFactoryPostProcessor扩展点完成自定义注解扫描

要求如下:

  • 自定义@MyComponent注解,使用在类上;
  • 使用资料中提供好的包扫描器工具BaseClassScanUtils 完成指定包的类扫描;
  • 自定义BeanFactoryPostProcessor完成注解@MyComponent的解析,解析后最终被Spring管理。

创建自定义注解MyComponent

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
    String value();
}

创建类OtherBean,并用MyComponent注解进行修饰。我们的最终目的是通过加注解的方式能够在Spring中创建该对象

@MyComponent("otherBean")
public class OtherBean {
}

类扫描工具。核心代码就是42-57行,判断哪个类上面带了MyComponent注解,然后获取类的全限定名和类的名称

package com.southwind.utils;

import com.southwind.anno.MyComponent;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.ClassUtils;

import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BaseClassScanUtils {

    //设置资源规则
    private static final String RESOURCE_PATTERN = "/**/*.class";

    public static Map<String, Class> scanMyComponentAnnotation(String basePackage) {

        //创建容器存储使用了指定注解的Bean字节码对象
        Map<String, Class> annotationClassMap = new HashMap<String, Class>();

        //spring工具类,可以获取指定路径下的全部类
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        try {
            String pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                    ClassUtils.convertClassNameToResourcePath(basePackage) + RESOURCE_PATTERN;
            Resource[] resources = resourcePatternResolver.getResources(pattern);
            //MetadataReader 的工厂类
            MetadataReaderFactory refractory = new CachingMetadataReaderFactory(resourcePatternResolver);
            for (Resource resource : resources) {
                //用于读取类信息
                MetadataReader reader = refractory.getMetadataReader(resource);
                //扫描到的class
                String classname = reader.getClassMetadata().getClassName();
                Class<?> clazz = Class.forName(classname);
                //判断是否属于指定的注解类型
                if(clazz.isAnnotationPresent(MyComponent.class)){
                    //获得注解对象
                    MyComponent annotation = clazz.getAnnotation(MyComponent.class);
                    //获得属value属性值
                    String beanName = annotation.value();
                    //判断是否为""
                    if(beanName!=null&&!beanName.equals("")){
                        //存储到Map中去
                        annotationClassMap.put(beanName,clazz);
                        continue;
                    }

                    //如果没有为"",那就把当前类的类名作为beanName
                    annotationClassMap.put(clazz.getSimpleName(),clazz);

                }
            }
        } catch (Exception exception) {
        }

        return annotationClassMap;
    }

    public static void main(String[] args) {
        Map<String, Class> stringClassMap = scanMyComponentAnnotation("com.southwind");
        System.out.println(stringClassMap);
    }
}

后处理器

public class MyComponentBeanFactoryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        // 使用扫描工具扫描指定目录,收集使用了MyComponent注解的类
        Map<String, Class> map = BaseClassScanUtils.scanMyComponentAnnotation("com.southwind");
        // 遍历map,组装beanDefinition,进行注册
        map.forEach((beanName, clazz)->{
            BeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClassName(clazz.getName());
            beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);
        });
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }
}

beans.xml

<bean class="com.southwind.processor.MyComponentBeanFactoryPostProcessor"></bean>

测试类:

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        Object object = context.getBean(OtherBean.class);
        System.out.println(object);
    }
}

输出结果:

com.southwind.beans.OtherBean@a7e666

4.3 BeanPostProcessor

Bean被实例化后,到最终缓存到名为singletonObjects单例池之前,中间会经过Bean的初始化过程,例如:属性的填充、初始方法init的执行等,其中有一个对外进行扩展的点BeanPostProcessor,我们称为Bean后处理。跟上面的Bean工厂后处理器相似,它也是一个接口,实现了该接口并被容器管理的BeanPostProcessor,会在流程节点上被Spring自动调用。

BeanFactoryPostProcessor是对Bean定义的操作,BeanPostProcessor是对Bean对象的操作

1、在创建对象的时候不为属性赋值,而是在BeanPostProcessor中进行赋值

UserDaoImpl类

public class UserDaoImpl implements UserDao {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public UserDaoImpl(){
        System.out.println("创建了UserDaoImpl对象");
    }
}

在beans.xml中只是配置了bean,没有为name属性赋值

<bean id="userDao" class="com.southwind.dao.impl.UserDaoImpl"></bean>

通过在MyBeanPostProcessor类中,获取UserDaoImpl对象,为它的name属性赋值为hahaha

public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if(bean instanceof UserDaoImpl){
            UserDaoImpl userDao = (UserDaoImpl) bean;
            userDao.setName("hahaha");
        }

        System.out.println("before");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("after");
        return bean;
    }
}

在beans.xml中注册MyBeanPostProcessor

<bean class="com.southwind.processor.MyBeanPostProcessor"></bean>

测试类:

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserDaoImpl userDao = (UserDaoImpl) context.getBean("userDao");
        System.out.println(userDao.getName());
    }
}

输出结果:可以看到name属性有值了

创建了UserDaoImpl对象
before
after
hahaha

在上面的例子中,我们看到控制台先执行了before,然后执行了after方法。那么在before和after方法中间执行哪个方法?答案是对象的初始化方法

在UserDaoImpl中添加初始化方法init()

public class UserDaoImpl implements UserDao,InitializingBean {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public UserDaoImpl(){
        System.out.println("创建了UserDaoImpl对象");
    }

    public void init(){
        System.out.println("init");
    }
    
    public void afterPropertySet(){
        System.out.println("afterPropertySet");
    }
}

在beans.xml中配置初始化方法init-method

<bean id="userDao" class="com.southwind.dao.impl.UserDaoImpl" init-method="init"></bean>

输出结果:

创建了UserDaoImpl对象
before
afterPropertySet
init
after
hahaha

2、对Bean方法进行执行时间日志增强,有点像AOP

要求如下:

  • Bean的方法执行之前控制台打印当前时间;
  • Bean的方法执行之后控制台打印当前时间。

分析:

  • 对方法进行增强主要就是代理设计模式和包装设计模式;
  • 由于Bean方法不确定,所以使用动态代理在运行期间执行增强操作;
  • 在Bean实例创建完毕后,进入到单例池之前,使用Proxy代替真是的目标Bean

接口

public interface UserDao {
    void show();
}

实现类,我们的目标是在show方法调用前后打出当前时间

public class UserDaoImpl implements UserDao {
    @Override
    public void show() {
        System.out.println("show方法在执行");
    }
}

自定义TimeLogBeanPostProcessor,使用Proxy.newProxyInstance创建代理对象并返回,从而将singletonObjects单例池中的目标对象进行替换增强。最终调用show方法的时候对应的是代理对象

public class TimeLogBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 对Bean进行动态代理,返回的是Proxy代理对象,进而存储到单例池singletonObjects中
        Object beanProxy = Proxy.newProxyInstance(
                bean.getClass().getClassLoader(),
                bean.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 1、输出开始时间
                        System.out.println("方法:" + method.getName() + "-开始时间" + new Date());
                        // 2、执行目标方法
                        Object result = method.invoke(bean, args);
                        // 3、输出结束时间
                        System.out.println("方法:" + method.getName() + "-结束时间" + new Date());
                        return result;
                    }
                }
        );

        return beanProxy;
    }
}

beans.xml

<bean class="com.southwind.processor.TimeLogBeanPostProcessor"></bean>

测试类

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserDao userDao = (UserDao) context.getBean("userDao");
        userDao.show();
    }
}

结果:

方法:show-开始时间Sat Dec 14 00:15:19 CST 2024
show方法在执行
方法:show-结束时间Sat Dec 14 00:15:19 CST 2024

完整流程图
在这里插入图片描述
我们通过reader读取xml文件,然后解析xml中的bean标签,将每个bean标签封装为beanDefinition,放入到beanDefinitionMap当中。然后从它里面取出beanDefinition,经过BeanDefinitonRegistryPostProcessor、BeanFactoryPostProcessor对bean的定义进行修改,得到右上角的BD(beanDefinition)。然后通过反射创建该对象obj。然后执行BeanPostProcessor的before和after方法,对obj对象进行增强,将增强后的对象存入到singletonObjects单例池当中。

5、Spring Bean的生命周期(重要)

Spring Bean的生命周期是从 Bean 实例化之后,即通过反射创建出对象之后,到Bean成为一个完整对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期。Spring Bean的生命周期大体上分为三个阶段:

  • Bean的实例化阶段:Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的,是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化;
  • Bean的初始化阶段:Bean创建之后还仅仅是个"半成品",还需要对Bean实例的属性进行填充、执行一些Aware接口方法、执行BeanPostProcessor方法、执行InitializingBean接口的初始化方法、执行自定义初始化init方法等。该阶段是Spring最具技术含量和复杂度的阶段,Aop增强功能,后面要学习的Spring的注解功能等、spring高频面试题Bean的循环引用问题都是在这个阶段体现的;
  • Bean的完成阶段:经过初始化阶段,Bean就成为了一个完整的Spring Bean,被存储到单例池singletonObjects中去了,即完成了Spring Bean的整个生命周期。

5.1 初始化阶段

Spring Bean的初始化过程涉及如下几个过程:这也是相应的执行顺序

  • Bean实例的属性填充
  • Aware接口属性注入
  • BeanPostProcessor的before()方法回调
  • InitializingBean接口的初始化方法回调
  • 自定义初始化方法init回调
  • BeanPostProcessor的after()方法回调
a、Bean实例属性填充

在这里插入图片描述
Spring在进行属性注入时,会分为如下几种情况:

  • 注入普通属性,String、int或存储基本类型的集合时,直接通过set方法的反射设置进去;
  • 注入单向对象引用属性时,从容器中getBean获取后通过set方法反射设置进去,如果容器中没有,则先创建被注入对象Bean实例(完成整个生命周期)后,在进行注入操作;
  • 注入双向对象引用属性时,就比较复杂了,涉及了循环引用(循环依赖)问题,下面会详细阐述解决方案
b、循环依赖/循环引用

多个实体之间相互依赖并形成闭环的情况就叫做"循环依赖",也叫做"循环引用"
在这里插入图片描述
我们要获取userService对象,首先实例化该对象,只是在内存中开辟了地址,然后就要执行初始化为它的属性进行填充。去容器中查找发现没有userDao,则去创建userDao对象,发现在实例化userDao对象的过程中需要填充userService对象,于是又去创建userService对象。出现了死锁问题。

对于上述的循环引用问题产生的死锁,我们想的是,userDao已经实例化了,内存空间就有该对象了,我们直接给userService填充userDao属性(把userDao的引用给到userService),虽然属性不全,但是可以后期再进行属性填充,只要保持引用关系不变就可以了,这就解决了死锁问题。这就是下一步用到的方法了

c、Spring的三级缓存

Spring提供了三级缓存存储 完整Bean实例 和 半成品Bean实例 ,用于解决循环引用问题

在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map:
在这里插入图片描述
我们调用context.getBean是从singletonObjects中获取bean

执行完初始化方法,还未给该对象的属性赋值,且对象未被引用,则会将该对象放入三级缓存,key是bean的名称,value是ObjectFactory,它相当于是对当前对象进行了一次包装,通过调用该接口的getObject()方法就可以获取到该对象

UserService和UserDao循环依赖的过程结合上述三级缓存描述一下

  • UserService 实例化对象,但尚未初始化,将UserService存储到三级缓存;
  • UserService 属性注入,需要UserDao,从缓存中获取,没有UserDao;
  • UserDao实例化对象,但尚未初始化,将UserDao存储到到三级缓存;
  • UserDao属性注入,需要UserService,从三级缓存获取UserService,UserService从三级缓存移入二级缓存(因为到这一步userService已经从没有被引用变成了被引用,缓存位置需要变化)
  • UserDao执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存;
  • UserService 注入UserDao;
  • UserService执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存

5.2 IOC整体流程总结

在这里插入图片描述

6、Spring的注解开发

注意,要添加注解扫描代码,不然即使加了注解也不生效

6.1 @Component

被该注解标识的类,会在指定扫描范围内被Spring加载并实例化
在这里插入图片描述

@Component的衍生注解

在这里插入图片描述

6.2 依赖注入相关注解

在这里插入图片描述

6.3 非自定义bean注解开发

上面说的几个注解适用于自定义bean(自己创建的bean),而非自定义bean(例如第三方jar包里面的bean,我们没办法在人家的源码或者jar包上加注解)就不适用了,需要使用非自定义bean注解。

非自定义Bean不能像自定义Bean一样使用@Component进行管理,非自定义Bean要通过工厂的方式进行实例化
使用@Bean标注方法即可,@Bean的属性为beanName,如不指定为当前工厂方法名称
在这里插入图片描述
注意:工厂方法所在类必须要被Spring管理,上面的例子中OtherBean要加注解,交给Spring管理

在这里插入图片描述

6.4 Spring配置类的开发

@Component等注解替代了<bean>标签,但是像<import><context:conponentScan> 等非<bean> 标签怎样去使用注解替代呢?

在这里插入图片描述
定义一个配置类替代原有的xml配置文件,<bean>标签以外的标签,一般都是在配置类上使用注解完成的

@Configuration注解标识的类为配置类,替代原有xml配置文件,该注解第一个作用是标识该类是一个配置类,第二个作用是具备@Component作用

在这里插入图片描述

@ComponentScan 组件扫描配置,替代原有xml文件中的<context:component-scan base-package=""/>

在这里插入图片描述

base-package的配置方式:

  • 指定一个或多个包名:扫描指定包及其子包下使用注解的类
  • 不配置包名:扫描当前@componentScan注解配置类所在包及其子包下的类

@PropertySource 注解用于加载外部properties资源配置,替代原有xml中的 <context:propertyplaceholder location=“”/> 配置

在这里插入图片描述

@Import 用于加载其他配置类,替代原有xml中的<import resource="classpath:beans.xml"/>配置

在这里插入图片描述

6.5 Spring注解的解析原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7、AOP

AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程。

7.1 AOP思想的实现方案

动态代理技术,在运行期间,对目标对象的方法进行增强,代理对象同名方法内可以执行原有逻辑,同时嵌入执行其他增强逻辑或其他对象的方法

7.2 模拟AOP的基础代码

先创建目标对象UserServiceImpl

public class UserServiceImpl implements UserService {
    @Override
    public void show1() {
        System.out.println("show1...");
    }

    @Override
    public void show2() {
        System.out.println("show2...");
    }
}

创建增强类,内部提供增强方法

public class MyAdvice {

    public void beforeAdvice(){
        System.out.println("前置增强...");
    }

    public void afterAdvice(){
        System.out.println("后置增强...");
    }
}

我们的目标是在调用show1和show2方法的前后,都会调用beforeAdvice和afterAdvice方法。这就联想到了之前学习的BeanPostProcessor

public class MockAopBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 目的:对UserServiceImpl中的show1和show2方法进行增强,增强方法在MyAdvice类中
        // 问题1:需要进行bean名称和方法名的判断,因为postProcessAfterInitialization方法是所有bean都会执行,解决方法是加if判断
        // 问题2:MyAdvice类怎么获取,解决方法是将MyAdvice类放入到Spring容器中
        if(bean.getClass().getPackage().getName().equals("com.southwind.service.impl")){
            // 生成bean的proxy对象
            Object beanProxy = Proxy.newProxyInstance(
                    bean.getClass().getClassLoader(),
                    bean.getClass().getInterfaces(),
                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            MyAdvice myAdvice = applicationContext.getBean(MyAdvice.class);
                            // 执行增强对象的before方法
                            myAdvice.beforeAdvice();

                            // 执行目标对象的目标方法
                            Object result = method.invoke(bean, args);

                            // 执行增强对象的after方法
                            myAdvice.afterAdvice();
                            return result;
                        }
                    }
            );

            // 属于增强的范围,则返回增强对象
            return beanProxy;
        }

        // 不属于增强的范围,则直接返回目标对象
        return bean;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

我们的目的:对UserServiceImpl中的show1和show2方法进行增强,增强方法在MyAdvice类中

问题1:需要进行bean名称和方法名的判断,因为postProcessAfterInitialization方法是所有bean都会执行,解决方法是加if判断

问题2:MyAdvice类怎么获取,解决方法是将MyAdvice类放入到Spring容器中

我们这里通过实现ApplicationContextAware接口,进而给applicationContext进行赋值,进而拿到MyAdvice对象

注意:属于增强的范围,则返回增强对象;不属于增强的范围,则直接返回目标对象

beas.xml

<bean id ="userService" class="com.southwind.service.impl.UserServiceImpl"></bean>

<bean id ="myAdvice" class="com.southwind.advice.MyAdvice"></bean>

<bean class="com.southwind.processor.MockAopBeanPostProcessor"></bean>

7.3 AOP相关概念

在这里插入图片描述
例如,我有一个userService类,里面有show1和show2方法,但是我的逻辑只对show1方法进行增强,则连接点就是show1和show2方法,而切入点就是show1方法

在这里插入图片描述

7.4 Spring的AOP开发

7.4.1 基于XML方式配置AOP

配置哪些包、哪些类、哪些方法需要被增强
配置目标方法要被哪些通知方法所增强,在目标方法执行之前还是之后执行增强

xml方式配置AOP的步骤:
1、导入AOP相关坐标;
2、准备目标类、准备增强类,并配置给Spring管理;
3、配置切点表达式(哪些方法被增强);
4、配置织入(切点被哪些通知方法增强,是前置增强还是后置增强)。

1、pom.xml

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

2、准备目标类、准备增强类,并配置给Spring管理

目标类UserServiceImpl

public class UserServiceImpl implements UserService {
    @Override
    public void show1() {
        System.out.println("show1...");
    }

    @Override
    public void show2() {
        System.out.println("show2...");
    }
}

增强类

public class MyAdvice {

    public void beforeAdvice(){
        System.out.println("前置增强...");
    }

    public void afterAdvice(){
        System.out.println("后置增强...");
    }
}

交给Spring管理,beans.xml

<bean id ="userService" class="com.southwind.service.impl.UserServiceImpl"></bean>

<bean id ="myAdvice" class="com.southwind.advice.MyAdvice"></bean>

3、配置切点表达式

只在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.southwind.service.impl.UserServiceImpl"></bean>

    <!-- 通知 -->
    <bean id ="myAdvice" class="com.southwind.advice.MyAdvice"></bean>

    <!-- aop配置 -->
    <aop:config>
        <!-- 配置切入点表达式,目的是指定哪些方法要被增强 -->
        <aop:pointcut id="myPointcut" expression="execution(void com.southwind.service.impl.UserServiceImpl.show1())"/>
        <!-- 配置织入,目的是指定哪些切点与哪些织入进行结合 -->
        <aop:aspect ref="myAdvice">
            <aop:before method="beforeAdvice" pointcut-ref="myPointcut"/>
            <aop:after method="afterAdvice" pointcut-ref="myPointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

注意:

  • 需要导入aop相应的命名空间
  • <aop:config>中进行aop相关的配置
  • <aop:pointcut>中指定切入点,目的是指定哪些方法要被增强。这里配置了只对show1()方法进行增强
  • <aop:aspect>中配置织入,目的是指定哪些切点与哪些织入进行结合。它的一个属性ref是对应的通知id,即myAdvice。然后通过<aop:before>配置前置通知,method是前置通知对应的前置增强方法,即myAdvice的beforeAdvice方法,pointcut-ref属性指定为哪个切入点添加前置通知。后置通知的原理类似

4、测试类

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        UserService userService = context.getBean(UserService.class);
        userService.show1();
    }
}

5、输出结果

前置增强...
show1...
后置增强...
7.4.2 xml方式AOP配置详解

五种通知类型
在这里插入图片描述
环绕通知的使用:

在MyAdvice中配置环绕通知方法。环绕通知是要在目标方法前后执行,所以这里around方法有参数ProceedingJoinPoint,正在执行的连接点,即切入点。调用joinPoint.proceed(),执行目标方法。

public class MyAdvice {

    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕前的增强");
        Object result = joinPoint.proceed();
        System.out.println("环绕后的增强");
        return result;
    }
}

beans.xml中配置环绕通知

<aop:config>
    <!-- 配置切入点表达式,目的是指定哪些方法要被增强 -->
    <aop:pointcut id="myPointcut" expression="execution(void com.southwind.service.impl.UserServiceImpl.show1())"/>
    <!-- 配置织入,目的是指定哪些切点与哪些织入进行结合 -->
    <aop:aspect ref="myAdvice">
        <aop:around method="around" pointcut-ref="myPointcut"/>
    </aop:aspect>
</aop:config>

通知方法在被调用时,Spring可以为其传递一些必要的参数
在这里插入图片描述
在这里插入图片描述

两种切面配置

AOP的XML有两种配置方式:

  • 使用<aspect>配置切面
  • 使用<advisor>配置切面

<aspect>配置方式前面已经学习过,这里学习<advisor>配置方式。

<advisor>配置方式:Spring定义了一个Advice接口,实现了该接口的类都可以作为通知类出现

public interface Advice{
    
}

看下advice配置的流程

1、由于Advice接口中没有任何方法,所以我们要实现的是Advice的子接口

通知类,这里实现MethodBeforeAdvice接口,重写before方法,相当于前置通知;实现AfterReturningAdvice接口,重写afterReturning方法,相当于后置通知

public class MyAdvice2 implements MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("前置通知...");
    }

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("后置通知...");
    }
}

2、beans.xml

<!-- 目标对象 -->
<bean id ="userService" class="com.southwind.service.impl.UserServiceImpl"></bean>

<!-- 通知 -->
<bean id ="myAdvice" class="com.southwind.advice.MyAdvice2"></bean>

<!-- aop配置 -->
<aop:config>
    <aop:pointcut id="myPointcut" expression="execution(void com.southwind.service.impl.UserServiceImpl.show1())"/>
    <aop:advisor advice-ref="myAdvice" pointcut-ref="myPointcut"/>
</aop:config>

3、执行结果:

前置通知...
show1...
后置通知...

相较于aspect方式需要在xml文件中指定哪个是前置通知,哪个是后置通知,advice在xml中不需要指定这些,它是通过实现对应的接口来实现的

aspect与advisor配置的不同点

1、语法形式不同:

  • advisor是通过实现接口来确认通知的类型
  • aspect是通过配置确认通知的类型,更加灵活

2、可配置的切面数量不同

  • 一个advisor只能配置一个固定通知和一个切点表达式
  • 一个aspect可以配置多个通知和多个切点表达式

3、使用场景不同

  • 允许随意搭配情况下,可以使用aspect进行配置
  • 如果通知类型单一、切面单一,可以使用advisor进行配置
  • 在通知类型已经固定,不用人为指定通知类型时,可以使用advisor进行配置,例如后面要学习的Spring事务控制的配置
7.4.3 动态代理的实现

在这里插入图片描述
JDK动态代理是基于接口生成代理对象;Cglib动态代理是基于目标类生成子类作为代理对象,因为涉及到父子类,所以目标类不能使用final修饰,final修饰的类是不能被继承的,不存在父子类关系了;同时,目标类的目标方法不能加final修饰,因为final修改的类不能被重写,添加了代理就不会生效。

在这里插入图片描述
Cglib例子

1、目标对象Target

public class Target {
    public void show(){
        System.out.println("show...");
    }
}

2、通知对象MyAdvice

public class MyAdvice {

    public void beforeAdvice(){
        System.out.println("前置增强...");
    }

    public void afterAdvice(){
        System.out.println("后置增强...");
    }
}

3、生成代理对象

public class Test {
    public static void main(String[] args) {
        // 目标对象
        Target target = new Target();

        // 通知对象
        MyAdvice myAdvice = new MyAdvice();

        // 编写Cglib代码
        // 1、增强器对象
        Enhancer enhancer = new Enhancer();
        // 2、设置父类,生成的代理对象就是Target的子类
        enhancer.setSuperclass(Target.class);
        // 3、设置回调
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            // intercept方法就相当于invoke方法
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                // 前置增强
                myAdvice.beforeAdvice();

                // 执行目标方法
                Object result = method.invoke(target, objects);

                // 后置增强
                myAdvice.afterAdvice();

                return result;
            }
        });

        // 4、创建代理对象
        Target proxy = (Target) enhancer.create();

        // 5、调用方法
        proxy.show();
    }
}

4、运行结果:

前置增强...
show...
后置增强...
7.4.4 基于注解的AOP

Spring的AOP也提供了注解方式配置,使用相应的注解替代之前的xml配置,xml配置AOP时,我们主要配置了三部分:目标类被Spring容器管理、通知类被Spring管理、通知与切点的织入(切面)

1、目标对象交给Spring管理,只需要添加@Service注解

@Service("userService")
public class UserServiceImpl implements UserService {
    @Override
    public void show1() {
        System.out.println("show1...");
    }

    @Override
    public void show2() {
        System.out.println("show2...");
    }
}

2、通知交给Spring管理,只需要添加@Component注解

@Component
public class MyAdvice {

    public void beforeAdvice(){
        System.out.println("前置增强...");
    }

    public void afterAdvice(){
        System.out.println("后置增强...");
    }
}

3、在MyAdvice类上添加@Aspect注解,表示它是一个切面类。然后在里面的方法上加@Before和@After注解,表示前置、后置通知

@Component
@Aspect
public class MyAdvice {

    @Before("execution(* com.southwind.service.impl.*.*(..))")
    public void beforeAdvice(){
        System.out.println("前置增强...");
    }

    @After("execution(* com.southwind.service.impl.*.*(..))")
    public void afterAdvice(){
        System.out.println("后置增强...");
    }
}

4、在beans.xml中进行配置

<!-- 组件扫描 -->
<context:component-scan base-package="com.southwind"/>

<!-- 使用注解配置AOP,需要开启AOP自动代理,让Spring解析,不加的话AOP不生效 -->
<aop:aspectj-autoproxy/>

开启组件扫描,因为是基于注解开发;还有是开启AOP自动代理,让Spring解析,不加的话AOP不生效

优化:上述的MyAdvice类中,前置、后置通知的execution是写在了方法之上,如果后期要修改,就需要修改两个地方。能不能将它抽象出来,只需要改动一次呢

@Component
@Aspect
public class MyAdvice {

    @Pointcut("execution(* com.southwind.service.impl.*.*(..))")
    public void myPointcut(){}

    @Before("MyAdvice.myPointcut()")
    public void beforeAdvice(){
        System.out.println("前置增强...");
    }

    @After("MyAdvice.myPointcut()")
    public void afterAdvice(){
        System.out.println("后置增强...");
    }
}

myPointcut就是抽象出来的公共地方,然后在前置、后置通知上指定公共地方。

7.5 基于AOP的声明式事务控制

7.5.1 Spring事务编程概述

Spring的事务分为:编程式事务控制 和 声明式事务控制
在这里插入图片描述

7.5.2 基于xml声明式事务控制

1、创建表tb_account,插入数据

create table tb_account(
id int primary key auto_increment,
account_name varchar(20),
money int
);

insert into tb_account values(null, 'tom', 5000);
insert into tb_account values(null, 'lucy', 5000);

在这里插入图片描述

2、导入pom依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.29</version>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.7</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.1.1</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.27</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.13</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.20</version>
    </dependency>
</dependencies>

3、创建数据库配置文件jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=root

4、创建tb_account表对应的AccountMapper文件,一个加钱方法,一个减钱方法

public interface AccountMapper {
    // 加钱
    @Update("update tb_account set money = money + #{money} where account_name = #{accountName}")
    public void incrMoney(@Param("accountName") String accountName, @Param("money") Integer money);

    // 减钱
    @Update("update tb_account set money = money - #{money} where account_name = #{accountName}")
    public void decrMoney(String accountName, Integer money);
}

5、创建service

public interface AccountService {
    public void transferMoney(String outAccount, String inAccount, Integer money);
}
@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void transferMoney(String outAccount, String inAccount, Integer money) {
        accountMapper.decrMoney(outAccount, money);
        accountMapper.incrMoney(inAccount, money);
    }
}

6、beans.xml

<!-- 组件扫描 -->
<context:component-scan base-package="com.southwind"/>

<!-- 加载jdbc.properties文件 -->
<context:property-placeholder location="classpath:jdbc.properties"/>

<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>

<!-- 配置sqlSessionFactoryBean,将sqlSessionFactory存入到Spring容器 -->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"></property>
</bean>

<!-- MapperScannerConfigurer,扫描指定的包,产生对象存入到Spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.southwind.mapper"></property>
</bean>

7、测试类,模拟tom向lucy转账5000

public class Test {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        AccountService accountService = context.getBean(AccountService.class);
        accountService.transferMoney("tom", "lucy", 500);
    }
}

执行后的结果:
在这里插入图片描述

在上面的基础上,如果在业务实现类中手动添上1/0,再次执行

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void transferMoney(String outAccount, String inAccount, Integer money) {
        accountMapper.decrMoney(outAccount, money);

        int i = 1 / 0;

        accountMapper.incrMoney(inAccount, money);
    }
}

执行的时候会抛异常,同时tom少了500,而lucy没有多500,这就是缺少事务控制。
在这里插入图片描述

上面出问题的原因是,减钱是一个事务,加钱是另一个事务。我们要求的是这两个过程在一个事务中。

结合上面我们学习的AOP的技术,很容易就可以想到,可以使用AOP对Service的方法进行事务的增强。

  • 目标类:AccountServiceImpl
  • 切点:service业务类中的所有业务方法
  • 通知类:Spring提供的,通知方法已经定义好,只需要配置即可

需要进行如下配置:

  • 通知类是Spring提供的,需要导入Spring事务的相关的坐标;
  • 配置目标类AccountServiceImpl;
  • 使用advisor标签配置切面。

1、之前导入的spring-jdbc对应jar包下已经有Spring-tx事务对应jar包了
在这里插入图片描述
2、配置目标类AccountServiceImpl,我们在该类上面加了@Service注解,它已经交给Spring管理了

3、配置事务,在beans.xml中引入事务的命名空间tx

<?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"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd"
>
    <!-- 组件扫描 -->
    <context:component-scan base-package="com.southwind"/>

    <!-- 加载jdbc.properties文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- 配置sqlSessionFactoryBean,将sqlSessionFactory存入到Spring容器 -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- MapperScannerConfigurer,扫描指定的包,产生对象存入到Spring容器中 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.southwind.mapper"></property>
    </bean>

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

    <!-- 配置Spring提供好的advice -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>

    <!-- 事务增强的AOP -->
    <aop:config>
        <!-- 切入点 -->
        <aop:pointcut id="txPoint" expression="execution(* com.southwind.service.impl.*.*(..))"/>
        <!-- 通知advice-ref引用Spring提供好的 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPoint"/>
    </aop:config>
</beans>

4、将表中两人的金额恢复到5000
在这里插入图片描述
5、在service中抛出异常,测试事务是否生效

6、执行测试,控制台抛出异常,查看数据库中的结果,发现金额没变,事务生效了
在这里插入图片描述

问题:在<aop:pointcut>的execution中配置了表达式后,为什么还要<tx:method>中还要配置一遍?

在这里插入图片描述

<aop:pointcut>的execution是筛选出哪些方法要被增强,被事务控制。例如上图的A、B、C、D、E五个方法,只有A、C、D需要增强。

<tx:method>是为每个方法配置事务属性,比如不同的隔离级别,不同的事务传播行为。

平台事务管理器PlatformTransactionManager

它是基于DAO层的实现来决定的,像jdbc、mybatis(底层还是用的jdbc)都是对应DataSourceTransactionManager;如果是hibernate,就是另外一个平台事务管理器了

<tx:method>标签

用于配置不同方法的事务属性。常用属性:

  • name:方法名称,可以指定具体的方法名。*代表通配符,任意方法都用当前的这套事务属性进行配置。事务属性都有默认值
  • isolation:指定事务的隔离级别,事务并发存在三大问题:脏读、不可重复读、幻读/虚读。可以通过设置事务的隔离级别来保证并发问题的出现,常用的是READ_COMMITTED 和 REPEATABLE_READ

在这里插入图片描述

  • timeout:设置事务执行的超时时间,单位是秒,如果超过该时间限制但事务还没有完成,则自动回滚事务,不在继续执行。默认值是-1,即没有超时时间限制

  • read-only:设置当前的只读状态,如果是查询则设置为true,可以提高查询性能,如果是更新(增删改)操作则设置为false

  • propagation:设置事务的传播行为,主要解决是A方法调用B方法时,事务的传播方式问题的,例如:使用单方的事务,还是A和B都使用自己的事务等。事务的传播行为有如下七种属性值可配置

在这里插入图片描述

原理

底层还是AOP
在这里插入图片描述

7.5.3 基于注解声明式事务控制

1、使用@Transactional注解指定,加到目标方法上,可以指定isolation和propagation等参数。加到哪个方法之上就是给哪个方法添加事务。如果要一个类下的所有方法都加事务,可以在类上加上该注解,表示该类下的所有方法共有这一套事务。

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountMapper accountMapper;

    @Override
    @Transactional
    public void transferMoney(String outAccount, String inAccount, Integer money) {
        accountMapper.decrMoney(outAccount, money);

        int i = 1 / 0;

        accountMapper.incrMoney(inAccount, money);
    }
}

2、beans.xml中去掉原来advice和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"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd"
>
    <!-- 组件扫描 -->
    <context:component-scan base-package="com.southwind"/>

    <!-- 加载jdbc.properties文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- 配置sqlSessionFactoryBean,将sqlSessionFactory存入到Spring容器 -->
    <bean class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- MapperScannerConfigurer,扫描指定的包,产生对象存入到Spring容器中 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.southwind.mapper"></property>
    </bean>

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

    <!-- 事务的自动代理(注解驱动) -->
    <tx:annotation-driven/>
</beans>

3、开始测试前查看数据库中的数据
在这里插入图片描述
4、在业务实现类中主动抛出异常,然后执行转账,查看数据库,发现金额没变,事务生效了
在这里插入图片描述

当然可以将上面的所有配置换成全注解方式配置:

配置类:

@Configuration
@ComponentScan("com.southwind")
@PropertySource("classpath:jdbc.properties")
@MapperScan("com.southwind.mapper")
@EnableTransactionManagement //相当于<tx:annotation-driven/>
public class SpringConfig {

    @Bean
    public DataSource dataSource(
            @Value("${jdbc.driver}") String driverClassName,
            @Value("${jdbc.url}") String url,
            @Value("${jdbc.username}") String username,
            @Value("${jdbc.password}") String password
    ){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean;
    }

    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        AccountService accountService = context.getBean(AccountService.class);
        accountService.transferMoney("tom", "lucy", 500);
    }
}

7.6 AOP实现方式

  • 上面讲的代理
  • aspectj编译器

1、我们需要在pom.xml中引入acpectj编译器
在这里插入图片描述
2、通知类
在这里插入图片描述
注意到,上面的通知类上面并没有添加像@Component这样的注解,即未添加到Spring容器中,但是确实实现了代理的功能。这是因为aspectj编译器修改了最终的字节码class,将通知类的逻辑加到了目标类上面

3、目标类
在这里插入图片描述
4、aspectj编译器修改了最终的字节码class
在这里插入图片描述

  • agent类加载。在类加载阶段实现类的增强

1、通知类
在这里插入图片描述
2、目标类
在这里插入图片描述
注意到,上面的通知类上面并没有添加像@Component这样的注解,即未添加到Spring容器中

3、添加JVM启动参数

-javaagent aspectjweaver.jar

8、Spring整合web环境

8.1 Javaweb三大组件

在这里插入图片描述

8.2 Spring整合web环境的思路及实现及环境特点

在进行Java开发时要遵循三层架构+MVC,Spring操作最核心的就是Spring容器,web层需要注入Service,service层需要注入Dao(Mapper),web层使用Servlet技术充当的话,需要在Servlet中获得Spring容器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

web层代码如果都去编写创建AnnotationConfigApplicationContext的代码,那么配置类重复被加载了, Spring容器也重复被创建了,不能每次想从容器中获得一个Bean都得先创建一次容器,这样肯定是不允许。 所以,我们现在的诉求很简单,如下:

  • ApplicationContext创建一次,配置类加载一次;

  • 最好web服务器启动时,就执行第1步操作,后续直接从容器中获取Bean使用即可;

  • ApplicationContext的引用需要在web层任何位置都可以获取到。

针对以上诉求我们给出解决思路,如下:

  • 在ServletContextListener的contextInitialized方法中执行ApplicationContext的创建。或在Servlet的init方法中执行ApplicationContext的创建,并给Servlet的load-on-startup属性一个数字值,确保服务器启动Servlet就创建;
  • 将创建好的ApplicationContext存储到ServletContext域(Application域,另外两个域是request、session)中,这样整个web层任何位置就都可以获取到了

代码实现

1、创建ContextLoaderListener,主要的实现流程都在这个类中

public class ContextLoaderListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("init............");
        // 1、创建Spring容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

        // 2、将容器存储到ServletContext/application域中
        sce.getServletContext().setAttribute("applicationContext", context);
    }
}

首先,我们创建Spring容器。然后通过sce.getServletContext()获取ServletContext对象,调用它的setAttribute方法,将spring容器注入到application域中

2、在web.xml中配置该listener

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

    <!-- 配置listener -->
    <listener>
        <listener-class>com.southwind.listener.ContextLoaderListener</listener-class>
    </listener>
</web-app>

3、AccountServlet代码,获取spring容器的方式有变化,这里先获取ServletContext对象,然后调用它的getAttribute方法获取spring容器

@WebServlet(urlPatterns = "/accountServlet")
public class AccountServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext servletContext = req.getServletContext();
        ApplicationContext context = (ApplicationContext) servletContext.getAttribute("applicationContext");
        AccountService accountService = context.getBean(AccountService.class);
        accountService.transferMoney("tom", "lucy", 500);
    }
}

上述的代码只是加深理解,实际开发都是调用第三方jar包已经写好的方法