Java世界的基石--Spring
Spring
中有3
个核心的概念:控制反转(IoC:Inversion of Controller
),依赖注入(DI:Dependecy Injection
),面向切面编程(AOP:Aspect Oriented Programming
)
Spring
的主要作用就是降低代码间耦合度,让对象和对象之间的关系不再是使用代码关联,而是通过配置,配置大于一切,开发者只需要将对象生成说明书写好,就不需要再手动的创建对象,统一让Spring
容器去统一管理,自动注入依赖。而AOP
使系统服务得到了最大的复用,开发者不需要再将这些和业务本无关的服务混入到业务逻辑代码中,Spring
会进行代码组织。
控制反转,依赖注入
public class Car{
public void run(){
System.out.println("car is running");
}
}
public class Person{
private Car car;
public Person(Car car){
this.car = car;
}
public void say(){
System.out.println("person start off!");
car.run();
}
}
上面这段例子中的依赖关系:Person
的say
函数中需要调用Car
对象中的run
方法,此时Person
依赖了Car
,Person
和Car
是依赖关系。
-
代码存在的问题
- 由于
Person
依赖了Car
,所以在创建Person
对象前需要先new Car()
,然后再new Person(car)
,代码本身没有问题,但当很多地方都用到依赖对象,并且后续需要新增依赖等操作,代码和工作量都会相对大,所有对象的创建都是由开发者去控制。
- 由于
一个新思路,找一个第三方托管这些对象,类似美团跑腿,把需要的沐浴露洗发水,牙膏鸡肉卷列一份清单出来,像牙膏鸡肉卷依赖了牙膏,就把牙膏也给买来,买完后放到一个桶里,可以称之为容器,当需要时,可以从这个桶里找需要的对象,创建和组装的过程就不用再手动管理了,Spring
就实现了这个功能,Spring
容器 控制反转 的概念就是如此。
使用Spring
先说说Bean的概念
所有由Spring
管理的对象统一称为Bean
,本质上就是普通的对象,和直接new XXX
出来的没区别,只不过由Spring
容器所管理着,需要通过清单告诉Spring
容器需要创建哪些bean
,这些配置称为bean
定义配置的元数据信息,Spring
容器读取这些信息就知道怎么玩了
新建一个Maven项目
在pom
文件添加Spring
依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
刷新maven
后,会自动下载Spring
的核心包
动手体验:先写一个对象,这里类名为Person
,配置里需要对应
package com.demo;
public class Person {
private String name;
private int age;
public Person() {
System.out.println("Person constructor");
}
}
在src/main/resources
目录下新建一个bean.xml
(叫啥无所谓,自己喜欢),这个beans
标签格式不需要背,用idea
新建xml
时有spring
配置的模板选项,或者用的时候cv
即可,在子标签添加一个<bean>
标签,id
为bean
的名字,后续从容器取出时需要用到,class
为对应的全限定类名
可以了解的XML配置知识
beans
是根元素,下面可以包含任意数量的import
,bean
,alias
标签
这里列出的属性只需要记id
和class
,其他的用的不多了解即可
<bean id="bean唯一标识,有重复就会报错" name="bean的名称,也可以成为别名" class="完整类型名称" factory-bean="工厂bean名称" factory-method="工厂方法" />
id和name的规则
- 当
id
存在的时候,不管name
有没有,取id为bean
的名称,name就是别名,起同等效果共存 - 当
id
不存在,此时要看name
有没有,name
的值可以通过逗号和分号还有空格分隔,会按照分割规则得到一个数组,数组的第一个元素作为bean
的名称,其他的作为bean
的别名 - 当
id
和name
都存在,id
为bean
名称,name
为别名 - 都不存在时,
bean
名称会自动生成,格式为全限定类名#序号
,别名则为全限定类名
,序号从0
开始,同类型则递增,且只有第一个bean
拥有别名 context.getBeanDefinitionNames
方法可以获取所有bean
的名称数组context.getAliases(beanDefinitionName)
传入bean
的名称可以获得该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">
<bean id="person" class="com.demo.Person"/>
</beans>
新建一个main
方法,通过Spring
提供的ClassPathXmlApplicationContext
类创建容器,容器提供了一个getBean
方法获取bean
的实例,传入的字符串就是xml
中bean
标签的id
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Application {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("Application.xml");
Person person = (Person) context.getBean("person");
System.out.println(person);
}
}
查看控制台,成功获取了Person
对象并输出
这便是Spring
的IOC
体验,开发者不用再去写new XXX
对象,要什么问Spring
管家要即可。Spring
会读取XML
配置,通过反射
去创建对象,感兴趣可以看看这篇文章,模拟了一个最基本的反射容器管理: Spring--手写一个简易的IoC容器,附思路原理
再看看依赖注入
注入分为简单类型和引用类型注入,在xml
配置中
-
简单类型(包含
String
)使用value
属性,引用类型使用ref
属性 -
注入又分为
setter
注入和构造函数
注入- 使用
setter
注入,必须提供无参构造方法
和对应的setXXX
方法,可以理解为先新建一个空对象再进行属性的setter
注入 - 使用
构造函数
注入,又分为属性名
和构造方法参数下标
,使用下标注入时需要和构造方法的参数类型一一对应,不推荐使用下标方式【应该说不推荐XML
形式:),后面会介绍注解形式】
- 使用
再体验下依赖注入功能
- 新建
Cat
类,只需要有空参构造 Person
类添加一个String
类型的name
属性,一个Integer
类型的age
属性,一个Cat
类型的cat
属性。再为Person
类添加全属性的getter
和setter
方法,以及全参
和无参构造方法
下面演示了3
种实现,先修改xml
文件配置
<!--通过属性名配置注入,位置随意-->
<bean id="person" class="com.demo.Person">
<property name="name" value="jack"/>
<property name="age" value="10"/>
<property name="cat" ref="cat"/>
</bean>
<!--通过构造方法的参数名注入,位置随意-->
<bean id="person2" class="com.demo.Person">
<constructor-arg name="name" value="mike"/>
<constructor-arg name="age" value="20"/>
<constructor-arg name="cat" ref="cat"/>
</bean>
<!--通过构造方法参数的下标注入(位置要讲究一对一匹配类型)-->
<bean id="person3" class="com.demo.Person">
<constructor-arg index="0" value="jay"/>
<constructor-arg index="1" value="30"/>
<constructor-arg index="2" ref="cat"/>
</bean>
用bean
的name
从容器中分别取出3
个同类型的bean
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("Application.xml");
Person person = (Person) context.getBean("person");
System.out.println(person.getName() + "," + person.getAge() + "," + person.getCat() + "\n");
Person person2 = (Person) context.getBean("person2");
System.out.println(person2.getName() + "," + person2.getAge() + "," + person2.getCat() + "\n");
Person person3 = (Person) context.getBean("person3");
System.out.println(person3.getName() + "," + person3.getAge() + "," + person3.getCat() + "\n");
}
xml
中配置的值和预期一样被注入到对象中,可以看到3
个不同的Person
对象,里面的Cat
对象都是同一个实例,说明Spring
的bean
默认是单例模式(可以配置其他模式)
bean作用域scope
有些bean
想要多例,有些想要单例,spring
提供了作用域的配置,bean
标签配置scope
属性
singleton
,不填写默认就是singleton
,整个容器中,该bean
只会创建一次,每次需要用时,都返回同一个对象(getBean
或者依赖注入
时),默认是在启动spring
容器时生成,bean
被设置lazy-init
属性为true
时,则需要被用到再生成。单例bean
是整个容器共享的,如果设计到修改更新操作有线程安全的问题,需要注意使用。prototype
,scope
设置为prototype
,则表示该bean
是多例的,每次获取时都会生成一个新的实例对象,如果对象的创建比较繁琐消耗时间,则会有性能问题。request
,每个http
请求都会创建一个bean
实例(3,4,5需要结合Spring web
)session
,session
级别共享,不同session
则创建不同的实例application
全局应用共享
bean
标签中的引用类型自动注入,还可以使用标签的属性autowire
- 设置为
byType
相当于注解的Autowired
(后面会说) - 设置为
byName
相当于注解的Autowired+Qualifier
,属性名
要与bean名
一致
xml
中的成员变量的property
标签就不用写了
<bean id="person" class="com.demo.Person" autowire="byName">
<property name="name" value="jack"/>
<property name="age" value="10"/>
<!--autowire设置里byName,这玩意就不用写了 <property name="cat" ref="cat"/>-->
</bean>
import导入
当项目越来越大,所有配置都在一个xml
中则难以管理,可以按照业务层去拆分,如按照经典的三层架构,controller
,service
,dao
分别新建一个文件夹,文件夹里专门放入对应模块的xml
配置,再用一个主入口xml
来导入配置
关于XML
的配置就说到这里,其实还有很多内容,但目前基于Springboot
(一个让开发更简单的集成方案)用注解为主流方案,所有对xml
有兴趣的可自行摸索学习,即使不用也应该简单了解,毕竟是曾经的方案
注解Annotation声明Bean
@Configuration和@Bean
@Configuration
这个注解用在类上,可以理解为beans
标签,要配合@Bean
注解使用@Bean
可以理解为bean
标签,用于声明方法是一个bean
@Configuration
public class BaseConfig {
@Bean
public String testAn(){
return "testAn";
}
}
修改启动类,ClassPathXmlApplicationContext
要改为AnnotationConfigApplicationContext
,参数为配置类的Class
对象
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BaseConfig.class);
String testAn = (String) context.getBean("testAn");
System.out.println(testAn);
运行后,Spring
会取扫描类中@Configuration
和@Bean
注解,凡是带有这两个注解的类和方法都会被注册成bean
,类的bean
名称会使用小驼峰形式:BaseConfig
转成baseConfig
,示例获取了配置的testAn
并输出
知识补充环节
- 即使类不添加
@Configuration
注解,类中带有@Bean
的方法也会被注册成bean
,如果获取对应的Configuration
配置bean
并直接输出,可以看到会带有cglib
字眼com.demo.BaseConfig$$EnhancerBySpringCGLIB$$75472aeb@429bd883
,说明这个类是被代理生成的,所有被@Bean
修饰的方法都会被拦截动态增强 - 如果类不加
@Configuration
注解,容器也会管理bean
,但这些bean
的类没有被代理,所以每次都会获取一个新的bean
对象,非单例模式
为了方便注册Bean
,还提供了其他注解
@Controller
专门用来创建控制器的对象(Servlet
),这种对象可以接受用户的请求,可以返回处理结果给客户端@Service
专门用来创建业务逻辑层的对象,负责访问数据,处理完后返回给界面层@Repository
专门用来创建数据访问层的对象@Component
可以创建任意对象,创建对象的默认名称是驼峰的命名法,也可以指定名称 这些类都是一个作用,只是带有语义
@ComponentScan和@ComponetnScans
使用new AnnotationConfigApplicationContext(BaseConfig.class)
容器只会去注册BaseConfig
配置类和其成员bean
,但一个项目显然会有很多地方需要注册bean
,并且通常项目会进行分层,使用@ComponentScan
可用于指定Spring
容器需要扫描哪些包和哪些类所在的包中带有特定注解的类
新建一个dao
包,并新建一个UserDao
添加上@Repository
(上面说的四种随意使用,不重要)
此时需要做指定扫描行为,才能让Spring
加载UserDao
作为容器成员bean
,演示三种
使用方式
// 1. 不给注解任何属性值,则会默认扫描当前类所在包下的所有子类和子包
@Configuration
@ComponentScan
public class BaseConfig {
}
// 2. 指定包名,会去扫描com.demo.dao包下的所有子类,如果要扫描多个包子传入一个包名字符串数组
@Configuration
@ComponentScan("com.demo.dao")
public class BaseConfig {
}
// 3. 指定类对象,会扫描对应类所在包下的所有子类,扫描多个类则传入类对象数组
@Configuration
@ComponentScan(basePackageClasses = UserDao.class)
public class BaseConfig {
}
目前来说以上3
种效果等价
@ComponentScans
注解的使用为传入@ComponentScan
注解的数组
注解也可以和
XML
配置文件结合使用,需要在XML
中配置如下标签,base-package
为需要扫描的包路径,可配置多个指定标签扫描多个包
<context:component-scan base-package="com.demo"/>
@Import
作用类似于XML
中的import
标签,该注解常用于将一个第三方类注册为bean
,因为第三方类库里没有自带Spring
的注解,当然也可以选择声明bean
,但这样需要多写一个方法,import
能更好的完成这项任务,bean
的名称默认是类的全限定类名(类名不会转小驼峰)
@Import(UserService.class)
public class BaseConfig {
}
注解接收一个类对象数组,传入的类都会被注册成为bean
,切记如果需要获取bean
则要使用全限定类名,如getBean("com.demo.service.UserService")
依赖注入: @Value,@Autowired,@Resource,setter方法,构造方法
假定一个类中,有两个成员属性,一个为String
类型(或者是基本数据类型),一个为引用类型,
@Value
可以在属性声明注入简单类型的值,包括String
,此时无需编写setter
方法,如有setter
添加到setter
也可以,支持从配置文件读取(配合@PropertySource
)@Autowired
可以自动注入引用类型,前提是必须为当前容器中被注册的bean
对象,默认情况下会根据bean
的类型去寻找bean
注入,匹配到多个类型时则会按照name
注入,有个required
属性默认为true
,匹配失败则会抛出异常
最基本字段注入
@Repository
public class UserDao {
@Value("testName")
private String daoName;
@Autowired
private Cat daoCat;
}
使用setter
方法注入
@Repository
public class UserDao {
private String daoName;
private Cat daoCat;
@Value("testName")
public void setDaoName(String daoName) {
this.daoName = daoName;
}
@Autowired
public void setDaoCat(Cat daoCat) {
this.daoCat = daoCat;
}
}
构造器注入(目前Spring
推荐的写法),由于当前没有注册过类型为String
类型的bean
,所以使用@Value
注入想要的值,如果有String
类型的bean
则可不写会自动注入。普通方法
或者方法参数
使用@Autowired
也可以实现注入,但一般不这么做,因为不好管理依赖。
@Repository
public class UserDao {
private String daoName;
private Cat daoCat;
// @Autowired // 写和不写的效果等价
public UserDao(@Value("test") String daoName, Cat daoCat) {
this.daoName = daoName;
this.daoCat = daoCat;
}
@Value和@PropertySource 注入值从配置文件中读取
在resource
目录下新建一个config.properties
文件,写入一行jdbc.name=test
,语法格式为@Value("${配置文件中的key:默认值}")
,@PropertySource
接受配置文件路径的数组
@Repository
@PropertySource("classpath:config.properties")
public class UserDao {
@Value("${jdbc.name:默认值}")
public String daoName;
}
// 当key不存在时,字段的值就是配置的默认值
实际开发中,可以用一个配置类加载需要从配置文件中读取的数据,并将配置类注册为bean
,在其他需要使用的地方注入配置类即可方便使用配置文件
不太常用的Collection
和Map
注入,需要使用接口,了解即可
- 使用
Collection
(List
是Collection
的子接口),会将所有符合泛型类型的bean
收集到list
中 - 使用
Map
时通常为key/value
,会将所有符合泛型类型的bean
的name
作为key
,实例对象作为value
// 假定有一个UserDao的接口,以及若干个实现类
@Autowired
private List<UserDao> interfaceList;
// 会将所有UserDao的实现类收集
@Autowired
private Map<String,UserDao> interfaceMap;
// 会将所有UserDao的实现类的name作为key,实例对象作为value存入map中
@Resource
Spring
还支持了Java
的原生注解@Resource
,效果和@Autowired
注解类型相同,但仍有细微差别,使用注意事项
- 当使用
@Resource
注解不传属性时,默认会按照当前注入字段的name
去寻找bean
@Resource
private UserService userService;
// 此时会去找名为userService的bean进行注入
- 传入了
name
属性,则会去寻找容器中同名bean
进行注入@Resource(name = "userService")
- 传入了
type
属性,则会按照类型去寻找bean
进行注入@Resource(name = "userService", type = UserService.class)
- 既传了
name
又传了type
,则必须二者都符合规则的bean
才会被注入 @Resource和@Autowired对比 - 默认情况,
@Resource
使用name
寻找bean
装配,@Autowired
使用type
寻找bean
装配 @Resource
传入type
属性支持按照类型注入,@Autowired
配合@Qualifier
可以指定name
注入
// 指定了bean的名称,而不是使用类型装配
@Autowired
@Qualifier("userService")
private UserService userService;
@Autowired
注解支持设置没有找到可注入bean
时跳过注入,不会报错,而@Resource
不支持
// 如果没有对应的bean,此时为null而不会报错
@Autowired(required = false)
private UserService userService;
- 都可以用在字段和
setter
方法上 @Resource
的使用可以减少和Spring
框架的耦合
同源类型的定义
- 被注入的类型和注入的类型是完全相同的类型
- 被注入的类型是父类型,注入的是子类型
- 被注入的类型是接口,注入的是实现类
当容器中存在多同源类型时,bean
的装配机制(以下均指使用类型注入方式)
- 当注入类型同时存在多个
bean
时,默认会按照注入的字段name
去匹配,如果匹配不到会抛出异常,此时需要指定name
去进行装配 - 当注入类型为父类型,父类
bean
和子类bean
同时存在时,默认会按照注入的字段name
去匹配,如果匹配不到会抛出异常,此时需要指定name
去进行装配 - 注入类型为接口,有诸多实现类
bean
实现了同一接口,由于匹配到了多个可注入类型,此时会按照字段name
去装配,此时需要指定name
去进行装配
@Primary
提高bean
的候选优先级,上面说过如果同类型bean
存在多个且没有特殊配置的情况下,会抛出异常,解决方案还可以使用@Primary
注解,相当于在匹配到多个可注入类型时,会优先注入带有@Primary
注解的bean
// 接口
public interface UserDao{}
// 实现类1
@Component
public class UserDao1 implements UserDao{}
// 实现类2
@Primary
@Component
public class UserDao2 implements UserDao{}
// 实现类3
@Component
public class UserDao3 implements UserDao{}
@Component
public class UserService{
@Autowired
private UserDao userDao;
}
// 由于匹配到了3个相同类型的bean,此时候选者列表中有3个bean对象
// spring会寻找是否有bean是带有@Primary注解的,有则注入bean
@Scope 指定bean的作用域
关于bean
的作用域在xml
的段落讲过了,这个@Scope
注解起同等作用,Spring
提供了一些常量支持
ConfigurableBeanFactory.SCOPE_PROTOTYPE
多例模式,每次使用都会生成新的实例bean
ConfigurableBeanFactory.SCOPE_SINGLETON
单例模式,默认行为,容器中只有一份实例org.springframework.web.context.WebApplicationContext.SCOPE_REQUEST
生成bean
的时机为request
级别org.springframework.web.context.WebApplicationContext.SCOPE_SESSION
生成bean
的时机为session
级别 每一次bean
被使用时都会new
一个新的实例,校验时调用多次context.getBean
,即可观察到输出了多次构造方法的内容
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Cat {
public Cat() {
System.out.println("Cat is created");
}
}
@DependsOn 指定依赖bean
效果等同于在xml
的bean
标签中配置depend-on
属性,作用为确保在当前bean
被创建前,依赖的bean
已经被创建好了,因为没有办法保证Spring
初始化bean
的顺序,这个注解则可以确保在创建当前bean
时,被依赖的bean
一定会被先创建好。注解接受字符串数组,字符串为bean
的name
(在idea
中可能会报红线,忽略即可)
@Component
@DependsOn("userDao")
public class UserService {
@Autowired
private UserDao userDao;
public UserService(){
System.out.println("UserService created");
}
}
使用了注解后,可以确保创建UserService
实例前,UserDao
会被先创建
@ImportResource导入bean的配置文件
可用于使用注解方式的场景下,通过此注解进行配置文件配置,沿用上述xml
讲解时编写的xml
文件
@Configuration
@ImportResource("classpath:Application.xml")
public class BaseConfig {
}
效果等同于将xml
文件中配置的bean
编写在配置类中,classpath:
表示检索当前目录下的classes
目录
@Lazy 延迟初始化
效果等于同在xml
的bean
标签中配置lazy-init
属性,可以让bean
的初始化延迟,意思就是只有用到时再去初始化实例
- 配合
@Componet
使用,则表示这个类延迟加载 - 配合
@Configuration
使用,表示这个配置类中,所有带@Bean
注解的bean
延迟加载 - 配合
@Bean
使用,表示当前被标注的bean
延迟加载 检验方式很简单,给bean
的构造方法添加一段log
输出,默认情况下容器会初始化实例,log
会输出,对比加了@Lazy
的bean
,没有被用到的bean
的构造方法并没有被执行
@Lazy
@Configuration
public class BaseConfig {
@Bean
public String test() {
return "test";
}
@Bean
@Lazy(false)
public String test2() {
return "test2";
}
}
如果这个配置类下的test bean
没有被使用,则不会实例化,在bean
上单独使用则优先级比配置类的高,test2 bean
没有被使用也会被创建
end :)