提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
读书笔记
前言
本次读书: 《Spring源码学习之IOC容器实现原理(一)》
IoC(控制反转:Inverse of Control),又称作依赖注入,是一种重要的面向对象编程的法则来削减计算机程序的耦合问题,也是轻量级的Spring框架的核心。
一、关键问题
什么是Spring中的bean?
Spring里面的bean就类似是定义的一个组件,而这个组件的作用就是实现某个功能的,这里所定义的bean就相当于给了你一个更为简便的方法来调用这个组件去实现你要完成的功能。
什么是控制反转?
-控制:是容器对bean进行创建、管理。控制bean的整个生命周期。
反转:把这个权利交给了 Spring 容器,而不是自己去控制,就是反转。由之前的自己主动创建对象,变成现在被动接收别人给我们的对象的过程,这就是反转。
举个生活中的例子,主动投资和被动投资。
自己炒股、选股票的人就是主动投资,主动权掌握在自己的手
中;而买基金的人就是被动投资,把主动权交给了基金经理,除非你把这个基金卖了,否则具体选哪些投资产品都是基金经理决定的。
什么是依赖注入?
DI—Dependency Injection,即依赖注入;
依赖:程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。
注入:配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系。
所以说控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式。
二、深入理解 IOC
这里用经典 class Rectangle 来举例:
- 两个变量:长和宽
- 自动生成 set() 方法和 toString() 方法
注意:一定要生成 set() 方法,因为 Spring IoC 就是通过这个 set() 方法注入的;
public class Rectangle {
private int width;
private int length;
public Rectangle() {
System.out.println("Hello World!");
}
public void setWidth(int widTth) {
this.width = widTth;
}
public void setLength(int length) {
this.length = length;
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", length=" + length +
'}';
}
}
然后在 test 文件中手动用 set() 方法给变量赋值。
public class MyTest {
@Test
public void myTest() {
Rectangle rect = new Rectangle();
rect.setLength(2);
rect.setWidth(3);
System.out.println(rect);
}
}
其实这就是 IoC 给属性赋值的实现方法,我们把「创建对象的过程」转移给了 set() 方法,而不是靠自己去 new,就不是自己创建的了。
这里我所说的“自己创建”,指的是直接在对象内部来 new,是程序主动创建对象的正向的过程;这里使用 set() 方法,是别人(test)给我的;而 IoC 是用它的容器来创建、管理这些对象的,其实也是用的这个 set() 方法,不信,你把这个这个方法去掉或者改个名字试试?
三、Spring IOC体系结构
其中BeanFactory作为最顶层的一个接口类,它定义了IOC容器的基本功能规范,BeanFactory 有三个子类:ListableBeanFactory、HierarchicalBeanFactory 和AutowireCapableBeanFactory。但是从上图中我们可以发现最终的默认实现类是 DefaultListableBeanFactory,他实现了所有的接口。那为何要定义这么多层次的接口呢?查阅这些接口的源码和说明发现,每个接口都有他使用的场合,它主要是为了区分在 Spring 内部在操作过程中对象的传递和转化过程中,对对象的数据访问所做的限制。例如 ListableBeanFactory 接口表示这些 Bean 是可列表的,而 HierarchicalBeanFactory 表示的是这些 Bean 是有继承关系的,也就是每个Bean 有可能有父 Bean。AutowireCapableBeanFactory 接口定义 Bean 的自动装配规则。这四个接口共同定义了 Bean 的集合、Bean 之间的关系、以及 Bean 行为.
在BeanFactory里只对IOC容器的基本行为作了定义,根本不关心你的bean是如何定义怎样加载的。正如我们只关心工厂里得到什么的产品对象,至于工厂是怎么生产这些对象的,这个基本的接口不关心。
而要知道工厂是如何产生对象的,我们需要看具体的IOC容器实现,spring提供了许多IOC容器的实现。比如XmlBeanFactory,ClasspathXmlApplicationContext等。其中XmlBeanFactory就是针对最基本的ioc容器的实现,这个IOC容器可以读取XML文件定义的BeanDefinition(XML文件中对bean的描述)。
四、IOC容器
既然说容器是 IoC 最重要的部分,那么 Spring 如何设计容器的呢?还是回到官网,第二段有介绍哦:
使用 ApplicationContext,它是 BeanFactory 的子类,更好的补充并实现了BeanFactory 的。
BeanFactory 简单粗暴,可以理解为 HashMap:
- Key - bean name
- Value - bean object
但它一般只有 get, put 两个功能,所以称之为“低级容器”。
而 ApplicationContext 多了很多功能,因为它继承了多个接口,可称之为“高级容器”。在下文的搭建项目中,我们会使用它。
ApplicationContext 的里面有两个具体的实现子类,用来读取配置配件的:
- ClassPathXmlApplicationContext - 从 class path 中加载配置文件,更常用一些;
- FileSystemXmlApplicationContext - 从本地文件中加载配置文件,不是很常用,如果再到 Linux 环境中,还要改路径,不是很方便。
当我们点开 ClassPathXmlApplicationContext 时,发现它并不是直接继承 ApplicationContext 的,它有很多层的依赖关系,每层的子类都是对父类的补充实现。
而再往上找,发现最上层的 class 回到了 BeanFactory,所以它非常重要。
要注意,Spring 中还有个 FactoryBean,两者并没有特别的关系,只是名字比较接近,所以不要弄混了顺序。
为了好理解 IoC,我们先来回顾一下不用 IoC 时写代码的过程。
五、Spring Bean的初始化和销毁
spring bean的生命周期:bean的创建–>初始化–>销毁的过程;
spring bean 的生命周期都是由spring容器管理,我们可以自定义初始化和销毁方法,spring容器在 bean进入到当前生命周期的时候来调用自定义的初始化和销毁方法。
5.1 通过@Bean指定初始化和销毁方法
创建一个实体类:
public class Goods {
public Goods() {
System.out.println("goods--default constructor");
}
public void init(){
System.out.println("goods---init");
}
public void destroy(){
System.out.println("goods---destroy");
}
}
创建一个新的配置类MyLifeCycleConfig.java:
@Configuration
public class MyLifeCycleConfig {
@Bean(initMethod = "init",destroyMethod = "destroy")
//@Scope("prototype")
@Scope("singleton")
public Goods goods(){
return new Goods();
}
}
测试类MyTestLifeCycle.java:
public class MyTestLifeCycle {
@Test
public void testLifeCycle(){
//容器启动的时候创建单例bean
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyLifeCycleConfig.class); System.out.println("spring--容器启动--------");
//每次获取bean的时候创建多例bean
Goods goods = (Goods) context.getBean("goods");
context.close();//容器关闭的时候,spring bean 中的单例bean的销毁方法被调用,多例的不会调用
}
}
5.2 通过bean实现 InitializingBean和DisposableBean接口
- InitializingBean : 定义初始化逻辑
- DisposableBean:定义销毁逻辑
创建实体类GoodsType.java 实现InitializingBean, DisposableBean两个接口:
public class GoodsType implements InitializingBean, DisposableBean {
@Override
public void destroy() throws Exception { System.out.println("GoodsType---destroy---");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("GoodsType---afterPropertiesSet----");
}
}
修改配置类MyLifeCycleConfig.java:该类中添加如下方法:
//方式2:要注册到spring 容器中的bean实现InitializingBean, DisposableBean接口 @Bean
public GoodsType goodsType(){
return new GoodsType();
}
5.3 @PostConstruct和 @PreDestroy注解
- @PostConstruct注解标注在方法上,在bean创建完成并完成属性赋值之后来执行的初始化方法。
- @PreDestroy:在容器移除对象之前调用。
创建实体类:需要添加依赖包。pom.xml文件添加如下依赖
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>jsr250-api</artifactId>
<version>1.0</version>
</dependency>
public class Comment {
public Comment() {
System.out.println("Comment---default constructor");
}
//这两个注解需要的导入javax.annotation
@PostConstruct //在bean对象创建并完成属性赋值之后执行的初始化方法 表明bean对象的初始 化工作方法
public void init(){
System.out.println("Comment----init----@PostConstruct");
}
@PreDestroy //在容器移除对象之前 表明bean对象的销毁工作方法
public void destroy(){
System.out.println("Comment----destroy----@PreDestroy");
}
}
修改配置类MyLifeCycleConfig.java:该类中添加如下方法:
//方式3:使用Java的两个注解 @PostConstruct @PreDestroy
@Bean
public Comment comment(){
return new Comment();
}
5.4 通过bean实现BeanPostProcessor接口
- BeanPostProcessor接口:Bean的后置处理器,在Bean的初始化前后进行一些处理工作
- postProcessBeforeInitialization()方法处理初始化之前的逻辑
- postProcessAfterInitialization()方法处理初始化之后的逻辑。
创建MyBeanPostProcessor类实现BeanPostProcessor接口;
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)throws BeansException {
System.out.println("MyBeanPostProcessor--before---=====bean:"+bean+"--- ----beanName:"+beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)throws BeansException {
System.out.println("MyBeanPostProcessor--after---=====bean:"+bean+"---- ---beanName:"+beanName);
return bean;
}
}
项目总结
本次项目是一个项目信息发布与报名的管理系统,设有学员,导师和管理员三个权限色。系统包括前台项目报名与展示系统及后台管理系统,基于SpringBoot+MyBatisPlus实现,采用Docker容器化部署。前台系统包含主页,名人堂,项目大厅,个人中心,登录注册,作品集管理,项目管理等模块。后台系统包括项目管理,名人堂页面管理,帐号管理,内推介绍管理以及首页管理。
由于知识储备不足,在这次的项目过程中也是一路磕磕碰碰,特在此记录遇到的一些问题和总结的一些经验,以助今后更高效的开发:
- 表达是与否概念的字段,必须使用is_xxx的方式命名
- 逻辑删除应搭配@TableLogic使用,1表示是,0表示否
- 表必备三字段:id, create_time, update_time
- 接收json等格式的对象请使用@RequestBody注解
- 根据请求接收的对象来添加注解,springMVC会进行封装对象操作,如果字段不符合注解要求直接会返回400badRequest( 如在属性上加@NotEmpty,并在对应的方法参数前加@Validated)
- 所有业务逻辑写service里,有问题直接抛异常,如调用Asserts.fail()(Asserts是common模块统一抛异常的工具类)
数据库方面
在这次项目中,我主要负责用户的登录与注册模块,除了编写基本的CRUD外,对我价值比较大还有数据库的设计。
比如正确的索引能够快速的检索数据。通常查询会根据多个条件进行查询,这个时候往往会使用联合索引。根据最左前缀匹配原则,尽量选择区分度高的列作为索引(区分度的公式是 COUNT(DISTINCT col)/COUNT(*))。当然,如果要进一步对索引进行优化的话,可以尝试实现索引覆盖。在mysql中,索引的数据结构是B+树,聚合索引(往往是主键索引)的叶子的节点存储了数据, 而辅助索引的叶子结点存储的是主键的值,所以通常使用辅助索引查询数据的时候,都要拿着叶子结点上的主键值再到主键索引上获取数据(俗称回表);
怎么解决呢? 其实可以使用覆盖索引。
比如用户表我们定义了一个(phoneNumber, password)的联合索引,在实现登录功能的时候如果我们是 select * ,那是查询所有的,会先去辅助索引上查到符合条件的主键值, 再到主键索引上根据查到的id值来得到数据。那如果我们只获取password呢?其实在(phoneNumber, password)字段的索引上就已经有了,那就不需要回表了。
除了业务的实现, 对mysql的主从复制和redis缓存熟悉和应用也是我在这次项目中的重要收获。
主从复制
主从复制是指将主数据库的DDL和DML操作通过二进制日志传到从数据库上,然后在从数据库上对这些日志进行重新执行,从而使从数据库和主数据库的数据保持一致。
主从复制的原理:
- MySql主库在事务提交时会把数据变更作为事件记录在二进制日志Binlog中;
- 主库推送二进制日志文件Binlog中的事件到从库的中继日志Relay Log中,之后从库根据中继日志重做数据变更操作,通过逻辑复制来达到主库和从库的数据一致性;
- MySql通过三个线程来完成主从库间的数据复制,其中Binlog Dump线程跑在主库上,I/O线程和SQL线程跑着从库上;
- 当在从库上启动复制时,首先创建I/O线程连接主库,主库随后创建Binlog Dump线程读取数据库事件并发送给I/O线程,I/O线程获取到事件数据后更新到从库的中继日志Relay Log中去,之后从库上的SQL线程读取中继日志Relay Log中更新的数据库事件并应用,如下图所示。
Redis实现缓存
redis数据库的作用就是有时限的存放从数据库中读出的数据,减小mysql的访问压力。
在使用下面的注解时,首先需要在启动类上添加@EnableCaching注解启动缓存功能。
springBoot与redis缓存有关的三个注解:
- @Cachable
- @CachePut
- @CacheEvict
@Cachable注解用在查询类的serviceImpl类上,因为@Cachable注解存在时,在向缓存中添加数据前,会先查询缓存中是否已经存在数据,然后在执行操作,可以避免重复添加相同key的数据
@CachePut注解用在新增类的serviceImpl类上,因为@CachePut注解会确保每次都执行,即每次都会向缓存中添加数据,而新增的数据一定是缓存数据库中没有的数据,每次都向缓存中添加也不会导致数据重复
@CacheEvict注解用在修改、删除类的serviceImpl类上,因为@CacheEvict会根据条件对缓存进行清空,修改类的serviceImpl类会返回相同key的新数据给缓存,所以需要对已经存在于数据库中的同key数据进行清除