Spring(万字详解版)

Posted bit_zhy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring(万字详解版)相关的知识,希望对你有一定的参考价值。

Spring

1.Spring是什么

Spring就是一个包含众多工具方法的IoC容器

1)什么是容器?

容器就是存储某种东西的基本装置,在Java的学习过程中,我们曾接触过很多容器,例如数据结构中的List,Map等是数据存储的容器,Tomcat就是web程序的容器

2)什么是IoC?

IoC(Inversion of Control)也就是控制反转的意思 那么也就是说spring是个控制反转的容器,但是这样说还是有点不好理解,我们可以理解为控制权转换了的对象的容器,那什么是控制转换,我们为什么要使用控制转换呢?举个例子,比方说我们要实现一个Car类,那么我们需要实现车身,车底盘,车轮胎,由上而下是一个层层递进,层层依赖的关系

按照上图的逻辑我们写出的代码是

class Car
    public void init()
        Body body = new Body();
        body.init();
    


class Body
    public void init()
        Bottom bottom = new Bottom();
        bottom.init();
    


class Bottom
    public void init()
        Tires tires = new Tires();
        tires.setSize(??);
    


class Tires
    private int size;
    public void setSize(int size)
        this.size = size;
    


public class traditionalMethod 
    public static void main(String[] args) 
        Car car = new Car();
        car.init();
    

当我们的业务需求是需要涉及不同尺寸的轮胎时,我们就需要层层调用直到底层的setSize,也就是我们需要在Bottom类中调用时需要传入??参数,但是由于我们每一个类都是在类自己内部new出自己的所需依赖,那么这个所需的依赖的控制权就相当于在我们手中,所有类中需要的参数都需要我们自己层层传输,我们在想要修改size时,就需要从顶层的car类中开始传入参数,一步一步的传到底层,这时我们需要修改涉及的代码就太多了,而且当代码的依赖逻辑更复杂时,或者需要给最底层的轮胎增删一些参数(例如增加颜色属性),我们需要更容易凌乱,出现这个问题的原因是上述代码耦合性太高了,我们不妨想,将new依赖对象这个操作交到外部,也就是将依赖对象的控制权交出去,当我们需要的时候直接拿对象来使用即可(也就是采用注入所需依赖对象的方法),那么这个时候就直接将依赖的对象当成了一个方法,当我们对所以来对象想要进行一些修改时,我们只需要在外部类创建时进行修改,然后在调用类中增删改参数即可,这时就完美的实现了程序的解耦合,用代码来实现就是

class Car2
    private Body2 body2;
    public Car2(Body2 body2)
        this.body2 = body2;
    
    public void run()
        body2.init();
    


class Body2
    private Bottom2 bottom2;
    public Body2(Bottom2 bottom2)
        this.bottom2 = bottom2;
    
    public void init()
        bottom2.init();
    


class Bottom2
    private Tires2 tires2;
    public Bottom2(Tires2 tires2)
        this.tires2 = tires2;
    
    public void init()
        tires2.init();
    


class Tires2
    private int size;
    public Tires2(int size)
        this.size = size;
    
    public void init()
        System.out.println("Size = " + size);;
    

public class IoCMethod 
    public static void main(String[] args) 
        Tires2 tires2 = new Tires2(20);
        Bottom2 bottom2 = new Bottom2(tires2);
        Body2 body2 = new Body2(bottom2);
        Car2 car2 = new Car2(body2);
        car2.run();
    

可以看到,在IoC方法中,我们将所有的依赖对象创建为上级类的属性,在构造方法中将在外边创建好的依赖对象当作参数传入,这时,如果我们仍然要修改最底层Tires的属性,我们只需要在Tires类中增删改参数,然后在main方法中将new Tires的参数增删改即可,剩下的几层代码是不需要改动的,因为我们将控制权发生了反转,这时new对象的顺序也是由下而上的,这样将依赖对象由自身new改为注入的方式,下级的控制权不是上级的了,那么无论下级如何改变上级都不需要改动,就很好的完成了解耦合

3)spring的主要作用

我们了解了容器和IoC之后,spring是什么就迎刃而解了,spring就是储存对象的容器,spring有着创建对象和销毁对象的权利,那么他的主要的作用就是:
1.可以将Bean(对象)存储到Spring容器当中
2.可以将Bean(对象)从Spring容器当中取出来

4)DI(Dependence Injection)依赖注入

所谓的“依赖注入”,就是指IoC容器在运行时将所需的依赖注入到类中,其实DI就是换了一个角度和IoC描述同样的一件事情

5)扩展:

a.IoC和DI有什么区别?

IoC是一种思想,DI是基于IoC这个思想的具体实现,我们代码耦合性高的解决思想是IoC,通过DI对象注入来事项IoC,类似于乐观锁和CAS的关系,乐观锁是一种思想,CAS是乐观锁的具体实现

b.spring是什么?spring的作用是什么?

spring就是包含有众多工具和方法的IoC容器,其主要作用是存储Bean和从其中取出Bean

2.Spring的创建和使用

1)spring的创建

1)先创建一个maven项目
2)添加spring框架支持(spring-context/spring-beans)
3)创建一个启动类并添加main,作为测试类看是否引入正常

2)将Bean存储(注册)到spring中(3步/2步)

如果是第一次添加对象,则需要先创建spring的配置文件(一般命名为spring-config.xml,在resources目录下),在spring-config.xml文件中创建bean标签,设置标签的id属性(默认命名格式为小驼峰)和class属性,id属性就是我们将对象注册入spring后在spring容器中的名字,class属性就是我们要注入的对象的路径,最终spring会以Map<String[beanName],Object> 来存储数据 也就是键值对的形式 id对应的是key,class对应的是value

3)使用容器中的Bean(对象)

1.得到spring的上下文对象

我们使用ApplicationContext类来取到spring的上下文对象,实例化其子类的ClassPathXmlApplicationContext,传入参数为刚刚创建的配置文件名(推荐是spring-config.xml),这样我们的进程就会扫描配置文件以及其中的bean标签,将bean注入到容器中

在这里我们也可以使用另一个类BeanFactory来获取上下文对象,这里需要new的是其子类xmlbeanfactory 同时xml对象中需要一个类资源文件ClassPathrResource其需要的参数也是配置文件名

2.获取spring中的bean

我们通过上边创建的(两个皆可)上下文对象提供的getBean()方法来获取到容器中的bean,传入的参数是注册bean时传入的id属性内容(beanName)
,需要注意的是getBean返回的对象类型是Object类,我们需要将其强转为bean本身的类型,但是这种方法有一个缺点,因为我们是直接传入字符串进去,那么如果我们所传入的beanName写错了等等,就会返回空的对象,我们在进行强制类型转换时就会报空指针异常
我们也可以通过getBean的重载方法来取到bean

第二个是传入对象的类型(类对象)

但是这种方法有一个缺点,就是如果我们将User这个类的实例化对象注册了两次的话,这种方法就会报错

Exception in thread “main” org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘beans.User’ available: expected single matching bean but found 2: user,user2
上述是编译器提示的异常信息,可以看到它提示我们user并不是single bean
那么我们就可以用第三种getBean,这一种将上述两种结合,是我们最推荐的方法

这种方法既可以防止第一种情况种出现的空指针异常(不涉及强转),也可以防止第二种方式非唯一的bean异常,因此我们一般推荐使用第三种

3.调用获取到的对象的方法

4.扩展

1)ApplicationContext 和 BeanFactory 的异同:

同:两种方法都可以获取到spring的上下文对象,也提供了getBean方法从而具体的获取到bean对象
异:
1.ApplicationContext 属于BeanFactory的子类,那么我们都知道,子类是继承了父类的所有非私有方法和属性的,但是父类并没有子类的属性方法,因此App类比BeanFact类要提供了更多的操作bean的方法,而不仅仅是简单的getBean,比如国际化的支持,资源访问的支持以及事件和传播等方面的支持
2.从性能方面二者并不相同 App类是在扫描spring-config时,一口气将所有的对象全部创建加载出来了的,属于饿汉模式,而BeanFact是按需加载bean的,下边代码中有使用bean的方法时,才会创建bean,我们以代码来观察

我们首先为User类加入了构造方法,当创建对象时就会提示


可以看到,我们都没有使用user对象,但是使用A方法直接会出现创建对象,B对象则什么都没有,那么我们加入user的方法再来看

可以发现,这时A,B类都调用了构造方法,也就是说B类更轻量

2)target文件夹


当我们修改了错误代码再次运行程序,可能源代码没有问题了,但是程序还是报错,我们这时不妨检查一下target文件夹下的内容,因为target文件夹下储存的是JVM真正运行的.class二进制文件,有时可能因为缓存问题,导致target目录下的.class文件没有及时更新,因此程序仍旧报错

3.使用Spring更简单的注册对象(使用注解)

前置工作:我们在使用更简单的方式时,需要在spring-config.xml(配置文件)中加入扫描对象的根路径
我们的base-package属性就是扫描bean对象的根路径,spring会自动扫描beans目录及其所有子目录,根据条件注入对象

1)使用五大类注解

我们的五大类注解有
1)@Controller【控制器】
2)@Service【服务】
3)@Repository【仓库】
4)@Configuration【配置】
5)@Component【组件】
我们在使用时只需要将注解加到要注册的目标类上边即可

加了五大类注解之后,spring在获取上下文对象时就会扫描刚刚设置的扫描根路径中的所有类,当类设置了五大类注解时,就会注册此类

1.五大类注解的区别和联系

区别:
在软件工程中,我们的代码分为四大层级
1.配置层(Configuration)
我们将程序运行所需的配置文件归属在该层,例如刚刚的spring-config.xml文件
2.控制层(Controller)
这一层负责检验前端所传过来的参数是否合法,像是安检一样,前端所传的参数会率先经过该层,如果参数有问题则会被返回并且提示异常
3.服务层(Service)
这一层负责数据组装和接口调用,例如我们需要使用哪些接口来处理前端传过来的参数,该返回怎样的数据给前端等
4.数据持久层(Repository/DAO)
这一层负责直接操作数据库,和数据库进行交互
仔细观察就会发现,我们的五大类注解和这几层的名字很相似,因此涉及五大类注解,每一类注解都对应了其中一层,第五大类@Component(组件)
则对应了一些不属于上述四大层级的其他类,作为程序的组件,我们在使用不同注解标记代码,就使得代码的可读性更强了,程序员可以直接判断当前类的作用
联系:

我们通过查看源码可以发现,前四个注解都是基于第五个注解实现的,也就是说前四个注解都是基于@Component,都是@Component的子类,所有的注解都属于程序的“组件”

2.spring使用五大类注解生成的beanName问题

在之前在spring-config.xml中添加bean标签实现对象注册时,我们的id属性对应了被注册对象的beanName,我们可以通过其访问到唯一的bean,但是使用五大类注解时我们没有显式的指定bean的beanName,那么我们如何访问非唯一的对象呢,我们这里可以查看spring的源码

查看该方法,发现“创建beanName方法(generateBeanName)”

可以发现其返回中调用了buildDefaultBeanName方法,查看该方法可以发现

其最后调用了decapitalize方法,那么这个方法就是最终答案了

可以发现,这个方法先执行了一个参数合法性的判断,之后第二个if中判断了传入的name长度是否大于一个字符,如果大于的话先查看这个name的第二个字符,看是否第二个字符是大写的,如果是,再检查name的第一个字符,如果第一个和第二个字符全是大写,那么返回的默认beanName就是这个类的name本身(传入的name),如果上述三个条件有一个不符合,那么就会将类名的首字母改为小写,作为beanName返回回去,因此我们上边才会说,beanName的默认命名方式为小驼峰,但是特殊情况就是前两个字符都是大写时beanName就是原类名

可以直接调用这个方法来查看结果,符合我们上述的分析

2)使用方法注解@Bean

@Bean方法只可以标记在类中的方法上,而不可以直接标记在类上,此注释表示将被标记方法返回的对象注册在spring当中,同时,使用方法注解时需要搭配一个五大类注解在方法所在的类上方,这样的目的是为了优化性能,因为我们的扫描路径下可能有的方法是非常多的,这时如果spring去扫描所有的方法看是否方法上查看了@Bean注解,这样效率会非常低,我们不妨为有方法注解的类添加五大类注解,达到缩小扫描范围的目的,优化性能,这个行为和添加扫描路径是一样的,不添加扫描路径的话扫描的就是整个java源代码目录,添加了扫描路径的话就只扫描目标路径及其子目录

根据分析我们可以得到一个name属性为zhangsan的user对象

可以发现确实如此
当然,我们想要访问@Bean方法注册的对象,也不一定非要使用方法名来访问,我们可以通过设置@Bean的属性来对beanName重命名(可以设置多个重命名,用包裹起来即可),但是当我们设置了这个属性后,我们便无法通过方法名的方式访问bean了

@Controller
public class setUser 
    @Bean(name = "user1","user2")
    public User getUser()
        User user = new User();
        user.name = "zhangsan";
        return user;
    
    


我们通过重命名的user1或者user2都可以获取到user对象了

4.使用Spring更简单的装配对象(对象注入)

顾名思义,就是使用spring更容易的将对象从spring中读取出来,之前我们要读取容器中的对象,需要先获取上下文对象,然后调用getBean方法才可以,现在我们只需要用注释的方法就可以轻松获取到了,我们可以用@Autowired注释和@Resource注释

1)属性注入(字段注入)

定义当前类的一个属性,属性类型就是要注入的对象类型,属性名采用小驼峰的方式,之后在该属性上添加@Autowired注释或者@Resource注释都可

@Controller
public class setUser2 
    @Autowired
    private User user;
    public void getName()
        System.out.println(user.name);
    


可以发现我们可以提取到setUser2中的user的name属性,注入成功

2)构造方法注入(官方推荐)

定义一个属性,属性类型是要注入的对象,对象名为小驼峰,定义构造方法,像构造方法传入参数(要注入的对象),将属性对象的引用设置为传入的对象,在构造方法上添加@Autowired注解,而这里不能使用@Resource注解

@Controller
public class setUser3 
    private User user;
    @Autowired
    public setUser3(User user)
        this.user = user;
    
    public void getName()
        System.out.println(user.name);
    


可以发现,我们仍然可以访问到向setUser3中注入的user对象的name属性
通过构造方法注入时,如果只写了一个构造方法,我们可以省略@Autowired注释,但是如果有多个构造方法,我们则不可以省略

3)Setter注入

定义私有属性,属性类型为要注入对象类型,属性名以小驼峰方式,通过设置私有属性的set方法来将对象注入到属性中,可以通过在set方法上添加@Autowired注解或@Resource注解

@Controller
public class setUser4 
    private User user;
    @Autowired
    public void setUser(User user)
        this.user = user;
    
    public void getName()
        System.out.println(user.name);
    

拓展

1.属性注入,构造方法注入和Setter注入的区别?(三种注入方式的区别)

1)属性注入的写法最为简单,但是只用于IoC容器中,如果程序运行在非IoC容器环境下,很容易注入失败,通用性不强,引发问题
2)setter注入是曾经spring最推荐的注入方式,但是不同语言的set方法可能不同,甚至可能没有set方法,因此这种方法的通用性并不高
3)构造方法注入的通用性是最强的,因为几乎所有的语言的构造方法格式都是相同的,同时,构造方法会在构造类是率先调用,那么采用构造方法注入,可以第一时间将所需依赖注入到当前类当中完成初始化,再使用当前类时不会引发空指针异常,但是使用构造方法注入需要程序员自己检查代码不要有过多的参数,必须要符合单一设计原则

2.@Autowired 和 @Resource 的区别(两种注入方法的区别)

1)出身不同:@Resource来自于JDK官方 @Autowired来自于Spring
2)用法不同:@Resource不支持构造方法注入,仅支持其他两种,@Autowired支持三种注入方式
3)支持的参数不同:@Resource支持更多的参数设置,例如name,type等,而@Autowired仅支持require参数

3.解决@Bean注入多个对象的问题

@Controller
public class setUser 
    @Bean(name = "user1")
    public User getUser()
        User user = new User();
        user.name = "zhangsan";
        return user;
    
    @Bean(name = "user2")
    public User getUser2()
        User user = new User();
        user.name = "wangwu";
        return user;
    

在上述代码中,我们写了两个getUser方法,都通过方法注解@Bean将两个方法返回的对象注册到了容器中

@Controller
public class NotSingle 
    @Autowired
    private User user;
    public void getName()
        System.out.println(user.name);
    

我们在NotSingle类中注入user对象,再在main方法中创建这个实例调用getName方法看是否可以访问到user对象的name属性

可以发现触发了不是唯一对象的异常,这是因为我们之前注入了两个user对象,这时我们再注入user类到其他类中就不知道到底该注入user1,还是user2了,我们解决这个方法有三种方式
1.精确的描述要注入的bean的名称
例如上述代码,我们可以将属性名修改为user1或者user2,这样达到精确注入的目的

@Controller
public class NotSingle 
    @Autowired
    private User user1;
    public void getName()
        System.out.println(user1.name);
    


运行后发现name为wangwu,符合user1的属性
2.通过设置@Resource注释的name属性来精确锁定某一对象
将@Resouce设置了name属性后,该类的私有属性(要注入的对象)的属性名可以达到给bean重命名的效果

@Controller
public class NotSingle 
    @Resource(name = "user1")
    private User user;
    public void getName()
        System.out.println(user.name);
    

我们通过设置@Resource的name属性,锁定了要注入对象为user1,同时下边创建的user可以将user1对象在当前类中重命名,user和user1引用指向同一个user的地址,这时我们访问的结果仍然是zhangsan

3.使用 @Autowired注释和 @Qualifier搭配锁定某一对象

@Controller
public class NotSingle 
    @Autowired
    @Qualifier("user2") //也可以是@Qualifier(value = "user2")
    private User user;
    public void getName()
        System.out.println(user.name);
    

这时我们达到的效果和方法2相同,都锁定了user2对象并且将其重命名为user,执行效果为输出wangwu

5.Bean的作用域

1)案例

我们先来看一个案例
首先,创建一个Users类,设置属性name初始值为”暗裔剑魔“

@Controller
public class Users 
    public String name = "暗裔剑魔";
    public void getName()
        System.out.println(this.name);
    

创建一个类BeanLife,注入一个users对象,并且在自己的方法中新建一个users对象使其指向刚注入进来的users属性所指的对象,在方法中修改其名称为”时间刺客“

public class BeanLife 
    @Autowired
    private Users users;

    public void getNewName()
        Users users_1 = users;
        System.out.println("修改前原name" + user2.name);
        users_1.name = "时间刺客";
    

再创建一个新的类,再注入users对象,打印users对象的名字

@Controller
public class BeanLife2 
    @Autowired
    private Users users;

    public void getName()
        System.out.println(users.name);
    

按照我们一直以来的了解,注入的对象应该是在堆中开辟了新的空间,其作用域在类范围内,那么我们在类中再新建一个指向它的引用并且修改其属性,应该不会影响原本注册在spring中的users对象,也就是说新的类BeanLife2中读取道德users对象的name属性应该是”暗裔剑魔“,那么我们来看一下运行结果

我们可以发现,beanLife2得到的users对象的name属性竟然是”时间刺客“,
这是为什么呢?
这是因为Bean在spring中默认是单例模式的,也就是说无论在哪里创建新的对象,只要和bean对象类型beanName相同,那么这个新创建的对象也指向同一个内存地址

2)作用域类型

1.singleton(单例模式)(默认模式)

通常无状态的bean可以设置作用域为单例模式(无状态指后续代码没有改变bean的属性)

2.prototype 原型模式(多例模式)

即使后边修改,从另一个类中拿到的还是原本的spring bean:通常有状态的bean使用此模式,每一次请求都会创建新的实例,例如使用getBean方法获取时或者使用@Autowired注入时都会创建新的对象
当我们将案例中users的作用域设置为prototype时可以观察到


beanLife2取到的仍是原beans对象,没有被beanLife1修改

3.request 请求作用域 (Spring MVC)

每一次发送请求都会得到一个对象

4.session 会话作用域(Spring MVC)

每同一个session共用一个对象

5.application 全局作用域(Spring MVC)

所有用户共用一个对象,3,4,5是一个逐渐升级的过程 和singleton的应用场景不同 application应用于Spring Web作用域 singleton是Spring Core作用域 singleton作用于IoC的容器 而application作用于servlet容器

3)作用域设置

1.可以通过@Scope(“”)来设置,例如上边使用的

2.通过@Scope(ConfigurableBeanFactory.)的方式来设置,这样可以有效的避免我们用1方法传字符串时将单词拼错

6.Spring的执行流程

1.启动容器,通过main方法中ApplicationContext获取上下文对象同时启动容器
2.根据配置文件,通过xml中加入的bean对象,或者扫描目录,将带五大类注解或bean方法注解的对象注册入spring当中,注册时如果有类需要依赖别的类,例如A依赖B,那么在注册A类时会先将B类注入A的属性中,完成初始化 防止空指针异常,然后再注册A类

7.Bean的生命周期

1.实例化Bean:为Bean对象分配内存空间
2.设置属性:为Bean对象注入所需依赖
3.初始化:
1)执行各种通知
2)执行初始化的前置方法
3)执行Bean的构造方法:一种是执行@PostConstruct 另一种是执行init-method,一个是注解方式的初始化,一个是xml时代的,两个方法的优先级中,注解优先级要比init高
4)执行初始化的后置方法
4.使用bean
5.销毁bean
可以使用@PreDestroy 也可以重写DisposableBean接口方法 也可以执行 destroy-method 一个是注释产物,一个是方法,一个是xml产物
我们用代码来观察一下各个方法的执行顺序是否如上述所说一般

public class BeanLifeTest 
    @Autowired
    private TestBean testBean;
    @PostConstruct
    public void PostConstruct()
        System.out.println("执行了@PostConstruct方法!")Spring(万字详解版)

万字详解Spring之IOC全部知识点

图文并茂,Spring Boot Starter 万字详解!还有谁不会?

Spring万字详解bean的实例化

Spring万字详解DI相关内容,一文掌握DI配置与使用

万字长文!Unix和Linux你不知道的那些历史(详解版)