前言
经过前面的开发,KellerNotes 项目已经全部完成,作为个人及团队(500 人以下)的云笔记服务器使用是不成问题的。大家可以在此基础上增删功能,完成属于自己的云笔记。接下来我们进入收尾工作,讲些额外的知识,可以帮助大家了解项目的组件化开发、分布式部署。
更正说明
由于作者水平和眼界有限,在项目开发过程中用到的部分技术比较落后,感谢文涛同学的指出,在本篇的代码中已作出更新,具体如下:
- jjwt => jjwt-impl:用于 JWT 的生成和解析
- javax.mail => spring-boot-starter-mail:用于邮件的发送
知识准备
项目的组件化,也就是微服务,当下比较主流的是 Spring Cloud。根据本项目实际情况,使用其中的服务治理组件 Spring Cloud Consul。消息队列使用 RabbitMQ,使用 spring-boot-starter-amqp 实现对 RabbitMQ 的支持。
Consul
Consul 是一个服务网格解决方案,特点是:
- 动态负载均衡
- 健康检查
- 自动化网络配置
- 强健的生态系统
Consul 提供了不同操作系统的下载方式,使用 Windows 系统直接下载 exe 文件,双击安装即可:
Consul 默认启动端口为 8500,安装完毕后,直接在浏览器访问 http://localhost:8500 即可。如图:
现在还没有配置其他服务,当我们配置一个服务并成功启动后,在这里就能看到服务的运行状态了。
RabbitMQ
RabbitMQ 是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。
官网地址:https://www.rabbitmq.com/
- Mac 安装参考:https://www.jianshu.com/p/60c358235705
- Windows 参考:https://blog.csdn.net/lihua5419/article/details/93006834
启动成功后,访问 http://localhost:15672 即可:
微服务
整体架构
Keller Notes 项目组件化后的结构如下:
将其按照功能模块细分为 4 个服务——Proxy、User、Note、Log,每个服务负责独立的功能。这些服务均注册在 Conful 中,按需部署不同的数量,通过 Spring Cloud Conful 进行调用。所有服务中日志记录相关的部分都发送到 RabbitMQ 中,由 Log 组件进行处理。
组件说明
组件这样拆分的好处在于:
- Proxy
- 代理服务,客户端只知道代理服务器的存在,不知道其他业务服务器,访问其他业务服务器的请求均有代理服务器进行验证和转发
- 其他业务服务器只接受来自代理服务器的请求,在网关上做限制,保障了业务服务器的安全性
- 代理服务器是一个与业务无关的通用服务,可以在其他项目中直接使用
- 因为现在只有上传头像与用户笔记中的图片,这些文件数量比较少,暂时不考虑单独搭建 FastDFS 等文件服务来专门处理文件读写操作,暂时由代理服务配合 Nginx 文件服务器处理。
- User
- 用户信息服务,与具体业务无关,只负责维护用户的基本信息与名片信息
- 在其他项目中可以直接使用 User 服务实现用户信息管理功能
- 当然,如果需要做用户权限管理,在此基础上进行修改、扩展就可以了
- Note
- 笔记服务,本项目中的业务服务器,负责笔记相关的逻辑处理
- 纯业务服务器,在不同的项目中有不同的业务处理,可以根据业务的实际情况拆分不同的业务服务器
- 本项目中业务逻辑比较简单,使用一个业务服务器就可以了,不再做拆分
- Log
- 日志服务,负责各种日志的读写操作
- 各服务需要记录日志的地方,统一发送到 RabbitMQ,由 Log 服务统一处理
- 这样能保证日志记录有问题时不影响到业务主流程
- 需要查询日志时,也调用 Log 服务的 RestFul 接口获取查询结果
- 当业务增加到一定程度时,可以将日志记录单独分一个库
具体的每个组件功能如下:
- Proxy:代理服务
- 路由代理
- 用户身份验证
- RabbitMq 消息队列维护
- 文件上传
- User:用户服务
- 登录、注册
- JWT 签发
- 用户信息维护
- 用户信息统计
- Note:笔记服务
- 笔记信息维护
- 笔记本信息维护
- Log:日志服务
- 日志记录
- 日志查询
公共组件
组件拆分后,需要将项目中的工具包 common 打成一个独立的 jar 包,以供各个组件使用。
创建 common 项目
创建新的 Spring Boot 项目,选择如下依赖:
创建完毕后,添加项目中用到的其他依赖,如:fastjson、thumbnailator 等,完整的项目依赖如下:
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<!-- 图片压缩 -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
<!-- 图片元数据读取 -->
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.11.0</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.1</version>
<scope>runtime</scope>
</dependency>
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--Spring Cloud Consul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<!--Spring Cloud Consul 健康检查依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
common 代码结构
将所有组件公用的代码整理出来,基本上是原项目中 common 包下的代码。部分代码做了改动,以适应微服务架构需要;部分代码重新进行整理,剥离出其中与业务相关的部分。代码结构如下:
- config:配置文件相关
- Constants.java:项目常量,存放项目中定义的一些常量
- MqConstants.java:RabbitMq 队列常量,存放固定的队列名
- entity:公用的实体类相关
- EmailLog.java:邮件发送记录实体类
- ErrorLog.java:错误日志实体类
- LoginLog.java:用户登录日志实体类
- RegisterLog.java:用户注册记录实体类
- http:http 相关
- HttpsClientRequestFactory.java:Https 请求工厂,用于支持 Https 请求
- RequestUtil.java:请求工具类,用于处理
HttpServletRequest
中的各种信息 - Response.java:统一应答格式
- ResponseUtils.java:应答工具类,用于解析应答、捕获
RestTemplate
处理过程中的的异常 - RestTemplateUtils.java:
RestTemplate
工具类,生成配置好的 Http、Https 格式的RestTemplate
实例 - ResultData.java:应答中的统一数据格式
- ServerRouter.java:服务路由,提供各个服务的名称及获取各服务地址的方法
- mybatis:MyBatis 通用 Mapper 相关
- annotation:注解相关
- FieldAttribute.java:字段注解,标示成员变量为数据库字段,并设置相应的约束信息,如:长度、非空、查询字段、索引字段等
- KeyAttribute.java:主键注解,标示成员变量为主键,支持标注为自增主键
- SortAttribute.java:排序注解,标示成员变量为排序字段
- TableAttribute.java:数据表注解,标示实体类对应的表名称、说明信息
- provider:Sql 语句生成相关
- BaseCreateProvider.java:数据表创建语句的生成器,支持生成索引
- BaseDeleteProvider.java:delete 语句生成器,支持根据 ID、主键、自定义条件删除
- BaseInsertProvider.java:insert 语句生成器,支持自增主键的
insert
操作 - BaseSelectProvider.java:select 语句生成器,支持分页查询、统计查询、自定义条件查询等
- BaseUpdateProvider.java:update 语句生成器,支持根据 ID、主键修改数据
- BaseEntity.java:所有实体类的父类,提供了自定义查询条件、分页查询、排序的扩展
- BaseException.java:自定义异常,在通用 Mapper 运行过程中抛出的异常
- BaseMapper.java:所有 Mapper 的父类,提供了通用 Mapper 功能
- SqlFieldReader.java:Sql 字段解析类,用于解析实体类中的自定义注解,为生成 Sql 语句服务
- TypeCaster.java:类型转换,用于将 Java 中的数据类型转换成相应的 MySql 数据类型
- annotation:注解相关
- util:工具类相关
- Console.java:日志输出工具类,用于在控制台、日志文件中输出相关信息
- DateUtils.java:日期工具类,用于各种日期字符串的处理
- ErrorLogUtils.java:错误日志工具类,用于处理请求异常、系统异常等异常日志
- FileUtils.java:文件工具类,用于处理文件名、文件类型、保存文件等
- ImageUtils.java:图片工具类,用于处理图片信息,如:图片压缩、图片保存等
- JwtUtils.java:JWT 工具类,用于 JWT 的生成与解析
- Md5Utils.java:MD5 工具类,用于处理 MD5 字符串
- MetadataUtils.java:元数据工具类,用于解析图片中的元数据信息
- ObjectUtils.java:Object 工具类,用于对象的空值判断
- StringUtils.java:字符串工具类,用于生成指定格式的字符串及对字符串进行各种处理
RequestUtils 改动说明
主要涉及到修改的类是 RequestUtils.java,将原有的 getUrl(HttpServletRequest request,Map<String,Object> params)
方法扩展为三个方法:
- getUrl:获取到普通用户访问的 Url
- 从 Header 中读取 JWT,验证用户身份
- 从 Header 中读取要访问的接口名
- 从传入的请求参数中读取参数名,构造完整的请求
- 过滤掉非法的请求参数,添加固定的 kellerUserId 参数作为用户 ID
- getBaseUrl:获取到不需要登录就可以访问的 Url
- 规定每个组件中 RequestMapping 为 /base 的接口为不需要登录就能访问的接口
- 原则上只有用户登录、注册、忘记密码接口等允许不登录就可以访问
- 从 Header 中读取要访问的接口名,将其缀在 /base 后面,限定其只能访问 /base 开头的接口
- 从传入的请求参数中读取参数名,构造完整的请求
- 不对请求的接口地址和参数做特殊处理
- getAdminUrl:获取到管理员才能访问的接口的 Url
- 规定每个组件中 RequestMapping 为 /admin 的接口为管理员才能访问的接口
- 从 Header 中读取 JWT,验证管理员身份
- 从 Header 中读取要访问的接口名,将其缀在 /admin 后面,限定其只能访问 /admin 开头的接口
- 从传入的请求参数中读取参数名,构造完整的请求
- 过滤掉非法的请求参数,添加固定的 kellerAdminId 参数作为管理员 ID
getBaseUrl(HttpServletRequest request, Map<String,Object> params)
:
/**
* 访问无需身份认证的接口(/base)
* @param request
* @param params
* @return
*/
public static String getBaseUrl(HttpServletRequest request, Map<String,Object> params){
HashMap<String,String> headers = getHeader(request);
StringBuilder builder = new StringBuilder();
// 添加固定接口名前缀 /base
builder.append(Constants.baseMapping).append("/")
.append(headers.get(METHOD));
if(params == null){
return builder.toString();
}
builder.append("?");
for(String key :params.keySet()){
builder.append(key)
.append("={")
.append(key)
.append("}&");
}
String url = builder.substring(0,builder.length() -1);
Console.info("getBaseUrl",url);
return url;
}
getUrl(HttpServletRequest request, Map<String,Object> params)
、getAdminUrl(HttpServletRequest request, Map<String,Object> params)
:
/**
* 访问普通接口
* @param request
* @param params
* @return
*/
public static String getUrl(HttpServletRequest request,Map<String,Object> params){
return getUrl(request, params,false);
}
/**
* 访问管理员接口(/admin)
* @param request
* @param params
* @return
*/
public static String getAdminUrl(HttpServletRequest request,Map<String,Object> params){
return getUrl(request, params,true);
}
private static String getUrl(HttpServletRequest request,Map<String,Object> params,boolean isAdmin){
//从 JWT 中解析出 UserId
HashMap<String,String> jwtMap= JwtUtils.readJwt(request.getHeader(TOKEN));
// 从 JWT 中取出用户类型
Integer userType = Integer.parseInt(jwtMap.get(JwtUtils.USER_TYPE));
// 从 JWT 中取出用户ID
Integer userId = Integer.parseInt(jwtMap.get(JwtUtils.ID));
// 添加固定的用户 ID,避免客户端通过伪造 ID 参数对接口进行非法访问
String userIdParamName;
String mapping = "";
// 用户类型和用户Id均不能为空
if(ObjectUtils.isEmpty(userId,userType)){
return null;
}
// 非管理员用户不能访问管理员接口
if(isAdmin){
if(userType != Constants.ADMIN_USER_TYPE){
return null;
}
userIdParamName = Constants.ADMIN_ID_KEY;
mapping = Constants.adminMapping;
}else {
userIdParamName = Constants.USER_ID_KEY;
}
StringBuilder builder = new StringBuilder();
HashMap<String,String> headers = getHeader(request);
builder.append(mapping)
.append("/")
.append(headers.get(METHOD))
.append("?")
.append(userIdParamName)
.append("=")
.append(userId);
if(params == null){
return builder.toString();
}else{
builder.append("&");
}
for(String key :params.keySet()){
//过滤掉请求中的 userId
if(Constants.USER_ID_KEY.equals(key)
|| Constants.ADMIN_ID_KEY.equals(key)){
continue;
}
builder.append(key)
.append("={")
.append(key)
.append("}&");
}
String url = builder.substring(0,builder.length() -1);
Console.info("getUrl",url,isAdmin);
return url;
}
组件打包
在 pom.xml 配置文件中指定打包的文件名和保存路径,只打出项目代码部分,不要依赖包,配置如下:
<build>
<finalName>kellerCommon</finalName>
<directory>
/Users/yangkaile/jars/kellerCommon/
</directory>
</build>
然后使用 mvn package
命令进行打包即可,打包成功后,可以在指定的目录下看到生成的 kellerCommon.jar。
添加到本地 Maven 仓库
可以将 kellerCommon.jar 加入本地 Maven 仓库,供其他组件引用,使用如下命令:
mvn install:install-file -Dfile=/Users/yangkaile/jars/kellerCommon/kellerCommon.jar -DgroupId=com.keller -DartifactId=common -Dversion=0.0.1 -Dpackaging=jar
- Dfile:文件路径
- DgroupId:groupId
- DartifactId:artifactId
- Dversion:version
- Dpackaging:打包格式
执行效果如下:
这样就可以在组件中使用 Maven 方式引入 kellerCommon.jar 了,如:
<dependency>
<groupId>com.keller</groupId>
<artifactId>common</artifactId>
<version>0.0.1</version>
</dependency>
源码地址
本篇完整的源码地址:
注意:后面章节的代码都在 30 篇中包含,直接在 IDEA 中打开 server 文件夹即可。
小结
本篇按照微服务应用的开发方式,将项目公共代码提取出来,并打成 jar 包,添加到本地 Maven,供各个微服务组件使用。并部署好服务治理组件 Consul,用于服务发现和治理、健康检查;部署好消息队列 RabbitMQ,用于组件间的异步通讯。