Mysql优化

Mysql性能

最大数据量

抛开数据量和并发数,谈性能都是耍流氓。MySQL没有限制单表最大记录数,它取决于操作系统对文件大小的限制。

文件系统 单文件大小限制
FAT32 4G
NTFS 64G
NTFS5.0 2TB
EXT2 块大小1024字节,文件最大容量16G;块大小4096字节,文件最大容量2TB
EXT3 块大小4k,文件最大容量4TB
EXT4 大于16TB

《阿里巴巴Java开发手册》提出单表行数超过500万行或者单表容量超过2GB,才推荐分库分表。性能由综合因素决定,抛开业务复杂度,影响程度依次是硬件配置、MySQL配置、数据表设计、索引优化。500万这个值仅供参考,并非铁律。微信搜索web_resource 关注获取更多推送。

博主曾经操作过超过4亿行数据的单表,分页查询最新的20条记录耗时0.6秒,SQL语句大致是select field_1,field_2 from table where id < #{prePageMinId} order by id desc limit 20,prePageMinId是上一页数据记录的最小ID。

虽然当时查询速度还凑合,随着数据不断增长,有朝一日必定不堪重负。分库分表是个周期长而风险高的大活儿,应该尽可能在当前结构上优化,比如升级硬件、迁移历史数据等等,实在没辙了再分。对分库分表感兴趣的同学可以阅读分库分表的基本思想。

最大并发数

并发数是指同一时刻数据库能处理多少个请求,由max_connections和max_user_connections决定。max_connections是指MySQL实例的最大连接数,上限值是16384,max_user_connections是指每个数据库用户的最大连接数。

MySQL会为每个连接提供缓冲区,意味着消耗更多的内存。如果连接数设置太高硬件吃不消,太低又不能充分利用硬件。一般要求两者比值超过10%,计算方法如下:

1
max_used_connections / max_connections * 100% = 3/100 *100% ≈ 3%

查看最大连接数与响应最大连接数:

1
2
show variables like '%max_connections%';
show variables like '%max_user_connections%';

在配置文件my.cnf中修改最大连接数

1
2
3
[mysqld]
max_connections = 100
max_used_connections = 20

查询耗时0.5秒

建议将单次查询耗时控制在0.5秒以内,0.5秒是个经验值,源于用户体验的3秒原则。如果用户的操作3秒内没有响应,将会厌烦甚至退出。响应时间=客户端UI渲染耗时+网络请求耗时+应用程序处理耗时+查询数据库耗时,0.5秒就是留给数据库1/6的处理时间。

实施原则

相比NoSQL数据库,MySQL是个娇气脆弱的家伙。它就像体育课上的女同学,一点纠纷就和同学闹别扭(扩容难),跑两步就气喘吁吁(容量小并发低),常常身体不适要请假(SQL约束太多)。如今大家都会搞点分布式,应用程序扩容比数据库要容易得多,所以实施原则是数据库少干活,应用程序多干活

  • 充分利用但不滥用索引,须知索引也消耗磁盘和CPU。
  • 不推荐使用数据库函数格式化数据,交给应用程序处理。
  • 不推荐使用外键约束,用应用程序保证数据准确性。
  • 写多读少的场景,不推荐使用唯一索引,用应用程序保证唯一性。
  • 适当冗余字段,尝试创建中间表,用应用程序计算中间结果,用空间换时间。
  • 不允许执行极度耗时的事务,配合应用程序拆分成更小的事务。
  • 预估重要数据表(比如订单表)的负载和数据增长态势,提前优化。

数据表设计

数据类型

数据类型的选择原则:更简单或者占用空间更小。

  • 如果长度能够满足,整型尽量使用tinyint、smallint、medium_int而非int。

  • 如果字符串长度确定,采用char类型。

  • 如果varchar能够满足,不采用text类型。

  • 精度要求较高的使用decimal类型,也可以使用BIGINT,比如精确两位小数就乘以100后保存。

  • 尽量采用timestamp而非datetime。

    类型 占用字节 描述
    datetime 8字节 ‘1000-01-01 00:00: 00.000000’ to ‘9999-12-31 23:59:59.999999’
    timestamp 4字节 ‘1970-01-01 00:00:00.000000’ to ‘2038-01-19 03:14:07.999999’

相比datetime,timestamp占用更少的空间,以UTC的格式储存自动转换时区。

避免空值

MySQL中字段为NULL时依然占用空间,会使索引、索引统计更加复杂。从NULL值更新到非NULL无法做到原地更新,容易发生索引分裂影响性能。尽可能将NULL值用有意义的值代替,也能避免SQL语句里面包含is not null的判断。微信搜索web_resource 关注获取更多推送。微信搜索web_resource 关注获取更多推送。

text类型优化

由于text字段储存大量数据,表容量会很早涨上去,影响其他字段的查询性能。建议抽取出来放在子表里,用业务主键关联。

索引优化

索引分类

  • 普通索引:最基本的索引。
  • 组合索引:多个字段上建立的索引,能够加速复合查询条件的检索。
  • 唯一索引:与普通索引类似,但索引列的值必须唯一,允许有空值。
  • 组合唯一索引:列值的组合必须唯一。
  • 主键索引:特殊的唯一索引,用于唯一标识数据表中的某一条记录,不允许有空值,一般用primary key约束。
  • 全文索引:用于海量文本的查询,MySQL5.6之后的InnoDB和MyISAM均支持全文索引。由于查询精度以及扩展性不佳,更多的企业选择Elasticsearch。

索引优化

  • 分页查询很重要,如果查询数据量超过30%,MYSQL不会使用索引。
  • 单表索引数不超过5个、单个索引字段数不超过5个。
  • 字符串可使用前缀索引,前缀长度控制在5-8个字符。
  • 字段唯一性太低,增加索引没有意义,如:是否删除、性别。

合理使用覆盖索引,如下所示:

  • 1
    select login_name, nick_name from member where login_name = ?

login_name, nick_name两个字段建立组合索引,比login_name简单索引要更快。

SQL优化

分批处理

博主小时候看到鱼塘挖开小口子放水,水面有各种漂浮物。浮萍和树叶总能顺利通过出水口,而树枝会挡住其他物体通过,有时还会卡住,需要人工清理。MySQL就是鱼塘,最大并发数和网络带宽就是出水口,用户SQL就是漂浮物。微信搜索web_resource 关注获取更多推送。

不带分页参数的查询或者影响大量数据的update和delete操作,都是树枝,我们要把它打散分批处理,举例说明:

业务描述:更新用户所有已过期的优惠券为不可用状态。

SQL语句:

  • 1
    update status=0 FROM `coupon` WHERE expire_date <= #{currentDate} and status=1;

如果大量优惠券需要更新为不可用状态,执行这条SQL可能会堵死其他SQL,分批处理伪代码如下:

1
2
3
4
5
6
7
8
9
int pageNo = 1;
int PAGE_SIZE = 100;
while(true) {
List<Integer> batchIdList =queryList('select id FROM `coupon` WHERE expire_date <= #{currentDate} and status = 1 limit #{(pageNo-1) * PAGE_SIZE},#{PAGE_SIZE}');
if(CollectionUtils.isEmpty(batchIdList)) {
return;
}
update('update status = 0 FROM `coupon` where status = 1 and id in #{batchIdList}') pageNo ++;
}

操作符<>优化

通常<>操作符无法使用索引,举例如下,查询金额不为100元的订单:

1
select id from orders where amount != 100;

如果金额为100的订单极少,这种数据分布严重不均的情况下,有可能使用索引。鉴于这种不确定性,采用union聚合搜索结果,改写方法如下:

1
(select id from orders where amount > 100) union all(select id from orders where amount < 100 and amount > 0)

OR优化

在Innodb引擎下or无法使用组合索引,比如:

1
select id,product_name from orders where mobile_no = '13421800407' or user_id = 100;

OR无法命中mobile_no + user_id的组合索引,可采用union,如下所示:

1
(select id,product_name from orders where mobile_no = '13421800407') union(select id,product_name from orders where user_id = 100);

此时id和product_name字段都有索引,查询才最高效。

IN优化

IN适合主表大子表小,EXIST适合主表小子表大。由于查询优化器的不断升级,很多场景这两者性能差不多一样了。

尝试改为join查询,举例如下:

1
select id from orders where user_id in (select id from user where level = 'VIP');

采用JOIN如下所示:

1
select o.id from orders o left join user u on o.user_id = u.id where u.level = 'VIP';

不做列运算

通常在查询条件列运算会导致索引失效,如下所示:

查询当日订单

1
select id from order where date_format(create_time,'%Y-%m-%d') = '2019-07-01';

date_format函数会导致这个查询无法使用索引,改写后:

1
select id from order where create_time between '2019-07-01 00:00:00' and '2019-07-01 23:59:59';

避免Select all

如果不查询表中所有的列,避免使用SELECT *,它会进行全表扫描,不能有效利用索引。

Like优化

like用于模糊查询,举个例子(field已建立索引):

1
SELECT column FROM table WHERE field like '%keyword%';

这个查询未命中索引,换成下面的写法:

1
SELECT column FROM table WHERE field like 'keyword%';

去除了前面的%查询将会命中索引,但是产品经理一定要前后模糊匹配呢?全文索引fulltext可以尝试一下,但Elasticsearch才是终极武器。

Join优化

join的实现是采用Nested Loop Join算法,就是通过驱动表的结果集作为基础数据,通过该结数据作为过滤条件到下一个表中循环查询数据,然后合并结果。如果有多个join,则将前面的结果集作为循环数据,再次到后一个表中查询数据。

驱动表和被驱动表尽可能增加查询条件,满足ON的条件而少用Where,用小结果集驱动大结果集。

被驱动表的join字段上加上索引,无法建立索引的时候,设置足够的Join Buffer Size。

禁止join连接三个以上的表,尝试增加冗余字段。微信搜索web_resource 关注获取更多推送。

Limit优化

limit用于分页查询时越往后翻性能越差,解决的原则:缩小扫描范围,如下所示:

1
select * from orders order by id desc limit 100000,10

耗时0.4秒

1
select * from orders order by id desc limit 1000000,10

耗时5.2秒

先筛选出ID缩小查询范围,写法如下:

1
select * from orders where id > (select id from orders order by id desc  limit 1000000, 1) order by id desc limit 0,10

耗时0.5秒

如果查询条件仅有主键ID,写法如下:

1
select id from orders where id between 1000000 and 1000010 order by id desc

耗时0.3秒

如果以上方案依然很慢呢?只好用游标了,感兴趣的朋友阅读JDBC使用游标实现分页查询的方法

其他数据库

作为一名后端开发人员,务必精通作为存储核心的MySQL或SQL Server,也要积极关注NoSQL数据库,他们已经足够成熟并被广泛采用,能解决特定场景下的性能瓶颈。

分类 数据库 特性
键值型 Memcache 缓存,大量数据给访问负载
键值型 Redis 缓存,比Memcache支持更多数据类型,支持持久化
列式存储 HBase 海量数据存储
文档型 MongoDB 知名文档型数据库,也可用于缓存
文档型 CouchDB Apache开源,专注于易用性, 支持REST API
文档型 SequolaDB 国内知名文档型数据库
图形 Neo4J 社交网络关系图谱,推荐系统等

SpringBoot原理深入剖析

依赖管理

构建spring boot工程时,统一引入的依赖spring-boot-starter-parent

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent<11./artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

代码底层引入了spring-boot-dependencies ,核心代码如下

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>

继续深究底层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<properties>
<activemq.version>5.15.11</activemq.version>
...
<solr.version>8.2.0</solr.version>
<mysql.version>8.0.18</mysql.version>
<kafka.version>2.3.1</kafka.version>
<spring-amqp.version>2.2.2.RELEASE</spring-amqp.version>
<spring-restdocs.version>2.0.4.RELEASE</spring-restdocs.version>
<spring-retry.version>1.2.4.RELEASE</spring-retry.version>
<spring-security.version>5.2.1.RELEASE</spring-security.version>
<spring-session-bom.version>Corn-RELEASE</spring-session-bom.version>
<spring-ws.version>3.0.8.RELEASE</spring-ws.version>
<sqlite-jdbc.version>3.28.0</sqlite-jdbc.version>
<sun-mail.version>${jakarta-mail.version}</sun-mail.version>
<tomcat.version>9.0.29</tomcat.version>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<thymeleaf-extras-data-attribute.version>2.0.1</thymeleaf-extras-dataattribute.version>
...
</properties>

从这可以看出, 该文件通过变迁对一些常用技术框架的依赖文件进行了统一管理,以至于我们在开发过程中,不用自己引入对应的依赖

自动配置

Spring Boot应用程序入口是@SpringBootApplication 注解标注类的main()方法, @SpringBootApplication 能够扫描Spring组件并自动配置, 下面看@SpringBootApplication 内部源码剖析

1
2
3
4
5
6
@SpringBootApplication
public class SpringbootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDemoApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Target(ElementType.TYPE)    //注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME) ///表示注解的生命周期,Runtime运行时
@Documented ////表示注解可以记录在javadoc中
@Inherited //表示可以被子类继承该注解

@SpringBootConfiguration //// 标明该类为配置类
@EnableAutoConfiguration // 启动自动配置功能
@ComponentScan(excludeFilters = { // 包扫描器 <context:component-scan base-package="com.xxx.xxx"/>
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}

从上述源码可以看出,@SpringBootApplication 注解是一个组合注解,主要功能由@SpringBootConfiguration̵ @EnableAutoConfiguration̵ @ComponentScan 三个核心注解组成

@SpringBootConfiguration 注解表示Spring Boot配置类, 实际是对 @Configuration 注解一个名称的改写,底层用的还是Spring提供的功能

1
2
3
4
5
6
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration //配置IOC容器
public @interface SpringBootConfiguration {
}

@EnableAutoConfiguration 注解表示开启自动配置功能,该注解也是最重要的注解, 也是实现了自动化配置的注解, 核心代码如下:

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited

@AutoConfigurationPackage //自动配置包 : 会把@springbootApplication注解标注的类所在包名拿到,并且对该包及其子包进行扫描,将组件添加到容器中
@Import(AutoConfigurationImportSelector.class) //可以帮助springboot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器(ApplicationContext)中
public @interface EnableAutoConfiguration {
...
}

可以发现,它也是一个组合注解,Spring中有很多以Enable开头的注解,其作用就算借助@Import来收集并注册特定场景相关的bean,并加载导IoC容器中.

其核心两个注解分别是

  • @AutoConfigurationPackage

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited

    //spring框架的底层注解,它的作用就是给容器中导入某个组件类,
    //例如@Import(AutoConfigurationPackages.Registrar.class),它就是将Registrar这个组件类导入到容器中
    @Import(AutoConfigurationPackages.Registrar.class) // 默认将主配置类(@SpringBootApplication)所在的包及其子包里面的所有组件扫描到Spring容器中
    public @interface AutoConfigurationPackage {

    }

    其中@Import(AutoConfigurationPackages.Registrar.class) 就是将Registrar类,加载导容器中去,查看Registrar 类的registerBeanDefinitions ,就算导入过程的具体实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 获取的是项目主程序启动类所在的目录
    //metadata:注解标注的元数据信息
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    //默认将会扫描@SpringBootApplication标注的主配置类所在的包及其子包下所有组件
    register(registry, new PackageImport(metadata).getPackageName());
    }
    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
    return Collections.singleton(new PackageImport(metadata));
    }
  • @Import({AutoConfigurationImportSelector.class})

    将AutoConfigurationImportSelector 这个类导入导容器中, AutoConfigurationImportSelector 可以借助SpringBoot应用程序将所有符合调价的@Configuration 配置加载到容器中, 继续深究AutoConfigurationImportSelector源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
    //判断 enableautoconfiguration注解有没有开启,默认开启(是否进行自动装配)
    if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
    }
    //1. 加载配置文件META-INF/spring-autoconfigure-metadata.properties,从中获取所有支持自动配置类的条件
    //作用:SpringBoot使用一个Annotation的处理器来收集一些自动装配的条件,那么这些条件可以在META-INF/spring-autoconfigure-metadata.properties进行配置。
    // SpringBoot会将收集好的@Configuration进行一次过滤进而剔除不满足条件的配置类
    // 自动配置的类全名.条件=值
    AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }

    深究loadMetadata方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
    //重载方法
    return loadMetadata(classLoader, PATH);
    }

    static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
    try {
    //1.读取spring-boot-autoconfigure.jar包中spring-autoconfigure-metadata.properties的信息生成urls枚举对象
    // 获得 PATH 对应的 URL 们
    Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path) : ClassLoader.getSystemResources(path);
    // 遍历 URL 数组,读取到 properties 中
    Properties properties = new Properties();

    //2.解析urls枚举对象中的信息封装成properties对象并加载
    while (urls.hasMoreElements()) {
    properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
    }
    // 将 properties 转换成 PropertiesAutoConfigurationMetadata 对象

    //根据封装好的properties对象生成AutoConfigurationMetadata对象返回
    return loadMetadata(properties);
    } catch (IOException ex) {
    throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
    }
    }

    回到AutoConfigurationImportSelector类 深究getAutoConfigurationEntry

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    // 1. 判断是否开启注解。如未开启,返回空串
    if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
    }
    // 2. 获得注解的属性
    AnnotationAttributes attributes = getAttributes(annotationMetadata);

    // 3. getCandidateConfigurations()用来获取默认支持的自动配置类名列表
    // spring Boot在启动的时候,使用内部工具类SpringFactoriesLoader,查找classpath上所有jar包中的META-INF/spring.factories,
    // 找出其中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的属性定义的工厂类名称,
    // 将这些值作为自动配置类导入到容器中,自动配置类就生效了
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);


    // 3.1 //去除重复的配置类,若我们自己写的starter 可能存在重复的
    configurations = removeDuplicates(configurations);
    // 4. 如果项目中某些自动配置类,我们不希望其自动配置,我们可以通过EnableAutoConfiguration的exclude或excludeName属性进行配置,
    // 或者也可以在配置文件里通过配置项“spring.autoconfigure.exclude”进行配置。
    //找到不希望自动配置的配置类(根据EnableAutoConfiguration注解的一个exclusions属性)
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    // 4.1 校验排除类(exclusions指定的类必须是自动配置类,否则抛出异常)
    checkExcludedClasses(configurations, exclusions);
    // 4.2 从 configurations 中,移除所有不希望自动配置的配置类
    configurations.removeAll(exclusions);

    // 5. 对所有候选的自动配置类进行筛选,根据项目pom.xml文件中加入的依赖文件筛选出最终符合当前项目运行环境对应的自动配置类

    //@ConditionalOnClass : 某个class位于类路径上,才会实例化这个Bean。
    //@ConditionalOnMissingClass : classpath中不存在该类时起效
    //@ConditionalOnBean : DI容器中存在该类型Bean时起效
    //@ConditionalOnMissingBean : DI容器中不存在该类型Bean时起效
    //@ConditionalOnSingleCandidate : DI容器中该类型Bean只有一个或@Primary的只有一个时起效
    //@ConditionalOnExpression : SpEL表达式结果为true时
    //@ConditionalOnProperty : 参数设置或者值一致时起效
    //@ConditionalOnResource : 指定的文件存在时起效
    //@ConditionalOnJndi : 指定的JNDI存在时起效
    //@ConditionalOnJava : 指定的Java版本存在时起效
    //@ConditionalOnWebApplication : Web应用环境下起效
    //@ConditionalOnNotWebApplication : 非Web应用环境下起效

    //总结一下判断是否要加载某个类的两种方式:
    //根据spring-autoconfigure-metadata.properties进行判断。
    //要判断@Conditional是否满足
    // 如@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })表示需要在类路径中存在SqlSessionFactory.class、SqlSessionFactoryBean.class这两个类才能完成自动注册。
    configurations = filter(configurations, autoConfigurationMetadata);


    // 6. 将自动配置导入事件通知监听器
    //当AutoConfigurationImportSelector过滤完成后会自动加载类路径下Jar包中META-INF/spring.factories文件中 AutoConfigurationImportListener的实现类,
    // 并触发fireAutoConfigurationImportEvents事件。
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // 7. 创建 AutoConfigurationEntry 对象
    return new AutoConfigurationEntry(configurations, exclusions);
    }

    @EnableAutoConfiguration 就是从classpath中搜寻META-INF/spring.factories 配置文件,并将其中的的org.springframework.boot.autoconfigure.EnableutoConfiguration 对应的配置项通过反射,加载到容器中.

    总结

    Spring Boot底层实现自动装配的步骤是:

    1. 程序启动
    2. @SpringBootApplication 起作用
    3. @EnableAutoConfiguration
    4. @AutoConfigurationPackage 主要是@Import(AutoConfigurationPackages.Registrar.class) 通过将Registrar 类导入容器中,而Registrar 主要作用是将主配置类同级目录及子包,并将相应的组件导入到容器中
    5. @Import(AutoConfigurationImportSelector.class) 它通过将AutoConfigurationImportSelector 导入容器中, AutoConfigurationImportSelector 通过selectImports 方法的执行, 会将内部工具类SpringFactoriesLoader, 查找classpath上所有jar包中的META-INF/spring.factories 惊醒加载,并通过反射将配置类夹给SpringFactory 加载器进行一系列的容器创建过程

执行原理

下面我们查看run()方法内部的源码,核心代码具体如下:

1
2
3
4
5
6
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
//SpringApplication的启动由两部分组成:
//1. 实例化SpringApplication对象
//2. run(args):调用run方法
return new SpringApplication(primarySources).run(args);
}

从上述源码可以看出,SpringApplication.run()方法内部执行了两个操作,分别是SpringApplication实 例的初始化创建和调用run()启动项目,这两个阶段的实现具体说明如下:

实例初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {

this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.isCustomEnvironment = false;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");

//项目启动类 SpringbootDemoApplication.class设置为属性存储起来
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

//设置应用类型是SERVLET应用(Spring 5之前的传统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用)
this.webApplicationType = WebApplicationType.deduceFromClasspath();

// 设置初始化器(Initializer),最后会调用这些初始化器
//所谓的初始化器就是org.springframework.context.ApplicationContextInitializer的实现类,在Spring上下文被刷新之前进行初始化的操作
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

// 设置监听器(Listener)
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

// 初始化 mainApplicationClass 属性:用于推断并设置项目main()方法启动的主程序启动类
this.mainApplicationClass = deduceMainApplicationClass();
}

从上述源码可以看出,SpringApplication的初始化过程主要包括4部分,具体说明如下。

  • (1) this.webApplicationType = WebApplicationType.deduceFromClasspath()

用于判断当前webApplicationType应用的类型。deduceFromClasspath()方法用于查看Classpath类路 径下是否存在某个特征类,从而判断当前webApplicationType类型是SERVLET应用(Spring 5之前的传 统MVC应用)还是REACTIVE应用(Spring 5开始出现的WebFlux交互式应用)

  • (2) this.setInitializers(this.getSpringFactorieslnstances(ApplicationContextlnitializer.class))

用于Spr ingApplication应用的初始化器设置。在初始化器设置过程中,会使用Sp ring类加载器

Spr ingFacto riesLoade r 从 META-INF/sp ring .facto ries 类路径下的 META-INF 下的 spr ing .facto res 文件中 获取所有可用的应用初始化器类ApplicationContextlnitialize r。

  • (3) this.setListeners(this.getSpringFactorieslnstances(ApplicationListener.class))

用于Spr ingApplication应用的监听器设置。监听器设置的过程与上一步初始化器设置的过程基本一样, 也是使用 Spr ingFacto riesLoade r 从 META-INF/sp ring .facto ries 类路径下的 META-INF 下的 spr ing .facto res文件中获取所有可用的监听器类ApplicationListene r。

  • (4) this.mainApplicationClass = this.deduceMainApplicationClass()

用于推断并设置项目main()方法启动的主程序启动类

项目初始化

分析完(new Spr ingApplication(p rimarySou rces)) .run()(args)源码前一部分 Spr ingApplication 实例对象 的初始化创建后,查看r un(a rgs)方法执行的项目初始化启动过程,核心代码具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public ConfigurableApplicationContext run(String... args) {
// 创建 StopWatch 对象,并启动。StopWatch 主要用于简单统计 run 启动过程的时长。
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 初始化应用上下文和异常报告集合
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 配置 headless 属性
configureHeadlessProperty();


// (1)获取并启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
// 创建 ApplicationArguments 对象 初始化默认应用参数类
// args是启动Spring应用的命令行参数,该参数可以在Spring应用中被访问。如:--server.port=9000
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

//(2)项目运行环境Environment的预配置
// 创建并配置当前SpringBoot应用将要使用的Environment
// 并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

configureIgnoreBeanInfo(environment);
// 准备Banner打印器 - 就是启动Spring Boot的时候打印在console上的ASCII艺术字体
Banner printedBanner = printBanner(environment);

// (3)创建Spring容器
context = createApplicationContext();
// 获得异常报告器 SpringBootExceptionReporter 数组
//这一步的逻辑和实例化初始化器和监听器的一样,
// 都是通过调用 getSpringFactoriesInstances 方法来获取配置的异常类名称并实例化所有的异常处理类。
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);


// (4)Spring容器前置处理
//这一步主要是在容器刷新之前的准备动作。包含一个非常关键的操作:将启动类注入容器,为后续开启自动化配置奠定基础。
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);

// (5):刷新容器
refreshContext(context);

// (6):Spring容器后置处理
//扩展接口,设计模式中的模板方法,默认为空实现。
// 如果有自定义需求,可以重写该方法。比如打印一些启动结束log,或者一些其它后置处理
afterRefresh(context, applicationArguments);
// 停止 StopWatch 统计时长
stopWatch.stop();
// 打印 Spring Boot 启动的时长日志。
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// (7)发出结束执行的事件通知
listeners.started(context);

// (8):执行Runners
//用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序
//Runner 运行器用于在服务启动时进行一些业务初始化操作,这些操作只在服务启动后执行一次。
//Spring Boot提供了ApplicationRunner和CommandLineRunner两种服务接口
callRunners(context, applicationArguments);
} catch (Throwable ex) {
// 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}

// (9)发布应用上下文就绪事件
//表示在前面一切初始化启动都没有问题的情况下,使用运行监听器SpringApplicationRunListener持续运行配置好的应用上下文ApplicationContext,
// 这样整个Spring Boot项目就正式启动完成了。
try {
listeners.running(context);
} catch (Throwable ex) {
// 如果发生异常,则进行处理,并抛出 IllegalStateException 异常
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
//返回容器
return context;
}

从上述源码可以看出,项目初始化启动过程大致包括以下部分:

  • 第一步:获取并启动监听器

    this.getRunListeners(args)和listeners.starting()方法主要用于获取SpringApplication 实例初始化过程中初始化的SpringApplicationRunListener监听器并运行。

  • 第二步:根据SpringApplicationRunListeners以及参数来准备环境

    this.prepareEnvironment(listeners, applicationArguments)方法主要用于对项目运行环境 进行预设置,同时通过this.configurelgnoreBeanlnfo(environment)方法排除一些不需要的运行环境

  • 第三步:创建Spring容器

    根据webApplicationType进行判断,确定容器类型,如果该类型为SERVLET类型,会通过反射装载对 应的字节码,也就是 A nnotationConfigServletWebServerApplicationContext,接着使用之前初 始化设置的context (应用上下文环境)、environment (项目运行环境)、listeners (运行监听 器)、applicationArguments (项目参数)和printedBanner (项目图标信息)进行应用上下文的组 装配置,并刷新配置

  • 第四步:Spring容器前置处理

    这一步主要是在容器刷新之前的准备动作。设置容器环境,包括各种变量等等,其中包含一个非常关键的操 作:将启动类注入容器,为后续开启自动化配置奠定基础

  • 第五步:刷新容器

    开启刷新spring容器,通过refresh方法对整个IOC容器的初始化(包括bea n资源的定位,解析,注册等 等),同时向JVM运行时注册一个关机钩子,在JVM关机时会关闭这个上下文,除非当时它已经关闭

  • 第六步:Spring容器后置处理

    扩展接口,设计模式中的模板方法,默认为空实现。如果有自定义需求,可以重写该方法。比如打印一些启 动结束log,或者一些其它后置处理。

  • 第七步:发出结束执行的事件

    获取EventPublishingRunListener监听器,并执行其started方法,并且将创建的Spring容器传进 去了,创建—ApplicationStartedEvent 事件,并执行 ConfigurableApplicationContext 的 publishEvent方法,也就是说这里是在Spring容器中发布事件,并不是在SpringApplication中发布 事件,和前面的starting是不同的,前面的starting是直接向SpringApplication中的监听器发布启动事件。

  • 第八步:执行Runners

    用于调用项目中自定义的执行器XxxRunner类,使得在项目启动完成后立即执行一些特定程序。其中, Spring Boot提供的执行器接口有A pplicationRunner和CommandLineRunner两种,在使用时只需 要自定义一个执行器类实现其中一个接口并重写对应的run()方法接口,然后Spring Boot项目启动后会 立即执行这些特定程序

SpringMVC & JPA

SpringMVC工作流程

开发过程

  1. 配置DispatcherServlet前端控制器
  2. 开发处理具体业务逻辑的Handler(@Controller、@RequestMapping)
  3. xml配置⽂件配置controller扫描,配置springmvc三⼤件
  4. 将xml⽂件路径告诉springmvc(DispatcherServlet)

SpringMVC 请求处理流程

第⼀步:⽤户发送请求⾄前端控制器DispatcherServlet

第⼆步:DispatcherServlet收到请求调⽤HandlerMapping处理器映射器

第三步:处理器映射器根据请求Url找到具体的Handler(后端控制器),⽣成处理器对象及处理器拦截
器(如果 有则⽣成)⼀并返回DispatcherServlet

第四步:DispatcherServlet调⽤HandlerAdapter处理器适配器去调⽤Handler

第五步:处理器适配器执⾏Handler

第六步:Handler执⾏完成给处理器适配器返回ModelAndView

第七步:处理器适配器向前端控制器返回 ModelAndView,ModelAndView 是SpringMVC 框架的⼀个
底层对 象,包括 Model 和 View

第⼋步:前端控制器请求视图解析器去进⾏视图解析,根据逻辑视图名来解析真正的视图。

第九步:视图解析器向前端控制器返回View

第⼗步:前端控制器进⾏视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域

第⼗⼀步:前端控制器向⽤户响应结果

Spring MVC 九⼤组件

  • HandlerMapping(处理器映射器)

    HandlerMapping 是⽤来查找 Handler 的,也就是处理器,具体的表现形式可以是类,也可以是
    ⽅法。⽐如,标注了@RequestMapping的每个⽅法都可以看成是⼀个Handler。Handler负责具
    体实际的请求处理,在请求到达后,HandlerMapping 的作⽤便是找到请求相应的处理器
    Handler 和 Interceptor.

  • HandlerAdapter(处理器适配器)

    一般handler分两种, 标注了@RequestMapping的每个⽅法以及实现了Controller接口的实现类都可以看成是⼀个Handler, HandlerAdapter的职责就是适配这不同的handler

  • HandlerExceptionResolver

    HandlerExceptionResolver ⽤于处理 Handler 产⽣的异常情况。它的作⽤是根据异常设置
    ModelAndView,之后交给渲染⽅法进⾏渲染,渲染⽅法会将 ModelAndView 渲染成⻚⾯。

  • ViewResolver

    ViewResolver即视图解析器,⽤于将String类型的视图名和Locale解析为View类型的视图,只有⼀
    个resolveViewName()⽅法。ViewResolver 在这个过程主要完成两件事情:
    ViewResolver 找到渲染所⽤的模板(第⼀件⼤事)和所⽤的技术(第⼆件⼤事,其实也就是找到
    视图的类型,如JSP)并填⼊参数。默认情况下,Spring MVC会⾃动为我们配置⼀个
    InternalResourceViewResolver,是针对 JSP 类型视图的。

  • RequestToViewNameTranslator

    RequestToViewNameTranslator 组件的作⽤是从请求中获取 ViewName.因为 ViewResolver 根据
    ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName,
    便要通过这个组件从请求中查找 ViewName。

  • LocaleResolver

    ViewResolver 组件的 resolveViewName ⽅法需要两个参数,⼀个是视图名,⼀个是 Locale。
    LocaleResolver ⽤于从请求中解析出 Locale,⽐如中国 Locale 是 zh-CN,⽤来表示⼀个区域。这
    个组件也是 i18n 的基础。

  • ThemeResolver

    ThemeResolver 组件是⽤来解析主题的。主题是样式、图⽚及它们所形成的显示效果的集合。

  • MultipartResolver

    MultipartResolver ⽤于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实
    现。MultipartHttpServletRequest 可以通过 getFile() ⽅法 直接获得⽂件。如果上传多个⽂件,还
    可以调⽤ getFileMap()⽅法得到Map<FileName,File>这样的结构,MultipartResolver 的作⽤就
    是封装普通的请求,使其拥有⽂件上传的功能。

  • FlashMapManager

    FlashMap ⽤于重定向时的参数传递,只需要在重定向之前将要传递的数据写⼊请求(可以通过
    ServletRequestAttributes.getRequest()⽅法获得)的属性OUTPUT_FLASH_MAP_ATTRIBUTE
    中,这样在重定向之后的Handler中Spring就会⾃动将其设置到Model中,在显示订单信息的⻚⾯
    上就可以直接从Model中获取数据。FlashMapManager 就是⽤来管理 FalshMap 的。

拦截器(Inteceptor)

监听器、过滤器和拦截器对⽐

  • Servlet:处理Request请求和Response响应

  • 过滤器(Filter):对Request请求起到过滤的作⽤,作⽤在Servlet之前,如果配置为/*可以对所有的资源访问(servlet、js/css静态资源等)进⾏过滤处理

  • 监听器(Listener):实现了javax.servlet.ServletContextListener 接⼝的服务器端组件,它随Web应⽤的启动⽽启动,只初始化⼀次,然后会⼀直运⾏监视,随Web应⽤的停⽌⽽销毁

    作⽤⼀:做⼀些初始化⼯作,web应⽤中spring容器启动ContextLoaderListener

    作⽤⼆:监听web中的特定事件,⽐如HttpSession,ServletRequest的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控,⽐如统计在线⼈数,利⽤HttpSessionLisener等。

  • 拦截器(Interceptor):是SpringMVC、Struts等表现层框架⾃⼰的,不会拦截jsp/html/css/image的访问等,只会拦截访问的控制器⽅法(Handler)。

    从配置的⻆度也能够总结发现:serlvet、filter、listener是配置在web.xml中的,⽽interceptor是配置在表现层框架⾃⼰的配置⽂件中的

    • 在Handler业务逻辑执⾏之前拦截⼀次
    • 在Handler逻辑执⾏完毕但未跳转⻚⾯之前拦截⼀次
    • 在跳转⻚⾯之后拦截⼀次

拦截器的执⾏流程

单个拦截器执行流程

  1. 程序先执⾏preHandle()⽅法,如果该⽅法的返回值为true,则程序会继续向下执⾏处理器中的⽅
    法,否则将不再向下执⾏。
  2. 在业务处理器(即控制器Controller类)处理完请求后,会执⾏postHandle()⽅法,然后会通过
    DispatcherServlet向客户端返回响应。
  3. 在DispatcherServlet处理完请求后,才会执⾏afterCompletion()⽅法。

多个拦截器的执⾏流程

多个拦截器执行流程

从图可以看出,当有多个拦截器同时⼯作时,它们的preHandle()⽅法会按照配置⽂件中拦截器的配置
顺序执⾏,⽽它们的postHandle()⽅法和afterCompletion()⽅法则会按照配置顺序的反序执⾏。

SpringMVC 源码

核心流程

  • SpringMVC处理请求的流程即为
    org.springframework.web.servlet.DispatcherServlet#doDispatch⽅法的执⾏过程,其中步骤
    2、3、4、5是核⼼步骤
    1)调⽤getHandler()获取到能够处理当前请求的执⾏链 HandlerExecutionChain(Handler+拦截
    器)
    但是如何去getHandler的?后⾯进⾏分析
    2)调⽤getHandlerAdapter();获取能够执⾏1)中Handler的适配器
    但是如何去getHandlerAdapter的?后⾯进⾏分析
    3)适配器调⽤Handler执⾏ha.handle(总会返回⼀个ModelAndView对象)
    4)调⽤processDispatchResult()⽅法完成视图渲染跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
protected void doDispatch(HttpServletRequest request, 
HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 1 检查是否是文件上传的请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
/*
2 取得处理当前请求的Controller,这里也称为Handler,即处理器
这里并不是直接返回 Controller,而是返回 HandlerExecutionChain 请求处理链对象
该对象封装了Handler和Inteceptor
*/
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
// 如果 handler 为空,则返回404
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
// 3 获取处理请求的处理器适配器 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
// 处理 last-modified 请求头
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request,
mappedHandler.getHandler());
if (new ServletWebRequest(request,
response).checkNotModified(lastModified)
&& isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
// 4 实际处理器处理请求,返回结果视图对象
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
// 结果视图对象的处理
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from
// handler methods as well,
// making them available for @ExceptionHandler methods
// and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//最终会调用HandlerInterceptor的afterCompletion 方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
//最终会调用HandlerInterceptor的afterCompletion 方法
triggerAfterCompletion(processedRequest, response,
mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

getHandler⽅法剖析

遍历两个HandlerMapping,试图获取能够处理当前请求的执⾏链

1
2
3
4
5
6
7
8
9
10
11
12
protected HandlerExecutionChain getHandler(HttpServletRequest request) 
throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

getHandlerAdapter⽅法剖析

遍历各个HandlerAdapter,看哪个Adapter⽀持处理当前Handler

1
2
3
4
5
6
7
8
9
10
11
12
protected HandlerAdapter getHandlerAdapter(Object handler)
throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}

ha.handle⽅法剖析

实际调用的是org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handleInternal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response,
HandlerMethod handlerMethod)
throws Exception {

ModelAndView mav;
checkRequest(request);

// Execute invokeHandlerMethod in synchronized block if required.
// 判断当前是否需要支持在同一个session中只能线性地处理请求
if (this.synchronizeOnSession) {
// 获取当前请求的session对象
HttpSession session = request.getSession(false);
if (session != null) {
// 为当前session生成一个唯一的可以用于锁定的key
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
// 对HandlerMethod进行参数等的适配处理,并调用目标handler
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
// 如果当前不存在session,则直接对HandlerMethod进行适配
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
// 如果当前不需要对session进行同步处理,则直接对HandlerMethod进行适配
mav = invokeHandlerMethod(request, response, handlerMethod);
}

if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response,
this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}

return mav;
}

processDispatchResult⽅法剖析

内部核心是调用了

org.springframework.web.servlet.DispatcherServlet#render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private void processDispatchResult(HttpServletRequest request, 
HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler,
@Nullable ModelAndView mv,
@Nullable Exception exception)
throws Exception {

boolean errorView = false;

if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//异常处理
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}

// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
/***************************************
****************************************
核心render方法
****************************************
****************************************/
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}

if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}

if (mappedHandler != null) {
//调用拦截器的拦截方法
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

再嵌套调用了

​ org.springframework.web.servlet.view.AbstractView#render

​ org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
protected void renderMergedOutputModel(
Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {

// Expose the model object as request attributes.
//对request 设置属性值
exposeModelAsRequestAttributes(model, request);

// Expose helpers as request attributes, if any.
exposeHelpers(request);

// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);

// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for ["
+ getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}

// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}

else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
/* 跳转页面的操作 */
rd.forward(request, response);
}
}

Spring Data JPA

Spring Data JPA 是 Spring 提供的⼀个封装了JPA 操作的框架,⽽ JPA 仅仅是规范,单独使⽤规范⽆法
具体做什么,那么Spring Data JPA 、 JPA规范 以及 Hibernate (JPA 规范的⼀种实现)之间的关系是什
么?

JPA

JPA 是⼀套规范,内部是由接⼝和抽象类组成的,Hiberanate 是⼀套成熟的 ORM 框架,⽽且
Hiberanate 实现了 JPA 规范,所以可以称 Hiberanate 为 JPA 的⼀种实现⽅式,我们使⽤ JPA 的 API 编
程,意味着站在更⾼的⻆度去看待问题(⾯向接⼝编程)。

Tomcat原理剖析

系统架构

请求流程

  • HTTP 服务器接收到请求之后把请求交给Servlet容器来处理,Servlet 容器通过Servlet接⼝调⽤业务
    类。Servlet接⼝和Servlet容器这⼀整套内容叫作Servlet规范。
  • 注意:Tomcat既按照Servlet规范的要求去实现了Servlet容器,同时它也具有HTTP服务器的功能
  • Tomcat的两个重要身份
    • http服务器
    • Tomcat是⼀个Servlet容器

Tomcat Servlet容器处理流程

当⽤户请求某个URL资源时

  • HTTP服务器会把请求信息使⽤ServletRequest对象封装起来
  • 进⼀步去调⽤Servlet容器中某个具体的Servlet
  • 在 上一步中,Servlet容器拿到请求后,根据URL和Servlet的映射关系,找到相应的Servlet
  • 如果Servlet还没有被加载,就⽤反射机制创建这个Servlet,并调⽤Servlet的init⽅法来完成初始化
  • 接着调⽤这个具体Servlet的service⽅法来处理请求,请求处理结果使⽤ServletResponse对象封装
  • 把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给客户端

Tomcat系统总体架构

通过上⾯的讲解,我们发现tomcat有两个⾮常重要的功能需要完成

  1. 和客户端浏览器进⾏交互,进⾏socket通信,将字节流和Request/Response等对象进⾏转换
  2. Servlet容器处理业务逻辑

Tomcat 设计了两个核⼼组件连接器(Connector)和容器(Container)来完成 Tomcat 的两⼤核⼼
功能。
连接器,负责对外交流: 处理Socket连接,负责⽹络字节流与Request和Response对象的转化;
容器,负责内部处理:加载和管理Servlet,以及具体处理Request请求;

Tomcat 连接器组件 Coyote

Coyote 是Tomcat 中连接器的组件名称 , 是对外的接⼝。客户端通过Coyote与服务器建⽴连接、发送请
求并接受响应 。

  1. Coyote 封装了底层的⽹络通信(Socket 请求及响应处理)
  2. Coyote 使Catalina 容器(容器组件)与具体的请求协议及IO操作⽅式完全解耦
  3. Coyote 将Socket 输⼊转换封装为 Request 对象,进⼀步封装后交由Catalina 容器进⾏处理,处
    理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写⼊输出流
  4. Coyote 负责的是具体协议(应⽤层)和IO(传输层)相关内容

在 8.0 之前 ,Tomcat 默认采⽤的I/O⽅式为 BIO,之后改为 NIO。 ⽆论 NIO、NIO2 还是 APR, 在性
能⽅⾯均优于以往的BIO。 如果采⽤APR, 甚⾄可以达到 Apache HTTP Server 的影响性能。

Coyote 组件及作⽤

组件 作⽤描述
EndPoint EndPoint 是 Coyote 通信端点,即通信监听的接⼝,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint⽤来实现TCP/IP协议的
Processor Processor 是Coyote 协议处理接⼝ ,如果说EndPoint是⽤来实现TCP/IP协议的,那么Processor⽤来实现HTTP协议,Processor接收来⾃EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应⽤层协议的抽象
ProtocolHandler Coyote 协议接⼝, 通过Endpoint 和 Processor , 实现针对具体协议的处理能⼒。Tomcat 按照协议和I/O 提供了6个实现类 : Ajp NioProtocol ,Ajp AprProtocol, Ajp Nio2Protocol , Http11NioProtocol ,Http11Nio2Protocol ,Http11AprProtocol
Adapter 由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了⾃⼰的Request类来封装这些请求信息。ProtocolHandler接⼝负责解析请求并⽣成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,不能⽤Tomcat Request作为参数来调⽤容器。Tomcat设计者的解决⽅案是引⼊CoyoteAdapter,这是适配器模式的经典运⽤,连接器调⽤CoyoteAdapter的Sevice⽅法,传⼊的是Tomcat Request对象, CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调⽤容器

Tomcat Servlet 容器 Catalina

结构

Tomcat(我们往往有⼀个认识,Tomcat就是⼀个Catalina的实例,因为Catalina是Tomcat的核⼼)

Tomcat/Catalina实例

可以认为整个Tomcat就是⼀个Catalina实例,Tomcat 启动的时候会初始化这个实例,Catalina

实例通过加载server.xml完成其他实例的创建,创建并管理⼀个Server,Server创建并管理多个服务,

每个服务⼜可以有多个Connector和⼀个Container。

⼀个Catalina实例(容器)

⼀个 Server实例(容器)

多个Service实例(容器)

每⼀个Service实例下可以有多个Connector实例和⼀个Container实例

  • Catalina
    负责解析Tomcat的配置⽂件(server.xml) , 以此来创建服务器Server组件并进⾏管理
  • Server
    服务器表示整个Catalina Servlet容器以及其它组件,负责组装并启动Servlaet引擎,Tomcat连接
    器。Server通过实现Lifecycle接⼝,提供了⼀种优雅的启动和关闭整个系统的⽅式
  • Service
    服务是Server内部的组件,⼀个Server包含多个Service。它将若⼲个Connector组件绑定到⼀个
    Container
  • Container
    容器,负责处理⽤户的servlet请求,并返回对象给web⽤户的模块

Container 组件的具体结构

Container组件下有⼏种具体的组件,分别是Engine、Host、Context和Wrapper。这4种组件(容器)
是⽗⼦关系。Tomcat通过⼀种分层的架构,使得Servlet容器具有很好的灵活性。

  • Engine
    表示整个Catalina的Servlet引擎,⽤来管理多个虚拟站点,⼀个Service最多只能有⼀个Engine,
    但是⼀个引擎可包含多个Host
  • Host
    代表⼀个虚拟主机,或者说⼀个站点,可以给Tomcat配置多个虚拟主机地址,⽽⼀个虚拟主机下
    可包含多个Context
  • Context
    表示⼀个Web应⽤程序, ⼀个Web应⽤可包含多个Wrapper
  • Wrapper
    表示⼀个Servlet,Wrapper 作为容器中的最底层,不能包含⼦容器

源码剖析

源码追踪部分我们关注两个流程:Tomcat启动流程和Tomcat请求处理流程

Tomcat启动流程

Tomcat请求处理流程

  • 请求处理流程分析

  • 请求处理流程示意图

Tomcat 的类加载机制

Tomcat 的类加载机制相对于 Jvm 的类加载机制做了⼀些改变。

没有严格的遵从双亲委派机制,也可以说打破了双亲委派机制

⽐如:有⼀个tomcat,webapps下部署了两个应⽤

app1/lib/a-1.0.jar com.lagou.edu.Abc

app2/lib/a-2.0.jar com.lagou.edu.Abc

不同版本中Abc类的内容是不同的,代码是不⼀样的

  • 引导类加载器 和 扩展类加载器 的作⽤不变
  • 系统类加载器正常情况下加载的是 CLASSPATH 下的类,但是 Tomcat 的启动脚本并未使⽤该变
    量,⽽是加载tomcat启动的类,⽐如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。
    位于CATALINA_HOME/bin下
  • Common 通⽤类加载器加载Tomcat使⽤以及应⽤通⽤的⼀些类,位于CATALINA_HOME/lib下,
    ⽐如servlet-api.jar
  • Catalina ClassLoader ⽤于加载服务器内部可⻅类,这些类应⽤程序不能访问
  • Shared ClassLoader ⽤于加载应⽤程序共享类,这些类服务器不会依赖
  • Webapp ClassLoader,每个应⽤程序都会有⼀个独⼀⽆⼆的Webapp ClassLoader,他⽤来加载
    本应⽤程序 /WEB-INF/classes 和 /WEB-INF/lib 下的类。
  • tomcat 8.5 默认改变了严格的双亲委派机制
    • ⾸先从 Bootstrap Classloader加载指定的类
    • 如果未加载到,则从 /WEB-INF/classes加载
    • 如果未加载到,则从 /WEB-INF/lib/*.jar 加载
    • 如果未加载到,则依次从 System、Common、Shared 加载(在这最后⼀步,遵从双亲委派
      机制)

一个java内存泄漏的排查案例

这是个比较典型的java内存使用问题,定位过程也比较直接,但对新人还是有点参考价值的,所以就纪录了一下。

下面介绍一下在不了解系统代码的情况下,如何一步步分析和定位到具体代码的排查过程
(以便新人参考和自己回顾)

初步的现象

业务系统消费MQ中消息速度变慢,积压了200多万条消息,通过jstat观察到业务系统fullgc比较频繁,到最后干脆OOM了:

进一步分析

既然知道了内存使用存在问题,那么就要知道是哪些对象占用了大量内存.

很多人都会想到把堆dump下来再用MAT等工具进行分析,但dump堆要花较长的时间,并且文件巨大,再从服务器上拖回本地导入工具,这个过程太折腾不到万不得已最好别这么干。

可以用更轻量级的在线分析,用jmap查看存活的对象情况(jmap -histo:live [pid]),可以看出HashTable中的元素有5000多万,占用内存大约1.5G的样子:

定位到代码

现在已经知道了是HashTable的问题,那么就要定位出什么代码引起的

接下来自然要看看是什么代码往HashTable里疯狂的put数据,于是用神器btrace跟踪Hashtable.put调用的堆栈。

首先写btrace脚本TracingHashTable.java:

1
2
3
4
5
6
7
8
9
import com.sun.btrace.annotations.*;import static com.sun.btrace.BTraceUtils.*;@BTracepublic class TracingHashTable {        /*指明要查看的方法,类*/
@OnMethod(
clazz="java.util.Hashtable",
method="put",
location=@Location(Kind.RETURN)) public static void traceExecute(@Self java.util.Hashtable object){
println("调用堆栈!!");
jstack();
}
}

然后运行:
bin/btrace -cp build 4947 TracingHashTable.java

看到有大量类似下图的调用堆栈

img

可以看出是在接收到消息后查询入库的代码造成的,业务方法调用ibatis再到mysql jdbc驱动执行statement时put了大量的属性到HashTable中。

通过以上排查已基本定位了由那块代码引起的,接下来就是打开代码工程进行白盒化改造了,对相应代码进行优化(不在本文范围内了。几个图中的pid不一致就别纠结了,有些是系统重启过再截图的).

RESTful API设计

  • rest⻛格请求是什么样的?

  • SpringMVC对rest⻛格请求到底提供了怎样的⽀持
    是⼀个注解的使⽤@PathVariable,可以帮助我们从uri中取出参数

什么是 RESTful

Restful 是⼀种 web 软件架构⻛格,它不是标准也不是协议,它倡导的是⼀个资源定位及资源操作的⻛
格。

什么是REST

REST(英⽂:Representational State Transfer,简称 REST)描述了⼀个架构样式的⽹络系统, ⽐如
web 应⽤程序。它⾸次出现在 2000 年 Roy Fielding 的博⼠论⽂中,他是 HTTP 规范的主要编写者之
⼀。在⽬前主流的三种 Web 服务交互⽅案中,REST 相⽐于 SOAP(Simple Object Access protocol,
简单对象访问协议)以及 XML-RPC 更加简单明了,⽆论是对 URL 的处理还是对 Payload 的编码,
REST 都倾向于⽤更加简单轻量的⽅法设计和实现。值得注意的是 REST 并没有⼀个明确的标准,⽽更像
是⼀种设计的⻛格。
它本身并没有什么实⽤性,其核⼼价值在于如何设计出符合 REST ⻛格的⽹络接⼝。
资源 表现层 状态转移

Restful 的优点

它结构清晰、符合标准、易于理解、扩展⽅便,所以正得到越来越多⽹站的采⽤。

Restful 的特性

  • 资源(Resources):⽹络上的⼀个实体,或者说是⽹络上的⼀个具体信息。
    它可以是⼀段⽂本、⼀张图⽚、⼀⾸歌曲、⼀种服务,总之就是⼀个具体的存在。可以⽤⼀个 URI(统
    ⼀资源定位符)指向它,每种资源对应⼀个特定的 URI 。要获取这个资源,访问它的 URI 就可以,因此
    URI 即为每⼀个资源的独⼀⽆⼆的识别符。
  • 表现层(Representation):把资源具体呈现出来的形式,叫做它的表现层 (Representation)。⽐
    如,⽂本可以⽤ txt 格式表现,也可以⽤ HTML 格式、XML 格式、JSON 格式表现,甚⾄可以采⽤⼆进
    制格式。
  • 状态转化(State Transfer):每发出⼀个请求,就代表了客户端和服务器的⼀次交互过程。
    HTTP 协议,是⼀个⽆状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务
    器, 必须通过某种⼿段,让服务器端发⽣“状态转化”(State Transfer)。⽽这种转化是建⽴在表现层
    之上的,所以就是 “ 表现层状态转化” 。具体说, 就是 HTTP 协议⾥⾯,四个表示操作⽅式的动词:
    GET 、POST 、PUT 、DELETE 。它们分别对应四种基本操作:GET ⽤来获取资源,POST ⽤来新建资
    源,PUT ⽤来更新资源,DELETE ⽤来删除资源。

RESTful 的示例

  • rest是⼀个url请求的⻛格,基于这种⻛格设计请求的url
    没有rest的话,原有的url设计http://localhost:8080/user/queryUserById.action?id=3 url中定义了动作(操作),参数具体锁定到操作的是谁

  • 有了rest⻛格之后
    rest中,认为互联⽹中的所有东⻄都是资源,既然是资源就会有⼀个唯⼀的uri标识它,代表它http://localhost:8080/user/3 代表的是id为3的那个⽤户记录(资源)锁定资源之后如何操作它呢?常规操作就是增删改查 根据请求⽅式不同,代表要做不同的操作

  • get 查询,获取资源

  • post 增加,新建资源

  • put 更新

  • delete 删除资源

  • rest⻛格带来的直观体现:就是传递参数⽅式的变化,参数可以在uri中了

    /account/1 HTTP GET :得到 id = 1 的 account
    /account/1 HTTP DELETE:删除 id = 1 的 account
    /account/1 HTTP PUT:更新 id = 1 的 account
    URL:资源定位符,通过URL地址去定位互联⽹中的资源(抽象的概念,⽐如图⽚、视频、app服务
    等)。

    RESTful ⻛格 URL:互联⽹所有的事物都是资源,要求URL中只有表示资源的名称,没有动词。
    RESTful⻛格资源操作:使⽤HTTP请求中的method⽅法put、delete、post、get来操作资源。分别对
    应添加、删除、修改、查询。不过⼀般使⽤时还是 post 和 get。put 和 delete⼏乎不使⽤。
    RESTful ⻛格资源表述:可以根据需求对URL定位的资源返回不同的表述(也就是返回数据类型,⽐如
    XML、JSON等数据格式)。

    Spring MVC ⽀持 RESTful ⻛格请求,具体讲的就是使⽤ @PathVariable 注解获取 RESTful ⻛格的请求
    URL中的路径变量。

示例代码

  • 前端jsp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <div>
    <h2>SpringMVC对Restful⻛格url的⽀持</h2>
    <fieldset>
    <p>测试⽤例:SpringMVC对Restful⻛格url的⽀持</p>
    <a href="/demo/handle/15">rest_get测试</a>
    <form method="post" action="/demo/handle">
    <input type="text" name="username"/>
    <input type="submit" value="提交rest_post请求"/>
    </form>
    <form method="post" action="/demo/handle/15/lisi">
    <input type="hidden" name="_method" value="put"/>
    <input type="submit" value="提交rest_put请求"/>
    </form>
    <form method="post" action="/demo/handle/15">
    <input type="hidden" name="_method" value="delete"/>
    <input type="submit" value="提交rest_delete请求"/>
    </form>
    </fieldset>
    </div>
  • 后台Handler方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    /*
    * restful get /demo/handle/15
    */
    @RequestMapping(value = "/handle/{id}",method ={RequestMethod.GET})
    public ModelAndView handleGet(@PathVariable("id") Integer id) {
    Date date = new Date();
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("date",date);
    modelAndView.setViewName("success");
    return modelAndView;
    }
    /*
    * restful post /demo/handle
    */
    @RequestMapping(value = "/handle",method = {RequestMethod.POST})
    public ModelAndView handlePost(String username) {
    Date date = new Date();
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("date",date);
    modelAndView.setViewName("success");
    return modelAndView;
    }
    /*
    * restful put /demo/handle/15/lisi
    */
    @RequestMapping(value = "/handle/{id}/{name}",method ={RequestMethod.PUT})
    public ModelAndView handlePut(@PathVariable("id") Integer
    id,@PathVariable("name") String username) {
    Date date = new Date();
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("date",date);
    modelAndView.setViewName("success");
    return modelAndView;
    }
    /*
    * restful delete /demo/handle/15
    */
    @RequestMapping(value = "/handle/{id}",method ={RequestMethod.DELETE})
    public ModelAndView handleDelete(@PathVariable("id") Integer id) {
    Date date = new Date();
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject("date",date);
    modelAndView.setViewName("success");
    return modelAndView;
    }
  • web.xml中配置请求⽅式过滤器(将特定的post请求转换为put和delete请求)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!--配置springmvc请求⽅式转换过滤器,会检查请求参数中是否有_method参数,如果有就
    按照指定的请求⽅式进⾏转换-->
    <filter>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
    <filter-name>encoding</filter-name>
    <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
    </filter-mapping>

Spring学习笔记

Spring的优势

  • ⽅便解耦,简化开发
    通过Spring提供的IoC容器,可以将对象间的依赖关系交由Spring进⾏控制,避免硬编码所造成的
    过度程序耦合。⽤户也不必再为单例模式类、属性⽂件解析等这些很底层的需求编写代码,可以更
    专注于上层的应⽤。

  • AOP编程的⽀持
    通过Spring的AOP功能,⽅便进⾏⾯向切⾯的编程,许多不容易⽤传统OOP实现的功能可以通过
    AOP轻松应付。

  • 声明式事务的⽀持
    @Transactional
    可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式⽅式灵活的进⾏事务的管理,提⾼
    开发效率和质量。

  • ⽅便程序的测试
    可以⽤⾮容器依赖的编程⽅式进⾏⼏乎所有的测试⼯作,测试不再是昂贵的操作,⽽是随⼿可做的
    事情。

  • ⽅便集成各种优秀框架
    Spring可以降低各种框架的使⽤难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、
    Quartz等)的直接⽀持。

  • 降低JavaEE API的使⽤难度
    Spring对JavaEE API(如JDBC、JavaMail、远程调⽤等)进⾏了薄薄的封装层,使这些API的使⽤
    难度⼤为降低。

  • 源码是经典的 Java 学习范例
    Spring的源代码设计精妙、结构清晰、匠⼼独⽤,处处体现着⼤师对Java设计模式灵活运⽤以及对
    Java技术的⾼深造诣。它的源代码⽆意是Java技术的最佳实践的范例。

Spring IoC

bean的生命周期

bean的生命周期是指一个bean对象从创建到销毁的过程. bean不等于普通对象, 是里胡一个java对象只是bean的生命周期过程的一步,只有走完了流程, 才能称之为bean. 核心过程如下:

  1. 实例化bean: 主要通过反射技术实例化

  2. 设置对象属性(依赖注入)

  3. 处理Aware接口

    如果实现了xxxAware接口, 会将相关的xxxAware实例注入给bean

    如果实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法

    如果事项了BeanFactoryAware接口, 会调用它实现的setBeanFactory()方法, 传递的是Spring工厂

    如果实现了ApplicationContextAware接口, 会调用它实现的setApplicationContext()方法,传递的是Spring上下文

  4. BeanPostProcessor:

    如果实现了此接口,会调用 postProcessBeforeInitialization()方法

  5. InitializingBean与 init-method:

    实现bean初始化的一些逻辑

    如果配置了init-method,则会自动调用此方法,完成自定义初始化逻辑

  6. 如果实现了BeanPostProcessor接口,会调用 postProcessAfterInitialization()方法

  7. DisposableBean:

    当bean不需要使用时, 会经过清理阶段, 如果实现了此接口, 则会调用它实现的destroy()方法

  8. 最后,如果配置了destroy-method:则会自动调用此方法,完成自定义销毁逻辑

高级特性

lazy-Init 延迟加载

设置 lazy-init 为 true 或者注解 @Lazy 的 bean 将不会在 ApplicationContext 启动时提前被实例化,⽽是第⼀次向容器
通过 getBean 索取 bean 或者在 bean被其他立即实例化的bean引用时实例化的。

FactoryBean 和 BeanFactory

BeanFactory接⼝是容器的顶级接⼝,定义了容器的⼀些基础⾏为,负责⽣产和管理Bean的⼀个⼯⼚,
具体使⽤它下⾯的⼦接⼝类型,⽐如ApplicationContext;此处我们重点分析FactoryBean

Spring中Bean有两种,⼀种是普通Bean,⼀种是⼯⼚Bean(FactoryBean),FactoryBean可以⽣成
某⼀个类型的Bean实例(返回给我们),也就是说我们可以借助于它⾃定义Bean的创建过程。

自定义FactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 可以让我们⾃定义Bean的创建过程(完成复杂Bean的定义)
public interface FactoryBean<T> {
@Nullable
// 返回FactoryBean创建的Bean实例,如果isSingleton返回true,
//则该实例会放到Spring容器的单例对象缓存池中Map
T getObject() throws Exception;
@Nullable
// 返回FactoryBean创建的Bean类型
Class<?> getObjectType();
// 返回作⽤域是否单例
default boolean isSingleton() {
return true;
}
}

后置处理器

Spring提供了两种后处理bean的扩展接⼝, 分别为 BeanPostProcessor 和BeanFactoryPostProcessor,两者在使⽤上是有所区别的。⼯⼚初始化(BeanFactory)—> Bean, 对象在BeanFactory初始化之后可以使⽤BeanFactoryPostProcessor进⾏后置处理, 做⼀些事情在Bean对象实例化(并不是Bean的整个⽣命周期完成)之后可以使⽤BeanPostProcessor进⾏后置处理做⼀些事情
注意:对象不⼀定是springbean,⽽springbean⼀定是个对象

自动装配

有五种自动装配方式, 可以用来知道Spring容器用自动装配方式来依赖注入:

  • no: 默认不进行自动装配, 通过显式设置ref属性来进行装配
  • byName: 通过参数名自动装配, Spring容器在配置文件中发现bean的autowire属性被设置成byName,止呕容器试图装配和该bean的属性具有相同名字的bean
  • byType: 通过参数类型自动装配, Spring容器在配置文件中发现bean的autowire属性被设置成byType,之后容器试图匹配、装配和该bean的属性具有相同类型的bean。如果有多个bean符合条件,则抛出错误。
  • constructor:这个方式类似于byType,但是要提供给构造器参数,如果没有确定的带参数的构造器参数类型,将会抛出异常。
  • autodetect:首先尝试使用constructor来自动装配,如果无法工作,则使用byType方式。

SpringAOP及应用

AOP的实现

Spring 实现AOP思想使⽤的是动态代理技术
默认情况下,Spring会根据被代理对象是否实现接⼝来选择使⽤JDK还是CGLIB。当被代理对象没有实现
任何接⼝时,Spring会选择CGLIB。当被代理对象实现了接⼝,Spring会选择JDK官⽅的代理技术,不过
我们可以通过配置的⽅式,让Spring强制使⽤CGLIB

Spring 声明式事务的⽀持

编程式事务:在业务代码中添加事务控制代码,这样的事务控制机制就叫做编程式事务
声明式事务:通过xml或者注解配置的⽅式达到事务控制的⽬的,叫做声明式事务

事务的四大特性

  • 原⼦性(Atomicity) 原⼦性是指事务是⼀个不可分割的⼯作单位,事务中的操作要么都发⽣,要么都
    不发⽣。从操作的⻆度来描述,事务中的各个操作要么都成功要么都失败

  • ⼀致性(Consistency)事务必须使数据库从⼀个⼀致性状态变换到另外⼀个⼀致性状态。
    例如转账前A有1000,B有1000。转账后A+B也得是2000。
    ⼀致性是从数据的⻆度来说的,(1000,1000) (900,1100),不应该出现(900,1000)

  • 隔离性(Isolation)事务的隔离性是多个⽤户并发访问数据库时,数据库为每⼀个⽤户开启的事务,
    每个事务不能被其他事务的操作数据所⼲扰,多个并发事务之间要相互隔离。
    ⽐如:事务1给员⼯涨⼯资2000,但是事务1尚未被提交,员⼯发起事务2查询⼯资,发现⼯资涨了2000
    块钱,读到了事务1尚未提交的数据(脏读)

  • 持久性(Durability)持久性是指⼀个事务⼀旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发⽣故障
    也不应该对其有任何影响。

事务的隔离级别

  • Serializable(串⾏化):可避免脏读、不可重复读、虚读情况的发⽣。(串⾏化) 最⾼

  • Repeatable read(可重复读):可避免脏读、不可重复读情况的发⽣。(幻读有可能发⽣) 第⼆
    该机制下会对要update的⾏进⾏加锁

  • Read committed(读已提交):可避免脏读情况发⽣。不可重复读和幻读⼀定会发⽣。 第三

  • Read uncommitted(读未提交):最低级别,以上情况均⽆法保证。(读未提交) 最低

    事务隔离级别 脏读 不可重复度 幻读
    Read uncommitted(读未提交)
    Read committed(读已提交)
    Repeatable read(可重复读)
    Serializable(串行化)

Spring设计模式

  • 工厂设计模式: BeanFactory, ApplicationContext 通过工厂模式创建对象

  • 代理设计模式: SrpingAOP 用到了JDK动态代理和CGLIB动态代理

  • 单例设计模式: bean默认都是单例

  • 模板方法模式: jdbcTemplate, hibernateTemplate 等以Template结尾的对数据库操作的类,用到了模板设计模式

  • 包装器设计模式: 动态数据源的支持

  • 观察者设计模式: Spring时间驱动模型

  • 适配器模式: SpringAOP的增强或者通知(Advice)使用了适配器模式, SpringMVC也是用到了该模式适配Controller

mybatis学习笔记

mybatis解决JDBC的问题:

1.数据库连接创建, 释放频繁造成系统资源的浪费

2.sql语句在代码中硬编码, 造成代码不易维护, 实际使用中sql变化比较大, 改变sql需要改变java代码.

3.使用preparedStatement向有占位符传参存在硬编码

4.对结果集封装也存在硬编码.

解决: mybatis 提供连接池, 通过配置文件解决硬编码, 通过反射内省自动封装结果集

自定义框架设计:

大概流程:

1.加载xml配置文件,封装成configuration对象(封装了数据库连接池的相关信息,以及所有配置文件的信息)

2.将配置文件中,以namespace.statementId为唯一表示,存放到map中,value为具体需要执行的,自己封装的mappedStatement(封装了数据库的连接)

3.在执行相对应的方法,通过传入的namespace.statementId获取对应的mappedStatement,注册驱动,获取连接,解析sql,填充参数,最后执行. 或者使用mapper的动态代理对象,直接调用相对应的方法执行.这里需要满足statementId与接口的方法名相同

框架核心所运用的设计模式:

构建者设计模式( 当一个复杂对象在初始化过于复杂时,我们使用该设计模式,一步一步构建小对象,最后构成复杂对象)

工厂设计模式(创建对象的工作在工厂方法代码里面,根据传入参数的不同,获取不通的对象)

代理模式(对象的方法执行由代理对象完成)

JDK动态代理:由代理模式衍生出来. 需要满足, 代理的对象实现了接口. 最终生成的代理对象,每次执行方法,都会去调用invoke(),这样就可以在方法执行前后,我们对方法进行增长.

mybatis动态sql:

mybatis动态sql是当我们的业务逻辑比较复杂时,需要将sql动态变化,根据我们传入的实体类或者参数,使用不同的sql语言进行查询.

​ 动态sql:

  1. if 语句 (简单的条件判断)

  2. choose(when, otherwize)

  3. trim (对包含的内容加上 prefix,或者 suffix 等,前缀,后缀)

  4. where (主要是用来简化sql语句中where条件判断的,能智能的处理 and or )

  5. set (用于update)

  6. foreach (用于 in语句)

    原理:

    mybatis将xml的sql语句封装成一个个节点,每个节点都是一种动态sql类型的描述,例如 IfSqlNode, 每个动态sql有多个SqlNode构成,都需要实现内部定义的抽象方法apply(), 在sql执行的时候, 这个apply方法会依次执行子节点的apply(), 这样递归执行下去, 构建动态sql中prepareStatement的参数, 并保存最终生成sql的StringBuilder对象. 最终执行 sql

mybatis映射:

一对一:

创建映射实体类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Order {
private int id;
private Date ordertime;
private double total;
private User user;
}
public class User {
private int id;
private String username;
private String password;
private Date birthday;
}

创建OrderMapper接口

1
2
3
public interface OrderMapper {
List<Order> findAll();
}

配置xml文件

1
2
3
4
5
6
7
8
9
10
11
<mapper namespace="com.lagou.mapper.OrderMapper">
<resultMap id="orderMap" type="com.lagou.domain.Order">
<result column="uid" property="user.id"></result>
<result column="username" property="user.username"></result>
<result column="password" property="user.password"></result>
<result column="birthday" property="user.birthday"></result>
</resultMap>
<select id="findAll" resultMap="orderMap">
select * from orders o,user u where o.uid=u.id
</select>
</mapper>

或者

1
2
3
4
5
6
7
8
9
10
11
<resultMap id="orderMap" type="com.lagou.domain.Order">
<result property="id" column="id"></result>
<result property="ordertime" column="ordertime"></result>
<result property="total" column="total"></result>
<association property="user" javaType="com.lagou.domain.User">
<result column="uid" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
</association>
</resultMap>
一对多(多对多类似):

修改实体

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Order {
private int id;
private Date ordertime;
private double total;
private User user;
}
public class User {
private int id;
private String username;
private String password;
private Date birthday;
private List<Order> orderList;
}

创建UserMapper接口

1
2
3
public interface UserMapper {
List<User> findAll();
}

配置XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<mapper namespace="com.lagou.mapper.UserMapper">
<resultMap id="userMap" type="com.lagou.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<collection property="orderList" ofType="com.lagou.domain.Order">
<result column="oid" property="id"></result>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select *,o.id oid from user u left join orders o on u.id=o.uid
</select>
</mapper>

mybatis注解开发(代码不做演示)

@Insert 新增
@Update 更新
@Delete 删除
@Select 查询
@Result 结果映射
@Results 与 @Result 封装多个结果集
@One 一对一
@Many 一堆多或者多对多

mybatis缓存(一级缓存与二级缓存):

存储结构: 一级缓存跟二级缓存底层的数据结构都是hashMap, 其中key是mybatis自己封装的CacheKey是, 生成方式主要由MappedStatement RowBounds(分页相关对象) BoundSql 构成. 一级缓存的value主要是保存sql查询之后的结果(包括具体的对象), 重复执行玩一次sql,都获取到同一个对象. 二级缓存缓存的是结果的数据(不能具体到对象), 即获取到不同的对象,但是对象的值是相等的. 如果二级缓存配置的是redisCache,则使用到的是redis中的哈希数据结构.

​ 范围: 一级缓存是针对同一个sqlSession而言,同一个sqlSession查询的内容,缓存共享. 二级缓存是针对整个namespace, 多个sqlSession共享同一个二级缓存

​ 失效场景: 一级缓存和二级缓存每次查询都会进行数据的缓存. 在进行 insert update delet等数据库写操作的时候会清空缓存. 除此之外, 一级缓存,可以手动调用缓存的 clearCache方法清空缓存, 后续有查询操作缓存可以继续使用; 调用 sqlSession.close()方法之后,也会清空缓存,后续缓存不可用.

mybatis插件:

mybatis四大组件(Executor, StatementHandler, ParameterHandler, ResultSetHandler) 允许对其内部的方法进行拦截, mybatis插件的原理就是拦截器对这些对象内部方法进行拦截.

​ 具体原理:

在四大组件的对象创建出来后, 每个对象都不是直接返回,而是优先经过interceptorChain.pluginAll(parameterHandler)

获取到所有的拦截器,调用interceptorChain.pluginAll(parameterHandler)

为目标对象利用动态代理创建代理对象; 面向切面的方式,拦截到每一个需要拦截的方法,加入业务逻辑,以达到插件的目的.

mybatis架构原理:

分三层:

(1) API接口层:对外提供使用的接口API, 开发人员通过这些API操作数据库. 接口层收到调用请求就会调用数据处理曾来完成数据的处理

Mybatis提供两种方式调用API:

a.使用传统方式(传入namespace.方法id)

b.通过Mapper代理方式(getMapper())

(2) 数据处理层: 复制具体的sql操作, sql解析, sql执行,以及执行结果的映射.

(3) 基础支撑层: 负责最基础的功能支撑,包括连接管理, 事务管理, 配置加载, 缓存处理.

主要构建及其相互关系
组件 描述
SqlSession 作为mybatis工作的主要顶层api,表示和数据库交互的会话,完成数据库操作
Executor 执行器,是调度的核心,负责sql语句的生成和查询缓存
StatementHandler 封装了JDBC Statement操作,设置参数已经封装结果为list
ParameterHandler 负责设置参数,被StatementHandler调用
ResultSetHandler 负责封装结果, 被StatementHandler调用
TypeHandler 负责数据库类型与java类型映射
MappedStatement 封装了sql语句的节点
SqlSource 根据用户传递的parameterObject,动态生成sql语句,将信息封装到BoundSql中
BoundSql 表示动态生成的sql语句已经相对应的参数信息
mybatis执行器:

最基本的有三种:

​ SimpleExecutor, ReuseExecutor, BathExecutor

​ 严格来讲在mybatis源码中还有 BaseExecutor, CachingExecutor, ClosedExecutor(方法都抛出异常)

​ 区别:

​ SimpleExecutor 每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

​ ReuseExecutor 执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。

​ BathExecutor 执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

懒加载:

仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。

​ 原理: 使用CGLIB动态代理,当调用目标方法时,实际上调用的是动态代理对象的invoke方法, 在法相目标方法返回的是null值,那么久会单独执行一次事先保存好的关联相对应对象的sql,将执行结果按照配置文件设置到对一个的字段上,接着完成 相对应的逻辑.