1、Spring概述
Spring 是分层的 Java SE/EE 应用 full-stack 轻量级开源框架,以 IoC(Inverse Of Control: 反转控制)和 AOP(Aspect Oriented Programming:面向切面编程)为内核,提供了展现层 Spring MVC 和持久层。Spring JDBC 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多 著名的第三方框架和类库,逐渐成为使用最多的 Java EE 企业应用开源框架
2、spring发展历程
1997 年 IBM 提出了 EJB 的思想
1998 年,SUN 制定开发标准规范 EJB1.0
1999 年,EJB1.1 发布
2001 年,EJB2.0 发布
2003 年,EJB2.1 发布
2006 年,EJB3.0 发布
Rod Johnson(spring之父) ,Expert One-to-One J2EE Design and Development(2002) 阐述了 J2EE 使用 EJB 开发设计的优点及解决方案。Expert One-to-One J2EE Development without EJB(2004) 。阐述了 J2EE 开发不使用 EJB 的解决方式(Spring 雏形) ,2017年9月份发布了spring的最新版本spring 5.0通用版(GA)
3、spring的优势
(1)、方便解耦,简化开发:IOC
通过 Spring 提供的 IoC容器,可以将对象间的依赖关系交由 Spring进行控制,避免硬编码所造 成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这 些很底层的需求编写代码,可以更专注于上层的应用。
(2)、AOP编程的支持
通过 Spring 的 AOP 功能,方便进行面向切面的编程,许多不容易用传统 OOP实现的功能可以。通过 AOP轻松应付。
(3)、声明式事务的支持
可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理, 提高开发效率和质量。
(4)、方便程序的测试
可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的操作,而是随手可 做的事情。
(5)、方便集成各种优秀框架
Spring 可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、Quartz等)的直接支持
(6)、降低 JavaEE API 的使用难度
Spring 对 JavaEE API(如 JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些 API 的 使用难度大为降低。
(7)、Java 源码是经典学习范例
Spring 的源代码设计精妙、结构清晰、匠心独用,处处体现着大师对 Java设计模式灵活运用以 及对 Java技术的高深造诣。它的源代码无疑是 Java技术的最佳实践的范例。
4、spring体系结构
二、Spring入门案例のHelloWorld
和大多数框架一样,使用第三方的框架,基本步骤:
1.引入框架使用需要的依赖******
2.配置文件(格式大多数xml格式或yaml、properties)
3.spring提供的API完成对象的获取和实例化
案例代码
-
pom.xml导入spring依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>5.3.28</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.28</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.28</version> </dependency>
-
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" xmlns:p="http://www.springframework.org/schema/p" xmlns:c="http://www.springframework.org/schema/c" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans https://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 http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- spring配置文件中配置对象的基本信息 --> <bean id="myUserDaoImpl1" class="com.woniu.dao.impl.UserDaoImpl" scope="prototype"></bean> </beans>
-
创建Person类型,利用spring获取Person对象
通过案例代码的运行,我们会发现,不用写new UserDaoImpl()也能得到UserDaoImpl对象。解决了传统自己new对象的"硬编码"问题。
思考1:Spring中获取的对象是单例的吗?
案例分析
-
UserDaoImpl.java
public class UserDaoImpl implements UserDao {
public UserDaoImpl() {
System.out.println("UserDaoImpl构造器被调用");
}
@Override
public int insert() {
System.out.println("链接mysql数据库,完成用户信息");
return 0;
}
}
-
测试类
@Test
public void mt01(){
ApplicationContext factory=new ClassPathXmlApplicationContext("ApplicationContext.xml");
//获取特定的对象 spring容器装的对象,默认:单例的!!!
Object obj1 = factory.getBean("myUserDaoImpl1");
Object obj2 = factory.getBean("myUserDaoImpl1");
System.out.println("obj1内存地址:"+obj1);
System.out.println("obj2内存地址:"+obj2);
System.out.println(obj1==obj2);
}
- 测试结果
观察测试代码运行结果,可以得出结论:默认情况下,所有由Spring构建的对象都是单例对象。
思考2:如何从Spring容器得到一个多例对象呢?
我们可以通过设置bean标签的scope属性来改变对象在Spring容器中的创建方式:
<bean id="userDao" class="cn.woniu.dao.UserDao" scope="prototype"></bean>
- 作用:
用于注册对象信息,让 spring 来创建。
默认情况下 它调用的是类中的无参构造函数。如果没有无参构造函数则不能创建成功。 - 属性:
id:给对象在容器中提供一个唯一标识。用于获取对象。
class:指定类的全限定类名。用于反射创建对象。默认情况下调用无参构造函数。
scope:指定对象的作用范围。
1、singleton :默认值,单例的.
2、prototype :多例的.
3、request :WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 request 域中
4、session :WEB 项目中,Spring 创建一个 Bean 的对象,将对象存入到 session 域中.
5、global session :WEB 项目中,应用在 Portlet 环境 .如果没有 Portlet 环境那么 globalSession 相当于 session.
三、解读Helloworld案例代码
1、ApplicationContext是什么?
我们可以把ApplicationContext理解成是一个装载对象的"大容器",其本质就是一个对象工厂。借助IDEA快捷键Alt+Ctrl+U可以看到ApplicationContext实现过的所有父接口:
2、BeanFactory和ApplicationContext的区别【常见面试题】
ApplicationContext底层就是对象工厂(即BeanFactory)。Spring提供两种创建对象工厂的方式(BeanFactory和ApplicationContext两个接口)。这种方式创建对象工厂时有什么区别呢?
案例演示ApplicationContext
-
在UserDaoImpl类中添加无参构造方法:代码略
-
测试方法
//1.创建spring ioc容器 ApplicationContext可以理解成是Spring容器,本质其实就是BeanFactory,职责就是负责获取对象
//ApplicationContext:工厂建好,立马将需要工厂管理对象也一起建好
ApplicationContext cnt=new ClassPathXmlApplicationContext("spring-config.xml");
-
结果如下:
UserDaoImpl对象的构造器被调用,意味着UserDaoImpl对象在创建ApplicationContext时就被创建
案例演示BeanFactory
-
修改测试方法
//1.创建spring ioc容器 ApplicationContext可以理解成是Spring容器,本质其实就是BeanFactory,职责就是负责获取对象
//BeanFactory延迟加载(也称为懒加载)创建工厂对象时,不会帮我们把工厂内部对象创建
Resource resource=new ClassPathResource("spring-config.xml");
BeanFactory cnt=new XmlBeanFactory(resource);
-
结果如下:
控制台什么也没输出,也就是UserDaoImpl对象的构造器没有调用,意味着UserDaoImpl对象在创建BeanFactory时没有被创建
通过以上案例对比,我们可以得出以下结论:
BeanFactory:SpringIoc容器基本实现,是Spring内部的使用接口,不提供开发人员进行使用。
BeanFactory对象工厂实现的特点是:
构建核心容器时,创建对象采取的策略是延迟加载的方式,什么时候调用getBean根据id获取对象了,什么时候才真正创建对象。适用于多例模式
ApplicationContext:ApplicationContext是BeanFactory接口的子接口,提供更多更强大的功能,一般由开发人员进行使用。
ApplicationContext实现的特点是:
在构建核心容器时,创建对象采取的策略是立即加载的方式,只要一读取完配置文件就马上创建配置文件中的配置对象。适用于单例模式
3、ApplicationContext的三个实现类【理解】
在入门Helloworld案例中,我们实例化spring的工厂对象时,使用的是ClassPathXmlApplicationContext对象,其实在ApplicationContext的继承体系中,除了ClassPathXmlApplicationContext这个实现类以外,还有另外两个子类也可以完成ApplicationContext对象工厂的实例化。
提示:idea中通过选中类,通过ctrl+H可以查看向下派生的继承体现,通过ctrl+alt+u可以查看该类向上的继承体系结构。
在ApplicationContext对象上按ctrl+h查看该类的结构
ClassPathXmlApplicationContext:加载类路径下的配置文件,要求配置文件必须在类路径下(常用)*****
FileSystemXmlApplicationContext:加载磁盘任意路径下的配置文件(必须有访问权限)
AnnotationConfigApplicationContext:读取注解配置容器
4、bean 的作用范围和生命周期 【面试题】
1、单例对象:scope="singleton"
一个应用只有一个对象的实例。它的作用范围就是整个应用。
生命周期:
对象出生:当应用加载,创建容器时,对象就被创建了。
对象活着:只要容器在,对象一直活着。
对象死亡:当应用卸载,销毁容器时,对象就被销毁了。
2、多例对象:scope="prototype"
每次访问对象时,都会重新创建对象实例。
生命周期:
对象出生:当使用对象时,创建新的对象实例。不使用就不创建
对象活着:只要对象在使用中,就一直活着。
对象死亡:当对象长时间不用时,被 java 的垃圾回收器回收了。
案例演示:bean的生命 周期
创建StudentService类,在该类中添加:一个构造方法、一个init方法、一个destory方法
public class StudentService {
//构造方法
public StudentService(){
System.out.println("创建对象实例");
}
//初始化方法
public void init(){
System.out.println("对象被创建了");
}
//销毁
public void destory(){
System.out.println("对象被销毁了");
}
}
配置spring-config.xml
<!-- 实例化StudentService -->
<bean id = "studentService"
class="cn.woniu.service.StudentService"
scope="singleton"
init-method="init"
destroy-method="destory">
</bean>
创建测试方法
我们发现,在===========上面时,对象就已经被创建了,这是单例对象的特点,立即加载,而且执行了studnetService类的init方法,但是我们并没有看到执行destory方法。原因很简单:当测试方法执行完时,线程结束,此时容器销毁,容器销毁意味着创建的单例对象也要销毁,只是此时没来得及打印。要想看到效果,我们需要手动让容器销毁,调用容器的close方法,此时对象就会销毁,即执行destory方法
如果我们把配置文件的对象改成多例模式呢?其它的代码不变
<!-- 实例化StudentService -->
<bean id = "studentService"
class="cn.woniu.service.StudentService"
scope="prototype"
init-method="init"
destroy-method="destory">
</bean>
改为多例模式后发现对象是懒加载的方式,即在用到的时候才会创建,而且我们手动关闭容器的时候也不会调用destory方法,原因很简单,多例对象的死亡是由java垃圾回收器回收的,不受容器管理。
四 IOC
IOC容器负责bean管理。什么是bean管理?从两个方面来理解:
- Spring容器负责为java项目创建对象(即IOC)
- Spring容器负责为创建的对象注入属性值(即DI)
Bean管理操作的方式有:
- 基于xml配置文件方式实现
- 基于注解方式实现
- 通过JavaConfig配置bean
IOC概念
IOC: Inversion Of Control 控制反转。
以前:java程序使用对象:开发人员 new 对象 正向控制
spring:java程序使用对象:找Spring的ApplicationContext对象拿对象
翻译:new对象称为开发人员对对象的控制,将以前程序员自己new对象"权利" 交个Spring的spring容器统一管理的现象就是控制的权利反转了。所以,在spring中IOC的主要作用就是利用spring容器完成不同类对象的创建【实体类不会通过spring的ioc完成创建】进而解决程序中new对象时硬编码的问题。
IOC只解决程序间的依赖关系,除此之外没有任何功能
如何使用ioc完成对象创建呢?通常步骤有:
1.配置文件利用<bean></bean>配置你要spring容器管理的对象
2.获取spring工厂对象【或spring容器】
3.调用getBean根据id获取对象即可。
五 DI
DI:Dependency Injection 依赖注入
翻译:IOC只管对象创建,DI负责对象属性赋值
DI常用方式
- setter注入方式
- 构造器注入方式
setter注入方式
Set注入就是在类中提供需要注入成员的 setter方法,通过调用setter完成属性赋值
案例
Person.java代码
@ToString
public class Person {
private Integer id;//1
private String name;//张三丰
public Person() {
System.out.println("Person的构造器被调用了");
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
构造器注入
注入属性值的对象提供了带参构造器,而且还保持类有无参构造器
案例
Person.java添加构造器
public class Person {
private Integer id;//1
private String name;//张三丰
public Person(Integer id, String name) {
this.id = id;
this.name = name;
}
public Person() {
}
}
使用c命名空间和p命名空间简化构造器赋值和setter赋值【了解】
使用步骤:
- xml文件根标签中引入p和c命名空间
- 使用c:和p:作为前缀实现赋值
不同数据类型的属性注入
Spring不管entity类型,主要对三层架构对象进行ioc和di管理
8种基本类型及对应的包装类及String类型
赋值方式都是通过value属性赋值
使用setter赋值,完整语法
<property name="属性名" value="属性值"/>
使用构造器赋值,完整语法
<constructor-arg name/index/type="形参名" value="属性值"/>
自定义对象类型
使用setter赋值,完整语法
<property name="属性名" ref="引用的对象ID"/>
使用构造器赋值,完整语法
<constructor-arg name/index/type="形参名" ref="引用的对象ID"/>
集合和数组类型【了解】
常见类型:数组、List集合、Set集合、Map集合、Properties集合
利用c命名空间和p命名空间简化属性赋值【了解】
c命名空间简化构造器注入方式,举例:
p命名空间简化setter注入方式
使用步骤:
- xml文件根标签中引入p和c命名空间
- 使用c:和p:作为前缀实现赋值
六 DI自动装配机制【理解】
自动装配是根据自动的装配规则(byName属性名称或byType属性类型),Spring自动将匹配的属性值进行注入的方式。自动装配是spring DI的一种方式。
在Spring中有三种自动装配的方式:
- 在xml中显式配置【理解】
- 在javal类中显示配置【Spring5+的新特性】【重点】
- 隐式的自动装配【注解方式,重点】
利用XML配置完成自动装配【理解】
自动装配常见异常
使用byType进行装配时,如果一个类型可以找到多个Bean对象,就会出现以下异常:
解决方案:利用byName进行装配
七 spring提供注解完成IOC和DI功能【实际开发都是注解完成ioc和di 重点】
IOC注解
@Component,@Repository,@Service,@Controller标记在类上,实现这个类由spring容器负责对象管理
作用类似xml配置
@Component:标注三层架构以外的类,比如全局异常、工具类、redis或minio工具类
spring为@Component衍生了三个注解,三个注解作用跟@Component一模一样,但是从词意来看,阅读性比@Component更好
@Repository:一般用在持久层
@Service:一般用在业务层
@Controller:一般用在控制器层
DI注解
@Value
完成8种基本类型和String类型的属性值注入。@Value的作用等价于以下代码:
<property name="" value='属性值'/>
或
<constructor-arg name/index/type="" value="属性值"/>
@Autowired
完成自定义类型属性注入。@Autowired的作用等价于以下代码:
<bean autowired="byName/byType"/>
@Autowired默认先根据byType,如果byType装配失败,退而其次,使用byName再装配,如果byName也失败,直接抛出异常
跟@Autowired注解一样,也可以为引用类型的对象属性注入值的的注解还有以下两个:
@Resource
@Resource注解与@Autowired注解的作用是一样的,都是用来为bean对象注入值的,唯一的区别是@Resource注解要根据实例化的bean名称为bean对象注入值。@Resource是javax包下注解,不是spring官方注解。
@Resource(value="byName属性名")
@Quanifier
byName注入,是spring提供的byName注解,它在给字段注入时不能独立使用,必须和@Autowire 一起使用,表示在自动按照类型注入的基础之上,再按照 Bean 的id 注入;@Quanifier也给方法形参注入,注入时可以独立使用。
语法:
@Quanifier(value="byName的名称")
小结注解开发
spring提供的注解,依据注解作用和使用位置不同,划分为三类:
1.注解在类上,作用IOC创建对象的作用:
@Component @Repository @Service @Controller
2.注解在属性上,作用DI属性注入值的作用:
@Resource:byName
@Quanifier:默认byName,但是不能独立使用在属性上,必须@Autowired结合使用
@AutoWired:默认先byType,byType失败,再byName
@Value:注入八种基本类型和String类型
3.注解在类上,设置对象单例模式还是多例模式,设置对象使用范围:[了解]
@Scope(设置对象使用范围:propotype singleton session request)
使用案例
-
UserDaoImpl.java
//这个类是否会被别的层的类使用,如果会,这个类交给spring容器管理
@Repository
public class UserDaoImpl implements UserDao {
@Override
public int insert() {
System.out.println("连接mysql数据库,执行insert操作...");
return 0;
}
}
-
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
//DI注入对象,注入方式
@Autowired
private UserDao userDao;
@Override
public void add() {
userDao.insert();
}
}
-
UserController.java
/**
- 注解开发里面,如果使用setter注入方式,类中不需要提供setter都可以注入成功
*/
@Controller
public class UserController {
@Autowired
private UserService userService;
public void service(){
userService.add();
}
}
- 注解开发里面,如果使用setter注入方式,类中不需要提供setter都可以注入成功
-
单元测试
@Test
public void service() {
ApplicationContext cnt=new ClassPathXmlApplicationContext("spring-config.xml");
UserController controller = cnt.getBean("userController", UserController.class);
controller.service();
}
当我们再为UserDao接口提供一个新的实现类,代码如下所示:
-
UserDaoImpl2.java
@Repository
public class UserDaoImpl2 implements UserDao {
public UserDaoImpl2() {
System.out.println("UserDaoImpl2");
}
@Override public int insert() { System.out.println("连接oracle数据库,执行insert操作..."); return 0; }
}
重新执行单元测试,会发现控制台爆出异常:
如何解决呢?可以借助@Resource或@Qualifier指定注入对象的名称,代码如下所示:
-
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService {
@Resource(name = "userDaoImpl2")//name属性设置注入对象的属性名,如果属性名和name注入的名称一致的,name可以不赋值
private UserDao userDao;
@Override
public void add() {
userDao.insert();
}
}
或
@Service
public class UserServiceImpl implements UserService {
@Autowired
@Qualifier("userDaoImpl") //默认byName,但是注入属性时不能独立使用,必须和@Autowired一起结合使用
private UserDao userDao;
@Override
public void add() {
userDao.insert();
}
}
如果,我们希望UserDaoImpl2是一个多例对象,也可以在类上通过@Scope注解注解对象的使用范围:
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Repository
public class UserDaoImpl2 implements UserDao {
public UserDaoImpl2() {
System.out.println("UserDaoImpl2");
}
@Override
public int insert() {
System.out.println("连接oracle数据库,执行insert操作...");
return 0;
}
}
测试多例模式
@Test
public void test02() {
ApplicationContext cnt=new ClassPathXmlApplicationContext("spring-config.xml");
Object o1 = cnt.getBean("userDaoImpl2");
Object o2 = cnt.getBean("userDaoImpl2");
Object o3 = cnt.getBean("userDaoImpl2");
}
控制台输出结果如下图所示:
扩展了解:程序的耦合和解耦
1、藕合和解藕的思路
藕合就是程序间的依赖关系。解藕就是降低程序间的依赖关系。在开发过程中应做到编译期不依赖,运行时再依赖
解藕思路:
使用反射来创建对象,而不使用new关键字
通过读取配置文件来获取要创建的对象全限定名
2、曾经代码的问题
创建java控制台程序
创建Dao类
创建service并调用Dao
创建测试类并调用service
层与层之间的关系通过new来实现调用,并且new对象时需要导入该对象所在的正确的包名,否则报错。这种关系称为藕合关系。
3、工厂类和配置文件解决藕合问题
a、创建properties属性文件
b、创建工厂类
package cn.woniu.utils;
import java.io.InputStream;
import java.util.Properties;
/**
* 对象工厂
*/
public class BeanFactory {
//定义一个Properties对象
private static Properties properties;
//使用静态代码块为Properties对象赋值
static {
try {
properties = new Properties();
//获取Properties文件流对象
InputStream input = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
properties.load(input);
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化Properties属性出错"+e.getMessage());
}
}
/**
* 根据Bean名称获取Bean对象
*
* @param beanName
*/
public static Object getBean(String beanName) {
Object bean = null;
try {
String beanPath = properties.getProperty(beanName);
bean = Class.forName(beanPath).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}
}
c、使用工厂类调用各层
service调用dao
测试调用service