3.5 Spring
一、引言
1.1 原生web开发中存在哪些问题?
- 传统Web开发存在硬编码所造成的过度程序耦合(例如:Service中作为属性Dao对象)。
- 部分Java EE API较为复杂,使用效率低(例如:JDBC开发步骤)。此时Spring会对JDBC封装,简化持久层开发。但其实这里又更专业的框架MyBatis,因为我们很少用Spring对JDBC的封装。
- 侵入性强,移植性差(例如:DAO实现的更换,从Connection到SqlSession)。
二、Spring框架
2.1 概念
- Spring是一个项目管理框架,同时也是一套Java EE解决方案。
- Spring是众多优秀设计模式的组合(工厂、单例、代理、适配器、包装器、观察者、模板、策略)。
- Spring并未替代现有框架产品,而是将众多框架进行有机整合,简化企业开发,俗称“胶水框架”。
2.2 访问与下载
官方网站:https://spring.io/
下载地址:http://repo.spring.io/release/org/springframework/spring/,我们现在学习,急需的是jar包,我们可以用Maven管理。那这里的地址是做什么的,等你学到后期,想了解更多的Spring内容,你可以到这里下Spring的发布包,里面有Spring官方文档等等内容。
三、Spring架构组成
Spring架构由诸多模块组成,可分类为:
- 核心技术:依赖注入,事件,资源,i18n,验证,数据绑定,SpEL,AOP。
- 测试:模拟对象,TESTContext框架,Spring MVC测试,WebTestClient。
- 数据访问:事务,DAO支持,JDBC,ORM,封送XML。
- Spring MVC和Spring WebFlux Web框架。
- 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
- 语言:Kotlin,Groovy,动态语言。
四、自定义工厂
4.1 类
我们不是要自定义工厂吗?工厂是干嘛的?不是生产对象的吗?我我们是不是要先有类:
先创建Maven项目:gavm坐标为:
<groupId>com.qf</groupId> <artifactId>spring01</artifactId> <version>1.0-SNAPSHOT</version>
我们来写两个接口,对应两个类:非常简单。
com.qf.dao包下的接口:
package com.qf.dao; public interface UserDAO { public void deleteUser(Integer id); }
com.qf.dao包下的接口的实现类:仅是打印一句输出。
package com.qf.dao; public class UserDAOImpl implements UserDAO { public void deleteUser(Integer id) { System.out.println("delete User in DAO"); } }
com.qf.service包下的接口:
package com.qf.service; public interface UserService { public void deleteUser(Integer id); }
com.qf.service包下的接口的实现类:其实按照以前,我们不是service层调用dao层吗?我们这里暂时就不调用了,也只是一句简单的输出。
package com.qf.service; public class UserServiceImpl implements UserService { public void deleteUser(Integer id) { System.out.println("delete User in Service"); } }
4.2 配置文件
现在我们在resources下写一个配置文件,bean.properties。为什么不用xml呢?因为。。。。
userDAO=com.qf.dao.UserDAOImpl
userService=com.qf.service.UserServiceImpl
这里说一下这个配置文件的作用,我们可以看到,这个配置文件是键值对的形式,里面有两个键值对。键值对的值是上面我们系的两个类的类全名,左侧的key是我们给这个类全名起的一个名字,或者说标识。
4.3 工厂类
这个工厂类做了两件事,一是加载配置文件,从而获得配置文件中一系列的键值对(别名和全类名);二是根据其中一个别名得到全类名,通过反射得到对象。
package com.qf.factory; import java.io.IOException; import java.io.InputStream; import java.util.Properties; // 工厂 // 1.加载配置文件 // 2.生产配置中记录的对应对象 public class MyFactory { private Properties properties = new Properties(); public MyFactory() throws IOException { InputStream resourceAsStream = MyFactory.class.getResourceAsStream("bean.properties"); properties.load(resourceAsStream); } public Object getBean(String name) throws ClassNotFoundException, IllegalAccessException, InstantiationException { // 1. 通过name,获取对应类路径 String classPath = properties.getProperty(name); // 2. 反射 构建对象 Class clazz = Class.forName(classPath); return clazz.newInstance(); } }
4.4 测试
这里的"bean.properties"要记得前面加/。
package com.qf.factory; import java.io.IOException; import java.io.InputStream; import java.util.Properties; // 工厂 // 1.加载配置文件 // 2.生产配置中记录的对应对象 public class MyFactory { private Properties properties = new Properties(); public MyFactory() throws IOException { InputStream resourceAsStream = MyFactory.class.getResourceAsStream("/bean.properties"); properties.load(resourceAsStream); } public Object getBean(String name) throws ClassNotFoundException, IllegalAccessException, InstantiationException { // 1. 通过name,获取对应类路径 String classPath = properties.getProperty(name); // 2. 反射 构建对象 Class clazz = Class.forName(classPath); return clazz.newInstance(); } }
测试结果:
delete User in DAO
delete User in Service
优化:我们上面的工厂类,在用构造函数初始化时,配置文件是写死的,我们可以给构造函数加参数传递,这样就不用写死了,可以调用时传入不同的配置文件。
代码略。
五、构建Maven项目
略,这有啥写的,上面我们已经构建过Maven项目了,gav坐标也写了。下面搭建Spring环境并使用。
六、Spring环境搭建
6.1 pom.xml中引入Spring常用依赖
依赖是spring-context,不要导错了。
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> </dependencies>
6.2 创建Spring配置文件
命名无限制,约定俗成命名有:spring-context.xml、applicationContext.xml、beans.xml。
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- 要工厂生产的对象 --> <bean id="userDAO" class="com.qf.dao.UserDAOImpl"></bean> <bean id="userService" class="com.qf.service.UserServiceImpl"></bean> </beans>
这个配置文件中除了表头,写的内容的含义和我们上面自己设计的bean.properties文件一致:就是给要生产的对象的完整类名起一个别名。
下面我们就可以测试Spring了,就是这么简单,哈,其实也就是少些了工厂类罢了。只是呢?Spring功能要强大的多,丰富的多。我们这里导入junit的以阿里,后面我们用junit做测试:
package com.qf.test; import com.qf.dao.UserDAO; import com.qf.service.UserService; import org.junit.Test; import org.springframework.context.support.ClassPathXmlApplicationContext; public class SpringFactoryTest { @Test public void test() { // 启动工厂 ApplicationContext context = new ClassPathXmlApplicationContext("/spring-context.xml"); // 获取对象 UserDAO userDAO = (UserDAO)context.getBean("userDAO"); UserService userService = (UserService)context.getBean("userService"); userDAO.deleteUser(1); userService.deleteUser(1); } }
我们看到ClassPathXmlApplicationContext是工厂类的实现类,而ApplicationContext是工厂类的接口。
测试结果:
delete User in DAO
delete User in Service
七、Spring工厂编码
这里上面已经有过了,我直接把笔记中的粘贴过来。其实就是三步:1.定义Bean类型;2.在配置文件中添加bean标签,写入该Bean类型;3.使用。
八、依赖与配置文件详解
Spring框架包括多个模块,每个模块各司其职,可结合需求引入相关依赖Jar包实现功能。
8.1 Spring依赖关系
其实我们上面在做Spring小案例的时候,在pom.xml文件中只引入了spring-context的依赖。但是我们发现项目只还多了其它五个Spring相关的依赖
我们再打开IDEA右侧的Maven图标看一下。其实我们已经发现了,当我们引入spring-context的依赖时,他需要下面的四个依赖,因此Maven帮我们自己自动引入(如果我们没有手动引入的话,假设我们手动引入了其中一个,那么这个Maven就不自动引入)。而这4个中的spring-core依赖又需要spring-jcl依赖,Maven工具看我们没有引入,因此也帮我们引入了。
这样我们以后引入依赖就很简单,我们引入最外层的依赖,其依赖的Maven就会自动帮我们引入。
8.2 schema
关于Spring的配置文件。
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- 要工厂生产的对象 --> <bean id="userDAO" class="com.qf.dao.UserDAOImpl"></bean> <bean id="userService" class="com.qf.service.UserServiceImpl"></bean> </beans>
配置文件中的顶级标签中包含了语义化标签的相关信息:
- xmlns:语义化标签所在的命名空间。
- xmlns:xsl:XMLSchema-instance 标准遵循Schema标签标准。
- xsi:schemaLocation:xsd文件位置,用以描述标签语义、属性、取值范围等。
九、IoC(Inversion of Control)控制反转【重点】
9.1 项目中强耦合问题
Inverse of Controll:控制反转
反转了依赖关系的满足方式,由之前的自己创建依赖对象,变为由工厂推送。(变主动为被动,即反转)
解决了具有依赖关系的组件之间的强耦合,使得项目形态更加稳健。
比如我们项目中Service肯定会用到Dao,Servelt肯定会用到Service,我们以Service用到Dao为例,讲解什么是项目中的强耦合问题。
package com.qf.service; import com.qf.dao.UserDAO; import com.qf.dao.UserDAOImpl; public class UserServiceImpl implements UserService { // !!!强耦合了UserDAOImpl!!!,使得UserServiceImpl变得不稳健!! private UserDAO userDAO = new UserDAOImpl(); public void deleteUser(Integer id) { userDAO.deleteUser(1); System.out.println("delete User in Service"); } }
这是我们经常写到的代码,如果一个类需要依赖另一个类,我们就用new另一个类的方式,在该类中创建实例,并调用另一个类的方法、属性等。
其实我们感觉这样写的没有什么问题。但是这不符合我们设计代码的要求。“高内聚、低耦合”。其实看到这里你并不明白这个代码为什么不符合这个要求。说实话,我也不明白,因此下面是个人的解读,不一定正确。
我们上面已经自己设计过一个工厂来生产对象,想要生产哪个对象,只需要传入标识即可(来自bean.properties文件,该文件key是标识,value是全类名,工厂是用反射来创建对象的。)
那么如果我们现在想用上面的工厂创建一个DAO,在这个service中使用,代码是怎么样的?
package com.qf.service; import com.qf.dao.UserDAO; import com.qf.dao.UserDAOImpl; import com.qf.factory.MyFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class UserServiceImpl implements UserService { // !!!强耦合了UserDAOImpl!!!,使得UserServiceImpl变得不稳健!! private UserDAO userDAO = new UserDAOImpl(); public void deleteUser(Integer id) { userDAO.deleteUser(1); System.out.println("delete User in Service"); } // 通过我们设计的工厂来创建dao对象,然后service层调用 public void deleteUser(Integer id) { UserDAO userDao = (UserDAO) new MyFactory().getBean("userDao"); userDao.deleteUser(1); System.out.println("delete User in Service"); } // 此时如果我们用Spring的控制反转,怎么注入Dao对象,然后service层调用呢? public void deleteUser(Integer id) { ApplicationContext context = new ClassPathXmlApplicationContext("/spring-cotext.xml"); UserDAO userDao = (UserDAO)context.getBean("userDao"); userDao.deleteUser(1); System.out.println("delete User in Service"); } }
这里需要提升一下,我们看到上面的三个方法显然是不能写在一个Class中的,这里只是为了演示。
我们上面第一种就是我们眼中的强耦合,第二种和第三种就是解耦合(第二个是我们自己实现的工厂类,第三个是借助Spring的工厂类)。其实我就不明白了,后两个怎么就解耦合了,按代码量来看,第二和第三都还是两句代码才引入这个UserDao对象呢(请注意,这里声明的是接口,其实他们三个引入的本质对象,调用的对象都是UserDAOImpl),而我第一种方式就一行代码搞定。
=============================下面就是自己的进一步体会了=================================
其实我们观察者三种方式(其实是两种哈,一个是硬编码new,一个是工厂方式引入),对于对一种方式new,前面是接口UserDao,而后面出现了具体的实现类UserDaoImpl;而对于工厂方式,我们这里只出现了一个"userDao"的String类型字符串。这个字符串类型具体指什么,是在配置文件中声明的,当然我们配置文件中要声明UserDao是实现类,即UserDaoImpl的全类名,如果我们声明其它的当然报错。
这两种方式有什么区别吗?我怎么感觉没有呢?试想,如果现在我们UserDao的实现类变化了,比如名字变化,变为CustomerDaoImpl了;或者是原本的实现类不满足我们的需求,我们要重新写一个实现类UserDaoImpl-Version2了。
那么对于第一种new的方式,我们肯定是这么样改写的。
public void demo() { //原本代码 UserDao userDao = new UserDaoImpl(); //对于第一种变化,Service需要变化为: UserDao userDao = new CustomerDaoImpl(); //对于第二种变化,Service需要变化为: UserDao userDao = new UserDaoImpl-Version2(); }
那么对于第二种工厂的方式,我们是不需要修改原有的Service的。
我们只需要把配置文件中相应keyuserDao对应的Value修改一下即可。
=====================再进一步思考====================================
有人可能想,这不都是修改吗?只是一个修改位置是Service层,一个是配置文件。
其实区别还是很大的,怎么说呢?我们现在的项目小,依赖的对象也少(解决依赖对象问题,一个是new,一个是采用工厂生产)。我们自然觉得在哪里修改都好,甚至觉得多了配置文件挺麻烦的。
但是如果我们的项目非常大,Service这个类非常大;或者是这个依赖的对象非常多,比如我们有n个service依赖了n个Dao,n个Servlet依赖了n个Service。那此时该怎么办?;再比如一个Dao在100个地方注入,而这个Dao的实现修改了,按照new的方式,岂不是我们要修改100个地方,但是用工厂生产对象,我们只需要在配置文件中修改一次,哎,刚刚怎么没有想到这个例子,这个例子多好了。
项目小,我们大不了LZ重写一个,怎么啦,我开心,电脑坏了,我重启电脑,程序有Bug了,我重写项目。但是如果大呢?几年维护的项目呢?可能就要你的团队加班一两个月了吧。
因此,“高内聚、低耦合很重要”,而控制反转(IOC)就解决了低耦合的一小部分问题,我们要欢迎Spring的IOC。
9.2 解决方案
上面我们已经提到了解决方案。当然了我们的Spring是更加强大的,如果Service需要依赖Dao,上面我们的解决方案还要在Service创建Spring的工厂(需要加载配置文件),然后通过配置文件中的key获得Bean实例。
我们的Spring是强大的,不需要这么做。
首先Service层需要依赖Dao,怎么办?
package com.qf.service; import com.qf.dao.UserDAO; import com.qf.dao.UserDAOImpl; import com.qf.factory.MyFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; // 不引入一个具体的组件(实现类),在需要其他组件的位置预留存取值入口(set/get) public class UserServiceImpl implements UserService { // !!!不再耦合任何DAO实现!!!,消除不稳定因素!!,因为我们会变化的就是实现类 // 有人可能说,那我们UserDao也可能变啊,那就是你程序设计的毛病了,别怪UserDao接口变化。接口设计本身不背锅 //private UserDAO userDAO = new UserDAOImpl(); private UserDAO userDAO; public UserDAO getUserDAO() { return userDAO; } public void setUserDAO(UserDAO userDAO) { this.userDAO = userDAO; } // 为UserDAO定义set/get,允许userDAO属性接收spring赋值 // Getters And Setters public void deleteUser(Integer id) { userDAO.deleteUser(1); System.out.println("delete User in Service"); } }
此时配置文件要怎么写?这里的<property>标签意思是类UserServiceImpl中有一个属性,我要在这个对象创建时,给这个属性赋值;name属性是这个类中的属性名(被赋值的这个属性);ref是指给这个属性赋什么值,就是赋那个对象的值,这里就引用了上一个<bean>标签中的id属性。
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- 要工厂生产的对象 --> <bean id="userDAO" class="com.qf.dao.UserDAOImpl"></bean> <bean id="userService" class="com.qf.service.UserServiceImpl"> <!-- 由spring为userDAO属性赋值,值为id="userDAO"的bean --> <property name="userDAO" ref="userDAO"></property> </bean> </beans>
此时,如果需要更滑其它UserDAO实现类,则UserServiceImpl不用任何改动!
则此时的UserServiceImpl组件变的更加稳健!
其实我们在设计程序时,如果一个类不需要修改,我们则不能因为其它类修改而修改这个类。这是什么意思,比如类A依赖B,B修改了,我们就要跑到A这里修改A。明明是B的问题,你却影响到了A。这是不好的,这就是我们上面举的Service和Dao之间的例子。
十、DI(Dependency Injection)依赖注入【重点】
其实依赖注入(DI)就是控制反转(IOC)。只是控制反转是从概念的层面出发命名的,而DI是从应用层面出发命名的。
<bean id="userService" class="com.qf.service.UserServiceImpl"> <!-- 由spring为userDAO属性赋值,值为id="userDAO"的bean --> <property name="userDAO" ref="userDAO"></property> </bean>
其实这种在工厂的配置文件中为属性赋值的方式被称为注入,那为什么叫依赖注入呢?其实我们注入的不是属性值,其实是依赖关系(表面上像是属性值,其实我们想要的是依赖关系)。
10.1 概念
在Spring创建对象的同时,为其属性赋值,称之为依赖注入。
其实就是在Spring配置文件<bean>标签(用于创建对象)下面设置一个字标签<property>(用于给属性赋值)。
10.2 Set注入
注入方式有三种:set注入、构造注入和自动注入。
创建对象时,Spring工厂会通过Set方法为对象的属性赋值。
这也是我们上面的Service里面在写了需要的对象后,后面跟着getter和setter方法的原因。当然可以没有getter。
10.2.1 定义目标Bean类型
首先我们要构建一个类,以这个类为目标来测试各种各样的注入。我这里特意没有写getter方法,是因为想告诉你set注入对于getter方法是非必要的。
package com.qf.entity; import java.util.*; public class User { private String id; private String password; private String sex; private Integer age; private Date bornDate; private String[] hobbys; private Set<String> phones; private List<String> names; private Map<String, String> countries; private Properties files; // 下面是setter方法,请注意我这里特意没有写getter方法 }
10.2.2 基本类型+字符串类型+日期类型
现在到工厂中去做一个bean。然后是给属性赋值(这里用set方法)我们先给基本类型赋值。
<!--set注入--> <bean id="user" class="com.qf.entity.User"> <!-- 简单:jdk8中基本数据类型 String Date --> <property name="id" value="10"></property> <property name="password" value="123abc"></property> <property name="sex" value="male"></property> <property name="age" value="19"></property> <property name="bornDate" value="2020/12/12 12:20:30"></property> </bean>
生日比较特殊,按道理来说,他是一个引用类型(我们定义是java.util.Date),但是Spring有一个默认的日期字符串,我们按照这个格式来写,Spring就会默认将他转换成日期对象。
我们这里小小测试一下:
@Test public void testSet() { // 启动工厂 ApplicationContext context = new ClassPathXmlApplicationContext("/spring-context.xml"); User user = (User)context.getBean("user"); System.out.println("========================"); }
我们这里在System.out.println("=======");这一行打了断点。因为我们没有设置getter方法,无法直接得到属性值,然后debug运行:发现基本属性赋值了。
注意:这里的基本类型赋值方式包括:8大基本类型、String和Date。
10.2.3 容器类型
容易类型包括:array、list、set、map、properties。
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- 要工厂生产的对象 --> <bean id="userDAO" class="com.qf.dao.UserDAOImpl"></bean> <bean id="userService" class="com.qf.service.UserServiceImpl"> <!-- 由spring为userDAO属性赋值,值为id="userDAO"的bean --> <property name="userDAO" ref="userDAO"></property> </bean> <!--set注入--> <bean id="user" class="com.qf.entity.User"> <!-- 简单:jdk8中基本数据类型 String Date --> <property name="id" value="10"></property> <property name="password" value="123abc"></property> <property name="sex" value="male"></property> <property name="age" value="19"></property> <property name="bornDate" value="2020/12/12 12:20:30"></property> <!--数组--> <property name="hobbys"> <array> <value>football</value> <value>basketball</value> </array> </property> <!--集合--> <property name="names"> <list> <value>tom</value> <value>jack</value> </list> </property> <property name="phones"> <set> <value>1311111</value> <value>131344</value> </set> </property> <property name="countries"> <map> <entry key="zh" value="china"></entry> <entry key="en" value="english"></entry> </map> </property> <property name="files"> <props> <prop key="url">jdbc:mysql:xxx</prop> <prop key="username">root</prop> </props> </property> </bean> </beans>
测试下:就是重新跑一下刚刚的测试文件。
10.2.4 自建类型
现在我们看如何给自定义类型赋值,我们这里再定义一个类Address。
package com.qf.entity; public class Address { private Integer id; private String city; // 下面是setter方法 }
然后在User类中加一个属性:自建类型属性
//自建类型 private Address address;
//要有setter方法
下面我们看自建类型怎么赋值?
首先我们要在工厂中创建一个bean,是Address的bean。再赋值
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- 要工厂生产的对象 --> <bean id="userDAO" class="com.qf.dao.UserDAOImpl"></bean> <bean id="userService" class="com.qf.service.UserServiceImpl"> <!-- 由spring为userDAO属性赋值,值为id="userDAO"的bean --> <property name="userDAO" ref="userDAO"></property> </bean> <bean id="addr" class="com.qf.entity.Address"> <property name="id" value="1"></property> <property name="city" value="bj"></property> </bean> <!--set注入--> <bean id="user" class="com.qf.entity.User"> <!-- 简单:jdk8中基本数据类型 String Date --> <property name="id" value="10"></property> <property name="password" value="123abc"></property> <property name="sex" value="male"></property> <property name="age" value="19"></property> <property name="bornDate" value="2020/12/12 12:20:30"></property> <!--数组--> <property name="hobbys"> <array> <value>football</value> <value>basketball</value> </array> </property> <!--集合--> <property name="names"> <list> <value>tom</value> <value>jack</value> </list> </property> <property name="phones"> <set> <value>1311111</value> <value>131344</value> </set> </property> <property name="countries"> <map> <entry key="zh" value="china"></entry> <entry key="en" value="english"></entry> </map> </property> <property name="files"> <props> <prop key="url">jdbc:mysql:xxx</prop> <prop key="username">root</prop> </props> </property> <!--自建类型--> <property name="address" ref="addr"></property> </bean> </beans>
测试:
10.3 构造注入【了解】
创建对象时,Spring工厂会通过构造方法为对象的属性赋值。
10.3.1 定义目标Bean类型
我们先创建一个Bean类型。
package com.qf.entity; public class Student { private Integer id; private String name; private String sex; private Integer age; // Constructors public Student(Integer id, String name, String sex, Integer age) { this.id = id; this.name = name; this.sex = sex; this.age = age; } }
10.3.2 注入
<!-- 构造注入 --> <bean name="student" class="com.qf.entity.Student"> <constructor-arg name="id" value="1234"></constructor-arg> <!--除标签名有变化,其它均和Set注入一致--> <constructor-arg name="name" value="tom" /> <constructor-arg name="age" value="20" /> <constructor-arg name="sex" value="male" /> </bean>
测试:
@Test public void testCons() { // 启动工厂 ApplicationContext context = new ClassPathXmlApplicationContext("/spring-context.xml"); Student student = (Student) context.getBean("student"); System.out.println("================="); }
当然你也可以在构造函数中打印一句话来验证一下。
当然我们上面说构造注入是了解的,那是为什么吗呢?是因为构造注入不够灵活,我们不经常用。比如现在我想给其中两个属性赋值,那就要再写构造函数。
10.4 自动注入【了解】
自动注入非常有意思,上面无论是set注入还是构造注入,我们都是明确了给哪个属性赋哪个值,但是自动注入没有。他是根据一种规则自动匹配。
不用在配中指定为哪个属性赋值,及赋什么值。
由Spring自动根据某个“原则”,在工厂中查找一个bean,及属性注入属性值。
原则有两种,一种是ByName,一种是ByType。上面的例子是ByName,意思是会为类UserServiceImple中的所有属性赋值,原则是类中的属性名同配置文件中的id相同。
如果是ByType,则意思是为类UserServiceImple中的所有属性赋值,原则是类中的属性类型同配置文件中的类型class相同。
十一、Bean细节
11.1 控制简单对象的单例、多例模式
单例模式是默认的一种模式。所谓的单例和多例在这里是什么意思呢?就是我们上面用<bean>标签构造一个对象放入容器,默认都是单例,就只是上产一个对象,如果你测试一下,获得多个对象,比较后,其实内存地址是一样的。
那如果修改为多个对象呢?非常简单,这里需要留意的是scope属性只有两个取值,一个是这里的prototype,一个就是默认的singleton。
<!-- 构造注入 --> <bean name="student" class="com.qf.entity.Student" scope="prototype"> <constructor-arg name="id" value="1234"></constructor-arg> <!--除标签名有变化,其它均和Set注入一致--> <constructor-arg name="name" value="tom" /> <constructor-arg name="age" value="20" /> <constructor-arg name="sex" value="male" /> </bean>
- 注意:需要根据场景决定对象的单例、多例模式。
- 可以共用:Service、DAO、SqlSessionFactory(或者是所有的工厂)。
- 不可共用:Connection、SqlSession、ShoppingCart。
- 其实上面的可以共用和不可以共用我还没有明白。
11.2 FactoryBean创建复杂对象【了解】
作用:Spring可以创建复杂对象、或者无法直接通过反射创建的对象。
我们上面创建的对象都是简单对象。其创建过程是怎么样的呢?
通过类名、反射、再通过构造方法获得(如果是set注入,则是用无参构造,如果是构造注入,则用有参构造)。一句话,简单对象用反射调用该类型的构造方法来创建。
但是有一些对象不能通过new完成,或者说是不能通过构造方法来构建的。比如说数据库的链接Connection对象,他就明显不是new出来的,我们要创建Connection对象,我们要先加载启动类,然后再通过DriverManager去传入URL、数据库账号和密码,从而获得。如果我们直接new Connection是无法使用的。
再比如SqlSessionFactory,这个对象也不是直接new出来的,他要先加载MyBatis的配置文件,然后再通过SqlSessionBuilder去构造得到。
像上面这两个就是复杂对象,不能通过new直接创建出来。那问题来了,我们要想生产复杂对象,怎么办?要用到工厂Bean,即FactoryBean。下面我们以创建一个复杂对象Connection为例来演示复杂对象的构建。
11.2.1 实现FactoryBean接口
MyFactoryBean类
package com.qf.factorybean;
import org.springframework.beans.factory.FactoryBean;
import java.sql.Connection;
import java.sql.DriverManager;
public class MyConnectionFactoryBean implements FactoryBean<Connection> {
// 这个方法我们要完成复杂对象的创建过程
@Override
public Connection getObject() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis_shine?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai","root","root");
return conn;
}
// 这个非常简单,是返回我们生产对象的类型对象
@Override
public Class<?> getObjectType() {
return Connection.class;
}
// 这个是我们的工厂Bean在生产时的模式是什么?是单例还是多例,我们的Connection不是单例,因此返回false
@Override
public boolean isSingleton() {
return false;
}
}
核心何止文件中用<bean>标签声明:
<!-- SqlSessionFactory 复杂对象 FactoryBean 当从工厂中索要一个bean时,如果是Factorybean, 实际返回的是工厂Bean的getObject方法的返回值 --> <bean id="conn" class="com.qf.factorybean.MyConnectionFactoryBean" />
测试:
@Test public void testFactoryBean() { // 启动工厂 ApplicationContext context = new ClassPathXmlApplicationContext("/spring-context.xml"); Connection conn = (Connection) context.getBean("conn"); System.out.println(conn); }
结果:报错为:java: Compilation failed: internal java compiler error
这是怎么回事呢?是我们的MySQL的驱动没有导入。
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency>
我上面的驱动确实忘添加了,但是上面那个错误并非是驱动的问题,编译没有通过应该从如下三个方面入手查看。
运行结果:
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
com.mysql.cj.jdbc.ConnectionImpl@41fecb8b
上面结果中的第一行大家应该知道什么意思,我就不解释了。
进一步测试,大家可以不用做,这个我跟着老师做下来了,因为我的JDBC快忘完了。
@Test public void testFactoryBean() throws SQLException { // 启动工厂 ApplicationContext context = new ClassPathXmlApplicationContext("/spring-context.xml"); Connection conn = (Connection) context.getBean("conn"); System.out.println(conn); PreparedStatement preparedStatement = conn.prepareStatement("select * from t_user"); ResultSet resultSet = preparedStatement.executeQuery(); resultSet.next(); System.out.println(resultSet.getInt("id")); }
结果:
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
com.mysql.cj.jdbc.ConnectionImpl@41fecb8b
4
下面再讲一个额外点,其实我们平时写这个工厂bean就是为了获得特殊对象,但是如果我们想获得这个工厂bean本身呢(比较少见)。其实用&conn获取即可。测试如下:
@Test public void testFactoryBean() throws SQLException { // 启动工厂 ApplicationContext context = new ClassPathXmlApplicationContext("/spring-context.xml"); Connection conn = (Connection) context.getBean("conn"); MyConnectionFactoryBean myConnectionFactoryBean = (MyConnectionFactoryBean) context.getBean("&conn"); System.out.println(myConnectionFactoryBean); // com.qf.factorybean.MyConnectionFactoryBean@41fecb8b System.out.println(conn); //com.mysql.cj.jdbc.ConnectionImpl@120f102b PreparedStatement preparedStatement = conn.prepareStatement("select * from t_user"); ResultSet resultSet = preparedStatement.executeQuery(); resultSet.next(); System.out.println(resultSet.getInt("id")); }
不知道大家理解这里不?应该不难理解吧,就是这样我们就可以获得MyConnectionFactoryBean类的实例了。
- 注意:isSingleton方法的返回值,需根据所创建的特点决定返回true/false
- 例如:Connection不应该被多个用户共享,返回false。
- 例如:SqlSessionFactory重量级资源,不该过多创建,返回true。
11.2.2 配置spring-Context.xml
这里我想解释一下,其实我们在11.2.1中已经把所有笔记做好了,里面就包括MyFactoryBean的创建步骤,Spring工厂配置文件的修改、测试和MyFactoryBean实例本身的获取,之所以这里又截图是来自笔记的图片,方便参考。
11.2.3 特例
十二、Spring工厂特性
12.1 饿汉式创建优势
工厂创建之后,会将Spring配置文件中的所有对象都创建完成(饿汉式)。
提高程序运行效率。避免多次IO,减少对象创建时间。(概念接近连接池,一次性创建好,使用时直接获取)
其实就是饿汉式会在工厂创建之后创建好配置文件中所有对象,可以直接用。
12.2 生命周期方法
- 自定义初始化方法:添加"init-method"属性,Spring则会在创建对象之后,调用此方法。
- 自定义销毁方法:添加“destory-method”属性,Spring则会在销毁对象之前,调用此方法。
- 销毁:工厂的close()方法被调用之后,Spring会销毁所有已创建的单例对象。
- 分类:Singleton对象由Spring容器销毁,Prototype对象由JVM销毁。
大家对上面的描述是一头雾水,我们来实战一下。
首先我们再复制一下Spring的核心配置文件(当然有时候我们就叫工厂了,因为是生产对象的嘛!)这里面就是把之前的删除了,剩下了这个Address类的实例,然后里面有两个属性,一个是init-method,一个是destory-method。其实你可能很疑惑,这里的值是什么?其实是方法名,别慌,接着看。
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- 构造方法 set方法 初始化 销毁方法 --> <bean id="addr" class="com.qf.entity.Address" init-method="init_qf" destroy-method="destory_qf"> <property name="id" value="1"></property> <property name="city" value="bj"></property> </bean> </beans>
类Address重新修改一下:我们致力修改只是在无参构造和setter方法里面打印了一句话,然后又添加了两个方法,这两个方法名就用于上面配置文件中属性init-method和destory-method的值。这些都是为了测试生命周期方法(就是这两个方法,init和destory)执行的位置。
package com.qf.entity; import javax.annotation.PostConstruct; public class Address { private Integer id; private String city; public Address() { System.out.println("Address 构造方法"); } public void setId(Integer id) { System.out.println("Address SetId"); this.id = id; } public void setCity(String city) { System.out.println("Address SetCity"); this.city = city; } public void init_qf() { System.out.println("Address 初始化"); } public void destory_qf() { System.out.println("Address 销毁"); } }
测试1:我们看到这里只有一行代码,即开启工厂,大家知道为什么吗?因为开启工厂,配置文件中的对象就会被创建了,饿汉式嘛!因为我们不需要进一步去获取对象。
@Test public void testLife() { // 启动工厂 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring-context2.xml"); }
结果:我们注意到初始化方法在setter方法后执行的,但是destory方法没有执行。我这里说一下,对于这种简单类型,就是通过反射,先通过无参构造方法(set注入,如果是构造注入,采用相应的构造方法),然后用setter方法设置值,再调用初始化方法,再调用destory方法(注意这个方法调用是有条件的)。
Address 构造方法
Address SetId
Address SetCity
Address 初始化
测试2:
@Test public void testLife2() { // 启动工厂 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring-context2.xml"); System.out.println("==================="); //关闭工厂 applicationContext.close(); }
运行结果:其实这里就看出来了,这个销毁方法是工厂调用了close方法后才执行的。而且要记住,这个关闭方法不是接口的方法,而是实现类的方法。
Address 构造方法
Address SetId
Address SetCity
Address 初始化
===================
Address 销毁
上面只是讲解了单例(默认),那如果是多例呢?我们将配置文件修改为多例,添加属性scope="prototype"即可
我们执行测试2,发现没有任何的输出,没有构造,setter、初始化和销毁。只有"============="。
其实原因是多例的对象不会随着工厂的创建而创建,也就是说饿汉式只对单例bean有效。
多例bean只有在被使用时才会创建。
测试:
@Test public void testLife2() { // 启动工厂 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring-context2.xml"); applicationContext.getBean("addr"); applicationContext.getBean("addr"); System.out.println("==================="); //关闭工厂 applicationContext.close(); }
运行结果:
Address 构造方法
Address SetId
Address SetCity
Address 初始化
Address 构造方法
Address SetId
Address SetCity
Address 初始化
===================
我们发现多例对象的生命周期方法完全不一样,是随着使用而创建。使用后悔调用构造方法、setter方法和初始化方法。但是这里要注意,他并不会随着容器的关闭而调用destory方法。
12.3 生命周期注解
这个注解有印象即可,后面我们会讲解。
12.4 生命周期阶段
单例bean:Singleton
随着工厂启动 创建 ==》 构造方法 ==》 set方法(注入值)==》 init(初始化)==》构造完成==》随工厂关闭销毁。
多例bean:prototype
被使用时 创建==》 构造方法 ==》 set方法(注入值)==》 init(初始化) ==》 构造完成 ==》 JVM垃圾回收销毁
这里需要说明一下:对于多例,对象的销毁是由JVM的垃圾回收机制确定的,我们无法自己确定。
我在这里向说明一下:上面的笔记可能有误,或者说表达不准确,我个人在刚开始的时候理解有所偏差,就是上面说的可以说是对象的生命周期,但是更想说的应该是初始化和销毁方法什么时候执行,对于单例和多例有什么区别。大家可以从这两方面理解,其实都可以。
十三、代理设计模式
下面我们要进入AOP的学习,但是在AOP学习之前,我们要先学习代理模式(是23种设计模式之一)。我们在Spring中会重点使用这个模式。下面我们看看在项目中存在什么样的问题,需要使用到这个模式呢?
13.1 概念
将核心功能与辅助功能(事务、日志、性能监控代码)分离,达到核心业务功能更纯粹、辅助业务功能可复用。
其实我们写service层时,我们只需要写dao接口的调用、dato的封装和Salt私盐等代码,这是业务特有的,别的地方不需要的,只有我们这里的service需要的。但是有一些功能比如事务、日志、性能监控代码,其实这些代码与核心逻辑无关,所有地方都需要但都又不是核心代码。怎么说呢?要这些辅助功能吧,会违背单一职能原则(我只是干核心业务的事情,事务处理、日志等我不干),但是不要吧这些也是需要的。对于用户来说他需要事务管理、日志等。怎么办?(这里要说一下辅助功能是很多地方都需要的,比如日志、事务控制和性能监控等,而核心功能是该业务需要的,独有的)
13.2 静态代理设计模式
通过代理类的对象,为原始类的对象(目标类的对象)添加辅助功能,更容易更换代理实现类,利于维护。
这里以组合和房东之间的关系举例。租户(业务调用者,想要调用租房业务),就去找房东(可以看成是service,他就是房东租房的service),这里面有四个方法,发布租房信息、带租户看房、签合同和收房租。但是其实租户更关心的是签合同和收房租,至于带客户看房和发布租房信息他是不关心的。那怎么办?
此时中介就来了(其实就是我们的代理类),他负责辅助功能(即发布租房信息和带租户看房),这些完成后再找房东(房东service)去完成签合同和收房租。
那么怎么写代理类呢?下面演示一下:
正常情况下,房东类是如此的(我们这里新建了工程,Spring02,引入Spring-Context依赖)
接口:
package com.qf; public interface FangDongService { public void zufang(); }
实现类:
package com.qf; // 原始业务类 public class FangDongServiceImpl implements FangDongService { @Override public void zufang() { // 辅助功能、额外功能 System.out.println("发布租房信息"); System.out.println("带租户看房"); // 核心功能 System.out.println("签合同"); System.out.println("收房租"); } }
但是我们知道,这样的话就会有代码冗余、非单一职能问题。
那怎么办?用代理类:这里要说明一下:其实这里的辅助功能就是从原始业务类复制过来的(此时原始业务类关于辅助功能的代码要删除)。然后需要其调用原始业务类的核心功能,因此要有其引用,然后调用。
package com.qf; // 静态代理类 // 代理类必须和原始业务类功能一致,也就是说原始业务类只有租房的方法,代理类也要如此。怎么实现呢?(实现共同接口接口) public class FandDongProxy implements FangDongService { private FangDongService fangDongService = new FangDongServiceImpl(); // 那么我们代理类做什么事呢?做原始业务类不愿意做的事情 @Override public void zufang() { // 辅助功能、额外功能 System.out.println("发布租房信息"); System.out.println("带租户看房"); // 核心=原始业务类 fangDongService.zufang(); } }
测试:测试的话,就只是掉代理啦。
package com.qf.test; import com.qf.FandDongProxy; public class ProxyTest { public static void main(String[] args) { FandDongProxy fandDongProxy = new FandDongProxy(); fandDongProxy.zufang(); } }
测试结果:
发布租房信息
带租户看房
签合同
收房租
注意:上面这个代理只是静态代理。其实他并没有完全解决单一职能原则,无非就是把原有的问题转移到了代理类中(这里面还是有辅助功能和核心功能在一起的情况)。
13.3 动态代理设计模式
动态创建代理类的对象,为原始类的对象添加辅助功能。
上面的静态代理是需要我们自己定义代理类的,把矛盾转化给了代理类。但是实际中是已经有代理类的jar包了,也可以说是对代理类的简单封装。其中就有JDK中的java.lang.reflect包下的方法,不知道为什么代理会在反射包下,可能是有一定的渊源吧!再有一个就是CGLIB包。
13.3.1 JDK动态代理实现(基于接口)
package com.qf.test; import com.qf.FangDongService; import com.qf.FangDongServiceImpl; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import org.junit.Test; public class DynamicProxyTest { @Test public void testJDK() { // 目标(这里面包含了核心功能) final FangDongService fangDongService = new FangDongServiceImpl(); // 额外功能 InvocationHandler ih = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 辅助功能、额外功能 System.out.println("发布租房信息1"); System.out.println("带租客看房1"); // 核心 fangDongService.zufang(); return null; } }; // 动态生成 代理类 FangDongService proxy = (FangDongService) Proxy.newProxyInstance(DynamicProxyTest.class.getClassLoader(), fangDongService.getClass().getInterfaces(), ih); // 看看这里强转成了什么类 proxy.zufang(); } }
13.3.2 CGlib动态代理实现(基于继承)
上面的JDK动态代理是基于接口实现的,也就是代理类和原有业务类是实现同一个接口。而这里的CGLIB则是基于继承实现的,该代理类是原有业务类的子类。
package com.qf.test; import com.qf.FangDongService; import com.qf.FangDongServiceImpl; import java.lang.reflect.Method; import org.junit.Test; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.InvocationHandler; public class DynamicProxyCGlibTest { @Test public void test() { // 目标 FangDongService fangDongService = new FangDongServiceImpl(); // Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(FangDongServiceImpl.class); // 设置其父类为FangDongServiceImpl enhancer.setCallback(new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { // 辅助功能、额外功能 System.out.println("发布租房信息2"); System.out.println("带租客看房2"); // 核心 fangDongService.zufang(); return null; } }); // 动态生成代理类 FangDongServiceImpl proxy = (FangDongServiceImpl)enhancer.create(); //看看这里转换成了什么类 proxy.zufang(); } }
其实我们看到上面这两种动态代理(其实就是我们不用写代码了,有一定的jar包封装了)是很抽象的,我们是不易懂的。讲这些是为了:1.让我们知道动态代理是有这两种方式的;2.想告诉大家动态的生成代理类并获得他的对象,这个事并不是空穴来风,是有对应的API支持的。
那么我们实际使用动态代理的时候,肯定不会自己写像上面这样的代码,在Srping的AOP章节,为我们提供了诸多的封装,他将JDK代理和CGlib代理放在了底层。在更高层为我们保留出一些API和开发流程。让我们更简单更易于控制的去做动态代理的编码。
十四、面向切面编程【重点】
14.1 概念
AOP(Aspect Oriented Programming),即面向切面编程,利用一种称为"横切"的技术,剖开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓切面",简单说就是那系诶与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于味蕾的可操作性和维护性。
我们原来有OOP,即面向对象编程。下面我们看下AOP和OOP的区别:
OOP是面向对象编程,核心思想是将客观存在的不同事物抽象成相互独立的类,然后把与事物相关的属性和行为封装到类里,并通过继承和多态来定义类彼此间的关系,最后通过操作类的实例来完成实际业务逻辑的功能需求。
AOP是面向切面编程,核心思想是将业务逻辑中与类不相关的通用功能切面式的提取分离出来,让多个类共享一个行为,一旦这个行为发生改变,不必修改类,而只需要修改这个行为即可。
OOP与AOP的区别:
1、面向目标不同:简单来说OOP是面向名词领域,AOP面向动词领域。
2、思想结构不同:OOP是纵向结构,AOP是横向结构。
3、注重方面不同:OOP注重业务逻辑单元的划分,AOP偏重业务处理过程的某个步骤或阶段。
OOP与AOP联系:
两者之间是一个相互补充和完善的关系。
AOP的优点:
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP的主要应用:
日志记录、事务处理、异常处理、安全控制和性能统计方面。
在Spring中提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务和事务进行内聚性的开发。
本段摘抄自博客:https://blog.csdn.net/pdsygt/article/details/46433537
14.2 AOP开发术语
这里的专业术语先过一下:后面学了再回头看。
- 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。
- 切入点(Pointcut):被Spring切入连接点。
- 通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。
- 目标对象(Target):代理的目标对象。
- 引介(Introduction):一种特殊的增强,可在运行期为类动态添加Field和Method。
- 织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
- 代理(Proxy):被AOP织入通知后,产生的结果类。
- 切面(Aspect):由切点和通知责成,将横切逻辑织入切面所指定的连接点中。
14.3 作用
Spring的AOP编程即是通过动态代理类为原始类的方法添加辅助功能。
其实怎么说呢?我们上面提到动态代理有JDK基于接口实现和CGlib基于继承实现。这里Spring对其再次封装,底层还是这两种方式,但是我们不需要关注底层。我们要做的是基于Spring提供的AOP相关接口和Spring指定的开发顺序即可实现动态代理。就是这么简单。
14.4 环境搭建
引入AOP相关依赖
14.5 开发流程
14.6 AOP小结
14.7 通知类【可选】
14.8 统配切入点
14.9 JDK和CGLIB选择
14.10 后处理器
14.10.1 后处理器定义
14.10.2 配置后处理器
14.10.3 bean生命周期
14.10.4 动态代理源码(了解)
十五、Spring+MyBatis【重点】
15.1 配置数据源
15.1.1 引入jdbc.properties配置文件
15.1.2 整合Spring配置文件和properties配置文件
15.1.3 Druid连接池可选参数
15.1.4 Druid监控中心
15.1.5 测试监测中心
15.2 整合MyBatis
15.2.1 导入依赖
15.2.2 配置SqlSessionFactory
15.2.3 配置MapperScannerConfigurer
15.2.4 配置Service
十六、事务【重点】
16.1 配置DataSourceTransactionManager