1 Spring框架简介
Spring是Java软件框架,利用这套框架可以快速的搭建Java软件。
可以这样理解框架:框架是半成品的软件组件,在具体的表现来看,是一系列的jar包文件,普通的jar包只是工具包,而框架更多的是解决了某些特定存在的问题,例如开发效率问题,执行效率问题,安全性问题,代码的可维护性问题等。。。使得开发人员在使用框架开发项目时,不必在关心这些问题,或者这些问题已经得到很大程度的缓解!在利用框架时,可能还需要遵循框架的特定使用方式来编程!
Spring框架的俩个核心功能,Spring的其他组件都是基于这俩个核心功能实现的:
- IOP/DI,控制反转依赖注入
- AOP,面向切面编程
框架的设计核心思想就是封装“复杂”使用“简便”,所以在学习框架课程时要站在使用者的角度,需要有一些“不求甚解”的心态,更多的应该是掌握框架的正确使用方式,对框架的原理不要过渡纠结。
1.1 IOC
IOC(Inversion of Control)直接翻译“控制反转”,理解控制反转就要理解什么是主动控制:
- 主动控制:由应用程序主动控制管理“对象组件”。适合管理创建过程简单的对象。
- 控制反转:由外部容器环境(框架)创建管理“对象组件”交给应用程序使用。适合管理创建过程复杂的对象。从对象使用者的角度看是简单的。
1.2 Spring IOC HelloWord
第一个Spring测试程序:
Spring IOC 的核心功能是创建管理对象,为了体验这个核心功能,我们首先利用Spring管理一个简单的对象。需要注意的是,Spring管理简单对象不能体现Spring强大的功能,Spring优势在于管理创建复杂的对象。这里利用Spring管理简单对象的目的是测试Spring基本的功能。
使用Spring,就必须按照Spring的约定使用其功能:
- 创建被Spring管理的对象类型
a.创建Maven项目
b.创建Demo类
2.利用Maven获取Spring jar
a.利用https://mvnrepository.com/搜索spring-context
b.将坐标复制到pom.xml
3.创建信息类 Config
a.使用注解@Configguration声明配置信息类
b.使用注解@Bean声明Bean组件
4.利用测试类测试
a.利用AnnotationConfigApplicationContext创建Spring IOC容器
b.从IOC容器中获取Spring创建的对象
步骤:
-
创建被Spring管理对象的类型
a.创建Maven项目
b.创建类
package cn.tedu.demo;
public class Demo {
@Override //重写方法时,系统可以帮助检查其重写的正确性,标注这是一个重新的方法
public String toString() {
return "HelloWord!";
}
}
== eclipse中,可以使用Alt+/快捷键快速输出toString方法 ==
-
利用Maven获得Spring jar
a.利用https://mvnrepository.com/搜索spring-context
b.将坐标复制到pom.xml,并添加JDK版本设置
<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>cn.tedu</groupId>
<artifactId>spring1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 设置JDK版本 -->
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.13</version>
</dependency>
</dependencies>
</project>
==右键选择Source-Format可以自动对齐代码 ==
更新时要注意记得先保存
3.创建信息类 Config
package cn.tedu.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.tedu.demo.Demo;
@Configuration //告诉SpringBoot这是一个配置类,相当于Spring中的xml配置文件。
public class Config {
@Bean //给容器中添加组件,相当于Spring中xml配置文件中的<bean>标签。
/*
* 告诉spring在启动时加载被bean标注的方法
* 返回值是Spring创建的对象,方法名是Bean的ID
*/
public Demo bean() {
return new Demo();
}
}
4.利用测试类测试
package cn.tedu.test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.tedu.context.Config;
import cn.tedu.demo.Demo;
public class Demo01 {
public static void main(String[] args) {
//其作用有俩种,分别是指定java文件配置类和指定扫描路径,这里指定java配置类为Config.class
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(Config.class);
//从Spring中获取, Spring控制管理的Bean对象
Demo demo = ctx.getBean("bean",Demo.class);
System.out.println(demo);
}
}
1.3 Java Bean
为了规范Java类的定义,Java提供Java Bean规范,所谓的Java Bean规范就是约定的Java类的定义规则,使程序规范一致,方便使用和交流。其约定如下:
- 需要定义包package
- 有无参数构造器
- 需要实现序列化接口
- 包含使用 getxxx setxxx 声明的“Bean属性”xxx
a.Bean属性(Bean Property)就是指setxxx getxxx方法
b.对象属性(Object Field)是指对象的实例变量
Spring 与JavaBean
- Spring建议被其管理的对象符合JavaBean规范,同时为了方便使用Spring支持对不同标准的Java对象进行管理
- 因为Spring管理的对象是JavaBean对象,所以也称Spring为JavaBean容器,Bean容器,Spring容器,IOC容器。
2 利用JUnit测试
2.1 使用JUnit
利用https://mvnrepository.com/搜索junit,获得junit坐标
<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>cn.tedu</groupId>
<artifactId>spring1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- 设置JDK版本 -->
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<!-- test表明其只在测试阶段生效 -->
<scope>test</scope>
</dependency>
</dependencies>
</project>
在测试文件夹src/java/test编写测试案例:
package spring1;
import org.junit.Test;
public class TestCase {
@Test //表示这是一个测试方法
public void testHello() {
System.out.println("Hello Word!");
}
}
运行测试
2.2 利用JUnit测试Spring
JUnit提供了@Before和@After注解,这俩个注解声明的方法可以在测试案例方法之前和之后执行
测试Spring时候需要Spring容器的代码冗长麻烦,利用JUnit的@Before和@After注解声明方法,封装Spring的初始化和销毁方法,可以一劳永逸的结局Spring的初始化问题
案例:
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import cn.tedu.context.Config;
import cn.tedu.demo.Demo;
public class TestCase {
AnnotationConfigApplicationContext ctx;
@Before
public void init() {
ctx = new AnnotationConfigApplicationContext(Config.class);
System.out.println("初始化spring已执行");
}
@After
public void destroy() {
ctx.close();
System.out.println("spring已销毁");
}
@Test
public void testBean() {
Demo demo = ctx.getBean("bean",Demo.class);
System.out.println(demo);
}
@Test //表示这是一个测试方法
public void testHello() {
System.out.println("Hello Word!");
}
}
3 组件扫描
3.1 Spring组件扫描功能
Spring提供组件扫描注解,利用组件扫描和组件注解配合,可以自动扫描包空间自动创建Bean对象,减少编码,提高编程效率。
使用步骤:
- 使用组件@Conponet注解声明类
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.stereotype.Component;
@Component //Spring在组件扫描时会找到标注@Component的类,将类创建为对象,并且其Bean ID为"demoBean"
public class DemoBean implements Serializable{//implements Serializable 实现其序列化接口,官方建议这么做
@Override
public String toString() {
// TODO Auto-generated method stub
return "DemoBean";
}
}
- 在配置类上使用注解扫描功能
package cn.tedu.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import cn.tedu.demo.Demo;
@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
public class Config {
@Bean //告诉spring在启动时加载被bean标注的方法
/*
* 告诉spring在启动时加载被bean标注的方法
* 返回值是Spring创建的对象,方法名是Bean的ID
*/
public Demo bean() {
return new Demo();
}
}
- 在测试类TestCase中测试
@Test
public void testDemoBean() {
/*
* 测试组件扫描
*/
DemoBean bean = ctx.getBean("demoBean",DemoBean.class);
System.out.println(bean);
}
3.2 Spring提供多种组件注解
Spring提供了多种组件注解,用于程序中不同用途的组件标准,这些注解名字不同,但其作用相同,包括:
- @Controller,控制器组件
- @Service,业务层组件
- @Component,通用组件
测试:
编写被扫描的组件;
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.stereotype.Controller;
@Controller
public class DemoController implements Serializable{
@Override
public String toString() {
return "DemoController";
}
}
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.stereotype.Service;
@Service
public class DemoService implements Serializable{
@Override
public String toString() {
return "DemoService";
}
}
编写测试案例:
@Test
public void testDemoController() {
DemoController controller = ctx.getBean("demoController",DemoController.class);
System.out.println(controller);
}
@Test
public void testDemoService() {
DemoService service = ctx.getBean("demoService",DemoService.class);
System.out.println(service);
}
3.3 自定义组件ID
在使用组件扫描功能的时候@Component注解可以添加属性,自定义组件的ID,比如:
package cn.tedu.demo;
import java.io.Serializable;
import org.springframework.stereotype.Controller;
@Controller("a")
public class DemoController implements Serializable{
@Override
public String toString() {
return "DemoController";
}
}
定义了自定义组件的ID以后,基于可以使用新的ID管理组件了,原有默认组件ID就不再有效了
@Test
public void testDemoController() {
DemoController controller = ctx.getBean("a",DemoController.class);
System.out.println(controller);
}
开发中很少这么设定Bean ID ,尽量采用默认规则,这样有助于减少代码,减少错误。
4 Spring Bean管理
Spring为了适合各种应用软件,提供了丰富的对象管理策略,我们可以根据软件中的实际业务场景选择适当的策略。
4.1 Bean的作用域(Scope)
应用软件中的对象有着不用的生存范围,比如飞机大战的“小飞机”和“天空”。
先说一下“小飞机”,小飞机是比较“短命”的对象,出场后往往很快被子弹击中销毁或者飞出屏幕被删除,这种现象被称为“非单例”对象,其作用范围非常短。
再说说“天空”对象,其生存周期与整个程序相同,启动程序就创建了“天空”对象,在程序关闭时候才与程序一起销毁,其作用范围非常长,这种一个对象始终唯一的现象叫作“单例”。
Spring针对对象的不用作用范围设计了Bean的作用域,Spring的Bean常用的作用域用俩个singieton(单例),prototype(原型):
- singieton(单例):是指在一个应用软件运行期间某个类型的对象始终只有一个对象的现象。比如飞机大战中的“天空”对象就是单例的。Spring中当一个对象作用域是单例时候,任何时候从Spring IOC容器中获得的对象都是同一个。
- prototype(原型):是指应用软件执行期间有多个同类型的对象存在。比如飞机大战中的“小飞机”对象,就是多个实例。当Spring中一个对象作用域是原型的时候,每次从Spring IOC容器获得的对象都是新对象。
Spring中默认情况下Bean组件作用域是单例,也就是说任何时候调用getBean方法获得的对象都是同一个实例。这么设计优点是一个Bean可以被复用,提高了内存资源利用率,但是也有问题:每个对象状态都不同的Bean就不合适了,比如飞机大战中的小飞机,每个飞机都需要记住自己的位置信息,显然不合适。
案例:
@Test
public void testSingleton() {
/*
* 默认情况下,Spring单例管理对象,任何时候得到的Bean都是同一个对象
*/
DemoBean bean1 = ctx.getBean("demoBean",DemoBean.class);
DemoBean bean2 = ctx.getBean("demoBean",DemoBean.class);
System.out.println(bean1 == bean2);
}
运行结果:
初始化spring已执行
true
spring已销毁
在设置了@Scope(“prototype”)注解以后,Bean的作用域是“原型”,此时是每次调用getBean都会创建一个对象实例。
案例:
package cn.tedu.demo;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")//声明范围:原型。SPringle会创建多个实例,每次使用bean都是同一个对象
public class ExampleBean {
@Override
public String toString() {
return "ExampleBean";
}
}
测试案例:
@Test
public void testPrototype() {
/*
* 测试:原型
* 每次都Spring获取的对象都是一个新的实例
*/
ExampleBean bean1 = ctx.getBean("exampleBean",ExampleBean.class);
ExampleBean bean2 = ctx.getBean("exampleBean",ExampleBean.class);
System.out.println(bean1 == bean2);
}
运行结果:
初始化spring已执行
false
spring已销毁
4.2 对象生命周期管理
软件中的对象都存在从创建到使用最后销毁的过程,这个过程我们称为对象的生命周期(Life cycle)。主动控制时候,我们可以主动的创建对象,使用对象,销毁对象,也就是主动控制对象的生命周期,此时就可以主动的在对象创建时候执行初始化,在销毁对象时候关闭资源,比如:写文件之前需要先将文件打开,然后写文件,最后需要将文件关闭。
当使用Spring IOC后,也就是Spring控制管理对象时候,Spring IOC提供了对象生命周期管理功能,你可以利用生命周期管理注解,标注到方法上,Spring就会在创建对象,销毁对象时候执行相应的方法。这样就可以让Spring 帮助管理对象的生命周期,充分利用IOC容器提供的生命对象周期管理功能。
Spring提供俩种管理对象生命周期的方式:
- 在使用@Component注解管理Bean组件时候,使用PostConstruct和@PreDestroy注解管理对象的声明周期方法
a.注意这俩个注解不是Spring注解,是Java提供的拓展注解,需要导入javax.annotation包,利用Maven导入即可。
b.@PostConstruct注解标注的方法在创建对象以后执行
c.@PreDestroy注解标注的方法在关闭容器关闭对象时候执行 - 在使用@Bean注解管理Bean组件时候,在其InlMethod和destroyMethod属性上标注生命周期管理方法名。
a.InlMethod属性引用的方法在创建对象以后执行
b.destroyMethod属性引用的方法在容器关闭时候执行 - @Scope(“prototype”)时候,每次使用都会创建一个对象,Spring为了避免内存泄漏,不会缓存对象的引用,所以Spring关闭时候就无法处理对象的销毁方法了!
测试案例1:@PostConstruct@PreDestroy
- 利用Maven导入javax.annotation注解
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
- 声明Bean组件类型
package cn.tedu.demo;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component
public class FileLogger {
public PrintWriter out;
@PostConstruct
public void open() throws IOException {
out = new PrintWriter("Demo.txt");
System.out.println("打开文件Demo.txt");
}
@PreDestroy
public void close() {
out.close();
System.out.println("关闭了Demo.txt");
}
}
- 测试案例
@Test
public void testFileLogger() {
/*
* 测试spring对象声明周期管理功能
*/
FileLogger logger = ctx.getBean("fileLogger",FileLogger.class);
logger.out.print("Hi");
logger.out.print("Test!");
}
测试结果:
打开文件Demo.txt
初始化spring已执行
关闭了Demo.txt
spring已销毁
注意:关闭文件Demo.txt是在关闭容器时候输出的,如果没有关闭容器,就没有这个输出
刷新项目文件目录后可以在测试文件Demo.txt中找到输出结果:Hi Test! 如果没有关闭容器,就造成没有执行关闭方法,文件中就可能没有信息!
测试案例2:
- 编程被测试的类型:
package cn.tedu.demo;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
public class DemoLogger {
public PrintWriter out;
public void open() throws IOException {
out = new PrintWriter("logger.txt");
System.out.println("打开文件logger.txt");
}
public void close() {
out.close();
System.out.println("关闭文件logger.txt");
}
}
- 在Config类中编写配置信息
/*
* 测试Spring提供对象声明周期管理功能
*/
@Bean(initMethod = "open",destroyMethod = "close")
public DemoLogger demoLogger() {
return new DemoLogger();
}
- 编写测试案例:
@Test
public void TestDemoLogger() {
/*
* 测试@Bean提供的生命周期管理功能
*/
DemoLogger logger = ctx.getBean("demoLogger",DemoLogger.class);
logger.out.print("say");
logger.out.print("hello");
}
测试结果:
打开文件Demo.txt
初始化spring已执行
打开文件logger.txt
关闭文件logger.txt
关闭了Demo.txt
spring已销毁
注意:刷新项目文件目录后可以在测试文件DemoLogger.txt中找到输出结果:Hi Test! 显然在控制台上出现了上一个案例中的“打开文件Demo.txt”信息,原因是Spring采用“立即初始化”方式创建了对象,立即初始化和懒惰初始化是下个小节讲述的内容。
测试案例:测试Scope(“prptotype”)时候destroyMethod不生效现象
重构Config
/*
* 测试Spring提供对象声明周期管理功能
*/
@Bean(initMethod = "open",destroyMethod = "close")
@Scope("prototype")
public DemoLogger demoLogger() {
return new DemoLogger();
}
重新执行测试案例:
@Test
public void TestDemoLogger() {
/*
* 测试@Bean提供的生命周期管理功能
*/
DemoLogger logger = ctx.getBean("demoLogger",DemoLogger.class);
logger.out.print("say");
logger.out.print("hello");
}
此时,已经没有关闭了logger.txt动作了,我们需要自己手动关闭
@Test
public void TestDemoLogger() {
/*
* 测试@Bean提供的生命周期管理功能
*/
DemoLogger logger = ctx.getBean("demoLogger",DemoLogger.class);
logger.out.print("say");
logger.out.print("hello");
logger.close();
}
4.3 懒惰初始化
为了尽可能提高性能,Spring默认情况下在启动时候将Bean立即实例化,这样在使用Bean对象就可以立即使用,减少了时间延迟。立即实例化也有缺点,如果某个Bean很少使用,甚至可能不使用的情况下,立即实例化为对象占用了宝贵的内存资源。比如上面案例中,没有使用FileLogger对象,但是却被创建了出来,照成了资源浪费。
为了解决这个问情况,Spring提供了“懒惰初始化”功能,开启这功能,可以在需要使用对象时候初始化对象,如果不使用对象就不初始化对象,这样就可以充分利用内存资源,避免浪费。对应使用率很少的Bean组件应用采用懒惰方式初始化。懒惰初始化第一次使用对象时候需要初始化,所以第一次使用对象时候会有延迟。对于性能要求高,肯定被使用的对象,不要采用懒惰初始化。
Spring中利用@Lazy注解开启懒惰初始化,开启懒惰初始化后如果不使用Bean组件,将不会初始化对象。
- @Lazy可以与@Component 一起使用
- @Lazy可以与@Bean 一起使用
案例1:
- 定义Bean组件类型
package cn.tedu.demo;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@Component
@Lazy
public class Person {
public Person() {
System.out.println("创建person");
}
@Override
public String toString() {
return "Tom";
}
}
- 测试
@Test
public void testPerson() {
/*
* 测试懒惰初始化
*/
//Person person = ctx.getBean("person",Person.class);
//System.out.println(person);
System.out.println("OK");
}
只要不使用Person就不会自动创建
案例2:
- 创建测试Bean的类型
package cn.tedu.demo;
public class Student {
public Student() {
System.out.println("创建了student");
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "Andy";
}
}
- 更新Config.java,创建Bean 组件
@Bean
@Lazy
public Student student() {
return new Student();
}
- 测试
@Test
public void testStudent() {
//Student student = ctx.getBean("student",Student.class);
//System.out.println(student);
System.out.println("ok");
}
测试时候如果从Spring中获取student对象,则会初始化,如果不获取就不再初始化了。
4.4 @Import导入配置
Spring提供了多配置类功能,利用@Import注解可以同时使用多个配置类。
这个功能好处多多,分模块开发,分层开发多会用到这个功能,比如分模块开发,如果员工甲和员工乙都使用同一个Config.java作为配置,则会出现这样的场景,员工甲正在修改配置,员工乙也同时进行了更新,这样情况下就需要有个人负责解决冲突负责合并并配置文件,这种合并操作是必照成开发效率的下降,问题多多。
如果俩个分别写自己的配置文件,利用@Import 导入到主配置中,使配置文件一起起作用,这样每个人只修改维护自己的模块配置,不会发生冲突问题,好处多多。
案例:
- 定义一个配置类SysConfig,其中配置了一个Date类型的Bean组件,ID为myDate
package cn.tedu.sys;
/*
* sys模块配置信息
*/
import java.util.Date;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SysConfig {
@Bean
public Date myDate() {
return new Date();
}
}
- 在主配置类中利用@Import 引入SysConfig
package cn.tedu.context;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import cn.tedu.demo.Demo;
import cn.tedu.demo.DemoLogger;
import cn.tedu.demo.Student;
@Configuration
@ComponentScan(basePackages = "cn.tedu.demo")
@Import({cn.tedu.sys.SysConfig.class}) //表示俩个配置文件合二为一
public class Config {
@Bean //告诉spring在启动时加载被bean标注的方法
/*
* 告诉spring在启动时加载被bean标注的方法
* 返回值是Spring创建的对象,方法名是Bean的ID
*/
public Demo bean() {
return new Demo();
}
/*
* 测试Spring提供对象声明周期管理功能
*/
@Bean(initMethod = "open",destroyMethod = "close")
@Scope("prototype")
public DemoLogger demoLogger() {
return new DemoLogger();
}
@Bean
@Lazy
public Student student() {
return new Student();
}
}
- 测试案例
@Test
public void testMyDate() {
Date date = ctx.getBean("myDate",Date.class);
System.out.println(date);
}