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多例模式,每次使用都会生成新的实例beanConfigurableBeanFactory.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 :)