Spring中的控制反转和依赖注入。
耦合与解耦
耦合:程序之间的依赖关系,包括类之间的依赖和方法中的依赖。
解耦:降低程序之间的依赖关系。
实际开发中应该做到:编译器不依赖,运行时才依赖。
解耦思路:
-
1.使用反射来创建对象,避免使用new关键字。
-
2.通过读取配置文件来读取要创建的对象全限定类名。
实例1:JDBC驱动注册
1 | //常规 |
1 | //通过反射加载驱动类 |
实例2:三层框架解耦
表现层调用业务层,业务层调用持久层。
1 | public class Client { |
业务层依赖持久层的接口和实现类,若编译时不存在没有持久层实现类,则编译期不能通过,造成了编译期依赖。
使用工厂模式解耦
Bean:在计算机英语中有可重用组件的含义。
JavaBean:用Java语言编写的可重用组件。不等于实体类,远大于实体类。
Bean工厂:创建service和dao对象。
- 1.需要一个配置文件配置service和dao。配置文件内容:唯一标识=全限定类名(key-value),xml或者properties。
- 2.通过读取配置文件中的内容,反射创建对象。
在实际开发中可以把三层的对象的全类名都使用配置文件保存起来,当启动服务器应用加载的时候,创建这些对象的实例并保存在容器
中。在获取对象时,不使用new的方式,而是直接从容器
中获取,这就是工厂设计模式
。
Spring中的IOC
降低程序间的耦合性(依赖关系)
Spring Framework Core:https://docs.spring.io/spring/docs/5.2.4.RELEASE/spring-framework-reference/core.html#spring-core
项目创建
- 创建Maven项目,配置pom.xml。
1 | <dependencies> |
-
创建框架,三层接口类和实现类。
-
配置bean文件。XML格式,可通过上述文档查找:
1 |
|
- 在表现层中通过容器创建对象,核心容器的
getBean()
方法获取具体对象。
1 | public class Client { |
常用容器
ClassPathXmlApplicationContext
: 它是从类的根路径下加载配置文件(常用)FileSystemXmlApplicationContext
: 它是从磁盘路径上加载配置文件(必须有访问权限)AnnotationConfigApplicationContext
: 读取注解创建容器
核心容器两个接口引发出的问题
ApplicationContext
: 它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说一读取完配置文件马上创建配置文件中配置的对象。单例对象适用。BeanFactory
: 它在构建核心容器时,创建对象采取的策略是延迟加载的方式,也就是说什么时候根据ID获取对象了什么时候才真正的创建对象。多例对象适用。
Bean的细节
Bean实例化的三种方式
-
使用默认无参构造函数创建对象:默认情况下会根据默认无参构造函数来创建类对象,若Bean类中没有默认无参构造函数,将会创建失败。
1
<bean id="accountService" class="cn.maoritian.service.impl.AccountServiceImpl"></bean>
-
使用普通工厂的方法创建对象(使用某个类中的方法创建对象并存入Spring容器):
1
2
3
4
5
6//工厂,即我们需要使用createAccountService()来实例化一个IAccountService
public class InstanceFactory {
public IAccountService createAccountService(){
return new AccountServiceImpl();
}
}先创建实例工厂对象
instanceFactory
,通过调用其createAccountService()
方法创建对象。1
2
3
4
5
6
7
8<bean id="instancFactory" class="cn.maoritian.factory.InstanceFactory"></bean>
<bean id="accountService"
factory-bean="instancFactory"
factory-method="createAccountService"></bean>
<!--
factory-bean属性: 指定实例工厂的id
factory-method属性: 指定实例工厂中生产对象的方法
--> -
使用工厂的静态方法创建对象:
1
2
3
4
5public class StaticFactory {
public static IAccountService createAccountService(){
return new AccountServiceImpl();
}
}使用
StaticFactory
中的静态方法createAccountServic()
创建对象。1
<bean id="accountService" class="cn.maoritian.factory.StaticFactory" factory-method="createAccountService"></bean>
Bean的作用范围和生存周期
单例对象
1 | <bean id="accountService" class="cn.maoritian.factory.StaticFactory" factory-method="createAccountService" scope="singleton"></bean> |
- 出生:当容器创建时对象出生
- 活着:只要容器还在,对象一直活着
- 死亡:容器销毁,对象消亡
- 总结:单例对象的生命周期和容器相同
多例对象
1 | <bean id="accountService" class="cn.maoritian.factory.StaticFactory" factory-method="createAccountService" scope="prototype"></bean> |
- 出生:当使用对象时,Spring框架创建
- 活着:对象在使用过程中一直活着
- 死亡:当对象长时间不用,且没有别的对象引用时,由Java的垃圾回收器回收
Bean标签
配置托管给spring的对象,默认情况下调用类的无参构造函数,如果没有无参构造函数则创建失败。
id
:指定对象在容器中的标识,将其作为参数传入getBean()
方法可以获取获取对应对象。class
:指定类的全类名,默认情况下调用无参构造函数。scope
:指定对象的作用范围,可选值:singleton
:单例对象,默认值prototype
:多例对象request
:将对象存入到web项目的request域
中session
:将对象存入到web项目的session域
中global session
:将对象存入到web项目集群的session域
中,若不存在集群,则global session
相当于session
init-method
:指定类中的初始化方法名称,在对象创建成功之后执行destroy-method
:指定类中销毁方法名称,对prototype
多例对象没有作用,因为多利对象的销毁时机不受容器
控制
Spring中的DI
依赖关系的维护,叫依赖注入。
注入的数据
- 基本类型和String(经常变化的数据并不适用注入的方式,如数据库的账号和密码)
- 其他Bean类型(在配置文件中或者注释配置过的Bean)
- 复杂类型/集合类型
注入的方式
-
使用构造函数注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//对有参构造函数进行注入
public class AccountServiceImpl implements IAccountService{
private String name;
private Integer age;
private Date birthday;
public AccountServiceImpl(String name, Integer age, Date birthday) {
this.name = name;
this.age = age;
this.birthday = birthday;
}
public void saveAccount() {
System.out.println("service中save"+name+","+age+","+birthday);
}
}使用的标签
constructor-arg
,属性有:type
:指定要注入的数据类型,该数据类型同时也是构造函数中某个或者某些参数的类型index
:用于指定要注入的数据给构造函数中指定索引位置的参数赋值,从0开始name
:用于指定给构造函数中指定名称的参数赋值(常用)value
:用于提供基本类型和String类型的数据ref
:用于指定其他的Bean类型数据,在Spring的IOC核心容器中出现过的Bean对象
1
2
3
4
5
6
7<bean id="accountService" class="factory.service.AccountServiceImpl">
<constructor-arg name="name" value="test"></constructor-arg>
<constructor-arg name="age" value="15"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>
<!-- 配置一个Bean -->
<bean id="now" class="java.util.Date"></bean>优势:在获取Bean对象时,注入数据是必须的,否则无法创建成功。
弊端:改变了Bean对象的实例化方式,使我们在创建对象时,用不到这些数据也必须提供。
在无可奈何时候才使用。
-
使用Set方法注入(更常用)
使用的标签
property
,属性有:name
:用于指定注入时所调用的Set方法名称value
:用于提供基本类型和String类型的数据ref
:用于指定其他的Bean类型数据,在Spring的IOC核心容器中出现过的Bean对象
1
2
3
4
5
6<!-- name后面接setter函数名字,如setUsername的username,与变量名无关 -->
<bean id="accountService2" class="factory.service.AccountServiceImpl2">
<property name="name" value="test"></property>
<property name="age" value="21"></property>
<property name="birthday" ref="now"></property>
</bean>优势:创建对象时没有明确的限制,可以直接使用默认构造函数。
弊端:如果有某个成员必须有值,但获取对象时set方法没有执行。
注入集合数据:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public class AccountServiceImpl3 implements IAccountService{
private String[] myStrs;
private List<String> myList;
private Set<String> mySet;
private Map<String,String> myMap;
private Properties myProps;
public void setMyStrs(String[] myStrs) {
this.myStrs = myStrs;
}
public void setMyList(List<String> myList) {
this.myList = myList;
}
public void setMySet(Set<String> mySet) {
this.mySet = mySet;
}
public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}
public void setMyProps(Properties myProps) {
this.myProps = myProps;
}
public void saveAccount() {
System.out.println(Arrays.toString(myStrs));
System.out.println(myList);
System.out.println(mySet);
System.out.println(myMap);
System.out.println(myProps);
}
}用于给List结构集合注入的标签:
list
,array
,set
用于Map结构集合注入的标签:
map
,props
结构相同,标签可以互换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37<bean id="accountService3" class="factory.service.AccountServiceImpl3">
<property name="myStrs">
<array>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</array>
</property>
<property name="myList">
<list>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</list>
</property>
<property name="mySet">
<set>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</set>
</property>
<property name="myMap">
<map>
<entry key="testaA" value="aaa"></entry>
<entry key="testB">
<value>bbb</value>
</entry>
</map>
</property>
<property name="myProps">
<props>
<prop key="testC">ccc</prop>
<prop key="testD">ddd</prop>
</props>
</property>
</bean> -
使用注解提供
基于注解的IOC配置
注解和XML的选择原则:使用哪个最方便就是用哪种。
配置bean.xml
1 |
|
配置标签
-
用于创建对象
@Component
:把当前类对象存入Spring容器中。属性value
用于指定bean的id,不写时默认当前类名首字母改小写。以下三个注解的作用和属性和
Component
一模一样,为Spring框架提供的明确的三层使用的注释,使三层对象更加清晰。@Controller
:一般用在表现层@Service
:一般用在业务层@Repository
:一般用在持久层 -
用于注入对象
@Autowired
:自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配,就可以诸如成功。出现位置可以是成员变量,也可以是方法上。如果IOC有多个类型匹配时,首先按照类型框定匹配的对象,再按照变量名称作为bean的id继续查找,如果有一样的也可以注入成功,没有注入失败。使用注解时,set方法不是必须的。@Qualifier
:在按照类中注入的基础上再按照名称注入。它在给类成员注入时不能单独使用要和@Autowired
配合使用,但是给方法参数注入时可以。value
用于指定注入bean的id。@Resource
:直接按照bean的id注入,可以直接使用,不依托@Autowired
使用。name
用于指定注入的bean的id。以上三个注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。此外,集合类型的注入只能通过XML来实现。
@Value
:用于注入基本类型和String类型的数据。属性value用于指定数据的值,可以使用Spring中的SpEL,即Spring的el表达式(${表达式})。 -
用于改变作用范围
@Scope
:用于指定bean的作用范围。属性value指定范围的取值,常用取值:singleton
和prototype
。 -
和生命周期相关
@PreDestroy
:指定销毁方法。@PostConstruct
:指定初始化方法。
示例:
1 |
|
1 |
|
Spring中的新注解
-
@Configuration
:指定当前类是一个配置类,作用和bean.xml是一样的。当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写,同理下面的扫包也不用写了。1
ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfig.class);
-
@ComponentScan
:用于通过注解指定Spring在创建容器时要扫描的包。value
属性和basePackage
的作用是一样的。都是用于指定创建容器时要扫描的包。使用此注解等同于在xml中配置了:1
<context:component-scan base-package="com.ztygalaxy"></context:component-scan>
-
@Bean
:用于把当前方法的返回值作为bean对象存入spring的IOC容器中。name
属性用于指定bean的id,不写时默认值是当前方法的名称。当使用注解配置方法时,如果方法有参数,Spring框架会去容器中查找是否有可用的bean对象。查找方式和Autowired
注解的作用是一样的,如果唯一类型匹配,多个按名称匹配。如示例中的dataSource。
示例:
1 | //配置类 |
-
@Import
:导入其他的配置类,使用import之后,有import注解的类就是主配置类(父配置类),而导入的都是子配置类。1
-
@PropertySource
:用于指定properties文件的位置。属性value
指定文件名称和文件路径,关键字classpath
表示类路径下。1
2//有包就写包名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class JdbcConfig {
private String driver;
private String url;
private String username;
private String password;
public QueryRunner createQueryRunner(DataSource dataSource){
return new QueryRunner(dataSource);
}
public DataSource createDataSource(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
}catch(Exception e){
throw new RuntimeException(e);
}
}
}1
2
3
4
5#jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=root
Junit
Junit单元测试中,没有main
方法也能执行。Junit集成了一个main
方法,该方法会自动判断当前测试类中有哪些方法有@Test
注解,Junit就让有@Test
注解的方法执行。在执行测试方法时,Junit不知道是否使用了Spring框架,所以也就不会为我们读取配置文件或者配置类创建Spring核心容器。因此,当测试方法执行时,没有IOC容器,就算写了@Autowired
也无法实现注入。
Spring整合Junit的配置
- 导入Spring整合Junit的Jar包(坐标)
1 | <dependency> |
- 使用Junit提供的注解把原有的main方法替换成spring提供的
1 |
- 告知Spring的运行器,spring合IOC创建是基于xml还是注解的,并告诉位置。
1 | //1. location:指定xml文件位置,加上classpath表示在类路径下 |
当使用Spring 5.x版本时,要求Junit的jar包是4.1.2及以上。
示例:
-
版本1
1
2
3
4
5
6
7
8
9
10
11
12public class AccountService {
public void testFindAll(){
ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfig.class);
IAccountService as= (IAccountService) ac.getBean("accountService");
//上面两行在每个函数中都重复出现
List<Account> accounts=as.findAllAccount();
for (Account account:accounts)
System.out.println(account);
}
//省略多个函数
} -
版本2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class AccountService {
private ApplicationContext ac;
private IAccountService as;
public void init(){//抽成init函数
ac=new AnnotationConfigApplicationContext(SpringConfig.class);
as= (IAccountService) ac.getBean("accountService");
}
public void testFindAll(){
List<Account> accounts=as.findAllAccount();
for (Account account:accounts)
System.out.println(account);
}
//省略多个函数
} -
版本3
1
2
3
4
5
6
7
8
9
10
11
12
13
14使用Spring装填
public class AccountService {
private IAccountService as;
public void testFindAll(){
List<Account> accounts=as.findAllAccount();
for (Account account:accounts)
System.out.println(account);
}
//省略多个函数
}