笔记基于以下教程,包括:Spring之一_IOC、Spring之二_AOP
Spring视频教程:https://www.bilibili.com/video/BV1PJ411w7bX
特别声明:
以下叙述参杂个人理解,以方便自己理解为首要目的,完备的正确性其次,如有谬误烦请指出
一 IOC的概念
IOC就是通过配置文件创建对象【对应Bean标签】。
1.1配置文件是由谁加载和解析?
Spring中ApplicationContext接口的实现类,负责创建IOC核心容器(或者说,Context就是核心容器?)、读取配置文件创建对象,并将对象装入核心容器。所谓的核心容器就是一个Map,创建的对象就是作为Map中的元素,以全限定类名为key,以类对象为value。
1.2 配置文件怎么知道要创建哪些对象?
这个问法存在有问题,在逻辑上应该是,开发中某步需要使用类对象,这时程序员才会在XML配置文件中配置这个对象,也就是说XML配置文件和开发程序是同步进行的。
那什么时候会"需要使用类对象",也就是开发时为什么需要创建实例?无外乎三种:需要调用类对象的成员方法、需要访问类对象的成员变量、需要将类对象赋值给其他变量。
1.3 解析配置文件后调用什么方法创建类对象?
创建类对象有三种方式:直接调用类的构造函数(new方法)、通过其他类的普通方法创建(该方法返回类对象)、通过其他类的静态方法创建(该方法返回类对象)
在配置文件中可以指定用哪种方式来创建类对象【虽然都是对应bean标签,但bean标签使用的方法不同】
注意:这里其实不应该说有哪几种方法创建类,而是在获取一个类对象时我们可能碰到哪些情况。特别是第二、三种。如果是自定义类或者提供了new方法的第三方类,完全没必要绕圈子通过一个中间类去获取想要的类对象,关键就在于如果要获取的是第三方jar包中的类而且没有提供new方法,只提供了由某个第三方的工厂类创建,该怎么办?直接修改第三方jar包?第三方jar包都是.class字节码文件,而不是.java文件,没法直接修改源码。【会有类没有提供构造函数吗?不会,但是要考虑new方法能不能访问到,比如:静态内部类实现单例模式,这种情况就要通过外部类的方法获取内部类的实例,不能直接new】
第二、三种方法就是在解决这个问题,首先获取这个第三方工厂类的对象。然后调用该类的方法获取目标第三方类对象,根据方法是否为静态方法,分别对应第二和第三种方法。
继续扩展:第三方类对象,既没有向外暴露new方法,又没有通过某个方法返回该对象,我们能不能创建该类对象?不能。所以,这里与其说在讲有三种创建类对象的方式,不如说是,只有三种情况下才能获取到类对象。而且,具体是哪种,必须要指明。【其实通过反射获取私有构造方法,然后解除私有,也能创建对象,但不管怎么说,不改变私有是无法创建的】
第一种方式:调用类的构造函数创建对象
<bean id="XXX" class="XXX"></bean>
第二种方式:先创建第三方类对象,然后调用该对象的实例方法
<bean id="XXX" class="XXX"></bean>
<bean id="XXX" factory-bean="XXX" factory-method="XXX"></bean>
第三种方式:直接调用第三方类的类方法,不需要创建第三方类的对象
<bean id="XXX" class="XXX" factory-method="XXX"></bean>
bean标签中用class属性,是反射创建类对象,而反射创建类对象其实就是自动查找类的合适的构造函数。【无论中间怎么绕、封装了多少层,创建类对象最后必定是基于类的构造函数】
1.4 解析配置文件后什么时候去创建对象?
这与类对象本身配置有关,如果类对象是一个单例对象,那么在读取完配置文件后立马创建对象并装入核心容器。如果类对象是一个多例对象,那么真正调用这个对象时才会创建。以上的区别是显然是为了提高效率。【从这里看来,对于创建多例对象并没有将异常从运行时转移到编译时】
在配置文件中可以声明类是单例还是多例。再进一步地讲,对单例和多例对象的创建,spring自动选择不同接口的方法去实现。
1.5 如何获取IOC容器中的对象?
从IOC容器中获取对象的方法有两种,一种是自动获取,一种是手动获取。
自动获取:仅适用获取对象后赋值给另一个对象的成员变量,必须要求两个对象都要保存在IOC核心容器中,否则无法进行。如:XML配置中,<bean>
标签创建类对象时,使用ref
属性获取其他对象给成员变量赋值;注解配置中,使用@Autowired
等注解。
手动获取:要先获取IOC核心容器,然后通过对象的id从中手动获取对象,获取到对象后的操作与IOC再无关系,甚至于Spring再无关系,可以当作一个普通的对象使用。【根据配置文件创建并获取ApplicationContext接口的实现类对象,返回的就是核心容器】【如果不使用注解,要想在代码中获取IOC容器的对象,貌似只有手动获取一条路】
1.6 创建的对象在哪些地方可以访问?
通过设置对象是单例和多例不仅决定了对象的创建时机,也决定了对象的作用范围。bean的作用范围怎么理解【对应bean标签中的scope属性】?如果是单例,那么代码中所有使用该类的对象的地方都共享这个对象,也就是配置的这个类对象作用范围广。而多例的话,那么代码中每个使用该类的对象的地方都有一个独立的对象。除了以上所讲的范围外,还可以作用在更广的范围:比如,分布式部署在多台物理机上的程序。
【多例对象时,spring怎么分辨哪个对象作用在哪个范围的?貌似配置文件中没有配置多个Bean啊?个人猜测:之前说了核心容器就是一个Map,可以理解为程序中创建对象时,spring先识别到该类对象在配置文件中定义的是单例还是多例,如果是多例的话,用程序中的变量名和新创建的Bean对象(比如地址什么的)作为一个键值对,如果是单例的话,也这样建一个键值对,不过程序中所有变量指向同一个对象。总之,一句话就是,spring自己负责分辨的,不用人配置。】
<bean id="XXX" class="XXX" scope="singleton/propotype/session/global-session"></bean>
1.7 创建对象后什么时候销毁对象呢?
这个还是与对象是单例还是多例有关。多例对象,由java的垃圾回收器决定销毁,当长时间不用又没被其他对象引用就会被销毁。单例对象,spring核心容器销毁时才会被一并销毁,如果不手动销毁容器的话,就是main程序结束的时候销毁。
销毁的时机是固定的,不需要也不能改变,但是在创建后、销毁前可以指定类对象执行某些方法。
<bean id="XXX" class="XXX" scope="singleton" init-method="XXX" destroy-method="XXX"></bean>
1.8 创建对象后怎么为成员变量赋值?
Java是面对对象语言,对象是对事物的抽象和封装。创建类是为了保存和调用数据,也就是对成员变量赋值和调用。在IOC中,为类的成员变量赋值被称为依赖注入DI。
注意:
IOC(DI)并不能实现"为任意变量赋值",IOC操作中涉及的只有类、类的成员变量,IOC(DI)只能为类的成员变量赋值,不能为方法中的局部变量赋值。
可以将IOC的作用理解为只有一个:为类的成员变量赋值,创建和管理对象只是为了实现这个作用必须的操作。
1.8.1 成员变量的数据类型
成员变量的数据类型可分为三种:基本数据类型、容器类型、对象,不同的类型需要使用<property>
标签不同的属性值来进行赋值。Java是面对对象语言,类的成员变量通常也是其他类的对象,所以最关注第三种情况。另外,要想将一个类对象赋值给成员变量,自然必须先创建该类对象,在IOC中被用于赋值的对象必须存在于IOC核心容器。
1.8.2 成员变量赋值的方法
根据Java中封装思想,对成员变量访问/赋值必须通过对应的gettert/setter方法。IOC中为成员变量赋值实际也是调用setter方法,因此实体类必须先为成员变量创建setter方法,才能进行赋值。
<property></property>
详情略,直接见下面的代码
1.8.3 补充:用构造函数赋值
严格来说1.3中的<bean>
不是"创建对象",而是"指示创建对象"。因为创建对象必然是调用构造函数,如果构造函数不是默认的无参构造函数的话,必须要指定所需的参数,而1.3中没有指定参数,所以1.3中只能创建"具有无参构造函数"的类对象。
就实体类来说,有参构造函数往往也是为了给必须的成员变量赋初值。所以,有参构造函数也可以看作是为成员变量赋值的一种方式。
<constructor-arg></constructor-arg>
详情略,直接见下面的代码
二 基于XML的IOC
之后开发主要还是用注解方法,但是理解XML配置是理解注解的基础。XML方法相当于手动档,注解相当于自动档。
2.1 maven依赖
spring核心容器context
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
2.2 XML配置文件
XML文件对IOC的配置就是对确定上面介绍的几个方面,包括:使用什么方法创建对象、什么时候真正创建对象(单例/多例)、对象的作用范围(作用域)、创建和销毁对象时附带执行什么操作(生命周期)、为类对象的成员变量赋值。
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置spring的IOC,将accountService对象加入容器-->
<!-- 注意:AccountServiceImpl类中只有带有一个参数的构造函数,只能通过下面的利用构造函数注入方式-->
<!-- constructor-arg标签:基于构造函数注入
name/type/index属性:分别基于变量名/变量类型/变量索引来匹配构造函数中的变量;
value属性:变量值;spring框架可自动将配置文件中的字符串转为构造函数中的基本数据类型,但无法转为对象类型
ref属性:变量引用;用于指定spring容器的其他bean对象(XML文件中配置的),实现为成员变量赋值对象类型的变量值;
-->
<!-- 注意:这里是xml文件和AccountServiceImpl类在一个目录下(是吗?),所以xml文件能够自动找到该类-->
<bean id="accountService" class="AccountServiceImpl">
<!-- 注入基本数据类型-->
<constructor-arg name="age" value="12"></constructor-arg>
<!-- 注入对象类型-->
<constructor-arg name="birthday" ref="birthday"></constructor-arg>
<!-- 注入容器类型-->
<constructor-arg name="myList">
<!-- list标签可以用于为list、array、set数据类型注入[单个值类型的容器]-->
<list>
<!-- 注意:不用加引号了-->
<value>AAA</value>
<value>BBB</value>
</list>
</constructor-arg>
<constructor-arg name="myMap">
<!-- map标签可用于为map、property数据类型注入[键值对类型的容器]-->
<map>
<!-- 注意:不用加引号了-->
<entry key="firtst" value="AAA"></entry>
<entry key="second" value="BBB"></entry>
</map>
</constructor-arg>
</bean>
<!-- 配置spring的IOC,将AccountServiceImpl2对象加入容器-->
<!-- 注意:AccountServiceImpl2类中只有默认的无参构造函数但有set方法,只能通过下面的利用set函数注入方式-->
<!-- property标签:基于set方法注入
name属性:匹配类中的set方法;取setXXX()方法的XXX字段并将首字母小写得到的字符串即为name匹配的目标
value属性:变量值;spring框架可自动将配置文件中的字符串转为构造函数中的基本数据类型,但无法转为对象类型
ref属性:变量引用;用于指定spring容器的其他bean对象(XML文件中配置的),实现为成员变量赋值对象类型的变量值;
-->
<bean id="accountService2" class="AccountServiceImpl2">
<property name="age" value="12"></property>
<property name="birthday" ref="birthday"></property>
<!-- 注入容器类型-->
<property name="myList">
<!-- list标签可以用于为list、array、set数据类型注入[单个值类型的容器]-->
<list>
<!-- 注意:不用加引号了-->
<value>AAA</value>
<value>BBB</value>
</list>
</property>
<property name="myMap">
<!-- map标签可用于为map、property数据类型注入[键值对类型的容器]-->
<map>
<!-- 注意:不用加引号了-->
<entry key="firtst" value="AAA"></entry>
<entry key="second" value="BBB"></entry>
</map>
</property>
</bean>
<!-- 使用Date类的无参构造函数创建Date对象-->
<bean id="birthday" class="java.util.Date"></bean>
</beans>
2.3 代码实现
public class AccountServiceImpl implements IAccountService{
private Integer age;
private Date birthday;
private List<String> myList;
private Map<String,String> myMap;
public AccountServiceImpl(Integer age,Date birthday,List<String> myList,Map<String,String> myMap){
this.age=age;
this.birthday=birthday;
this.myList=myList;
this.myMap=myMap;
}
}
public class AccountServiceImpl2 implements IAccountService {
private Integer age; // 基本数据类型
private Date birthday; // 对象类型
private List<String> myList; // 复杂(集合)数据类型,list
private Map<String,String> myMap; // 复杂(集合)数据类型,map
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public List<String> getMyList() {
return myList;
}
public void setMyList(List<String> myList) {
this.myList = myList;
}
public Map<String, String> getMyMap() {
return myMap;
}
public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}
}
public class MyTest {
public static void main(String[] args) {
// 1.获取核心容器对象
ApplicationContext ac = new ClassPathXmlApplicationContext("beans.xml");
// 2.根据对象id手动从IOC容器中获取对象
IAccountService as1 = (IAccountService) ac.getBean("accountService");
IAccountService as2 = (IAccountService) ac.getBean("accountService2",IAccountService.class);
System.out.println(as1.toString());
System.out.println(as2.toString());
}
}
三 基于注解的IOC
注解配置与XML配置的关系,粗略地说:XML是集中配置,将所有需要的IOC配置集中在一个文件中;注解是分散配置,哪段代码要使用IOC就在该段代码上进行配置。
3.1 maven依赖
https://www.cnblogs.com/nwu-edu/p/9542074.html
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 这里只要一个依赖就够了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
</dependency>
</dependencies>
</project>
3.2 配置包扫描
包扫描用于告诉spring哪些文件有使用注解(反过来说就是,指明要识别哪些文件中的注解)。也相当于指明了,分散的配置文件(Bean)都在哪里。包扫描的设置可以使用XML配置文件,也可以使用配置类。
3.2.1 基于XML配置包扫描
注意,这是使用注解配置方式时,即使仍然存在XML文件,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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 告知spring在创建容器时要扫描的包-->
<context:component-scan base-package="service"></context:component-scan>
</beans>
3.3 基于配置类配置包扫描
用于取代上面的XML配置文件,实现全注解开发
@Configuration: 作用于类上,表明为配置类,代替XML配置文件;
@ComponentScan: 作用于配置类上,用于指定包扫描,代替XML配置文件中的<context:component-scan>;
basePackages属性指定要扫描的包;
@Configuration
@ComponentScan(basePackages = "service")
public class springConfiguration {
}
-
注解要在包扫描之后才会被识别和加载,那么配置类上的
@Configuration
是如何被识别的呢?
这个问题类似于XML文件自身是如何被识别和加载的,和加载XML配置文件类似,配置类的加载需要在AnnotationConfigApplicationContext
对象中手动加载。该类也是ApplicationContext
接口的实现类。
AnnotationConfigApplicationContext
中可指定多个类,表示多个同等级的配置文件。对于被指定的类,spring框架自动视为配置类、自动扫描和加载,不用配置@Configuration,其所在包也不需要被@ComponentScan 扫描。 -
@Configuration
通过设置包扫描可实现多级的配置类,AnnotationConfigApplicationContext
中指定的配置类可以通过设置包扫描其他的配置类;除此之外,也可以在配置类上用@Import
注解,@Import注解可以将其他类作为配置类。注意:其他类并不需要被@Configuration修饰。
【@Configuration本身可以和@Controller这些一样被包扫描,可以发现,@Configuration修饰的类本身也是一个Bean,看@Configuration源码可知,这是一个组合注解,其中包含了@Component注解,那么问题来啦,直接@Component+@ComponentScan能不能实现@Configuration+@ComponenScan的效果呢?当然是可以的吧,再看源码,发现里面写着就是Component类的别名。所以重要的不是@Configuration这个注解,而是@ComponentScan这个注解】
【再再另外,既然@Configuration
的作用只是为自定义类创建Bean对象,那么特殊之处在哪,为什么要单独成立这个注解?好吧,其实和@Controller/@Service
等相比,这个注解没有任何特殊之处,就像@Controller
、@Service
本质上没有任何区别,只是出于情景和语义上的考虑,为@Component
注解起了这两个别名,@Configuration
其实也只是@Component
的一个别名而已,表明被修饰的类起配置类的作用,具体情景就是:所谓的配置类,就是专门集中创建各种第三方类的对象,这些第三方类往往起到配置的作为,比如:数据源类】
【总结一下:配置类主要用于配置包扫描,为第三方类创建对象,这些第三方类才是真正起配置作用,比如:数据源类】
主配置类Config1,其他配置类Config2
实现一:
ApplicationContext ac = new AnnotationConfigApplicationContext(Config1.class,Config2.class);
public class Config1{
...
}
public class Config2{
...
}
实现二:
ApplicationContext ac = new AnnotationConfigApplicationContext(Config1.class);
@ComponentScan(basePackages = "...")
public class Config1{
...
}
@Configuration
public class Config2{
...
}
实现三:
ApplicationContext ac = new AnnotationConfigApplicationContext(Config1.class);
@Import(Config2.class)
public class Config1{
...
}
public class Config2{
...
}
@PropertySource
:用于指定配置application.properties
文件位置;作用于@Configuration
修饰的配置类上;value属性,用于指定properties文件名称和路径;由于部署时配置文件会放到类路径下,所以要使用classpath关键字。application.properties
配置文件一般用于为类的成员变量赋基本数据类型的值,因此一般和@Value
注解一起用。@Bean
注解用于将方法的返回值中的对象加入IOC容器中,注意了:@Bean
所修饰的方法的类,必须要先有对象加入到IOC容器中,不然@Bean
不会起作用;换句话说,要想在方法上使用@Bean
,先要保证类上有@Component/@Configuration/...
;
3.3 实现代码
XML中IOC、DI关注的问题有4个:创建对象、注入值、作用范围、生命周期。分别对应:bean标签、property标签、bean标签的scope属性、bean标签的destroy属性。
注解实现也还是解决上面4个问题。
3.3.1 创建核心容器
public class MyTest {
public static void main(String[] args) {
// 1.获取核心容器对象
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
// 2.手动获取IOC中的对象,getBean的参数是IOC容器中对象的id,默认值是对象类名(首字母小写)
DataSource datasource1 = (DataSource) ac.getBean("datasource1");
DataSource datasource2 = (DataSource) ac.getBean("datasource2");
}
}
3.3.2 创建对象
@Bean:
用于把当前方法的返回值作为bean对象存入spring的ioc容器中
在spring容器中,name属性作为key,bean对象作为value,name属性默认值为当前方法名;【注意:key是当前方法名!!!不是类名】
【注意:当方法被@Bean修饰时,类上必须被@Component等价的注解修饰,不然@Bean不会起作用】
@Component:
作用:作用于类上,用于将当前类对象存入spring容器;默认创建单例对象。
在spring容器中,value属性作为key,类对象作为value;value属性默认值为当前类名且首字母改小写;
注意:注解中只有一个value属性,属性名value可省略;
@Controller、@Service、@Repository、@Configuration四个注解和@Component完全一样,互相之间可以代替;
@Scope:
作用于@Bean、@Component作用的方法或类上,用于设置创建对象的作用范围;
@PreDestroy、@PostConstruct 作用在类的成员方法上;
根据类成员方法的返回值创建对象 @Bean
- 当成员方法是含参的,Spring框架会自动在ioc容器中查找合适的对象作为参数,查找方式同@Autowired。如以下代码中的第一处。注意:经试验,如果ioc有多个该类型bean对象的话,没有按照@Autowire那样继续用变量名匹配,直接报错了,这和教程中说的不同。【注意:@Bean修饰含参的成员方法,这点一直每注意】
- 以下是基于注解时创建第三方类对象的方法:自己额外定义一个类,类中成员方法调用第三方类的构造函数,创建并返回第三方类对象。
@Configuration
public class DataBaseConfig {
@Bean(name="runner")
@Scope("prototype")
public QueryRunner createQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
@Bean(name="datasource")
public DataSource createDatasource(){
ComboPooledDataSource ds= new ComboPooledDataSource();
try {
ds.setDriverClass("com.mysql.jdbc.Driver");
ds.setJdbcUrl("XXX");
ds.setUser("root");
ds.setPassword("123456");
return ds;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
根据类的构造函数创建对象 @Component
上面 @Configuration 已经起到这个作用了
3.3.3 根据application.yml 配置文件为成员变量赋值(基本数据类型)
数据注入:【注意:@Autowired等还能作用在成员方法的参数上,一直没注意】
@Autowired:
作用:自动按照类型注入,在spring容器中找和目标变量类型相同的bean对象;可作用于成员变变量、成员方法上的参数;
如果ioc容器中有多个符合类型的bean对象,再将bean对象的变量名称(key)与目标变量匹配,如果有相同的则选中并注入,如果没有相同的则报错;
所谓的“类型相同”,可以是类型完全相同,也可是目标变量为接口、ioc容器中为接口实现类;
@Qualifier:
作用:先按照类型注入,再按照变量名注入;可作用于成员变量、成员方法的参数
value属性用于指定bean对象变量名
作用与类成员变量时,无法单独存在,必须和@Autowired一起写;
感觉很鸡肋,对比@Autowired就是多了个value属性
@Resource:
作用:按照变量名注入;name属性用于指定bean对象变量名;可作用于类成员变量、方法参数
@Value:用于为成员变量赋基本类型的值;值的来源往往是配置文件,只要写成变量形式,spring会自动在指定位置找XXX.properties配置文件,在配置文件中找对应变量名,用于赋值;
@Autowired过程:
// jdbcConfig.properties 配置文件
jdbc.driver= com.mysql.jdbc.Driver
jdbc.url= XXX
jdbc.username= root
jdbc.password= 123456
@Configuration
@ComponentScan(basePackages = "...")
@Import(JdbcConfiguration.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration{
}
public class JdbcConfiguration {
// el表达式,指定application.properties配置文件中的变量名
@Value("{$jdbc.driver}")
private String driver;
@Value("{$jdbc.url}")
private String url;
@Value("{$jdbc.username}")
private String username;
@Value("{$jdbc.password}")
private String password;
@Bean(name="runner")
@Scope("prototype")
public QueryRunner createQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
@Bean(name="datasource")
public DataSource createDatasource(){
ComboPooledDataSource ds= new ComboPooledDataSource();
try {
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
==================================================
四 杂项
4.1 为什么要使用IOC?
个人觉得,IOC最显著的作用在于将异常从运行时提前到编译时了。如何理解?其实只要看一下ioc干了一件什么事就清楚了:ioc就干了一件事,通过配置文件创建类对象。
那么问题来了,什么时候创建的呢?(以单例对象为例)配置文件一加载完就创建了。
那么问题又来了,spring项目中配置文件是什么时候加载的?项目一启动就加载配置文件了。那么问题就清楚了
IOC使得项目一启动时就先创建了整个项目中要用到的类对象。而不是像原来,项目启动后,某个方法被调用,方法中new了某个对象,这个时候才创建对象。
那么spring ioc是怎么知道项目中总共要创建哪些类对象呢?理一下就能发现,其实是程序员在写项目代码时,每碰到需要创建一个对象,就把这歌对象记录到统一的XML文件中。等项目写完了,XML中也就记录了所有需要创建的对象。
顺着理一下过程就是:程序员在开发过程中,每碰到要创建对象就把这个对象记录到XML文件中,(并且代码写成直接从一个容器中获取该对象),等项目写完了,XML中就保存了整个项目中所有需要创建的对象。之后,程序开始运行时,首先读取这个XML文件看需要创建哪些对象,直接先创建并存到一个容器中。等程序执行到要创建对象的地方,直接从容器中获取已经预先创建好的对象。
这么看,多例用延迟加载的原因就很清楚了,他读完配置文件后都无法知道要创建多少个相同的类对象,自然只能等调用的时候才创建
那么,ioc的另一个作用降低耦合是怎么回事呢?
https://blog.youkuaiyun.com/qq_36226453/article/details/82790375
首先得明白耦合是怎么回事,简单地说,一个类里面调用另一个类,如果被调用的类发生某种改变,所有用到它的地方都要修改,那就是耦合紧密
就拿ioc的场景来说,某个地方是通过new来创建类对象,如果该类构造函数的参数变了,比如说多加了一个参数,那所有用new方法创建该类对象的地方都要修改,这就是耦合紧密
那这个场景中怎么解决这个问题呢?也就是怎么降低耦合呢?
只要不直接使用类的new方法,而是表述为“调用该类的初始化方法创建一个类对象”就可以了,不管构造函数怎么变,都是“初始化方法”,都能被这个表述覆盖到。也就是说,只给类名,自动去搜索该类的init方法。
那这种表述怎么实现呢?这种方法其实就叫通过反射创建类对象,通过类的全权限定类名来创建。
这里如果类的全限定类名变了的话还是无法创建类队形,但是要注意:如果连类名都变了,或者类的路径变了,那就已经不是原来的那个类了,那自然无法创建对象了。类的全限定类名是类的唯一标识
个人认为,如果要讲清楚ioc,应该先讲将异常从运行时移到了加载时,然后讲降低耦合。
降低耦合可以看作是统一创建对象后的一个细节,也就是根据配置文件创建对象,具体是怎么创建的。
统一创建时,使用new、反射,都能创建,都能实现将异常从运行时移到加载时,但是反射创建还降低了耦合,这也是spring ioc实际使用的创建方式。
其实不论spring ioc实际通过new还是反射创建类对象,配置文件中肯定是要写全限定类名的,因为这才是类的唯一标识,不然都找不到要创建那个类对象。
当然,即使不使用spring ioc,咱们自己也能在每个使用new的地方改为通过反射创建,只不过写的更繁琐了。
从这个角度看,统一创建和反射创建(不是同一个维度)是相互促进的关系,反射创建使得统一创建的鲁棒性更强了,统一创建使得反射创建更加简洁了(集中了、且只用写一次)
====================================================
依赖注入DI【对应property标签】:
依赖注入DI是和控制反转IOC联合使用的,且基于IOC。
先看一下IOC干了什么事:开发者事先将程序中所有要创建的对象记录到XML配置文件中,Spring根据配置文件程序一启动就先创建这些对象。由于程序员已经打算好了由框架自己创建,所以在代码中只定义了类对象的引用变量,并没有实际创建类对象【创建类对象的控制权由程序员转到框架】。
仔细看一下上面这段就会发现一个问题:由框架负责创建类对象没问题,我们假设框架还是通过new方法来创建(实际是反射),框架怎么知道new里面要填哪些参数,也就是类初始化时成员变量的值框架怎么知道?如果不知道这些值的话,那即使有创建的权力,也没法真正创建类对象。依赖注入就是解决这个问题,用于指定类的成员变量的值【当然,如果能为初始化必须的成员变量赋值,那自然也能为其他的成员变量赋值了,也就是说依赖注入实际是控制类对象所有成员变量的值,而不仅仅是new所需要的】。也就是说,IOC使框架拿到类对象的创建权,DI使框架拿到为类对象中成员变量的赋值权。再顺着理一下,程序员写程序时碰到要创建类对象,所以记录到XML文件中了,既然要创建类对象,自然要为类对象的各个成员变量赋值,程序员也把这些值记录到XML文件中,之后程序启动时,框架自动根据XML记录创建对象并为对象的成员变量赋值。【IOC和DI其实算是一体的,如果类中没有无参构造函数,无论是new还是反射创建类对象,都必须为类的成员变量指定值。】
那怎么为那些在类创建时必须赋值的成员变量,以及其他成员变量赋值呢?
这其实取决于类中本身定义和类本身存在的为成员变量赋值的方法,一是:构造函数中指定【需要沟站函数中包括该成员变量】,二是:直接用点的方式访问类的成员变量并赋值【需要该属性为public】,三是:通过set方法为成员变量赋值【需要定义了该成员变量的set方法】
方法二不符合java数据封装的要求,不考虑。只有为一个类对象中的成员变量赋值就两种方法。【无论是手动还是框架都无外乎这两种】
而且,到底能使用哪种方式还要取决于类定义中对哪种方式可以支持:如果类中只有一个有参的构造函数,那么无论怎么样(框架或者是自己手动)都只能通过这个构造函数创建类对象。如果类中没有构造函数(也就是使用默认无参构造函数),那就不能基于构造函数来指定变量值了,只能用set方法【如果也没定义set方法的话,那就是无法通过配置文件注入】。如果类中既有有参构造函数,又有无参构造函数,那么两种方式都可以。
【只有一个有参构造函数,能通过set注入吗?我猜不能,确实不能】
可以基于构造函数,直接为构造函数中的成员变量指定值;
可以基于类的set方法,
=====================================================
依赖注入不是解决对上号的问题的,咦,那对上号的问题是怎么解决的,特别是多例对象?
从上面可以看到一个问题:框架创建了全部类对象,程序中定义了类对象的引用变量。怎么让类对象和引用变量对上号呢?怎么让引用变量指向正确的类对象呢?这个工作就叫依赖注入,让引用变量和类对象对上号,也即为引用变量注入值【依赖注入】。
那这个对上号的工作到底怎么实现的呢?或者说,根据什么来对号?
============================================
补充:
ApplicationContext是一个接口,有三个常用实现类:
ClassPathXmlApplicationContext:加载类路径下的XML配置文件,创建核心容器【常用】
FileSystemXmlApplicationContext:根据磁盘目录加载XML配置文件,创建核心容器
AnnotationConfigApplicationContext:根据注解创建核心容器
ApplicationContext和Beanfactory两个接口的关系:
ApplicationContext和Beanfactory两个接口都是用于创建核心容器,基于IOC创建类对象。而且ApplicationContext继承自Beanfactory,大概是Beanfactory只有延迟加载创建类对象的方法,ApplicationContext增加了立即加载创建类对象的方法,而且默认采用立即加载。使用ApplicationContext接口时,如果在配置中指定了对象是单例还是多例,spring也会自动对单例用立即加载,多例用延迟加载。【不指定就是默认延迟加载】
【注意:不管是单例还是多例,实际上都可以用立即加载和延迟加载,只不过出于效率考虑,让单例用立即加载,多例用延迟加载】
IDEA:怎么看依赖包的相互依赖关系?怎么查看类的继承关系?怎么看接口的实现类?
1.maven——>show dependencies
2.选中类——>diagrams——>show diagrams
3.在上一步的基础上,——> show implementations
补充二:
上面那个main程序很有意思,用了多态。
注意,一是:ac.getBean返回的是Object类型对象,导致只能使用Object类型的方法,所以要进行强制类型转换。二是:这里是用接口的类型进行强制转换,而不是实现类的类型。不知道为啥,貌似都是这么写的。另外,由于是转为接口的类型(相当于父类类型),转换后还是只能使用接口中已经定义了的方法,无法使用实现类中特有的方法。比如这里:as2 无法调用getXXX方法。
对@Bean注解测试如下:
@Bean注解不一定要和@Configuration搭配,只要和@Component等价的注解都行;类上必须要加@Component等价的注解;
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(MyConfig.class);
// @Component加入的Bean对象,默认key为类名(首字母小写)
Test2 test2 = (Test2) ac.getBean("test2");
System.out.println(test2.test.val);
// @Bean加入的Bean对象,默认key为方法名
Test test = (Test) ac.getBean("getTest");
System.out.println(test.val);
}
}
@Configuration
@ComponentScan(basePackages = {"com.example.demo"})
public class MyConfig {
}
public class Test {
public int val = 1;
}
@Component // 这里必须加一个注解,不然 @Bean 不会生效
public class Test1 {
@Bean
public Test getTest(){
return new Test();
}
}
@Component
public class Test2 {
@Autowired
Test test;
}
输出:
1
1