Spring中的IOC和AOP

时间:2024-03-25 22:39:27

Spring两大核心机制:IOC和AOP

一、IOC:控制反转

传统开发中,需要调用对象的时候,需要调用者手动来创建被调用者的实例,即对象是由调用者new出来的;
但在Spring框架中,创建对象的工作不再由调用者来完成,而是交给IOC容器来创建,再推送给调用者,用完再还回来,类似于缓冲池,整个流程完成反转,所以是控制反转。

  • Spring全家桶各个功能模块的基础,是创建对象的容器。
  • IOC的特点是解耦合:比如说A需要用到B,传统的开发,我们要直接创建B的实例,但是在Spring中,IOC这个容器会创建B的实例,然后把这个B注入到A。

IOC的使用:基于xml配置文件、基于注解(主要)


 注:以下内容,获取DataConfig的对象是最终目的

1.基于xml:借助与DataConfig类对应的xml文件-spring.xml,主要靠Bean

1.1 创建Maven项目,在pom.xml配置文件中导入spring依赖
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.15</version>
</dependency>
1.2 创建一个类-DataConfig

@Data是可以不用手动set、get

package com.circle.ioc;

import lombok.Data;

@Data
public class DataConfig {
    private String url;
    private String driverName;
    private String username;
    private String password;
}
1.3 在resources路径下创建与类对应的配置文件,IOC容器通过读取配置文件,加载配置bean标签来创建对象
<bean class="com.circle.ioc.DataConfig" id="config">
        <property name="driverName" value="Driver"></property>
        <property name="url" value="localhost:3306"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
</bean>
1.3.1配置文件:
  • bean:通过配置bean标签来完成对象的管理
  • id:对象名
  • class:对象的模板类(所有交给IOC容器来管理的类必须要有无参构造函数,因为Spring底层是通过反射机制来创建对象,调用的是无参构造)
  • property:对象的成员变量通过property标签完成赋值
  • name:成员变量名
  • value:成员变量值(基本数据类型,String可以直接赋值,如果是其他引用类型不可以通过value赋值)
  • ref:把IOC中的另一个bean赋给当前成员变量(DI依赖注入),见下图(一个类中有另一个类的对象)此时用ref,不能用value,否则会抛出类型转换异常

1.3.2 IOC容器创建bean的两种方法: 
  • 无参构造函数(需要提供对应的set方法或使用lombok):property
  • 有参构造函数:constructor-arg
        type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型。
        index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始。
        name:用于指定给构造函数中指定名称的参数赋值。
<bean id="stu1" class="com.zyh.pojo.Student">
    <constructor-arg name="id" value="1">  </constructor-arg>
    <constructor-arg name="name" value="李四"></constructor-arg>
</bean>

<bean id="stu1" class="com.zyh.pojo.Student">
    <constructor-arg index=0 value="1">  </constructor-arg>
    <constructor-arg index=1 value="李四"></constructor-arg>
</bean>
 1.3.3 bean是根据scope来生成的,表示bean的作用域
  1. singleton,单例,表示通过Spring容器获取的对象是唯一的,是默认值。只要加载IOC容器,不管是否从IOC种取出bean,配置文件中的bean都会被创建,而且只会创建一个对象
  2. prototype,原型,表示通过Spring容器获取的对象是不同的。如果不从IOC中取出bean,则不创建对象,取一次bean,就会创建一个对象
    <bean id="user" class="com.zyh.pojo.User" scope="prototype">
            <property name="id" value="1"></property>
            <property name="name" value="张三"></property>
        </bean>
    
  3. request,请求,表示在异常HTTP请求内有效,一般用于web项目
  4. session,会话,表示在一个用户会话内有效,一般用于web项目
1.3.4 复杂类型的依赖注入:
public class Student {
    private String name;
    private Person person;
    private String[] arr;
    private List<String> myList;
    private Map<String,String> myMap;
    private Set<String> mySet;
    private String wife;
    private Properties myPro;
}
<bean id="student" class="com.kang.pojo.Student">
        <!--普通值注入,value:具体属性值-->
        <property name="name" value="jerry"/>

        <!--Bean注入,ref:对象-->
        <property name="person" ref="person"/>

        <!--数组注入-->
        <property name="arr">
            <array>
                <value>AAA</value>
                <value>BBB</value>
                <value>CCC</value>
            </array>
        </property>

        <!--List注入-->
        <property name="myList">
            <list>
                <value>111</value>
                <value>222</value>
                <value>333</value>
            </list>
        </property>

        <!--Map注入-->
        <property name="myMap">
            <map>
                <entry key="aaa" value="aaaa"></entry>
                <entry key="bbb" value="bbbb"></entry>
                <entry key="ccc" value="cccc"></entry>
            </map>
        </property>

        <!--Set注入-->
        <property name="mySet">
            <set>
                <value>111</value>
                <value>222</value>
                <value>333</value>
            </set>
        </property>

        <!--null注入-->
        <property name="wife">
            <null/>
        </property>

        <!--Properties注入-->
        <property name="myPro">
            <props>
                <prop key="aaa">aaaa</prop>
                <prop key="bbb">bbbb</prop>
                <prop key="ccc">cccc</prop>
            </props>
        </property>
</bean>
1.3.5 bean的属性如果包含特殊字符:使用CDATA

1.3.6 依赖注入之p、c命名空间
p命名空间是set注入的一种快捷实现方式,对应<property> ,想要使用p命名空间注入,需要注意一下几点。
1. 实体类中必须有set方法;
2. 实体类中必须有无参构造器(默认存在);
3. 必须导入p命名空间注入方式依赖。(类对应的xml配置文件)
xmlns:p="http://www.springframework.org/schema/p"
4. 导入后即可使用:
<bean id="user" class="com.yd.pojo.User" p:age="18" p:name="老王"/>
c命名空间是构造器注入的一种快捷实现方式,对应<constructor-arg>,想要使用c命名空间,需要注意一下几点。
1. 实体类中必须存在有参构造器;
2. 必须导入c命名空间注入方式依赖。
xmlns:c="http://www.springframework.org/schema/c"

3. 导入后即可使用:

<bean id="user2" class="com.yd.pojo.User" c:age="23" c:name="中王"/>
1.4 创建一个test文件,调用API,从IOC获取对象
package com.circle.ioc;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.xml.crypto.Data;

public class Test {
    public static void main(String[] args) {
//        // 不用ioc,所有对象开发者自己创建
//        DataConfig dataConfig = new DataConfig();
//        dataConfig.setDriverName("Driver");
//        dataConfig.setUrl("localhost::3306/dbname");
//        dataConfig.setUsername("root");
//        dataConfig.setPassword("root");

        // 使用ioc,对象不用开发者创建,交给spring框架完成
        // 两种方式:基于注解和XML(bean),主要是注解
        // 基于xml:把需要的对象在xml中进行配置,spring框架读取这个配置文件,根据配置文件的内容和反射机制来创建对象
        ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");  // ioc容器
        System.out.println(context.getBean("config"));  // 从ioc中取出bean,通过id
    }
}
1.4.1从IOC容器中取bean的方法:
  • 通过id取值:
 Student stu = (Student)applicationContext.getBean("stu");
  • 通过类型取值
 Student stu = applicationContext.getBean(Student.class);
   当IOC容器中存在两个以上Student Bean的时候就会抛出异常,因为此时没有唯一的bean
1.5 在XML方式中有三种方式来实例化bean
  • 反射模式
  • 工厂方法模式:静态工厂类不需要实例化,实例工厂类需要实例化
静态工厂类:
  1. 创建一个目标类对应的静态工厂类,写一个静态方法
  2. 在目标类对应的配置文件中
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="car" class="com.zyh.factory.StaticCarFactory" factory-method="getCar">
        <constructor-arg  name="num" value="1"></constructor-arg>
    </bean>

</beans>

                factory-method 指向静态方法

                constructor-arg的name、value属性是调用静态方法传入的参数

  • 静态工厂方法创建对象,不需要实例化工厂对象,因为静态工厂的静态方法,不需要创建对象就可以调用了
实例工厂类:
  1. 创建一个目标类对应的实例工厂类,写一个方法
  2. 在目标类对应的配置文件中
<!--    实例工厂类-->
    <bean id="instanceCarFactory" class="com.zyh.factory.InstanceCarFactory"></bean>
<!--    通过实例工厂获取Car-->
    <bean id="car1"  factory-bean="instanceCarFactory" factory-method="getCar">
        <constructor-arg value="2"></constructor-arg>
    </bean>
  • 实例工厂方法创建对象,需要实例化工厂对象,因为方法是非静态的,就必须通过实例化对象才能调用,所以必须创建工厂对象,spring.xml需要配置两个bean,一个是工厂类bean,一个是方法Bean

  • FactoryBean模式

2. 基于注解:两种方式(配置类、扫包+注解)

2.1 配置类:用一个java文件(配置类)替代xml文件,将xml中的配置内容放在配置类中

在类前加@Configuration即为配置类,整个配置类可以类比为xml文件;在类中写一个方法,方法前加@Bean,返回一个对象存入IOC容器中,类比为bean。

package com.circle.configuration;

import com.circle.ioc.DataConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 基于注解
// 1. 配置类:用一个java类替代xml文件,把在xml中配置的内容放在配置类中
// 2. 扫包 + 注解
@Configuration    // 配置类
public class BeanConfiguration {
    // 整个BeanConfiguration类对应spring.xml文件
    // 方法返回的对象对应xml中的bean
    // 加载配置类时需调用这个方法,把返回的对象存入ioc
    // value或name取别名,但不能再使用原方法名了,id唯一
    @Bean(value = "config")
    public DataConfig dataConfig(){
        DataConfig dataConfig = new DataConfig();
        dataConfig.setDriverName("Driver");
        dataConfig.setUrl("localhost::3306/dbname");
        dataConfig.setUsername("root");
        dataConfig.setPassword("root");
        return dataConfig;
    }
}
 2.2 测试
package com.circle.ioc;

import com.circle.configuration.BeanConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.xml.crypto.Data;

public class Test {
    public static void main(String[] args) {
        // 基于注解:配置类
        // 读取配置类初始化IOC容器
        ApplicationContext context = new AnnotationConfigApplicationContext(BeanConfiguration.class);
//        System.out.println(context.getBean("dataConfig"));  // 方法名作为id
        System.out.println(context.getBean("config"));   // @Bean后加上value或name赋值,也可用value的值作为id,但取了value后,不能再用原方法名了,因为只有一个id
    }
}
存在问题:new哪行代码BeanConfiguration.class只能传一个类,实际应用中会有很多配置类。 
解决:扫包(配置类的包)
package com.circle.ioc;

import com.circle.configuration.BeanConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.xml.crypto.Data;

public class Test {
    public static void main(String[] args) {
        // 扫包:配置类的包
        ApplicationContext context = new AnnotationConfigApplicationContext("com.circle.configuration");
        System.out.println(context.getBean("dataConfig"));
    }
}

2.2 扫包+注解:直接将Bean的创建交给目标类-DataConfig,在目标类中添加注解@Component来创建
@Component注解告诉Spring框架,这个类需要创建对象注入到IOC容器
@value注解赋值
package com.circle.ioc;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component
public class DataConfig {
    @Value("localhost:3306")
    private String url;
    @Value("Drive")
    private String driverName;
    @Value("root")
    private String username;
    @Value("root")
    private String password;
}
测试:
package com.circle.ioc;

import com.circle.configuration.BeanConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.xml.crypto.Data;

public class Test {
    public static void main(String[] args) {
        // 扫包:目标类的包
        ApplicationContext context = new AnnotationConfigApplicationContext("com.circle.ioc");
        System.out.println(context.getBean(DataConfig.class));
    }
}

3. 依赖注入:两个类A和B,A中有B的对象,创建AB两个对象,自动将B装入A

A~GlobalConfig  B~DataConfig
1. @Autowired 自动装载注解,自动去ioc中找DataConfig类型的Bean,默认通过类型注入(ByType),找到了就拿过来赋值(DataConfig类也一定要有@Component注解)。
2. 如果想通过名字注入(ByName),使用@Qualifier注解
 1. 类型注入@Autowired
package com.circle.ioc;

import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
@Data
@Component
public class GlobalConfig {
    @Value("8080")
    private String port;
    @Value("/")
    private String path;
    @Autowired
    private DataConfig dataConfig;
}
package com.circle.ioc;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component
public class DataConfig {
    @Value("localhost:3306")
    private String url;
    @Value("Drive")
    private String driverName;
    @Value("root")
    private String username;
    @Value("root")
    private String password;
}
测试:
package com.circle.ioc;

import com.circle.configuration.BeanConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.xml.crypto.Data;

public class Test {
    public static void main(String[] args) {
        // 扫包:目标类的包
        ApplicationContext context = new AnnotationConfigApplicationContext("com.circle.ioc");
        System.out.println(context.getBean(DataConfig.class));
    }
}

2.  使用名字注入:@Qualifier,注意DataConfig类中要取别名,与@Qualifier一致
package com.circle.ioc;

import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.SecureRandom;
@Data
@Component
public class GlobalConfig {
    @Value("8080")
    private String port;
    @Value("/")
    private String path;
//    @Autowired
//    private DataConfig dataConfig;  // 类型注入
    @Qualifier("config")
    private DataConfig dataConfig; // 名字注入
}
package com.circle.ioc;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Data
@Component(value = "config")
public class DataConfig {
    @Value("localhost:3306")
    private String url;
    @Value("Drive")
    private String driverName;
    @Value("root")
    private String username;
    @Value("root")
    private String password;
}

二、AOP:以IOC为基础,面向切面编程,是抽象的面向对象。

        多个方法在相同位置有相同的操作,切一刀,将切面抽象为一个对象,把相同的操作代码写进对象,对对象进行编程,底层使用动态代理机制。
        可以打印日志、事务、权限管理。

1. 原代码:代码维护性、复用性差

接口:

package com.circle.aop;

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);

}

实现接口的类:

package com.circle.aop;

public class calculate implements cal{
    @Override
    public int add(int num1, int num2) {
        System.out.println("add方法的参数为:" + num1 + "、" + num2);
        int result = num1 + num2;
        System.out.println("add方法的结果为:" + result);
        return result;
    }

    @Override
    public int sub(int num1, int num2) {
        System.out.println("sub方法的参数为:" + num1 + "、" + num2);
        int result = num1 - num2;
        System.out.println("sub方法的结果为:" + result);
        return result;
    }

    @Override
    public int mul(int num1, int num2) {
        System.out.println("mul方法的参数为:" + num1 + "、" + num2);
        int result = num1 * num2;
        System.out.println("mul方法的结果为:" + result);
        return result;
    }

    @Override
    public int div(int num1, int num2) {
        System.out.println("div方法的参数为:" + num1 + "、" + num2);
        int result = num1 / num2;
        System.out.println("div方法的结果为:" + result);
        return result;
    }
}

把参数和结果部分的日志代码抽离出业务代码,统一处理,使核心业务代码与非业务代码解耦合。

2. 使用AOP

AOP的优点:
  • 可以降低模块之间的耦合性
  • 提供代码的复用性
  • 提高代码的维护性
  • 集中管理非业务代码,便于维护
  • 业务代码不受非业务代码影响,逻辑更加清晰

2.1 在pom.xml中引入依赖

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.15</version>
        </dependency>

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

2.2 创建切面类

将切面对象注入IOC容器,目标类对象也注入IOC,目标类对象与切面对象整合形成一个代理对象(动态代理机制),操作代理对象

package com.circle.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.util.Arrays;

// 切面类
@Component
@Aspect
public class LoggerAspect {
    @Before("execution(public int com.circle.aop.calculate.*(..))")
    public void before(JoinPoint joinPoint){  // 切面对象与方法之间有个连接点对象Joinpoint,需要被横切的位置,即通知要插入业务代码的具体位置
        String name = joinPoint.getSignature().getName();
        System.out.println(name + "方法的参数是:" + Arrays.toString(joinPoint.getArgs()));
    }

    @AfterReturning(value = "execution(public int com.circle.aop.calculate.*(..))", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result){
        String name = joinPoint.getSignature().getName();
        System.out.println(name + "方法的结果是:" + result);
    }
}
  • @Before,表示方法的执行时机是在业务方法之前,execution表达式表示切入点是calculate中的所有方法
  • @AfterReturning,表示方法的执行时机是在业务方法返回结果后,execution表达式表示切入点是calculate类中的方法,returning是把业务方法的返回值和切面类方法的形参进行绑定
  • @AfterThrowing,表示方法的执行时机是在业务方法抛出异常后,execution表达式表示切入点是calculate类中的方法,throwing是把业务方法的异常和切面类方法的形参进行绑定
  • @After,表示方法的执行时机是在业务方法结束以后,execution表达式表示切入点是calculate类中的方法

2.3 实现类

package com.circle.aop;

import org.springframework.stereotype.Component;
// 目标类
@Component
public class calculate implements cal{
    @Override
    public int add(int num1, int num2) {
        int result = num1 + num2;
        return result;
    }

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

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

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

2.4 配置自动扫包,开启自动生成代理对象

在spring.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: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/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

<!--    自动扫包-->
    <context:component-scan base-package="com.circle.aop"></context:component-scan>
    
<!--    开启自动生成代理对象-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

2.5 测试

package com.circle.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        cal bean = context.getBean(cal.class);  // 代理对象和目标类对象都是实现了接口的类
        bean.add(9, 8);
        bean.sub(9, 8);
        bean.mul(9, 8);
        bean.div(9, 8);
    }
}

当有多个实现类,会报错,因为切面对象不知道与谁整合成代理对象,目标类对象和切面对象和代理对象是一对一对一的关系。

参考:http://t.csdnimg.cn/EOXh8 

        b站楠哥教你学java-1、什么是IoC和AOP_哔哩哔哩_bilibili