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的作用域
singleton,单例,表示通过Spring容器获取的对象是唯一的,是默认值。只要加载IOC容器,不管是否从IOC种取出bean,配置文件中的bean都会被创建,而且只会创建一个对象
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>
request,请求,表示在异常HTTP请求内有效,一般用于web项目
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注入的一种快捷实现方式,想要使用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命名空间是构造器注入的一种快捷实现方式,想要使用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
- 反射模式
- 工厂方法模式:静态工厂类不需要实例化,实例工厂类需要实例化
静态工厂类:
- 创建一个目标类对应的静态工厂类,写一个静态方法
- 在目标类对应的配置文件中
<?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属性是调用静态方法传入的参数
- 静态工厂方法创建对象,不需要实例化工厂对象,因为静态工厂的静态方法,不需要创建对象就可以调用了
实例工厂类:
- 创建一个目标类对应的实例工厂类,写一个方法
- 在目标类对应的配置文件中
<!-- 实例工厂类--> <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