为什么要学Spring?
在回答这个问题时,我们先来看看现在的Java程序是如何实现的,以最简单的服务层与持久层为例,其遵循接口与具体实现类的这种方式:
Service
层接口:BookService.java
package service;
public interface BookService {
public void save();
}
Service层具体实现BookServiceImpl.java
package service.impl;
import dao.BookDao;
import dao.impl.BookDaoImpl;
import service.BookService;
public class BookServiceImpl implements BookService {
BookDao bookdao=new BookDaoImpl();
public void save() {
System.out.print("执行service...\n");
bookdao.save();
}
}
Dao接口:BookDao.java
package dao;
public interface BookDao {
public void save();
}
BookDao
接口实现类 BookDaoImpl.java
package dao.impl;
import dao.BookDao;
public class BookDaoImpl implements BookDao {
public void save(){
System.out.print("执行dao...");
}
}
定义一个main
方法来执行,看下结果:
import service.BookService;
import service.impl.BookServiceImpl;
public class Test {
public static void main(String[] args) {
BookService bs=new BookServiceImpl();
bs.save();
}
}
可以看到这是Java
的实现方式,这存在什么问题呢?
如果这时我们的业务层实现我们重写了的话,那么原本在Service
层的定义也要发生改变,造成这样问题的原因便是由于我们在类里定义了其他类的实现,这就导致代码耦合度过高。
那么该如何解决呢,其实很简单,那就让代码这不要出现其他类的实现呗,即只定义一个接口,但这样会报错的。
而Spring说,不就是想要一个对象吗?我来给提供,从而实现了解耦。
Spring是如何解耦的?
前面已经说到,造成Java
项目耦合度过高的原因便是由于在类中定义了其他类的实现(对象),那么Spring
是如何做的呢?
在使用对象时,不再通过程序自己new一个对象,而是通过外部提供对象,这里的外部即为Spring
容器,又称IOC
容器 ,而实现这种方法的思想被称为IOC
(Inversion of control
,控制反转)指的是生成对象的控制权限被反转了。
IOC容器赋值对这些对象的创建与管理,这些对象在IOC
容器中被称为Bean
。
此时再次运行还是会出错的,因为只是创建了一个Service
对象,而Service
对象的运行是需要依赖于Dao对象的,这个问题该如何解决呢,Spring
提供了一种思想DI
(Dependency Injection
,依赖注入),将Serive
与Dao
的依赖关系进行绑定。
最终达到的效果:在使用对象时,不仅可以从IOC
容器中直接获取,还可以获得其依赖的对象。
创建Spring的配置文件之前,需要先导包Spring-context
随后new
一个XML
配置文件,选择Spring
配置文件即可。
<?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">
<!-- 1.创建Spring容器之前需要先导包:Spring-Context-->
<!-- 2.创建Spring配置文件。-->
<!-- 3.配置bean。id代表名字,class代表要给bean定义的类型-->
<bean id="bookservice" class="service.impl.BookServiceImpl"></bean>
<bean id="bookdao" class="dao.impl.BookDaoImpl"></bean>
</beans>
随后我们要获取IOC
容器,然后获取容器内的对象:
import dao.BookDao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.BookService;
public class TestSpring {
public static void main(String[] args) {
//1.要想获得Bean,应该先得到IOC容器
ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
// BookDao bdao= (BookDao) ctx.getBean("bookdao");
// bdao.save();
BookService bs= (BookService) ctx.getBean("bookservice");
bs.save();
}
}
在只获取BookDao
的对象时,执行没有问题,但是在获取BookSerive
的的对象时,却报错了:
造成这个原因便是由于我们只是获取了BookService
对象,但该对象的实现中我们已经去除了原本new
一个BookDao
对象的代码:
package service.impl;
import dao.BookDao;
import dao.impl.BookDaoImpl;
import service.BookService;
public class BookServiceImpl implements BookService {
//BookDao bookdao=new BookDaoImpl();
BookDao bookdao;//这里只有一个BookDao接口,没有new其具体实现
public void save() {
System.out.print("执行service...\n");
bookdao.save();
}
}
那么难道是要我们再获取一下BookDao
对象吗,不需要的,前面已经说过,Spring
另一个功能称为依赖注入。具体该怎么做呢?很简单,只需要在配置文件中使用ref
参数即可。
如下面代码所示,将bookservice这个Bean内加一个属性property,名字叫bookdao,然后ref(依赖)前面创建的bookdao这个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">
<!-- 1.创建Spring容器之前需要先导包:Spring-Context-->
<!-- 2.创建Spring配置文件。-->
<!-- 3.配置bean。id代表名字,class代表要给bean定义的类型-->
<bean id="bookdao" class="dao.impl.BookDaoImpl"></bean>
<bean id="bookservice" class="service.impl.BookServiceImpl">
<property name="bookdao" ref="bookdao"></property>
</bean>
</beans>
此时我们运行会发生报错:大致意思是在创建bookservice时需要依赖bookdao,尽管生成了,但没有对应的写入方法(set方法)造成这个问题的原因是什么呢?因为现在的在bookservice.java中的成员变量bookdao是没有值的,即没有将Spring创建的bookdao与这个bookservice.java中的bean联系起来。
Error creating bean with name 'bookservice' defined in class path resource [applicationContext.xml]: Error setting property values; nested exception is org.springframework.beans.NotWritablePropertyException: Invalid property 'bookdao' of bean class [service.impl.BookServiceImpl]: Bean property 'bookdao' is not writable or has an invalid setter method.
在BookServiceImpl.java中添加对应的set方法:
public void setBookdao(BookDaoImpl bookdao) {
this.bookdao=bookdao;
}
再次运行就OK了,这里要明确在xml配置文件中的含义,第一个bookdao是名字要与BookServiceImpl.java中的成员变量bookdao一致,或者叫Bookdao也可,但叫BookDao则会报错,这可能是由于set方法起名就叫Bokkdao的原因,第二个bookdao,即依赖的bookdao是要与xml文件中生成的bookdao这个Bean一致,它会作为参数传入到set方法中。
<property name="bookdao" ref="bookdao"></property>
至此,Spring最大的两个特点:控制反转与依赖注入便讲解完成了。
Bean的配置
前面已经学习了Bean的基础配置,如通过id来指定名字,通过ref来引用其他Bean,此次则是对Bean的别名、作用范围的学习。
Bean的别名,由于每个人的命名习惯不同,因此Spring提出使用Bean的别名来标识同一个Bean,从而方便使用。
如下修改后的配置文件,使用name来表示别名,多个别名可以通过逗号、分号以及空格来分隔,但依旧建议使用id来表示。
<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">
<!-- 1.创建Spring容器之前需要先导包:Spring-Context-->
<!-- 2.创建Spring配置文件。-->
<!-- 3.配置bean。id代表名字,class代表要给bean定义的类型-->
<bean id="bookdao" class="dao.impl.BookDaoImpl"></bean>
<bean id="bookservice" name="service,books1;bookl books" class="service.impl.BookServiceImpl">
<property name="bookdao" ref="bookdao"></property>
</bean>
</beans>
Bean的作用范围,即创造的Bean到底是单例对象还是多例对象呢,我们从Spring容器中获取两个Bean并输出其地址,发现两者的地址相同,这说明Spring创建的Bean是单例的。
BookService bs= (BookService) ctx.getBean("service");
BookService bs1= (BookService) ctx.getBean("service");
System.out.print(bs+"\n");
System.out.print(bs1);
那么如果我们想让创建的Bean是多例的呢,这就涉及到Bean的作用范围了,Spring为我们提供了一个参数:scope
<bean id="bookservice" name="service,books1;bookl books" class="service.impl.BookServiceImpl" scope="prototype">
<property name="bookdao" ref="bookdao"></property>
</bean>
事实上,Spring创建的单例对象都是可以复用的,可以大大的减缓了数据存储量。那么,那些对象不适合使用Spring来创建单例对象呢,那些封装实体的域对象,比如其内成员变量有给定值时。在大多数情况下,表现层,业务层以及工具层对象都是可以被复用的。
Spring中的Bean是如何创建的?
在Java中,对象都是构造方法new出来的,那么Spring是如何创建的对象呢
事实上,Spring中的对象也是通过构造方法创建的。
我们重写了BookDaoImpl的构造方法,发现该构造方法被执行了。
public class BookDaoImpl implements BookDao {
public BookDaoImpl(){
System.out.print("bookdao constructor running...\n");
}
public void save(){
System.out.print("执行dao...");
}
}
这说明Spring也是通过构造方法来构建Bean的。
不仅如此,当我们将构造方法私有后,已经可以调用,这是通过反射实现的。
private BookDaoImpl(){
System.out.print("bookdao constructor running...\n");
}
此外,如果我们此时将无参构造方法中添加一个参数,此时则会创建失败,这证明Spring是通过无参构造方法来创建Bean的。
编写一个类时没有添加无参构造方法,那么编译器会自动添加无参构造方法;(如果自己添加构造函数,无论有参数或是没参数,默认构造函数都将无效)
编写时添加了有参构造方法而未添加无参构造方法,那么编译器只认有参构造方法而不会默认添加无参构造方法!
如果需要使用无参构造方法,一定要在类里面添加
因此,这个无参构造函数是被默认创建的,即不需要我们自己创建。
Spring使用工厂创建Bean
静态工厂创建Bean
(这种方法已经过时了,但要知道,通过静态工厂创建对象可以解耦)
关于Java中的工厂,我们需要知道:
实例化对象不使用new,用工厂方法创建对象 使用工厂统一管理对象的创建,将调用者跟实现类解耦
实例工厂创建Bean
然而,这种实例工厂创建Bean的方式有以下问题:
因此,Spring提出了FactoryBean,即Spring给创建一个工厂Bean,我们直接拿到这个工厂Bean来获取对象即可。
package dao.impl;
import org.springframework.beans.factory.FactoryBean;
public class BookDaoImplFactoryBean implements FactoryBean<BookDaoImpl> {
@Override
public BookDaoImpl getObject() throws Exception {
return new BookDaoImpl();
}
@Override
public Class<?> getObjectType() {
return BookDaoImpl.class;//字节码
}
@Override
public boolean isSingleton() {//是否是单例
return true;
}
}
<bean id="bookdao" class="dao.impl.BookDaoImplFactoryBean">
</bean>
Bean的生命周期
简单来讲,Bean的生命周期即为从Bean的创建到销毁的过程,为什么用它呢,事实上,Bean在创建前与销毁前一般都是需要完成一系列操作的,我们可以模拟一下Bean的创建与销毁操作
package dao.impl;
import dao.BookDao;
public class BookDaoImpl implements BookDao {
public BookDaoImpl(){
System.out.print("bookdao constructor running...\n");
}
public void save(){
System.out.print("执行dao...");
}
public void init(){
System.out.print("init...");
}
public void destory(){
System.out.print("destory..");
}
}
这个init与destory方法是我们自己写的,是不会执行的,因此我们还需要在配置文件中配置一下:指明初始化方法与销毁方法
<bean id="bookdao" class="dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"></bean>
此时运行代码发现只执行了init方法,这很容易理解,因为我们的程序是运行在JVM虚拟机上的,创建这个过程被执行了,但程序在执行完后,就直接关闭虚拟机了,根本就没有执行销毁操作。
怎么办呢?我们需要在程序中进行主动销毁,因为在Spring中,是使用关闭Spring容器来销毁Bean的,因为在Spring容器中的Bean是一同创建的,销毁那么也就一起销毁。
那么如何销毁容器呢,使用的是close方法。
但这种方式太过暴力,现在多是采用注册关闭钩子的方式来销毁Bean的,这种注册钩子与close的区别在于,注册钩子放在任何位置都可以,即告诉程序只要在退出JVM之前执行一下就可以,而close则只能在所有的Bean操作都完成后才能够执行,否则就会报错。
ClassPathXmlApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bdao= (BookDao) ctx.getBean("bookdao");
ctx.registerShutdownHook();
以上是我们自己生成的初始化与销毁方法,事实上,Spring为了我们操作方便,为我们提供了对应的方法,只不过我们需要按照他的标准来操作:
我们需要实现两个接口:InitializingBean
, DisposableBean
package service.impl;
import dao.BookDao;
import dao.impl.BookDaoImpl;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import service.BookService;
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
//BookDao bookdao=new BookDaoImpl();
BookDao bookdao;
public void save() {
System.out.print("执行service...\n");
bookdao.save();
}
public void setBookdao(BookDaoImpl bookdao) {
this.bookdao=bookdao;
}
@Override
public void destroy() throws Exception {
System.out.print("bookservice destory..\n");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.print("bookservice init..\n");
}
}
注意,尽管在下面的代码中我们只使用了Bookdao的Bean,但事实上在加载xm文件时,Service的Bean也是被创建的。
ClassPathXmlApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bdao= (BookDao) ctx.getBean("bookdao");
bdao.save();
ctx.registerShutdownHook();
同时博主还发现,当BookService的Bean被指定为多例时,其不执行init和destory过程,此时,只有当我们调用了BookService的Bean时才会执行。
ClassPathXmlApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bdao= (BookDao) ctx.getBean("bookdao");
bdao.save();
BookService bs= (BookService) ctx.getBean("service");
ctx.registerShutdownHook();
并且,其依旧没有执行BookService的destory
单例时的结果:
这是由于什么呢,因为Spring提供的方法afterPropertiesSet()叫做属性设置之后,而多例对象是需要调用时给定属性的,此时不调用也就不会给定属性,自然也就不会执行init了。
Spring中的Bean遵循下面的过程:
其中创建对象就相当于new对象。