0. 前言
这篇文章可能不是常规的笔记风格,而更偏向于随记,我会将听课过程中觉得是我不了解的或者是很有意思的点记录下来,启发思维
本文很多地方都指出某一款开源项目体现出了标准化的优点,那么在我的认知中,标准化究竟包括哪些方面?
- 结构标准化:某一个项目或者说某一个插件除实现自身设计目的外,应该确保Java程序设计核心思路:代码体现框架思路,配置文件完成实现,也就是:(1)代码仅完成思路的框架搭建,不涉及硬编码;(2)各类实例化所需信息都由配置文件完成
除标准化外,本文还提出了一个切合框架编程的,体现某开源项目优势的另一角度——简洁化,这里也需要说明我所认为的简洁化包括哪些方面?
- 调用简洁化:某一个项目被调用时,应存在一个高维度的接口,封装了本项目的功能,从而让调用者在完成框架设计时,只需要知道一个通用形的入口,而具体实例化谁则完全由配置文件决定
- 功能简洁化:某一个项目之所以被实现,其目的就是对某一类繁琐的功能需求进行包装和简化,功能简洁化存在于每一个有意义的项目中
1. Spring的core container
Spring针对软件开发过程中,由于在代码中书写实例化对象,从而产生的代码之间的耦合问题
,提供了loc思想
的解法
首先说明耦合问题:
比如当前有一个
UserDaoImpl
对象,实现自UserDao
接口,还有一个UserServiceImpl
对象,实现自UserService
接口。在UserServiceImpl
中有一个private UserDao
变量,其是UserDaoImpl
的实例化对象
在上述情形下,如果UserDaoImpl名称发生变化,那么你需要去UserServiceImpl中修改实例化时的名称,导致需要对这部分代码重新执行编译,测试与发布,这就是
对象之间的耦合
我们的希望是减少这样的耦合,一个切实可行的思路就是:
通过
配置文件
来书写具体的对象名称,由外部容器
进行实例化,并提供给调用者使用。而在代码中则只书写其相对固定的接口名
。
这样即使实例化对象名称发生变化,我们也只需要重新编译该对象,以及修改
配置文件
即可,而不需要针对耦合问题进行更多的重新编译和发布。这个通过外部容器
控制实例化对象的思想就是loc思想
Spring就是loc思想的一个实现者。其底层架构core container
通过配置文件或注解的方式管理单例对象(也可以是非单例,但不建议通过Spring来管理)的创建和调用。同时,针对对象之间的依赖,也设计了依赖注入(DI)
的模式(主要分为setter注入,构造器注入和自动注入三种方式)来完成管理的对象之间的依赖调用
因此,Spring的core container
体现出对对象管理的标准化
,即由外部容器控制对象的创建和注入,而功能实现时,只需要通过对应的get方法即可获取到在外部容器中完成实例化的对象。
而简洁化
主要是体现在后续的维护开发
过程中,由于代码之间的耦合度降低,部分功能的修改不会影响其他部分,从而降低了维护难度
最后,附一张SpringFramework4.0的图:
2. Spring的注解开发
注解开发简化了开发流程,但与之对应的,写在源码中的注解其实某种程度上破坏了spring的非侵入式思想
注解开发与配置文件的区别就在于,注解开发写在源码
中,如果注解内容发生改变
,那么源码还是需要重新编译
生成字节码文件;而配置文件内容改变,不需要经过重新编译
即可直接读取并使用
因此,配置文件更加的标准化,也更符合非侵入式的编程思想,适合大型的复杂任务
,便于后期的维护;而注解开发则更加简洁,阅读性更高,适合小型的简单任务
,完成功能的快速实现
3. AOP面向切面编程
AOP解决的痛点问题是:当需要在非侵入式开发
的前提下,对源代码已有方法的功能进行增强
,并且该功能可以复用
单看描述,其实已经可以把AOP的实现方式猜个大概出来,那就是proxy动态代理类,有关于proxy动态代理类的原理详述可以参考这篇博客:详解Java的proxy动态代理机制
这里,我们简单归纳以下proxy的实现。首先,proxy的实现有三个要素:
- ClassLoader:(目标类)加载器;
- Interfaces:(目标类)接口数组;
- InvocationHandler:代理调用机制;
前两者都是实现类和接口定位的描述,第三个才是proxy可以做到功能增强的原因,所以这里我们只针对说明第三个InvocationHandler。
在InvocationHandler中,可以通过method.invoke(target, args);
完成对原对象方法的反射,而功能的增强,则可以写在该行代码的之前或之后,表征在原对象方法的执行前或执行后运行功能。
AOP就是对proxy的封装框架,我们不需要去考虑编写proxy的三要素,只需要通过注解来提供切入点
(对应于前两个要素),编写method方法
(对应于功能增强的代码),并通过注解来标识method方法与切入点的执行顺序
(对应于InvocationHandler的顺序)。其他的则由AOP自动完成
因此,AOP的简洁性体现在两个方面
- AOP简化了动态代理的声明与使用,我们可以通过注解与声明两个特定方法的形式完成对原对象的动态代理
- AOP简化了功能增强的实现,我们既不需要进行源代码修改,也不需要声明复杂的proxy对象,就可以实现对源代码的功能增强,并且该增强是可
复用
的
AOP还有一个很有趣的内容,这个内容也进一步体现了Spring和AOP的一些实现思想,所以这里我们也把它记录下来
AOP一般在写切入点表达式的时候,是面向接口编程,以保证代码间的低耦合。但是,proxy在实现时,需要提供具体的实现
类的class对象
以及接口的class对象,AOP目前的配置信息缺少
类的class对象,还不足以保证完成proxy的创建
Spring框架在声明Bean时,要求其应该有具体的
实现类
,即使写在接口上的Compenent,也应该有隐式的处理逻辑来实例化该接口(如Spring整合Mybatis时,Mybatis自动代理做的事)
AOP在结合Spring后,就可以通过匹配判断来保证proxy创建的条件:如果表达式匹配失败,那么Spring正常创建Bean;如果表达式匹配成功,Spring保证了Bean的实例化
,然后AOP借由此创建proxy对象
,Spring中最终保存的是完成了功能增强
的proxy对象
值得注意的是,由于上述描述,AOP即使面向的是接口编程,其功能增强也是增强到了具体的实现类上
4. Spring的事务管理
Spring的事务管理解决了多方法间的同执行同回滚
,并且最关键的是,其配置方式简单
,模式全面
。
因为Spring将事务管理分成了两个角色,分别是事务管理员
与事务协调员
,在第一次遇到@transactional
注解时建立事务管理员,之后每次遇到新的事务,都根据其配置
决定是否让其称为事务协调员加入。具体的配置由propagation
字段决定,模式全面:
5. SpringMVC
SpringMVC是对Servlet框架的再封装,首先解决了Servlet框架的对请求数据读取
和响应数据转HttpResponse
的重复工作:
- Request数据通过直接赋值的方式给与对应java变量(一般有三种方式@RequestBody,@RequestParam,@PathVariable),转化交由框架处理(功能由
ConditionalConverter
及其实现类负责) - Response数据直接返回java对象,转化交由框架处理(功能由
HttpMessageConverter
及其实现类负责)
因此,SpringMVC相比于Servlet框架,其处理数据
更加方便简洁,这是SpringMVC的核心竞争力
此外,SpringMVC还解决了Servlet框架在整合对同一pojo,不同业务层对应的不同表现层功能时,需要自写地址分发与反射实现的繁复操作,通过@RequestMapping
,SpringMVC自动组装了地址分发工作,在整合同一pojo表现层功能
需求上书写更加简洁
6. REST风格
REST风格定义了访问资源路径的描述方式,由于其可以隐藏访问意图
,且书写更为简洁
,所以比传统风格更受欢迎
REST风格定义了增删改查不同功能的访问资源路径规范,如下图所示:
7. 表现层返回数据以及异常处理的标准化
开发过程中,前后端的通信是需要标准化格式的,也就是在面对正常返回、错误数据、异常发生等等情况,后端反馈给前端的都应保持一个固定的格式
这样仍然符合Java
通用性的思想,即一套代码可以适应不同的环境,那么待解决的问题就变成了以下两个:
- 表现层返回的数据
标准
的格式应该怎么定义 - 异常处理种类多而繁杂,要怎么做到
标准化
,又怎么做到尽可能的简洁化
首先,第一个问题好解决,那就是在表现层中定义一个Result
类,规定返回数据的格式必须为此,一般Result
类包含三个字段:
{
code: xxxxx,
data: [java对象,简单变量,null],
msg: String字符串
}
其中,code
又会被抽成一个单独的类放在表现层中,进行标识码的总览与规范
第二个问题的解决思路,按如下的考量进行:
- 首先,异常种类过多,其实不方便分析,所以需要重新
归类异常
,并定义新的异常类来封装
各个异常 - 在有了新的分类后,为了
简洁化
,我们希望所有的异常都被抛到表现层
处理,这样就可以只在表现层写异常处理函数 - 表现层可能有多种方法,异常处理函数相当于对方法的
增强
,因此为了简洁化
,我们还应使用AOP思想
第一个考量,我们根据需求,分为业务异常(由用户错误操作导致)、系统异常(由服务器、操作系统出问题等导致)、其他异常(未能提前预期的异常)
第二个考量和第三个考量是一体化的。我们可以在业务层建立try-catch或是throw表达式,来将数据层和业务层的异常向上抛出,表现层在接收到异常后,会被AOP程序捕捉异常,从而转至AOP的异常处理函数
异常处理函数则需要根据新的异常分类,分别撰写不同的异常处理:
- 面对业务异常
- 发送消息给用户,提醒正确操作
- 面对系统异常
- 发送固定消息给用户,安抚用户
- 发送消息给运维,提醒维护
- 发送邮件给开发人员,提醒检查
- 记录日志
- 面对其他异常,安抚用户,提醒开发人员(由于未预期的异常出现,因此可以排查该异常并判断是否可以划分到前两类),写日志
8. 拦截器
拦截器是SpringMVC用来在表现层替换Filter的一项技术,它比之Filter主要有两方面的优势:
- 地址配置细粒度更高,更方便。通过
addInterceptors
方法可以精准的注册要拦截的地址,而且可以一次注册多个,相比较而言,Filter难以实现这样精细化
的操作 - 功能划分更为细致。规定preHandler, postHandler, afterCompletion三个方法,确定了拦截前,拦截后,完成后三个时间段各自需要增强的功能书写区域,相比于Filter划分更细,书写更为
标准化
但Filter也有自己的优势区间,拦截器仅可以拦截对表现层的访问,而Filter可以拦截对服务器所有资源的访问
拦截器之所以划分postHandler和afterComplete两类,是因为它们与preHandler的返回结果有着不同的关系。当构建拦截器链时,拦截器x返回false,所有的postHandler都不可执行,但afterComplete编号在x之前的都可以执行,因此可以通过两类方法构建不同的执行需求
9. Maven的分模块开发与私服
Maven分模块开发解决了同一项目的各模块间分别开发
的需求,由此,一个团队可以合理的安排进度表,可并行
的任务由不同小组负责,串行
任务则按照依赖关系
安排负责顺序,实现开发过程的高效
而Maven分模块开发的主要管理手段是模块间的继承与聚集,即类似于Java的父类和子类
首先,一个项目应声明一个管理模块,其打包方式为pom,该模块是整个项目的父类。父类中需要定义项目公用的属性值以及依赖jar包,这样就可以保证整个项目依赖版本的一致性
其次,各个功能应分别定义一个模块,并继承父类地址从而完成公共依赖的导入。然后,如果有依赖于项目内的其他模块,应把该模块的地址导入
Maven的私服则解决了自定义模块和第三方模块的共享与更新
问题。我们将写好的模块通过develop
指令发布到私服的host
仓库,在需要使用时,访问group
仓库组获取模块(使用较为广泛的私服项目是nexus
)
10. SpringBoot开发相关的知识总结(非原理分析)
SpringBoot在我看来,主要的简化优势体现在:
- 非常多的依赖jar包配置,编程者再也不用使用一个jar包就去maven仓库查配置,直接写就可以出来(与本地仓库有该jar包时的效果一致)
- 非常多的起步依赖,编程者再也不用操心Spring,SpringMVC,test环境的各种配置,基本上SpringBoot都搞定了,没搞定的也只需要提供一个注解指定一下
- 非常方便的运行,SpringBoot通过设定的main方法,使得任何用户可以通过java -jar来启动SpringBoot项目
- 非常方便的属性配置,通过设置四种属性配置优先级,以及由yaml文件提供的“—”环境分割,分离了开发与发布的配置需求
因此,这部分就不再是原理分析和优势总结了,而是与开发相关的知识总结(实用)
1)SpringBoot默认配置的调整
- 默认服务器的更改
<!-- TODO 更换服务器:从依赖中删除tomcat-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
<!-- TODO 更换服务器:添加jetty作为服务器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
- 默认数据库连接池的更改
# TODO 整合SSM(Mybatis):修改数据库连接池,导入坐标后在这里修改type属性即可
type: com.alibaba.druid.pool.DruidDataSource
# TODO 整合SSM(Mybatis):如果想要调整数据库连接池的配置信息,如Druid,在type的同级书写字段druid,然后进行配置
druid:
initial-size: 10
max-active: 200
更详细的配置信息可参考:https://blog.csdn.net/leinminna/article/details/123916151
2)配置文件的使用技巧
- YAML的格式与书写
# TODO 要点1:键与值之间必须空格;层级之间只能打空格而不能打tab
# 举例如下:修改服务器端口
server:
port: 80
# TODO 要点2:通过---可以分割不同的环境配置,同一时间只有一个配置起效
# 举例如下:配置开发,测试,生产环境下的服务器端口,设置默认环境为dev
spring:
profiles:
active: dev
---
# 开发
server:
port: 80
spring:
profiles: dev
---
# 测试
server:
port: 81
spring:
profiles: test
---
# 生产
server:
port: 82
spring:
profiles: produce
- 配置文件的四个优先级
因此,根据优先级的关系,我们可以进行紧急覆盖
以及实现不修改jar包中的配置而调整相关属性的需求(如果需要调整的属性少,也可以通过命令行 --server.port = 81
这样的方式来调整)
- 如何在程序中读取YAML的配置
// 方法1:"${name}"的形式(适合少量数据加载)
// 举例:读取server.port的值
@Value("${server.port}")
private int port;
// 方法2:定义Enviorment类型变量,通过自动装配的形式将所有配置属性加载,在通过getproperty("name")来获取值(适合大量复杂数据一次加载)
// 举例:读取server.port的值
@Autowired
private Environment environment;
environment.getProperty("server.port")
// 方法3:自定义要装配的类(字段名称必须和配置一致),然后通过自动装配将赋值完的类加载(适合单层级数据加载)
// 举例:
// 1.定义class
@Component
// TODO 关键点:配置要加载的属性的名称
@ConfigurationProperties(prefix = "server")
public class Server(){
private int port;
public Integer getPort() {
return this.port;
}
public void setPort(Integer port) {
this.id = port;
}
}
// 2,自动装配
@Autowired
private Server server;
server.port
3)SpringBoot对JUnit和Mybatis的整合
- 整合JUnit有一个起步依赖,导入后项目自动生成的test中,存在一个注解
@SpringBootTest
,其完成了Spring测试需要的配置,所以我们直接书写测试代码即可 - 整合Mybatis也有一个起步依赖,导入后只需要做两件事:(1)在Dao的接口上添加注解
@Mapper
;(2)在配置文件中添加spring.datasource相关配置。其他所有工作均由SpringBoot完成
11. MybatisPlus的使用Tips
- MybatisPlus的意义和大致原理
- MybatisPlus是对Mybatis的简化,将常规的Mybatis数据库操作的SQL语句和方法全部整合重写,只需要在自己定义的Dao层继承接口
BaseMapper<T>
即可 - MybatisPlus的原理就在于自定义的接口
BaseMapper<T>
,其内封装了大量常规数据库操作的代码,默认情况下,会自动在数据库中查询以你所定义的数据类型T
为表名(第一个单词小写)的表,同时以数据类型T所定义的字段
为匹配字段。最后就是根据功能来“填空”
- MybatisPlus是对Mybatis的简化,将常规的Mybatis数据库操作的SQL语句和方法全部整合重写,只需要在自己定义的Dao层继承接口
- MybatisPlus查询相关知识
- 条件设置
- 在设置条件时,MybatisPlus提供了一个强大的自定义类型
QueryWrapper
,它可以进行条件语句的设定,提供多种类型的条件判定方法,如:范围匹配(=,>,<,between);模糊匹配(like);包含匹配(in);分组(group);排序(order);空判定(null)(更为详细的实现和描述参看官方文档条件构造器)。链式编程接.or()
方法表示条件间的或关系
,直接.接条件
表示且关系
QueryWrapper
还有一个子类LambdaQueryWrapper
,可以使用lambda表达式,在设置条件时,以类名::字段名的形式来替代"字段名",防止出错
- 在设置条件时,MybatisPlus提供了一个强大的自定义类型
- 查询投影(设置查询字段)
- 查询投影select()方法可以设置本次要查询的字段,
QueryWrapper
以(“字段名”,“字段名”)的方式进行传参,LambdaQueryWrapper
则是以lambda表达式进行传参。 - 由于select()方法内在逻辑是字段的拼接,所以可以用诸如"count(*) as count"的方法来替换传入的“字段名”,实现sql的函数方法。此时Dao层应使用
selectByMap
来查数据,接收List<Map>
的返回值,Map的key就是设定的字段名
- 查询投影select()方法可以设置本次要查询的字段,
- 查询映射
- 字段映射可以处理数据类定义的字段的多种需求,如:
- 数据类字段与数据库字段不匹配,通过
@TableField(value = "数据库字段名")
来处理 - 数据类字段不存在与数据库中,通过
@TableField(exist = false)
来处理 - 更多字段映射知识,可以参考官方文档注解:TableField
- 数据类字段与数据库字段不匹配,通过
- 表名映射同理,通过
@TableName(value = "数据库表名")
- 值得拓展的是,可以在
application.yml
中配置mybatis-plus:global-config:db-config
中的字段来实现对Dao层的全局通用配置,例如常见的table-prefix
就可以统一设置在Dao类名前增加字符串来匹配数据库的表名
- 字段映射可以处理数据类定义的字段的多种需求,如:
- 条件设置
- MybatisPlus插件
- MybatisPlus其他知识
- 多段数据查询:使用方法
selectBatchIds
- id生成:官方文档注解:TableId
- 条件查询null判定:在各种条件构造器的首位参数新增判定条件
"null != "${字段值}""
,只有该条件成立时,条件构造器才构造方法对应的判定条件 - 日志的开启与关闭:在
application.yml
中配置mybatis-plus:configuration:log-impl
设置日志信息的输出 - Lombok包:Lombok提供了多样的编译时执行的注解,可以为我们自动生成一些可使用的方法或变量,Lombok提供的常用注解如下图,更详细的原理描述参看Lombok介绍
- MybatisPlus代码模板生成器:官方文档代码生成器
- 多段数据查询:使用方法