Merge branch '5.0'
# Conflicts: # sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/entity/ConfigServiceRoute.java # sop-gateway/src/main/java/com/gitee/sop/gateway/entity/ConfigServiceRoute.java
18
.gitignore
vendored
Normal file → Executable file
@@ -1,4 +1,4 @@
|
|||||||
/target/
|
target/
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
### STS ###
|
### STS ###
|
||||||
@@ -8,20 +8,18 @@
|
|||||||
.project
|
.project
|
||||||
.settings
|
.settings
|
||||||
.springBeans
|
.springBeans
|
||||||
.sts4-cache
|
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
### IntelliJ IDEA ###
|
||||||
.idea
|
.idea
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
*.ipr
|
*.ipr
|
||||||
*.png
|
|
||||||
|
|
||||||
### NetBeans ###
|
### NetBeans ###
|
||||||
/nbproject/private/
|
nbproject/private/
|
||||||
/nbbuild/
|
build/
|
||||||
/dist/
|
nbbuild/
|
||||||
/nbdist/
|
dist/
|
||||||
/.nb-gradle/
|
nbdist/
|
||||||
/build/
|
.nb-gradle/
|
||||||
|
/local-config/
|
||||||
|
6
Dockerfile
Normal file → Executable file
@@ -4,10 +4,8 @@ VOLUME /sop
|
|||||||
|
|
||||||
# 将所有应用放到一个镜像当中
|
# 将所有应用放到一个镜像当中
|
||||||
ADD sop-gateway/target/*.jar sop/sop-gateway/sop-gateway.jar
|
ADD sop-gateway/target/*.jar sop/sop-gateway/sop-gateway.jar
|
||||||
ADD sop-admin/sop-admin-server/target/*.jar sop/sop-admin/sop-admin.jar
|
ADD sop-admin/sop-admin-backend/backend-boot/target/*.jar sop/sop-admin/sop-admin.jar
|
||||||
ADD sop-website/sop-website-server/target/*.jar sop/sop-website/sop-website.jar
|
ADD sop-example/example-story/target/*.jar sop/sop-story/sop-story.jar
|
||||||
ADD sop-auth/target/*.jar sop/sop-auth/sop-auth.jar
|
|
||||||
ADD sop-example/sop-story/target/*.jar sop/sop-story/sop-story.jar
|
|
||||||
|
|
||||||
# 拷贝启动脚本
|
# 拷贝启动脚本
|
||||||
COPY docker-entrypoint.sh /usr/local/bin/
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
215
README.md
Normal file → Executable file
@@ -1,144 +1,137 @@
|
|||||||
# SOP(Simple Open Platform)
|
# SOP(Simple Open Platform)
|
||||||
|
一个开放平台解决方案项目,基于dubbo实现,目标让用户快速搭建自己的开放平台。
|
||||||
一个开放平台解决方案项目,基于Spring Cloud实现,目标让用户快速搭建自己的开放平台。
|
|
||||||
|
|
||||||
通过简单的配置后,你的项目就具备了和支付宝开放平台的一样的接口提供能力。
|
通过简单的配置后,你的项目就具备了和支付宝开放平台的一样的接口提供能力。
|
||||||
|
|
||||||
SOP封装了开放平台大部分功能包括:签名验证、统一异常处理、统一返回内容 、业务参数验证(JSR-303)、秘钥管理等,未来还会实现更多功能。
|
SOP封装了开放平台大部分功能包括:签名验证、统一异常处理、统一返回内容 、业务参数验证(JSR-303)、秘钥管理等,未来还会实现更多功能。
|
||||||
|
|
||||||
## 项目特点
|
|
||||||
|
|
||||||
- 接入方式简单,与老项目不冲突,老项目注册到注册中心,然后在方法上加上注解即可。
|
## 项目特点
|
||||||
- 架构松耦合,业务代码实现在各自微服务上,SOP不参与业务实现,这也是Spring Cloud微服务体系带来的好处。
|
+ 接入方式简单,与老项目不冲突,老项目注册到注册中心,然后在方法上加上注解即可。
|
||||||
- 扩展简单,开放平台对应的功能各自独立,可以自定义实现自己的需求,如:更改参数,更改签名规则等。
|
+ 架构松耦合,业务代码实现在各自微服务上,SOP不参与业务实现,这也是dubbo微服务体系带来的好处。
|
||||||
|
+ 扩展简单,开放平台对应的功能各自独立,可以自定义实现自己的需求,如:更改参数,更改签名规则等。
|
||||||
|
|
||||||
## 谁可以使用这个项目
|
## 谁可以使用这个项目
|
||||||
|
+ 有现成的项目,想改造成开放平台供他人调用
|
||||||
- 有现成的项目,想改造成开放平台供他人调用
|
+ 有现成的项目,想暴露其中几个接口并通过开放平台供他人调用
|
||||||
- 有现成的项目,想暴露其中几个接口并通过开放平台供他人调用
|
+ 想搭一个开放平台新项目,并结合微服务的方式去维护
|
||||||
- 想搭一个开放平台新项目,并结合微服务的方式去维护
|
+ 对开放平台感兴趣的朋友
|
||||||
- 对开放平台感兴趣的朋友
|
|
||||||
|
|
||||||
以上情况都可以考虑使用SOP
|
以上情况都可以考虑使用SOP
|
||||||
|
|
||||||
## 例子
|
## 例子
|
||||||
|
开放接口定义
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// 加一个注解即可
|
/**
|
||||||
@Open("story.get")
|
* 支付接口
|
||||||
@RequestMapping("/get")
|
*
|
||||||
public StoryResult get() {
|
* @author 六如
|
||||||
StoryResult result = new StoryResult();
|
*/
|
||||||
result.setId(1L);
|
@Api("支付接口")
|
||||||
result.setName("海底小纵队");
|
public interface OpenPayment {
|
||||||
return result;
|
|
||||||
|
@ApiOperation(
|
||||||
|
value = "手机网站支付接口",
|
||||||
|
notes = "该接口是页面跳转接口,用于生成用户访问跳转链接。" +
|
||||||
|
"请在服务端执行SDK中pageExecute方法,读取响应中的body()结果。" +
|
||||||
|
"该结果用于跳转到页面,返回到用户浏览器渲染或重定向跳转到页面。" +
|
||||||
|
"具体使用方法请参考 <a href=\"https://torna.cn\" target=\"_blank\">接入指南</a>"
|
||||||
|
)
|
||||||
|
@Open("pay.trade.wap.pay")
|
||||||
|
PayTradeWapPayResponse tradeWapPay(PayTradeWapPayRequest request);
|
||||||
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
接口实现
|
||||||
|
|
||||||
|
```java
|
||||||
|
/**
|
||||||
|
* 开放接口实现
|
||||||
|
*
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
@DubboService(validation = "true")
|
||||||
|
public class OpenPaymentImpl implements OpenPayment {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PayTradeWapPayResponse tradeWapPay(PayTradeWapPayRequest request) {
|
||||||
|
PayTradeWapPayResponse payTradeWapPayResponse = new PayTradeWapPayResponse();
|
||||||
|
payTradeWapPayResponse.setPageRedirectionData(UUID.randomUUID().toString());
|
||||||
|
return payTradeWapPayResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
调用:
|
调用:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
// 公共请求参数
|
@Test
|
||||||
Map<String, String> params = new HashMap<String, String>();
|
public void testGet() throws Exception {
|
||||||
params.put("app_id", appId);
|
// 公共请求参数
|
||||||
params.put("method", "story.get");
|
Map<String, String> params = new HashMap<String, String>();
|
||||||
params.put("format", "json");
|
params.put("app_id", appId);
|
||||||
params.put("charset", "utf-8");
|
params.put("method", "pay.trade.wap.pay");
|
||||||
params.put("sign_type", "RSA2");
|
params.put("format", "json");
|
||||||
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
params.put("charset", "utf-8");
|
||||||
params.put("version", "1.0");
|
params.put("sign_type", "RSA2");
|
||||||
|
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||||
|
params.put("version", "1.0");
|
||||||
|
|
||||||
// 业务参数
|
// 业务参数
|
||||||
Map<String, String> bizContent = new HashMap<>();
|
Map<String, Object> bizContent = new HashMap<>();
|
||||||
bizContent.put("id", "1");
|
bizContent.put("outTradeNo", "70501111111S001111119");
|
||||||
bizContent.put("name", "葫芦娃");
|
bizContent.put("totalAmount", "9.00");
|
||||||
|
bizContent.put("subject", "衣服");
|
||||||
|
bizContent.put("productCode", "QUICK_WAP_WAY");
|
||||||
|
|
||||||
params.put("biz_content", JSON.toJSONString(bizContent));
|
params.put("biz_content", JSON.toJSONString(bizContent));
|
||||||
|
String content = AlipaySignature.getSignContent(params);
|
||||||
|
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
|
||||||
|
params.put("sign", sign);
|
||||||
|
|
||||||
System.out.println("----------- 请求信息 -----------");
|
System.out.println("----------- 请求信息 -----------");
|
||||||
System.out.println("请求参数:" + buildParamQuery(params));
|
System.out.println("请求参数:" + buildParamQuery(params));
|
||||||
System.out.println("商户秘钥:" + privateKey);
|
System.out.println("商户秘钥:" + privateKey);
|
||||||
String content = AlipaySignature.getSignContent(params);
|
System.out.println("待签名内容:" + content);
|
||||||
System.out.println("待签名内容:" + content);
|
System.out.println("签名(sign):" + sign);
|
||||||
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
|
System.out.println("URL参数:" + buildUrlQuery(params));
|
||||||
System.out.println("签名(sign):" + sign);
|
|
||||||
|
|
||||||
params.put("sign", sign);
|
System.out.println("----------- 返回结果 -----------");
|
||||||
System.out.println("URL参数:" + buildUrlQuery(params));
|
String responseData = postJson(url, params);// 发送请求
|
||||||
|
System.out.println(responseData);
|
||||||
System.out.println("----------- 返回结果 -----------");
|
}
|
||||||
String responseData = get(url, params);// 发送请求
|
|
||||||
System.out.println(responseData);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 架构图
|
SDK调用
|
||||||
|
|
||||||

|
```java
|
||||||
|
@Test
|
||||||
|
public void test() {
|
||||||
|
PayTradeWapPayRequest request = new PayTradeWapPayRequest();
|
||||||
|
|
||||||
> 如上图所示,整个系统运行后,开发者只需关注微服务中的业务代码,接口变更后重新部署微服务应用即可
|
PayTradeWapPayModel model = new PayTradeWapPayModel();
|
||||||
|
model.setOutTradeNo("70501111111S001111119");
|
||||||
|
model.setTotalAmount(new BigDecimal("1000"));
|
||||||
|
model.setSubject("衣服");
|
||||||
|
model.setProductCode("QUICK_WAP_WAY");
|
||||||
|
|
||||||
|
request.setBizModel(model);
|
||||||
|
|
||||||
## 已完成列表
|
Result<PayTradeWapPayResponse> result = client.execute(request);
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
PayTradeWapPayResponse response = result.getData();
|
||||||
|
System.out.println(response);
|
||||||
|
} else {
|
||||||
|
System.out.println(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- 签名验证
|
## 整体架构
|
||||||
- 统一异常处理
|

|
||||||
- 统一返回内容
|
|
||||||
- session管理
|
|
||||||
- 秘钥管理
|
|
||||||
- 微服务端自动验证(JSR-303)
|
|
||||||
- Admin管理平台,统一管理微服务配置,管理路由管理,微服务上下线
|
|
||||||
- 门户网站,提供用户注册账号
|
|
||||||
- 接入方管理+秘钥管理
|
|
||||||
- 接口权限分配
|
|
||||||
- 文件上传/下载
|
|
||||||
- 提供基础SDK(含:Java,C++,C#,Python,Go,Rust,Nodejs)
|
|
||||||
- 接口限流
|
|
||||||
- 文档整合
|
|
||||||
- 应用授权
|
|
||||||
- 监控日志
|
|
||||||
- 注册中心支持nacos/eureka
|
|
||||||
- 网关动态修改参数
|
|
||||||
- 预发布/灰度环境切换
|
|
||||||
|
|
||||||
## 界面预览
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 门户网站
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 工程说明
|
|
||||||
|
|
||||||
> 运行环境:JDK8,Maven3,[Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html),Mysql
|
|
||||||
|
|
||||||
- doc:开发文档
|
|
||||||
- sop-common:公共模块,封装常用功能,包含签名校验、错误处理、限流等功能
|
|
||||||
- sop-gateway:网关,统一访问入口,含`Spring Cloud Zuul`和`Spring Cloud Gateway`实现
|
|
||||||
- sop-example:微服务示例,含springboot,springmvc示例
|
|
||||||
- sop-website:开放平台对应网站,提供文档API、沙箱测试等内容
|
|
||||||
- sop-auth:应用授权服务示例
|
|
||||||
- sop-admin:后台管理
|
|
||||||
- sop-sdk:基础sdk,含Java、C#版本
|
|
||||||
- sop-test:接口调用测试用例
|
|
||||||
|
|
||||||
## 分支说明
|
|
||||||
|
|
||||||
- master:发版分支
|
|
||||||
- develop:日常开发分支
|
|
||||||
- eureka:使用eureka注册中心
|
|
||||||
- pr:接受PR的分支,提交PR请提交到此分支
|
|
||||||
|
|
||||||
[更新说明](./changelog.md)
|
|
||||||
|
|
||||||
## 相关文档
|
|
||||||
|
|
||||||
[开发文档](http://durcframework.gitee.io/sop)
|
|
||||||
|
BIN
asset/arc.jpg
Executable file
After Width: | Height: | Size: 72 KiB |
49
build-admin.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
# 构建admin, 结果输出在dist/sop-admin目录
|
||||||
|
|
||||||
|
# 获取当前路径并赋值给变量 current_path
|
||||||
|
current_path=$(pwd)
|
||||||
|
|
||||||
|
# 打印变量的值,以验证赋值是否成功
|
||||||
|
echo "当前路径是: $current_path"
|
||||||
|
|
||||||
|
# 构建目录
|
||||||
|
dist_dir="dist"
|
||||||
|
# 服务端文件夹名称
|
||||||
|
# 执行文件名称
|
||||||
|
app_name="sop-admin"
|
||||||
|
version="5.0"
|
||||||
|
build_folder="${app_name}-${version}"
|
||||||
|
# 输出目录
|
||||||
|
target_dir="$dist_dir/${build_folder}"
|
||||||
|
|
||||||
|
server_source=sop-admin/sop-admin-backend/admin-boot
|
||||||
|
# admin前端路径
|
||||||
|
front_source=sop-admin/sop-admin-frontend
|
||||||
|
|
||||||
|
# ------ 构建前端 ------
|
||||||
|
echo "开始构建sop-admin前端..."
|
||||||
|
cd $front_source
|
||||||
|
sh build.sh
|
||||||
|
cd $current_path
|
||||||
|
|
||||||
|
|
||||||
|
# ------ 构建后端 ------
|
||||||
|
echo "开始构建sop-admin服务端..."
|
||||||
|
|
||||||
|
mvn clean package -pl $server_source -am -DskipTests
|
||||||
|
|
||||||
|
# ------ 复制文件 ------
|
||||||
|
if [ ! -d "$target_dir" ]; then
|
||||||
|
mkdir -p $target_dir
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf ${target_dir}/*
|
||||||
|
|
||||||
|
# 复制前端资源
|
||||||
|
echo "复制前端文件到$target_dir"
|
||||||
|
cp -r ${front_source}/dist ./$target_dir
|
||||||
|
|
||||||
|
# 复制服务端资源
|
||||||
|
cp -r ${server_source}/target/*.jar $target_dir
|
||||||
|
|
||||||
|
echo "服务端构建完毕,构建结果在${target_dir}文件夹下"
|
37
build-gateway.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 构建网关
|
||||||
|
|
||||||
|
|
||||||
|
# 获取当前路径并赋值给变量 current_path
|
||||||
|
current_path=$(pwd)
|
||||||
|
|
||||||
|
# 打印变量的值,以验证赋值是否成功
|
||||||
|
echo "当前路径是: $current_path"
|
||||||
|
|
||||||
|
# 构建目录
|
||||||
|
dist_dir="dist"
|
||||||
|
# 服务端文件夹名称
|
||||||
|
# 执行文件名称
|
||||||
|
app_name="sop-gateway"
|
||||||
|
version="5.0"
|
||||||
|
build_folder="${app_name}-${version}"
|
||||||
|
# 输出目录
|
||||||
|
target_dir="$dist_dir/${build_folder}"
|
||||||
|
|
||||||
|
server_source=sop-gateway
|
||||||
|
|
||||||
|
# ------ 构建后端 ------
|
||||||
|
echo "开始构建sop-gateway..."
|
||||||
|
|
||||||
|
mvn clean package -pl $server_source -am -DskipTests
|
||||||
|
|
||||||
|
# ------ 复制文件 ------
|
||||||
|
if [ ! -d "$target_dir" ]; then
|
||||||
|
mkdir -p $target_dir
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf ${target_dir}/*
|
||||||
|
|
||||||
|
# 复制服务端资源
|
||||||
|
cp -r ${server_source}/target/*.jar $target_dir
|
||||||
|
|
||||||
|
echo "服务端构建完毕,构建结果在${target_dir}文件夹下"
|
49
build-website.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
# 构建admin, 结果输出在dist/sop-admin目录
|
||||||
|
|
||||||
|
# 获取当前路径并赋值给变量 current_path
|
||||||
|
current_path=$(pwd)
|
||||||
|
|
||||||
|
# 打印变量的值,以验证赋值是否成功
|
||||||
|
echo "当前路径是: $current_path"
|
||||||
|
|
||||||
|
# 构建目录
|
||||||
|
dist_dir="dist"
|
||||||
|
# 服务端文件夹名称
|
||||||
|
# 执行文件名称
|
||||||
|
app_name="sop-website"
|
||||||
|
version="5.0"
|
||||||
|
build_folder="${app_name}-${version}"
|
||||||
|
# 输出目录
|
||||||
|
target_dir="$dist_dir/${build_folder}"
|
||||||
|
|
||||||
|
server_source=sop-website/sop-website-backend/website-boot
|
||||||
|
# admin前端路径
|
||||||
|
front_source=sop-website/sop-website-frontend
|
||||||
|
|
||||||
|
# ------ 构建前端 ------
|
||||||
|
echo "开始构建sop-website前端..."
|
||||||
|
cd $front_source
|
||||||
|
sh build.sh
|
||||||
|
cd $current_path
|
||||||
|
|
||||||
|
|
||||||
|
# ------ 构建后端 ------
|
||||||
|
echo "开始构建sop-website服务端..."
|
||||||
|
|
||||||
|
mvn clean package -pl $server_source -am -DskipTests
|
||||||
|
|
||||||
|
# ------ 复制文件 ------
|
||||||
|
if [ ! -d "$target_dir" ]; then
|
||||||
|
mkdir -p $target_dir
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf ${target_dir}/*
|
||||||
|
|
||||||
|
# 复制前端资源
|
||||||
|
echo "复制前端文件到$target_dir"
|
||||||
|
cp -r ${front_source}/dist ./$target_dir
|
||||||
|
|
||||||
|
# 复制服务端资源
|
||||||
|
cp -r ${server_source}/target/*.jar $target_dir
|
||||||
|
|
||||||
|
echo "服务端构建完毕,构建结果在${target_dir}文件夹下"
|
418
changelog.md
Normal file → Executable file
@@ -1,419 +1,5 @@
|
|||||||
# changelog
|
# changelog
|
||||||
|
|
||||||
## 4.4.2
|
## 5.0
|
||||||
|
|
||||||
- 优化参数绑定
|
全面重构,欢迎体验:[文档](https://www.yuque.com/u1604442/sop)
|
||||||
|
|
||||||
## 4.4.1
|
|
||||||
|
|
||||||
- 修复单值参数绑定问题
|
|
||||||
|
|
||||||
## 4.4.0
|
|
||||||
|
|
||||||
**【重要】:升级前请阅读 [升级到4.4.0注意事项](./升级到4.4.0注意事项.md)**
|
|
||||||
|
|
||||||
- 优化异常处理
|
|
||||||
- 优化网关多实例数据库重复保存问题
|
|
||||||
- 修复`IP`,`IP+路由ID`,`IP+APP_ID`限流不生效问题
|
|
||||||
|
|
||||||
## 4.3.4
|
|
||||||
|
|
||||||
- 修复Request参数在第一位导致绑定失败问题
|
|
||||||
|
|
||||||
## 4.3.3
|
|
||||||
|
|
||||||
- 修复获取eureka地址问题
|
|
||||||
|
|
||||||
## 4.3.2
|
|
||||||
|
|
||||||
- 修复微服务方法获取不到OpenContext问题
|
|
||||||
|
|
||||||
## 4.3.1
|
|
||||||
|
|
||||||
- 修复serviceId有大小写出现404问题
|
|
||||||
- 修复路有监控错误面板分页问题
|
|
||||||
|
|
||||||
## 4.3.0
|
|
||||||
|
|
||||||
- 升级`spring-boot/spring-cloud/spring-cloud-alibaba`版本
|
|
||||||
- 修复`DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144`问题
|
|
||||||
- 修复压测出现`ClosedChannelException`,`Connection has been closed BEFORE response`问题
|
|
||||||
|
|
||||||
## 4.2.7
|
|
||||||
|
|
||||||
- 修复两个微服务相同path问题
|
|
||||||
- 修复Python-SDK下参数传递问题
|
|
||||||
|
|
||||||
## 4.2.6
|
|
||||||
|
|
||||||
- 优化网关超时处理
|
|
||||||
|
|
||||||
## 4.2.5
|
|
||||||
|
|
||||||
- 修复restful负载均衡问题
|
|
||||||
|
|
||||||
## 4.2.4
|
|
||||||
|
|
||||||
- 修复sop-auth模块启动失败问题
|
|
||||||
|
|
||||||
## 4.2.3
|
|
||||||
|
|
||||||
- 修复nacos的group不生效问题
|
|
||||||
|
|
||||||
## 4.2.2
|
|
||||||
|
|
||||||
- 优化版本号
|
|
||||||
|
|
||||||
## 4.2.1
|
|
||||||
|
|
||||||
- 可指定nacos的group,(`spring.cloud.nacos.discovery.group=xx`)
|
|
||||||
- 修复spring循环依赖问题
|
|
||||||
|
|
||||||
## 4.2.0
|
|
||||||
|
|
||||||
需要执行`sop-upgrade-4.2.0.sql`
|
|
||||||
|
|
||||||
- 新增ISV用户平台
|
|
||||||
- 新增门户网站(portal)
|
|
||||||
- 新增`C++`,`Rust`语言SDK
|
|
||||||
|
|
||||||
## 4.1.0
|
|
||||||
|
|
||||||
需要执行`sop-upgrade-4.1.0.sql`
|
|
||||||
|
|
||||||
- 重构路由监控功能
|
|
||||||
- 升级SpringBoot,SpringCloud,SpringCloudAlibaba版本
|
|
||||||
|
|
||||||
## 4.0.3
|
|
||||||
|
|
||||||
- 可定义业务错误码(见`@Open`注解中的`bizCode`属性)
|
|
||||||
- 文档参数可指定最大长度(使用`@Length(max = xx)`)
|
|
||||||
- 修复返回大文本导致的错误(Exceeded limit on max bytes to buffer : 262144)
|
|
||||||
- 增强参数绑定
|
|
||||||
|
|
||||||
## 4.0.2
|
|
||||||
|
|
||||||
- 支持swagger排序(position属性)
|
|
||||||
|
|
||||||
## 4.0.1
|
|
||||||
|
|
||||||
- 修复超大文本无法请求BUG(设置`spring.codec.max-in-memory-size`无效)
|
|
||||||
|
|
||||||
## 4.0.0(不兼容3.x)
|
|
||||||
|
|
||||||
- 新增@Open注解,代替ApiMapping和ApiAbility
|
|
||||||
- service接入减少代码入侵
|
|
||||||
- 修改admin密码存储规则
|
|
||||||
- 完善example
|
|
||||||
|
|
||||||
### 不兼容部分
|
|
||||||
|
|
||||||
- 移除ApiMapping和ApiAbility,改为Open注解
|
|
||||||
- 移除OpenContext
|
|
||||||
- 移除zuul
|
|
||||||
- 移除对easyopen支持
|
|
||||||
|
|
||||||
## 3.2.1
|
|
||||||
|
|
||||||
- 强化RouteInterceptorContext,可获取微服务信息
|
|
||||||
|
|
||||||
## 3.2.0
|
|
||||||
|
|
||||||
- 使用alibaba cloud
|
|
||||||
- 新增Python,Go版本SDK
|
|
||||||
- 返回结果新增全局request_id
|
|
||||||
- 沙箱环境可填写token
|
|
||||||
|
|
||||||
Hoxton.SR3(Spring Cloud Version), 2.2.1.RELEASE(Spring Cloud Alibaba Version), 2.2.5.RELEASE(Spring Boot Version)
|
|
||||||
|
|
||||||
- 优化pom文件
|
|
||||||
- 优化灰度发布
|
|
||||||
|
|
||||||
## 3.1.5
|
|
||||||
|
|
||||||
修复文件上传大小不一致问题
|
|
||||||
|
|
||||||
## 3.1.4
|
|
||||||
|
|
||||||
- 优化跨域
|
|
||||||
|
|
||||||
## 3.1.3
|
|
||||||
|
|
||||||
- 修复监控日志乱码问题
|
|
||||||
|
|
||||||
## 3.1.2
|
|
||||||
|
|
||||||
- 修复图片上传保存后图片破损问题
|
|
||||||
|
|
||||||
## 3.1.1
|
|
||||||
|
|
||||||
- 修复继承WebMvcConfigurationSupport导致的jackson序列化时间问题
|
|
||||||
- 修复微服务接口返回void网关不会返回code和msg问题
|
|
||||||
|
|
||||||
## 3.1.0
|
|
||||||
|
|
||||||
- 新增路由监控功能
|
|
||||||
- 新增路由拦截器
|
|
||||||
- 优化负载均衡策略
|
|
||||||
|
|
||||||
## 3.0.1
|
|
||||||
|
|
||||||
- 增强国际化消息(现SpringCouldGateway支持英文国际化)
|
|
||||||
- 优化限流配置页
|
|
||||||
|
|
||||||
## 3.0.0
|
|
||||||
|
|
||||||
- 重构spring cloud gateway网关
|
|
||||||
- 重构`预发布/灰度发布环境选择`
|
|
||||||
- zuul和gateway网关二合一,可随意切换
|
|
||||||
- 精简配置文件
|
|
||||||
- 优化文档中心页面
|
|
||||||
- 优化接口限流
|
|
||||||
|
|
||||||
## 2.5.10
|
|
||||||
|
|
||||||
- 优化签名认证,优化校验日志打印
|
|
||||||
|
|
||||||
## 2.5.9
|
|
||||||
|
|
||||||
- 优化获取context-path
|
|
||||||
- 修复结果返回产生的NPE问题
|
|
||||||
|
|
||||||
## 2.5.8
|
|
||||||
|
|
||||||
- 优化参数绑定
|
|
||||||
|
|
||||||
## 2.5.7
|
|
||||||
|
|
||||||
- 优化restful接口调用
|
|
||||||
|
|
||||||
## 2.5.6
|
|
||||||
|
|
||||||
- 优化文档显示
|
|
||||||
- 修复路由拉取接口重复BUG
|
|
||||||
|
|
||||||
## 2.5.5
|
|
||||||
|
|
||||||
- 优化文档显示
|
|
||||||
|
|
||||||
## 2.5.4(不建议使用)
|
|
||||||
|
|
||||||
- 可排除其它服务(`sop.service.exclude=your-serviceId1,your-serviceId2`)
|
|
||||||
|
|
||||||
## 2.5.3(不建议使用)
|
|
||||||
|
|
||||||
- 修复JSR-303校验问题
|
|
||||||
|
|
||||||
## 2.5.2(不建议使用)
|
|
||||||
|
|
||||||
- 修复JSR-303校验枚举对象问题
|
|
||||||
|
|
||||||
## 2.5.1
|
|
||||||
|
|
||||||
- JSR-303支持嵌套校验
|
|
||||||
|
|
||||||
## 2.5.0
|
|
||||||
|
|
||||||
**(需要执行`sop-2.5.0.sql`升级文件)**
|
|
||||||
|
|
||||||
- 网关可校验token [doc](https://durcframework.gitee.io/sop/#/files/10089_%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%A1%E9%AA%8Ctoken?t=1572076365259)
|
|
||||||
|
|
||||||
## 2.4.1
|
|
||||||
|
|
||||||
- 优化restful接口调用(如果正在使用此功能,必看 [doc](https://durcframework.gitee.io/sop/#/files/10100_%E6%8F%90%E4%BE%9Brestful%E6%8E%A5%E5%8F%A3?t=1571107529449))
|
|
||||||
|
|
||||||
## 2.4.0
|
|
||||||
|
|
||||||
- 支持自定义限流持续时间(每n秒允许m个请求,需要执行`sop-2.4.0.sql`升级脚本)
|
|
||||||
|
|
||||||
## 2.3.2
|
|
||||||
|
|
||||||
- 支持spring cloud gateway下restful接口调用
|
|
||||||
|
|
||||||
## 2.3.1
|
|
||||||
|
|
||||||
- 修复restful接口调用通配符问题
|
|
||||||
|
|
||||||
## 2.3.0
|
|
||||||
|
|
||||||
- 支持请求restful接口(设置`sop.restful.enable=true`)
|
|
||||||
|
|
||||||
## 2.2.0(需要执行`sop-2.2.0.sql`升级文件)
|
|
||||||
|
|
||||||
- 支持eureka注册中心,见`eureka`分支
|
|
||||||
- 签名内容支持urlencode(设置`sign.urlencode=true`)
|
|
||||||
- 可扩展其它注册中心
|
|
||||||
|
|
||||||
## 2.1.3
|
|
||||||
|
|
||||||
- 优化文件上传校验
|
|
||||||
|
|
||||||
## 2.1.2
|
|
||||||
|
|
||||||
- 优化获取路由配置
|
|
||||||
|
|
||||||
## 2.1.1
|
|
||||||
|
|
||||||
- 修复springmvc获取路由问题
|
|
||||||
|
|
||||||
## 2.1.0
|
|
||||||
|
|
||||||
- 支持分布式限流(redis实现)
|
|
||||||
- 可调整JSR-303校验顺序
|
|
||||||
- 修复springmvc工程注册到nacos无法读取路由配置问题
|
|
||||||
|
|
||||||
## 2.0.0
|
|
||||||
|
|
||||||
- 全面使用nacos,舍弃zookeeper(1.x版本见1.x分支)
|
|
||||||
- 可自定义文档模块显示顺序
|
|
||||||
|
|
||||||
## 1.15.2
|
|
||||||
|
|
||||||
- 优化SpringCloudGateway上传文件功能
|
|
||||||
- 优化SpringCloudGateway动态修改参数功能
|
|
||||||
|
|
||||||
## 1.15.1
|
|
||||||
|
|
||||||
- 修复未配置正确MessageConverter导致的异常
|
|
||||||
|
|
||||||
## 1.15.0
|
|
||||||
|
|
||||||
- 优化预发布、灰度
|
|
||||||
- 网关动态修改请求参数
|
|
||||||
- 支持swagger-bootstrap插件
|
|
||||||
- 优化admin服务列表显示
|
|
||||||
- 优化文档刷新逻辑
|
|
||||||
- 新增测试all in one
|
|
||||||
- 修复中文乱码问题
|
|
||||||
|
|
||||||
## 1.14.0
|
|
||||||
|
|
||||||
- 支持预发布、灰度发布环境
|
|
||||||
|
|
||||||
## 1.13.7
|
|
||||||
|
|
||||||
- 修复修复context-path识别问题
|
|
||||||
|
|
||||||
## 1.13.6
|
|
||||||
|
|
||||||
- 修复@RequestBody不能绑定问题
|
|
||||||
|
|
||||||
## 1.13.5
|
|
||||||
|
|
||||||
- 修复postJson下version获取不到问题
|
|
||||||
|
|
||||||
## 1.13.4
|
|
||||||
|
|
||||||
- 修复admin服务列表最后更新时间不显示问题
|
|
||||||
- 优化上传路由配置逻辑
|
|
||||||
- 微服务可获得access_token, notify_url参数
|
|
||||||
|
|
||||||
## 1.13.3
|
|
||||||
|
|
||||||
- 优化参数绑定
|
|
||||||
|
|
||||||
## 1.13.2
|
|
||||||
|
|
||||||
- 修复json方式请求获取不到参数问题
|
|
||||||
- 微服务端新增获取开放平台请求参数
|
|
||||||
|
|
||||||
## 1.13.1
|
|
||||||
|
|
||||||
- 支持json方式请求(application/json)
|
|
||||||
- 支持传统web服务开发(见文档`传统web开发`)
|
|
||||||
|
|
||||||
## 1.13.0
|
|
||||||
|
|
||||||
- 新增IP黑名单
|
|
||||||
|
|
||||||
## 1.12.4
|
|
||||||
|
|
||||||
- 优化属性文件配置
|
|
||||||
- 新增sleuth接入文档
|
|
||||||
- admin的isv列表新增备注字段
|
|
||||||
|
|
||||||
## 1.12.3
|
|
||||||
|
|
||||||
- 修复删除zk节点导致的BUG
|
|
||||||
|
|
||||||
## 1.12.2
|
|
||||||
|
|
||||||
- 沙盒支持文件上传
|
|
||||||
|
|
||||||
## 1.12.1
|
|
||||||
|
|
||||||
- 修复重启网关路由状态重置BUG
|
|
||||||
- 优化SpringCloudGateway
|
|
||||||
|
|
||||||
## 1.12.0
|
|
||||||
|
|
||||||
- admin后台新增角色管理
|
|
||||||
- 支持nacos作为注册中心
|
|
||||||
|
|
||||||
## 1.11.0
|
|
||||||
|
|
||||||
- 秘钥管理改造
|
|
||||||
- 服务端返回sign
|
|
||||||
- 新增SDK返回sign处理
|
|
||||||
- 新增沙箱环境
|
|
||||||
|
|
||||||
## 1.10.0
|
|
||||||
|
|
||||||
- 新增监控日志
|
|
||||||
|
|
||||||
## 1.9.0
|
|
||||||
|
|
||||||
- 改造限流
|
|
||||||
- 增强参数绑定
|
|
||||||
|
|
||||||
## 1.8.0
|
|
||||||
|
|
||||||
- 支持文件上传
|
|
||||||
|
|
||||||
## 1.7.2
|
|
||||||
|
|
||||||
- 修复微服务参数绑定BUG
|
|
||||||
- Admin新增vue界面
|
|
||||||
|
|
||||||
## 1.7.1
|
|
||||||
|
|
||||||
- 支持接口名版本号放在url后面
|
|
||||||
|
|
||||||
## 1.7.0
|
|
||||||
|
|
||||||
- 可自定义数据节点名称
|
|
||||||
|
|
||||||
## 1.6.0
|
|
||||||
|
|
||||||
- 新增应用授权
|
|
||||||
|
|
||||||
## 1.5.0
|
|
||||||
|
|
||||||
- admin新增signType字段
|
|
||||||
- 修复easyopen接入无法访问BUG
|
|
||||||
|
|
||||||
## 1.4.0
|
|
||||||
|
|
||||||
- 新增文档分组显示
|
|
||||||
- 支持easyopen文档注解
|
|
||||||
- BUG修复
|
|
||||||
|
|
||||||
## 1.3.0
|
|
||||||
|
|
||||||
- 新增接口限流功能 [doc](http://durcframework.gitee.io/sop/#/files/10092_%E6%8E%A5%E5%8F%A3%E9%99%90%E6%B5%81?t=1555378655699)
|
|
||||||
- 新增文档整合功能 [doc](http://durcframework.gitee.io/sop/#/files/10041_%E7%BC%96%E5%86%99%E6%96%87%E6%A1%A3?t=1555378655698)
|
|
||||||
- 新增springmvc项目接入demo
|
|
||||||
|
|
||||||
## 1.2.0
|
|
||||||
|
|
||||||
- SOP Admin新增用户登录
|
|
||||||
- 新增基础SDK(Java,C#) [doc](http://durcframework.gitee.io/sop/#/files/10095_SDK%E5%BC%80%E5%8F%91?t=1554693919597)
|
|
||||||
|
|
||||||
## 1.1.0
|
|
||||||
|
|
||||||
- 新增ISV管理 [doc](http://durcframework.gitee.io/sop/#/files/10085_ISV%E7%AE%A1%E7%90%86?t=1554123435621)
|
|
||||||
- 新增接口授权 [doc](http://durcframework.gitee.io/sop/#/files/10090_%E8%B7%AF%E7%94%B1%E6%8E%88%E6%9D%83?t=1554123435621)
|
|
||||||
|
|
||||||
## 1.0.0
|
|
||||||
|
|
||||||
- 第一次发布
|
|
||||||
|
176
checkstyle.xml
Executable file
@@ -0,0 +1,176 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE module PUBLIC
|
||||||
|
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
||||||
|
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
|
||||||
|
|
||||||
|
<module name="Checker">
|
||||||
|
<!-- 文件长度不超过1500行 -->
|
||||||
|
<module name="FileLength">
|
||||||
|
<property name="max" value="2500"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- 长度检查 -->
|
||||||
|
<!-- 每行不超过200个字符 -->
|
||||||
|
<module name="LineLength">
|
||||||
|
<property name="max" value="400"/>
|
||||||
|
</module>
|
||||||
|
<module name="SuppressWarningsFilter" />
|
||||||
|
<!-- 每个java文件一个语法树 -->
|
||||||
|
<module name="TreeWalker">
|
||||||
|
<module name="SuppressWarningsHolder" />
|
||||||
|
<!-- import检查-->
|
||||||
|
<!-- 检查是否从非法的包中导入了类 -->
|
||||||
|
<module name="IllegalImport"/>
|
||||||
|
<!-- 检查是否导入了多余的包 -->
|
||||||
|
<module name="RedundantImport"/>
|
||||||
|
<!-- 没用的import检查,比如:1.没有被用到2.重复的3.import java.lang的4.import 与该类在同一个package的 -->
|
||||||
|
<module name="UnusedImports"/>
|
||||||
|
|
||||||
|
<!-- 注释检查 -->
|
||||||
|
<!-- 检查构造函数的javadoc -->
|
||||||
|
<module name="JavadocType">
|
||||||
|
<property name="allowUnknownTags" value="true"/>
|
||||||
|
<message key="javadoc.missing" value="类注释:缺少Javadoc注释。"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- 命名检查 -->
|
||||||
|
<!-- 局部的final变量,包括catch中的参数的检查 -->
|
||||||
|
<module name="LocalFinalVariableName"/>
|
||||||
|
<!-- 局部的非final型的变量,包括catch中的参数的检查 -->
|
||||||
|
<module name="LocalVariableName"/>
|
||||||
|
<!-- 包名的检查(只允许小写字母),默认^[a-z]+(\.[a-zA-Z_][a-zA-Z_0-9_]*)*$ -->
|
||||||
|
<module name="PackageName">
|
||||||
|
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
|
||||||
|
<message key="name.invalidPattern" value="包名 ''{0}'' 要符合 ''{1}''格式."/>
|
||||||
|
</module>
|
||||||
|
<!-- 仅仅是static型的变量(不包括static final型)的检查 -->
|
||||||
|
<module name="StaticVariableName"/>
|
||||||
|
<!-- Class或Interface名检查,默认^[A-Z][a-zA-Z0-9]*$-->
|
||||||
|
<module name="TypeName">
|
||||||
|
<property name="severity" value="warning"/>
|
||||||
|
<message key="name.invalidPattern" value="名称 ''{0}'' 要符合 ''{1}''格式."/>
|
||||||
|
</module>
|
||||||
|
<!-- 非static型变量的检查
|
||||||
|
<module name="MemberName"/>
|
||||||
|
-->
|
||||||
|
<!-- 方法名的检查 -->
|
||||||
|
<module name="MethodName"/>
|
||||||
|
<!-- 方法的参数名
|
||||||
|
<module name="ParameterName "/>
|
||||||
|
-->
|
||||||
|
<!-- 常量名的检查(只允许大写),默认^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$ -->
|
||||||
|
<module name="ConstantName"/>
|
||||||
|
|
||||||
|
<!-- 定义检查 -->
|
||||||
|
<!-- 检查数组类型定义的样式 -->
|
||||||
|
<module name="ArrayTypeStyle"/>
|
||||||
|
<!-- 检查long型定义是否有大写的“L” -->
|
||||||
|
<module name="UpperEll"/>
|
||||||
|
<!-- 方法不超过100行 -->
|
||||||
|
<module name="MethodLength">
|
||||||
|
<property name="tokens" value="METHOD_DEF"/>
|
||||||
|
<property name="max" value="300"/>
|
||||||
|
</module>
|
||||||
|
<!-- 方法的参数个数不超过8个。 并且不对构造方法进行检查-->
|
||||||
|
<module name="ParameterNumber">
|
||||||
|
<property name="max" value="8"/>
|
||||||
|
<property name="ignoreOverriddenMethods" value="true"/>
|
||||||
|
<property name="tokens" value="METHOD_DEF"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- 空格检查-->
|
||||||
|
<!-- 方法名后跟左圆括号"(" -->
|
||||||
|
<module name="MethodParamPad"/>
|
||||||
|
<!-- 在类型转换时,不允许左圆括号右边有空格,也不允许与右圆括号左边有空格 -->
|
||||||
|
<module name="TypecastParenPad"/>
|
||||||
|
<!-- 检查在某个特定关键字之后应保留空格 -->
|
||||||
|
<module name="NoWhitespaceAfter"/>
|
||||||
|
<!-- 检查在某个特定关键字之前应保留空格 -->
|
||||||
|
<module name="NoWhitespaceBefore"/>
|
||||||
|
<!-- 圆括号空白 -->
|
||||||
|
<module name="ParenPad"/>
|
||||||
|
<!-- 检查分隔符是否在空白之后 -->
|
||||||
|
<module name="WhitespaceAfter"/>
|
||||||
|
<!-- 检查分隔符周围是否有空白 -->
|
||||||
|
<module name="WhitespaceAround"/>
|
||||||
|
|
||||||
|
<!-- 修饰符检查 -->
|
||||||
|
<!-- 检查修饰符的顺序是否遵照java语言规范,默认public、protected、private、abstract、static、final、transient、volatile、synchronized、native、strictfp -->
|
||||||
|
<module name="ModifierOrder"/>
|
||||||
|
<!-- 检查接口和annotation中是否有多余修饰符,如接口方法不必使用public -->
|
||||||
|
<module name="RedundantModifier"/>
|
||||||
|
|
||||||
|
<!-- 代码块检查 -->
|
||||||
|
<!-- 检查是否有嵌套代码块 -->
|
||||||
|
<module name="AvoidNestedBlocks"/>
|
||||||
|
<!-- 检查是否有空代码块 -->
|
||||||
|
<module name="EmptyBlock"/>
|
||||||
|
<!-- 检查左大括号位置 -->
|
||||||
|
<module name="LeftCurly"/>
|
||||||
|
<!-- 检查代码块是否缺失{} -->
|
||||||
|
<module name="NeedBraces"/>
|
||||||
|
<!-- 检查右大括号位置 -->
|
||||||
|
<module name="RightCurly"/>
|
||||||
|
|
||||||
|
<!-- 代码检查 -->
|
||||||
|
<!-- 检查空的代码段 -->
|
||||||
|
<module name="EmptyStatement"/>
|
||||||
|
<!-- 检查在重写了equals方法后是否重写了hashCode方法 -->
|
||||||
|
<module name="EqualsHashCode"/>
|
||||||
|
<!-- 检查局部变量或参数是否隐藏了类中的变量 -->
|
||||||
|
<module name="HiddenField">
|
||||||
|
<property name="tokens" value="VARIABLE_DEF"/>
|
||||||
|
</module>
|
||||||
|
<!-- 检查子表达式中是否有赋值操作 -->
|
||||||
|
<module name="InnerAssignment"/>
|
||||||
|
<!-- 检查switch语句是否有default -->
|
||||||
|
<module name="MissingSwitchDefault"/>
|
||||||
|
<!-- 检查是否有过度复杂的布尔表达式 -->
|
||||||
|
<module name="SimplifyBooleanExpression"/>
|
||||||
|
<!-- 检查是否有过于复杂的布尔返回代码段 -->
|
||||||
|
<module name="SimplifyBooleanReturn"/>
|
||||||
|
|
||||||
|
<!-- 类设计检查 -->
|
||||||
|
<!-- 检查类是否为扩展设计l -->
|
||||||
|
<!-- 检查只有private构造函数的类是否声明为final
|
||||||
|
<module name="FinalClass"/>
|
||||||
|
-->
|
||||||
|
<!-- 检查接口是否仅定义类型 -->
|
||||||
|
<module name="InterfaceIsType"/>
|
||||||
|
<!-- 检查类成员的可见度 检查类成员的可见性。只有static final 成员是public的
|
||||||
|
除非在本检查的protectedAllowed和packagedAllowed属性中进行了设置-->
|
||||||
|
<module name="VisibilityModifier">
|
||||||
|
<property name="packageAllowed" value="true"/>
|
||||||
|
<property name="protectedAllowed" value="true"/>
|
||||||
|
</module>
|
||||||
|
|
||||||
|
<!-- 语法 -->
|
||||||
|
<!-- String的比较不能用!= 和 == -->
|
||||||
|
<module name="StringLiteralEquality"/>
|
||||||
|
<!-- 限制for循环最多嵌套2层 -->
|
||||||
|
<module name="NestedForDepth">
|
||||||
|
<property name="max" value="2"/>
|
||||||
|
</module>
|
||||||
|
<!-- if最多嵌套3层 -->
|
||||||
|
<module name="NestedIfDepth">
|
||||||
|
<property name="max" value="10"/>
|
||||||
|
</module>
|
||||||
|
<!-- 检查未被注释的main方法,排除以Appllication结尾命名的类 -->
|
||||||
|
<module name="UncommentedMain">
|
||||||
|
<property name="excludedClasses" value=".*[Application,Test]$"/>
|
||||||
|
</module>
|
||||||
|
<!-- 禁止使用System.out.println -->
|
||||||
|
<module name="Regexp">
|
||||||
|
<property name="format" value="System\.out\.println"/>
|
||||||
|
<property name="illegalPattern" value="true"/>
|
||||||
|
</module>
|
||||||
|
<!--try catch 异常处理数量 3-->
|
||||||
|
<module name="NestedTryDepth ">
|
||||||
|
<property name="max" value="3"/>
|
||||||
|
</module>
|
||||||
|
<!-- clone方法必须调用了super.clone() -->
|
||||||
|
<module name="SuperClone"/>
|
||||||
|
<!-- finalize 必须调用了super.finalize() -->
|
||||||
|
<module name="SuperFinalize"/>
|
||||||
|
</module>
|
||||||
|
</module>
|
27
doc/.gitignore
vendored
@@ -1,27 +0,0 @@
|
|||||||
/target/
|
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
|
||||||
|
|
||||||
### STS ###
|
|
||||||
.apt_generated
|
|
||||||
.classpath
|
|
||||||
.factorypath
|
|
||||||
.project
|
|
||||||
.settings
|
|
||||||
.springBeans
|
|
||||||
.sts4-cache
|
|
||||||
|
|
||||||
### IntelliJ IDEA ###
|
|
||||||
.idea
|
|
||||||
*.iws
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.png
|
|
||||||
|
|
||||||
### NetBeans ###
|
|
||||||
/nbproject/private/
|
|
||||||
/nbbuild/
|
|
||||||
/dist/
|
|
||||||
/nbdist/
|
|
||||||
/.nb-gradle/
|
|
||||||
/build/
|
|
||||||
|
|
@@ -1,11 +0,0 @@
|
|||||||
# 开发文档
|
|
||||||
|
|
||||||
文档放在docs/files下,写完文档记得执行下`SidebarTest.main()`方法
|
|
||||||
|
|
||||||
配合gitee pages服务使用,gitee pages服务指定docs目录。
|
|
||||||
|
|
||||||
## 本地查看开发文档
|
|
||||||
|
|
||||||
- 前提:先安装好npm,[npm安装教程](https://blog.csdn.net/zhangwenwu2/article/details/52778521)。建议使用淘宝镜像。
|
|
||||||
- 安装docsify,执行npm命令`npm i docsify-cli -g --registry=https://registry.npm.taobao.org`
|
|
||||||
- cd到当前目录,运行命令`docsify serve docs`,然后访问:`http://localhost:3000`即可查看。
|
|
@@ -1,3 +0,0 @@
|
|||||||
# SOP开发文档
|
|
||||||
|
|
||||||
Git地址:[SOP](https://gitee.com/durcframework/SOP)
|
|
@@ -1 +0,0 @@
|
|||||||
include: [_navbar,_sidebar]
|
|
@@ -1,12 +0,0 @@
|
|||||||

|
|
||||||
|
|
||||||
# docsify <small>4.6.10</small>
|
|
||||||
|
|
||||||
> A magical documentation site generator.
|
|
||||||
|
|
||||||
* Simple and lightweight (~19kB gzipped)
|
|
||||||
* No statically built html files
|
|
||||||
* Multiple themes
|
|
||||||
|
|
||||||
[GitHub](https://github.com/QingWei-Li/docsify/)
|
|
||||||
[Get Started](#docsify)
|
|
@@ -1,3 +0,0 @@
|
|||||||
- 关于
|
|
||||||
- [帮助](/zh-cn/)
|
|
||||||
- [API](/)
|
|
@@ -1,36 +0,0 @@
|
|||||||
* [首页](/?t=1616211903021)
|
|
||||||
* 开发文档
|
|
||||||
* [快速体验](files/10010_快速体验.md?t=1616211903027)
|
|
||||||
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1616211903048)
|
|
||||||
* [新增接口](files/10020_新增接口.md?t=1616211903048)
|
|
||||||
* [开发流程](files/10021_开发流程.md?t=1616211903048)
|
|
||||||
* [用户注册](files/10022_用户注册.md?t=1616211903049)
|
|
||||||
* [业务参数校验](files/10030_业务参数校验.md?t=1616211903049)
|
|
||||||
* [错误处理](files/10040_错误处理.md?t=1616211903049)
|
|
||||||
* [编写文档](files/10041_编写文档.md?t=1616211903049)
|
|
||||||
* [接口交互详解](files/10050_接口交互详解.md?t=1616211903049)
|
|
||||||
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1616211903049)
|
|
||||||
* [ISV管理](files/10085_ISV管理.md?t=1616211903050)
|
|
||||||
* [自定义返回结果](files/10087_自定义返回结果.md?t=1616211903050)
|
|
||||||
* [自定义过滤器](files/10088_自定义过滤器.md?t=1616211903050)
|
|
||||||
* [自定义校验token](files/10089_自定义校验token.md?t=1616211903050)
|
|
||||||
* [网关拦截器](files/10090_网关拦截器.md?t=1616211903050)
|
|
||||||
* [路由授权](files/10091_路由授权.md?t=1616211903050)
|
|
||||||
* [接口限流](files/10092_接口限流.md?t=1616211903051)
|
|
||||||
* [路由监控](files/10093_路由监控.md?t=1616211903051)
|
|
||||||
* [SDK开发](files/10095_SDK开发.md?t=1616211903051)
|
|
||||||
* [应用授权](files/10097_应用授权.md?t=1616211903051)
|
|
||||||
* [提供restful接口](files/10100_提供restful接口.md?t=1616211903051)
|
|
||||||
* [文件上传](files/10104_文件上传.md?t=1616211903051)
|
|
||||||
* [配置Sleuth链路追踪](files/10109_配置Sleuth链路追踪.md?t=1616211903052)
|
|
||||||
* [预发布灰度发布](files/10110_预发布灰度发布.md?t=1616211903052)
|
|
||||||
* [动态修改请求参数](files/10111_动态修改请求参数.md?t=1616211903052)
|
|
||||||
* [使用eureka](files/10112_使用eureka.md?t=1616211903052)
|
|
||||||
* [超时设置](files/10113_超时设置.md?t=1616211903052)
|
|
||||||
* 原理分析
|
|
||||||
* [网关性能测试](files/90001_网关性能测试.md?t=1616211903052)
|
|
||||||
* [原理分析之如何存储路由](files/90011_原理分析之如何存储路由.md?t=1616211903052)
|
|
||||||
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1616211903052)
|
|
||||||
* [原理分析之文档归纳](files/90013_原理分析之文档归纳.md?t=1616211903052)
|
|
||||||
* [原理分析之预发布灰度发布](files/90014_原理分析之预发布灰度发布.md?t=1616211903052)
|
|
||||||
* [常见问题](files/90100_常见问题.md?t=1616211903053)
|
|
@@ -1,52 +0,0 @@
|
|||||||
# 快速体验
|
|
||||||
|
|
||||||
## 方式1
|
|
||||||
|
|
||||||
> 运行环境:JDK8,Maven3,[Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html),Mysql
|
|
||||||
|
|
||||||
- 安装并启动Nacos,[安装教程](https://nacos.io/zh-cn/docs/quick-start.html)
|
|
||||||
- 执行Mysql脚本`sop.sql`(Mysql版本5.6+),5.6以下运行`sop-mysql5.6以下版本.sql`
|
|
||||||
- IDE安装lombok插件,然后打开项目(IDEA下可以打开根pom.xml,然后open as project)
|
|
||||||
- 启动网关:打开sop-gateway下的`application-dev.properties`
|
|
||||||
1. 修改数据库`username/password`
|
|
||||||
2. 指定nacos地址,如果nacos安装在本机则不用改
|
|
||||||
3. 运行`SopGatewayApplication.java`
|
|
||||||
- 启动微服务:打开`sop-example/sop-story`下的`application-dev.properties`文件
|
|
||||||
1. 指定nacos地址,如果nacos安装在本机则不用改
|
|
||||||
2. 运行`SopStoryApplication.java`
|
|
||||||
- 找到sop-test,运行`com.gitee.sop.test.AlipayClientPostTest.testGet`进行接口调用测试
|
|
||||||
|
|
||||||
## 方式2(docker)
|
|
||||||
|
|
||||||
> 前提:安装好docker
|
|
||||||
|
|
||||||
- 安装并启动Nacos,[安装教程](https://nacos.io/zh-cn/docs/quick-start.html)
|
|
||||||
- 执行Mysql脚本`sop.sql`(Mysql版本5.6+),5.6以下运行`sop-mysql5.6以下版本.sql`
|
|
||||||
- 打开`docker-entrypoint.sh`,修改mysql,nacos配置
|
|
||||||
- 执行`docker-build.sh`
|
|
||||||
- 找到sop-test,运行`com.gitee.sop.test.AlipayClientPostTest.testGet`进行接口调用测试
|
|
||||||
|
|
||||||
|
|
||||||
> - admin地址:http://ip:8082 登录账号:admin/123456
|
|
||||||
> - 文档地址:http://ip:8083/
|
|
||||||
|
|
||||||
## 使用admin
|
|
||||||
|
|
||||||
- 找到`sop-admin/sop-admin-server`工程,打开sop-admin-server下的`application-dev.properties`,修改相关配置
|
|
||||||
- 运行`SopAdminServerApplication.java`
|
|
||||||
- 访问:`http://localhost:8082`
|
|
||||||
|
|
||||||
登录账号:admin/123456
|
|
||||||
|
|
||||||
## 启动门户网站
|
|
||||||
|
|
||||||
见:`sop-website/sop-portal/README.md`
|
|
||||||
|
|
||||||
## 启动文档中心/ISV后台
|
|
||||||
|
|
||||||
文档中心代码在sop-website工程中
|
|
||||||
|
|
||||||
- 确保注册中心、网关、微服务正常启动
|
|
||||||
- 修改sop-website-server下的application-dev.properties相关配置
|
|
||||||
- 运行WebsiteServerApplication.java
|
|
||||||
- 访问http://localhost:8083
|
|
@@ -1,217 +0,0 @@
|
|||||||
# 项目接入到SOP
|
|
||||||
|
|
||||||
以springboot项目为例,完整项目可参考sop-example下的sop-story
|
|
||||||
|
|
||||||
- pom.xml添加版本配置
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- springboot 版本-->
|
|
||||||
<spring-boot.version>2.6.15</spring-boot.version>
|
|
||||||
<!-- spring cloud 版本 -->
|
|
||||||
<spring-cloud.version>2021.0.5</spring-cloud.version>
|
|
||||||
<!-- spring cloud alibaba 版本 -->
|
|
||||||
<!-- 具体版本对应关系见:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
|
|
||||||
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
|
|
||||||
```
|
|
||||||
|
|
||||||
- pom.xml添加`<dependencyManagement>`控制版本
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependencyManagement>
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-dependencies</artifactId>
|
|
||||||
<version>${spring-boot.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-dependencies</artifactId>
|
|
||||||
<version>${spring-cloud.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.alibaba.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
|
|
||||||
<version>${spring-cloud-alibaba.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
```
|
|
||||||
|
|
||||||
- pom.xml依赖sop-service-common和nacos服务发现
|
|
||||||
|
|
||||||
```xml
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.gitee.sop</groupId>
|
|
||||||
<artifactId>sop-service-common</artifactId>
|
|
||||||
<version>最新版本</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.alibaba.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
- application.properties配置文件添加
|
|
||||||
|
|
||||||
```properties
|
|
||||||
server.port=2222
|
|
||||||
spring.application.name=story-service
|
|
||||||
# nacos注册中心
|
|
||||||
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
|
|
||||||
```
|
|
||||||
|
|
||||||
- 在springboot启动类上添加`@EnableDiscoveryClient`
|
|
||||||
- 新增一个配置类,继承`AlipayServiceConfiguration.java`,内容为空
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class OpenServiceConfig extends AlipayServiceConfiguration {
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 全局异常处理
|
|
||||||
|
|
||||||
在微服务项目的全局异常处理中添加一句:`ExceptionHolder.hold(request, response, exception);`
|
|
||||||
|
|
||||||
```java
|
|
||||||
@ExceptionHandler(Exception.class)
|
|
||||||
@ResponseBody
|
|
||||||
public Object exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
|
|
||||||
...
|
|
||||||
// 在返回前加这一句
|
|
||||||
ExceptionHolder.hold(request, response, exception);
|
|
||||||
...
|
|
||||||
return ..;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果没有配置全局异常,可参考下面配置
|
|
||||||
|
|
||||||
```java
|
|
||||||
@ControllerAdvice
|
|
||||||
@Slf4j
|
|
||||||
public class StoryGlobalExceptionHandler {
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 捕获手动抛出的异常
|
|
||||||
*
|
|
||||||
* @param request request
|
|
||||||
* @param response response
|
|
||||||
* @param exception 异常信息
|
|
||||||
* @return 返回提示信息
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(Exception.class)
|
|
||||||
@ResponseBody
|
|
||||||
public Object exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
|
|
||||||
// 在返回前加这一句
|
|
||||||
ExceptionHolder.hold(request, response, exception);
|
|
||||||
// 下面可以实现自己的全局异常处理
|
|
||||||
return new ErrorResult(500, exception.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class ErrorResult {
|
|
||||||
private int code;
|
|
||||||
private String msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
到此准备工作就完成了,接下来可前往`新增接口`查看如何新增接口。
|
|
||||||
|
|
||||||
## 非Java项目接入
|
|
||||||
|
|
||||||
如果是非Java项目接入,如php,需要做到以下几点:
|
|
||||||
|
|
||||||
> 1. 提供路由配置接口
|
|
||||||
> 2. 服务注册到nacos,并在nacos的metadata中指定接口路径,metadata的key为:`sop.routes.path`
|
|
||||||
|
|
||||||
|
|
||||||
- 提供路由配置接口
|
|
||||||
|
|
||||||
php应用提供的接口需要返回如下json内容:
|
|
||||||
|
|
||||||
假设请求的接口为:`http://open.xxx.com/get_routes`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"serviceId": "goods-service",
|
|
||||||
"createTime": 1568603471646,
|
|
||||||
"updateTime": 1568603471646,
|
|
||||||
"description": null,
|
|
||||||
"routeDefinitionList": [
|
|
||||||
{
|
|
||||||
"id": "goods.list1.0",
|
|
||||||
"name": "goods.list",
|
|
||||||
"version": "1.0",
|
|
||||||
"uri": "lb://goods-service",
|
|
||||||
"path": "/goods/list_goods",
|
|
||||||
"order": 0,
|
|
||||||
"ignoreValidate": 0,
|
|
||||||
"status": 1,
|
|
||||||
"mergeResult": 1,
|
|
||||||
"permission": 0
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
json参数说明
|
|
||||||
|
|
||||||
|参数名|是否必填|说明|
|
|
||||||
|:----|:----|:----|
|
|
||||||
|serviceId |是|serviceId,服务id |
|
|
||||||
|createTime |是|创建时间,Unix timestamp,毫秒 |
|
|
||||||
|updateTime |是|修改时间,Unix timestamp,毫秒 |
|
|
||||||
|description|否|描述|
|
|
||||||
|routeDefinitionList元素参数说明|是|路由配置,见:routeDefinitionList元素参数说明|
|
|
||||||
|
|
||||||
routeDefinitionList元素参数说明
|
|
||||||
|
|
||||||
|参数名|是否必填|说明|
|
|
||||||
|:----|:----|:----|
|
|
||||||
|id |是|路由id,全局唯一,格式:接口名+版本号 |
|
|
||||||
|name|是|接口名称|
|
|
||||||
|version|是|版本号|
|
|
||||||
|uri|是|格式:lb:// + serviceId,如:lb://goods-service|
|
|
||||||
|path|是|接口path,填端口号后面的path,如你的接口为`http://open.domain.com:8080/goods/list_goods`,填:`/goods/list_goods`|
|
|
||||||
|order|是|固定填:0|
|
|
||||||
|ignoreValidate|是|忽略签名验证,1:是,0:否|
|
|
||||||
|status|是|启用状态,1:启用,2:禁用|
|
|
||||||
|mergeResult|是|是否统一返回结果,1:是,0:否|
|
|
||||||
|permission|是|是否需要权限访问,1:是,0:否|
|
|
||||||
|
|
||||||
- 服务注册到nacos
|
|
||||||
|
|
||||||
可前往nacos官网,参考[open-api](https://nacos.io/zh-cn/docs/open-api.html),使用nacos提供的接口完成服务注册
|
|
||||||
|
|
||||||
|
|
||||||
- 在nacos的metadata中指定接口路径
|
|
||||||
|
|
||||||
伪代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Instance instance = new Instance();
|
|
||||||
instance.setServiceName("goods-service");
|
|
||||||
instance.setIp("192.168.0.11");
|
|
||||||
instance.setPort(8080);
|
|
||||||
// 在nacos的metadata中指定接口路径
|
|
||||||
instance.getMetadata().put("sop.routes.path", "http://open.xxx.com/get_routes");
|
|
||||||
|
|
||||||
namingService.registerInstance(serviceId, instance);
|
|
||||||
```
|
|
||||||
|
|
||||||
完成以上步骤后,php服务注册到nacos,网关会触发监听事件,获取新注册的服务,然后会向你的服务拉取路由配置。
|
|
@@ -1,147 +0,0 @@
|
|||||||
# 新增接口
|
|
||||||
|
|
||||||
假设要对下面这个接口提供开放能力。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
public class StoryDemoController {
|
|
||||||
|
|
||||||
@RequestMapping("/story/get")
|
|
||||||
public StoryResult getStory() {
|
|
||||||
StoryResult result = new StoryResult();
|
|
||||||
result.setId(1L);
|
|
||||||
result.setName("海底小纵队");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
只需要在方法上新增一个`@Open`注解,指定接口名即可
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 添加一个@Open注解
|
|
||||||
@Open("story.demo.get")
|
|
||||||
@RequestMapping("/story/get")
|
|
||||||
public StoryResult getStory() {
|
|
||||||
StoryResult result = new StoryResult();
|
|
||||||
result.setId(1L);
|
|
||||||
result.setName("海底小纵队");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果要加上版本号,指定`version`参数:`@Open(value = "story.demo.get", version = "2.0")`
|
|
||||||
|
|
||||||
- 重启服务,这样接口就可以使用了。
|
|
||||||
|
|
||||||
## 绑定业务参数
|
|
||||||
|
|
||||||
网关校验通过后,请求参数会传递到微服务上来,完整的参数如下所示:
|
|
||||||
|
|
||||||
```
|
|
||||||
请求参数:charset=utf-8&biz_content={"goods_remark":"iphone6"}&method=goods.add&format=json&app_id=2019032617262200001&sign_type=RSA2&version=1.0×tamp=2019-04-29 19:18:38
|
|
||||||
```
|
|
||||||
|
|
||||||
其中biz_content部分是我们想要的,在方法上申明一个对象,对应biz_content中的内容即可完成参数绑定,并且对参数进行JSR-303校验。
|
|
||||||
|
|
||||||
**注意:接口方法必须有且只有一个对象参数,如果申明多个会出现参数绑定失败**
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Open("goods.add")
|
|
||||||
@RequestMapping("/goods/add")
|
|
||||||
public Object addGoods(GoodsParam param) {
|
|
||||||
return param;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class GoodsParam {
|
|
||||||
@NotEmpty(message = "不能为空") // 支持JSR-303校验
|
|
||||||
private String goods_remark;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
一般情况下,只需要获取业务参数即可,如果想要获取更多的参数,可在后面跟一个`HttpServletRequest`对象。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Open("goods.add")
|
|
||||||
@RequestMapping("/goods/add")
|
|
||||||
public Object addGoods(GoodsParam param, HttpServletRequest request) {
|
|
||||||
System.out.println(request.getParameter("method"));
|
|
||||||
return param;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 接口命名
|
|
||||||
|
|
||||||
接口命名没有做强制要求,但我们还是推荐按照下面的方式进行命名:
|
|
||||||
|
|
||||||
接口名的命名规则为:`服务模块.业务模块.功能模块.行为`,如:
|
|
||||||
|
|
||||||
- mini.user.userinfo.get 小程序服务.用户模块.用户信息.获取
|
|
||||||
- member.register.total.get 会员服务.注册模块.注册总数.获取
|
|
||||||
|
|
||||||
如果觉得命名规则有点长可以精简为:`服务模块.功能模块.行为`,如`member.usercount.get`,前提是确保前缀要有所区分,不和其它服务冲突。
|
|
||||||
|
|
||||||
## 测试接口
|
|
||||||
|
|
||||||
- 在sop-test工程下新建一个测试用例,`StoryDemoTest`,继承TestBase
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class StoryDemoTest extends TestBase {
|
|
||||||
|
|
||||||
String url = "http://localhost:8081";
|
|
||||||
String appId = "2019032617262200001";
|
|
||||||
// 私钥
|
|
||||||
String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=";
|
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testDemo() throws Exception {
|
|
||||||
// 公共请求参数
|
|
||||||
Map<String, String> params = new HashMap<String, String>();
|
|
||||||
params.put("app_id", appId);
|
|
||||||
// 这里对应@Open.value属性
|
|
||||||
params.put("method", "story.demo.get");
|
|
||||||
params.put("format", "json");
|
|
||||||
params.put("charset", "utf-8");
|
|
||||||
params.put("sign_type", "RSA2");
|
|
||||||
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
|
||||||
// 这里对应@Open.version属性
|
|
||||||
params.put("version", "1.0");
|
|
||||||
|
|
||||||
// 业务参数
|
|
||||||
Map<String, String> bizContent = new HashMap<>();
|
|
||||||
|
|
||||||
params.put("biz_content", JSON.toJSONString(bizContent));
|
|
||||||
|
|
||||||
System.out.println("----------- 请求信息 -----------");
|
|
||||||
System.out.println("请求参数:" + buildParamQuery(params));
|
|
||||||
System.out.println("商户秘钥:" + privateKey);
|
|
||||||
String content = AlipaySignature.getSignContent(params);
|
|
||||||
System.out.println("待签名内容:" + content);
|
|
||||||
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
|
|
||||||
System.out.println("签名(sign):" + sign);
|
|
||||||
|
|
||||||
params.put("sign", sign);
|
|
||||||
|
|
||||||
System.out.println("----------- 返回结果 -----------");
|
|
||||||
String responseData = post(url, params);// 发送请求
|
|
||||||
System.out.println(responseData);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 请求成功后,控制台会打印:
|
|
||||||
|
|
||||||
```
|
|
||||||
----------- 请求信息 -----------
|
|
||||||
请求参数:charset=utf-8&biz_content={}&method=story.demo.get&format=json&app_id=alipay_test&sign_type=RSA2&version=1.0×tamp=2019-03-23 15:41:22
|
|
||||||
商户秘钥:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=
|
|
||||||
待签名内容:app_id=alipay_test&biz_content={}&charset=utf-8&format=json&method=story.demo.get&sign_type=RSA2×tamp=2019-03-23 15:41:22&version=1.0
|
|
||||||
签名(sign):YMbxTPdovi6htcn1K3USTS6/Tbg6MOAMigG6x/kG0kQFCYH8ljvxXzcY86UT056nUG3OXxnj0xkw07eV6E03HMlu7bn3/jrT3PCcV3YguhA92aWz720x2xJWdfXY13OUPS9VOCC9zIVxu6EBD+PoZ7ojYChYvOfCR5I8bR/oOc0ZLjK63PWTBdf0eFS4sybXzRf81uNLMROsMhmBDDy0Fhml3ml77qzWBIpsmq5ECZ+89rMPbkNhAUcnFAe7ik7xZIL6WcUhAOhKVa8ZQK1GMjoGnAbGRed1FbuOHZGubgffg4/vMqrY10Bcy6h9jt/zK5w9L3HVgK3aPgQlfP16Gg==
|
|
||||||
----------- 返回结果 -----------
|
|
||||||
{"story_demo_get_response":{"msg":"Success","code":"10000","name":"白雪公主","id":1},"sign":"YMbxTPdovi6htcn1K3USTS6/Tbg6MOAMigG6x/kG0kQFCYH8ljvxXzcY86UT056nUG3OXxnj0xkw07eV6E03HMlu7bn3/jrT3PCcV3YguhA92aWz720x2xJWdfXY13OUPS9VOCC9zIVxu6EBD+PoZ7ojYChYvOfCR5I8bR/oOc0ZLjK63PWTBdf0eFS4sybXzRf81uNLMROsMhmBDDy0Fhml3ml77qzWBIpsmq5ECZ+89rMPbkNhAUcnFAe7ik7xZIL6WcUhAOhKVa8ZQK1GMjoGnAbGRed1FbuOHZGubgffg4/vMqrY10Bcy6h9jt/zK5w9L3HVgK3aPgQlfP16Gg=="}
|
|
||||||
```
|
|
||||||
|
|
@@ -1,20 +0,0 @@
|
|||||||
# 开发流程
|
|
||||||
|
|
||||||
如果您打算使用SOP做开放平台开发,流程大致如下:
|
|
||||||
|
|
||||||
- cd到/SOP/sop-common目录,执行命令`mvn clean deploy`,把jar上传到maven私服,如果没有maven私服,可以打包到本地`mvn clean install`
|
|
||||||
- 打包`sop-gateway`(网关)、`sop-admin`(后台管理)、`sop-website`(文档),部署到服务器上
|
|
||||||
|
|
||||||
以上服务是固定的,启动一次即可,后续不用做改动。
|
|
||||||
|
|
||||||
- 你的项目接入到SOP,参考[项目接入到SOP](10011_项目接入到SOP.md),在微服务端开发接口,编写swagger注解文档
|
|
||||||
- 接口开发完成,启动微服务,注册到注册中心。
|
|
||||||
- 【可选】编写sdk,在`SDK管理`页发布SDK,提供下载地址
|
|
||||||
|
|
||||||
## ISV对接流程
|
|
||||||
|
|
||||||
假设接口开发完毕,开始对接ISV,大致步骤如下:
|
|
||||||
|
|
||||||
1. ISV访问门户网站,注册账号并登陆
|
|
||||||
2. 上传应用公钥
|
|
||||||
3. 如果有SDK,下载SDK,进行接口调用
|
|
@@ -1,11 +0,0 @@
|
|||||||
# 用户(ISV)注册
|
|
||||||
|
|
||||||
新增ISV有两种方式
|
|
||||||
|
|
||||||
- 方式1
|
|
||||||
|
|
||||||
启动sop-admin,在admin后台`ISV管理添加`
|
|
||||||
|
|
||||||
- 方式2
|
|
||||||
|
|
||||||
启动`sop-website-server`,用户访问自主注册
|
|
@@ -1,88 +0,0 @@
|
|||||||
# 业务参数校验
|
|
||||||
|
|
||||||
业务参数校验采用JSR-303方式,关于JSR-303介绍可以参考这篇博文:[JSR 303 - Bean Validation 介绍及最佳实践](https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/)
|
|
||||||
|
|
||||||
在参数中使用注解即可,框架会自动进行验证。如下面一个添加商品接口,它的参数是GoodsParam
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Open("goods.add")
|
|
||||||
@RequestMapping("/goods/add")
|
|
||||||
public void addGoods(GoodsParam param) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
在GoodsParam中添加JSR-303注解:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class GoodsParam {
|
|
||||||
@NotEmpty(message = "商品名称不能为空")
|
|
||||||
private String goods_name;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果不传商品名称则返回
|
|
||||||
|
|
||||||
```
|
|
||||||
{"goods_add_response":{"msg":"Success","code":"10000","sub_msg":"商品名称不能为空","sub_code":"isv.invalid-parameter"},"sign":"Eh3Z5CxDCHsb4MyYFVxsPSmBpwVi1LISJdOkrzglxXoqG7RVyEOt4ef1kNpznUvMI3FDQU1suR7Rsmx6NjGdEVS6NSH2Kt0d8TFBRpLhWz8hApnxOtgzqMqbYeMuJie7X5gF6m8hTnvuuxF21IrkixMe+lyBcXw7dk0C3w1SwdEZkHQ+xC+M4bLqAZt5/3kl79/FWSMFJWHiZmg5YeEi8e8XhYCNcz+xlJRJL0x2Y87fFxqSY0UYWNxbQHgdVI8xRfn1n31nzkcLxiAtTh4LPtNRrG7w7absK/C1Oi/vczuBlFeq2EWUsYVWOVpKiJifUwvYVUUsztSLElzplzOjbg=="}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
- 校验顺序
|
|
||||||
|
|
||||||
如果存在多个注解,可以指定groups来控制校验顺序,如下代码所示:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@NotBlank(message = "NotBlank", groups = Group1.class)
|
|
||||||
// 优先校验Group2
|
|
||||||
// 可交换下面Group2,Group3,看下校验顺序
|
|
||||||
@Length(min = 2, max = 20, message = "length must 10~20", groups = Group2.class)
|
|
||||||
@Pattern(regexp = "[a-zA-Z]*", message = "name must letters", groups = Group3.class)
|
|
||||||
private String name;
|
|
||||||
```
|
|
||||||
|
|
||||||
优先校验`@Length`,通过后再校验`@Pattern`
|
|
||||||
|
|
||||||
## 参数校验国际化
|
|
||||||
|
|
||||||
国际化的配置方式如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@NotEmpty(message = "{goods.remark.notNull}")
|
|
||||||
private String goods_remark;
|
|
||||||
```
|
|
||||||
|
|
||||||
国际化资源文件`bizerror_en.properties`中添加:
|
|
||||||
```
|
|
||||||
goods.remark.notNull=The goods_remark can not be null
|
|
||||||
```
|
|
||||||
|
|
||||||
bizerror_zh_CN.properties中添加:
|
|
||||||
|
|
||||||
```
|
|
||||||
# 商品备注不能为空
|
|
||||||
goods.remark.notNull=\u5546\u54c1\u5907\u6ce8\u4e0d\u80fd\u4e3a\u7a7a
|
|
||||||
```
|
|
||||||
|
|
||||||
## 参数校验国际化传参
|
|
||||||
|
|
||||||
下面校验商品评论的长度,要求大于等于3且小于等于20。数字3和20要填充到国际化资源中去。
|
|
||||||
|
|
||||||
```
|
|
||||||
// 传参的格式:{xxx}=value1,value2...
|
|
||||||
@Length(min = 3, max = 20, message = "{goods.comment.length}=3,20")
|
|
||||||
private String goods_comment;
|
|
||||||
```
|
|
||||||
|
|
||||||
bizerror_en.properties:
|
|
||||||
```
|
|
||||||
goods.comment.length=The goods_comment length must >= {0} and <= {1}
|
|
||||||
```
|
|
||||||
|
|
||||||
bizerror_zh_CN.properties中添加:
|
|
||||||
|
|
||||||
```
|
|
||||||
# 商品评论长度必须在{0}和{1}之间
|
|
||||||
goods.comment.length=\u5546\u54c1\u8bc4\u8bba\u957f\u5ea6\u5fc5\u987b\u5728{0}\u548c{1}\u4e4b\u95f4
|
|
||||||
```
|
|
||||||
这样value1,value2会分别填充到{0},{1}中
|
|
@@ -1,91 +0,0 @@
|
|||||||
# 错误处理
|
|
||||||
|
|
||||||
SOP对错误处理已经封装好了,简单做法是`throw ServiceException`,在最顶层的Controller会做统一处理。例如:
|
|
||||||
|
|
||||||
```java
|
|
||||||
if(StringUtils.isEmpty(param.getGoods_name())) {
|
|
||||||
throw new ServiceException("goods_name不能为null");
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
为了保证编码风格的一致性,推荐统一使用ServiceException
|
|
||||||
|
|
||||||
## i18n国际化
|
|
||||||
|
|
||||||
SOP支持国际化消息。通过Request对象中的getLocale()来决定具体返回那种语言,客户端通过设置Accept-Language头部来决定返回哪种语言,中文是zh,英文是en。
|
|
||||||
|
|
||||||
SOP通过模块化来管理国际化消息,这样做的好处结构清晰,维护方便。下面就来讲解如何设置国际化消息。
|
|
||||||
|
|
||||||
以story服务为例,假设我们要对商品模块进行设置,步骤如下:
|
|
||||||
|
|
||||||
- 在`resource/i18n/isp`目录下新建goods_error_zh_CN.properties属性文件
|
|
||||||
|
|
||||||
属性文件的文件名有规律, **i18n/isp/goods_error** 表示模块路径, **_zh_CN.properties** 表示中文错误消息。如果要使用英文错误,则新建一个`goods_error_en.properties`即可。
|
|
||||||
|
|
||||||
- 在goods_error_zh_CN.properties中配置错误信息
|
|
||||||
|
|
||||||
```
|
|
||||||
# 商品名字不能为空
|
|
||||||
isp.goods_error_100=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
|
||||||
```
|
|
||||||
|
|
||||||
isp.goods_error_为固定前缀,100为错误码,这两个值后续会用到。
|
|
||||||
|
|
||||||
接下来是把属性文件加载到国际化容器当中。
|
|
||||||
|
|
||||||
- 添加国际化配置,在OpenServiceConfig中的static块中添加,代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class OpenServiceConfig extends AlipayServiceConfiguration {
|
|
||||||
|
|
||||||
static {
|
|
||||||
ServiceConfig.getInstance().getI18nModules().add("i18n/isp/goods_error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 新建一个枚举用来定义错误
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 按模块来定义异常消息,团队开发可以分开进行
|
|
||||||
public enum GoodsErrorEnum {
|
|
||||||
/** 参数错误 */
|
|
||||||
NO_GOODS_NAME("100"),
|
|
||||||
;
|
|
||||||
private ServiceErrorMeta errorMeta;
|
|
||||||
|
|
||||||
StoryErrorEnum(String subCode) {
|
|
||||||
this.errorMeta = new ServiceErrorMeta("isp.goods_error_", subCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServiceErrorMeta getErrorMeta() {
|
|
||||||
return errorMeta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
接下来就可以使用了
|
|
||||||
|
|
||||||
```java
|
|
||||||
if (StringUtils.isEmpty(param.getGoods_name())) {
|
|
||||||
throw GoodsErrorEnum.NO_GOODS_NAME.getErrorMeta().getException();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 国际化消息传参
|
|
||||||
|
|
||||||
即代码中变量传入到properties文件中去,做法是采用{0},{1}占位符。0代表第一个参数,1表示第二个参数。
|
|
||||||
|
|
||||||
```
|
|
||||||
# 商品名称太短,不能小于{0}个字
|
|
||||||
isp.goods_error_101=\u5546\u54C1\u540D\u79F0\u592A\u77ED\uFF0C\u4E0D\u80FD\u5C0F\u4E8E{0}\u4E2A\u5B57
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
if (param.getGoods_name().length() <= 3) {
|
|
||||||
throw GoodsErrorEnum.LESS_GOODS_NAME_LEN.getErrorMeta().getException(3);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
直接放进getException(Object... params)方法参数中,因为是可变参数,可随意放。
|
|
@@ -1,126 +0,0 @@
|
|||||||
# 编写文档
|
|
||||||
|
|
||||||
作为开放平台,必须要提供API文档。
|
|
||||||
|
|
||||||
SOP采用微服务架构实现,因此文档应该由各个微服务各自实现。难点就是如何统一归纳各个微服务端提供的文档信息,并且统一展示。
|
|
||||||
|
|
||||||
写完接口后使用swagger注解来定义自己的文档信息。步骤如下:
|
|
||||||
|
|
||||||
- maven添加swagger
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!-- swagger2 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.springfox</groupId>
|
|
||||||
<artifactId>springfox-swagger2</artifactId>
|
|
||||||
<version>2.9.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
|
||||||
<artifactId>swagger-bootstrap-ui</artifactId>
|
|
||||||
<version>1.9.5</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
- 在config中添加swagger配置
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class OpenServiceConfig extends AlipayServiceConfiguration {
|
|
||||||
/**
|
|
||||||
* 开启文档,本地微服务文档地址:http://localhost:2222/doc.html
|
|
||||||
* http://ip:port/v2/api-docs
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableSwagger2
|
|
||||||
public static class Swagger2 extends SwaggerSupport {
|
|
||||||
@Override
|
|
||||||
protected String getDocTitle() {
|
|
||||||
return "故事API";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
其中`getDocTitle()`返回文档模块名,不能和其它微服务重复。比如订单服务返回:`订单API`;库存服务返回:`库存API`
|
|
||||||
|
|
||||||
- 编写swagger注解
|
|
||||||
|
|
||||||
分别在请求参数和返回结果类中编写`@ApiModelProperty`
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 请求参数
|
|
||||||
@Data
|
|
||||||
public class StoryParam {
|
|
||||||
@ApiModelProperty(value = "故事ID", example = "111")
|
|
||||||
private int id;
|
|
||||||
|
|
||||||
@ApiModelProperty(value = "故事名称", required = true, example = "白雪公主")
|
|
||||||
private String name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回结果
|
|
||||||
@Data
|
|
||||||
public class StoryResult {
|
|
||||||
@ApiModelProperty(value = "故事ID", example = "1")
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@ApiModelProperty(value = "故事名称", example = "海底小纵队")
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
@ApiModelProperty(value = "创建时间", example = "2019-04-14 19:02:12")
|
|
||||||
private Date gmt_create;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- 在接口方法上编写`@ApiOperation`注解
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 参数绑定
|
|
||||||
*
|
|
||||||
* @param story 对应biz_content中的内容,并自动JSR-303校验
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@ApiOperation(value = "获取故事信息", notes = "说明接口的详细信息,介绍,用途,注意事项等。")
|
|
||||||
@Open(value = "alipay.story.find", bizCode = {
|
|
||||||
// 定义业务错误码,用于文档显示
|
|
||||||
@BizCode(code = "100001", msg = "姓名错误", solution = "填写正确的姓名"),
|
|
||||||
@BizCode(code = "100002", msg = "备注错误", solution = "填写正确备注"),
|
|
||||||
})
|
|
||||||
public StoryResult getStory2(StoryParam story) {
|
|
||||||
log.info("获取故事信息参数, story: {}", story);
|
|
||||||
// 获取其它参数
|
|
||||||
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
|
|
||||||
String app_id = openContext.getAppId();
|
|
||||||
StoryResult result = new StoryResult();
|
|
||||||
result.setName("白雪公主, app_id:" + app_id);
|
|
||||||
result.setGmt_create(new Date());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
其中`value`属性填接口名称,简明扼要。`notes`填写接口的详细信息,介绍,用途,注意事项等。
|
|
||||||
|
|
||||||
## 查看文档
|
|
||||||
|
|
||||||
- 确保注册中心、网关、微服务正常启动
|
|
||||||
- 运行WebsiteServerApplication.java
|
|
||||||
- 访问http://localhost:8083
|
|
||||||
|
|
||||||
效果图如下
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 注解对应关系
|
|
||||||
|
|
||||||
swagger注解和文档界面显示关系如下图所示:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||

|
|
@@ -1,61 +0,0 @@
|
|||||||
# 接口交互详解
|
|
||||||
|
|
||||||
开放平台所提供的接口有几十个到几百个不等,同样支持的服务也是多个的。就拿[支付宝开放平台](https://docs.open.alipay.com/api)来说
|
|
||||||
它所提供的服务有,支付服务、会员服务、店铺服务、芝麻信用服务等。相信这些服务接口肯定不是写在同一个项目中,但是它的接口地址只有一个:https://openapi.alipay.com/gateway.do
|
|
||||||
从地址信息中可以看到,这是一个网关服务。也就是说,网关是所有请求的入口,然后通过请求分发的方式,把请求路由到具体某个服务中去。
|
|
||||||
虽然支付宝开放平台的实现方式我们不得而知,但是这种思路是可行的。
|
|
||||||
|
|
||||||
SOP也是采用这种方式实现,大致步骤如下:
|
|
||||||
|
|
||||||
- 每个服务注册到nacos
|
|
||||||
- 网关启动时同样注册到nacos,然后从各服务中拉取路由信息
|
|
||||||
- 网关收到客户端请求后,先进行签名校验,通过之后根据接口信息找到对应的服务,然后进行路由
|
|
||||||
- 网关对返回结果进行处理(或不处理),返回给客户端。
|
|
||||||
|
|
||||||
如何通过接口参数找到对应的服务呢?
|
|
||||||
|
|
||||||
在网关定义一个`Map<String, RouteInfo> routeMap = ...`,key为接口名+版本号。
|
|
||||||
|
|
||||||
网关启动时,从各微服务中获取路由信息,并保存到routeMap中
|
|
||||||
|
|
||||||
```java
|
|
||||||
routeMap = requestFormServices();
|
|
||||||
```
|
|
||||||
|
|
||||||
接口请求进来后,根据`方法名+版本号`获取路由信息,然后进行路由转发。
|
|
||||||
|
|
||||||
```java
|
|
||||||
String method = request.getParameter("method");
|
|
||||||
String version = request.getParameter("version");
|
|
||||||
|
|
||||||
RouteInfo routeInfo = routeMap.get(method + version);
|
|
||||||
|
|
||||||
doRoute(routeInfo);
|
|
||||||
```
|
|
||||||
|
|
||||||
因为nacos需要拉取各个微服务的路由信息,接口名有可能会冲突,因此需要确保接口名唯一,即`method`全局唯一。
|
|
||||||
|
|
||||||
我们推荐接口名的命名规则应该是:`服务模块.业务模块.功能模块.行为`,如:
|
|
||||||
|
|
||||||
mini.user.userinfo.get 小程序服务.用户模块.用户信息.获取
|
|
||||||
|
|
||||||
member.register.total.get 会员服务.注册模块.注册总数.获取
|
|
||||||
|
|
||||||
如果觉得命名规则有点长可以精简为:`服务模块.功能模块.行为`,如`member.usercount.get`,前提是确保前缀要有所区分,不和其它服务冲突。
|
|
||||||
|
|
||||||
得益于Spring Cloud的注册中心和和网关功能,我们能很方便的进行接口路由,并且还能实现LoadBalance,不需要自己再去实现。
|
|
||||||
|
|
||||||
整个SOP的架构如下图所示:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 完整请求路线
|
|
||||||
|
|
||||||
```
|
|
||||||
客户端生成签名串 → 客户端发送请求 →【网关签名校验 → 权限校验 → 限流处理 → 路由转发】→ {微服务端业务参数校验 → 处理业务逻辑 → 微服务端返回结果}
|
|
||||||
↓
|
|
||||||
客户端业务处理 ← 客户端验证服务端签名 ← 客户端收到结果 ← -------------【网关返回最终结果 ← 生成服务端签名 ← 网关处理结果】← 结果返回到网关
|
|
||||||
|
|
||||||
【】:表示网关处理
|
|
||||||
{}:表示微服务端处理
|
|
||||||
```
|
|
@@ -1,41 +0,0 @@
|
|||||||
# 使用签名校验工具
|
|
||||||
|
|
||||||
## 生成公私钥
|
|
||||||
|
|
||||||
SOP默认签名算法仿照的是支付宝开放平台,因此我们可以使用支付宝开放平台提供的密钥生成工具,[下载地址](https://docs.open.alipay.com/291/105971/)
|
|
||||||
|
|
||||||
工具下载完后,运行工具
|
|
||||||
|
|
||||||
- 秘钥格式选择:PKCS8(JAVA适用)
|
|
||||||
- 秘钥长度:2048
|
|
||||||
|
|
||||||
然后点击`生成秘钥`,下面文本框会生成,公私钥,如下图所示:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
sop-admin创建一个新ISV,将公私钥放入对应文本框中,保存。
|
|
||||||
|
|
||||||
接着私钥放入客户端进行调用。参见AlipayClientPostTest类
|
|
||||||
|
|
||||||
## 签名校验
|
|
||||||
|
|
||||||
验证工具切换到`签名`tab页
|
|
||||||
|
|
||||||
例如执行com.gitee.sop.AlipayClientPostTest.testPost()方法,控制台会打印如下信息:
|
|
||||||
|
|
||||||
```
|
|
||||||
----------- 请求信息 -----------
|
|
||||||
请求参数:charset=utf-8&biz_content={"name":"葫芦娃","id":"1"}&method=alipay.story.get&format=json&app_id=2019032617262200001&sign_type=RSA2&version=1.0×tamp=2019-03-26 17:37:41
|
|
||||||
商户秘钥:MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=
|
|
||||||
待签名内容:app_id=2019032617262200001&biz_content={"name":"葫芦娃","id":"1"}&charset=utf-8&format=json&method=alipay.story.get&sign_type=RSA2×tamp=2019-03-26 17:37:41&version=1.0
|
|
||||||
签名(sign):JCZMSFkXSjw/4TokyM9/9shyrMl7KxQGIZDHIm7+Bvl49Z816/iF/xXLYjUiPXWAXYfp+HlEs3VVQp1Kjh4tIKuKX/i1+exNVs+ICcqVGBewPSZwiWHGpZTfEUiYOoPyUL/eoRIj7Mvlaow0sI9uP7NXNo0kxEFjUOMCzZA7eKm/pu2FHRXt4OhgXq2Go30K5a9oCbbMc/2xcQCc2+zwvOgV3o0A6eMyeAXDJW+eQ2KLhtlqPQvbRV+xyfSut7TkwYSEuNXVVQAfN2lwAS3ru9CQIs8Uz7lK1ITkLu80yLapZVL7tS1PdxK0e3QYToCWD43Wtuoow4ZdDwwzir90HQ==
|
|
||||||
----------- 返回结果 -----------
|
|
||||||
{"alipay_story_get_response":{"msg":"Success","code":"10000","name":"海底小纵队(alipay.story.get)","id":1},"sign":"JCZMSFkXSjw/4TokyM9/9shyrMl7KxQGIZDHIm7+Bvl49Z816/iF/xXLYjUiPXWAXYfp+HlEs3VVQp1Kjh4tIKuKX/i1+exNVs+ICcqVGBewPSZwiWHGpZTfEUiYOoPyUL/eoRIj7Mvlaow0sI9uP7NXNo0kxEFjUOMCzZA7eKm/pu2FHRXt4OhgXq2Go30K5a9oCbbMc/2xcQCc2+zwvOgV3o0A6eMyeAXDJW+eQ2KLhtlqPQvbRV+xyfSut7TkwYSEuNXVVQAfN2lwAS3ru9CQIs8Uz7lK1ITkLu80yLapZVL7tS1PdxK0e3QYToCWD43Wtuoow4ZdDwwzir90HQ=="}
|
|
||||||
```
|
|
||||||
|
|
||||||
字符集选UTF-8,签名方式RSA2
|
|
||||||
|
|
||||||
把控制台中的`请求参数`和`商户秘钥`填入文本框中,然后点击`开始签名`,下方会出现待签名内容和sign。
|
|
||||||
|
|
||||||
通过比对判断签名过程是否正确。
|
|
||||||
|
|
@@ -1,24 +0,0 @@
|
|||||||
# ISV管理
|
|
||||||
|
|
||||||
ISV:独立软体开发商(independent software vendor),即接入方或者说接口调用者,在SOP中称为ISV。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
在1.1.0版本中新增了ISV管理功能,在sop-admin中ISV管理模块下。功能如下:
|
|
||||||
|
|
||||||
- 基本信息的增查改
|
|
||||||
- 设置对应角色
|
|
||||||
|
|
||||||
界面如下图所示:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 秘钥管理
|
|
||||||
|
|
||||||
点击操作列的`秘钥管理`,可对ISV的秘钥进行设置。
|
|
||||||
|
|
||||||
- 如果采用淘宝开放平台签名方式,签名方式选择`MD5`,如果采用支付宝开放平台签名方式,选择`RSA`
|
|
||||||
- 如果对接的开发者使用非Java语言,秘钥格式选择`PKCS1`
|
|
||||||
- 带 ★ 的分配给开发者
|
|
||||||
|
|
||||||

|
|
@@ -1,110 +0,0 @@
|
|||||||
# 自定义返回结果
|
|
||||||
|
|
||||||
网关默认对业务结果进行合并,然后返回统一的格式。
|
|
||||||
|
|
||||||
针对`alipay.story.find`接口,微服务端返回结果如下:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "白雪公主",
|
|
||||||
"id": 1,
|
|
||||||
"gmtCreate": 1554193987378
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
网关合并后,最终结果如下
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"alipay_story_find_response": {
|
|
||||||
"msg": "Success",
|
|
||||||
"code": "10000",
|
|
||||||
"name": "白雪公主",
|
|
||||||
"id": 1,
|
|
||||||
"gmtCreate": 1554193987378
|
|
||||||
},
|
|
||||||
"sign": "xxxxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
其中`alipay_story_find_response`是它的数据节点。规则是:
|
|
||||||
|
|
||||||
> 将接口名中的点`.`转换成下划线`_`,后面加上`_response`
|
|
||||||
|
|
||||||
代码实现如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
String method = "alipay.story.find";
|
|
||||||
return method.replace('.', '_') + "_response";
|
|
||||||
```
|
|
||||||
|
|
||||||
详见`DefaultDataNameBuilder.java`
|
|
||||||
|
|
||||||
如果要更改数据节点,比如`result`,可使用`CustomDataNameBuilder.java`。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class MyConfig {
|
|
||||||
|
|
||||||
static {
|
|
||||||
...
|
|
||||||
ApiConfig.getInstance().setDataNameBuilder(new CustomDataNameBuilder());
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
设置后,网关统一的返回结果如下:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"result": {
|
|
||||||
...
|
|
||||||
},
|
|
||||||
"sign": "xxxxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
此外,构造方法可指定自定义字段名称:`new CustomDataNameBuilder("data");`。
|
|
||||||
设置后,数据节点将变成`data`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
...
|
|
||||||
},
|
|
||||||
"sign": "xxxxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**注**:网关设置了CustomDataNameBuilder后,SDK也要做相应的更改:`OpenConfig.dataNameBuilder = new CustomDataNameBuilder();`
|
|
||||||
|
|
||||||
## 自定义结果处理
|
|
||||||
|
|
||||||
如果想要对微服务结果做更深一步处理,步骤如下:
|
|
||||||
|
|
||||||
1. 新增一个类,继承`GatewayResultExecutor.java`,并重写`String mergeResult(T request, String serviceResult)`方法
|
|
||||||
|
|
||||||
2. 配置自定义类
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class MyConfig {
|
|
||||||
|
|
||||||
static {
|
|
||||||
...
|
|
||||||
ApiConfig.getInstance().setGatewayResultExecutor(new MyGatewayResultExecutor());
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 不合并结果
|
|
||||||
|
|
||||||
如果不希望对结果进行合并,可在application.properties中设置`sop.api-config.merge-result=false`
|
|
||||||
|
|
||||||
|
|
||||||
这样,网关最终返回结果即为微服务端的返回结果。
|
|
@@ -1,40 +0,0 @@
|
|||||||
# 自定义过滤器
|
|
||||||
|
|
||||||
演示在网关追加一个header
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class CustomFilter implements GlobalFilter, Ordered {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
|
||||||
// 演示在网关追加header
|
|
||||||
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange);
|
|
||||||
String token = apiParam.fetchAccessToken();
|
|
||||||
ServerWebExchange serverWebExchange = ServerWebExchangeUtil.addHeaders(exchange, httpHeaders -> {
|
|
||||||
httpHeaders.add("token", token);
|
|
||||||
});
|
|
||||||
return chain.filter(serverWebExchange);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getOrder() {
|
|
||||||
// 自定义过滤器可以从0开始
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
使用过滤器,在sop-gateway中找到MyConfig,添加:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Configuration
|
|
||||||
public class MyConfig {
|
|
||||||
|
|
||||||
...
|
|
||||||
@Bean
|
|
||||||
CustomFilter customFilter() {
|
|
||||||
return new CustomFilter();
|
|
||||||
}
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
@@ -1,52 +0,0 @@
|
|||||||
# 自定义校验token
|
|
||||||
|
|
||||||
在`@Open`注解中有一个属性`needToken`,用来告诉网关是否校验token
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 是否需要appAuthToken,设置为true,网关端会校验token是否存在
|
|
||||||
*/
|
|
||||||
boolean needToken() default false;
|
|
||||||
```
|
|
||||||
|
|
||||||
使用方式:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@ApiOperation(value="传递token", notes = "传递token")
|
|
||||||
@Open(value = "story.get.token", needToken = true/* 设置true,网关会校验token是否存在 */)
|
|
||||||
@RequestMapping("token")
|
|
||||||
public StoryResult token(StoryParam story, HttpServletRequest request) {
|
|
||||||
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
|
|
||||||
StoryResult result = new StoryResult();
|
|
||||||
result.setName("appAuthToken:" + openContext.getAppAuthToken());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
指定了needToken=true后,网关会判断客户端是否传了`app_auth_token`参数,没有传则返回错误信息。
|
|
||||||
|
|
||||||
网关默认简单校验参数值是否存在,如果要校验有效性,需要自己实现。
|
|
||||||
|
|
||||||
自己实现步骤:
|
|
||||||
|
|
||||||
- 设置`ApiConfig中的tokenValidator属性`
|
|
||||||
|
|
||||||
`TokenValidator`是一个函数式接口,可以直接使用Lambda表达式,示例代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class MyConfig {
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void after() {
|
|
||||||
ApiConfig.getInstance().setTokenValidator(apiParam -> {
|
|
||||||
// 获取客户端传递过来的token
|
|
||||||
String token = apiParam.fetchAccessToken();
|
|
||||||
return !StringUtils.isBlank(token);
|
|
||||||
// TODO: 校验token有效性,可以从redis中读取
|
|
||||||
|
|
||||||
// 返回true表示这个token真实、有效
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
@@ -1,57 +0,0 @@
|
|||||||
# 网关拦截器
|
|
||||||
|
|
||||||
从3.1.0开始新增了网关拦截器,使用该拦截器可做一些数据统计,日志记录等工作。
|
|
||||||
|
|
||||||
使用方法如下:
|
|
||||||
|
|
||||||
- 在sop-gateway工程下新增一个类,实现`RouteInterceptor`接口,实现接口中的方法。别忘了加`@Component`
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Component
|
|
||||||
public class MyRouteInterceptor implements RouteInterceptor {
|
|
||||||
@Override
|
|
||||||
public void preRoute(RouteInterceptorContext context) {
|
|
||||||
ApiParam apiParam = context.getApiParam();
|
|
||||||
System.out.println("请求接口:" + apiParam.fetchNameVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterRoute(RouteInterceptorContext context) {
|
|
||||||
System.out.println("请求成功,微服务返回结果:" + context.getServiceResult());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getOrder() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
RouteInterceptor接口方法说明:
|
|
||||||
|
|
||||||
- `public void preRoute(RouteInterceptorContext context)`
|
|
||||||
|
|
||||||
路由转发前执行,在签名验证通过之后会立即执行这个方法。
|
|
||||||
|
|
||||||
- `public void afterRoute(RouteInterceptorContext context)`
|
|
||||||
|
|
||||||
路由转发完成后,即拿到微服务返回结果后执行这个方法
|
|
||||||
|
|
||||||
- `public int getOrder()`
|
|
||||||
|
|
||||||
指定拦截执行顺序,数字小的优先执行,建议从0开始。
|
|
||||||
|
|
||||||
- `default boolean match(RouteInterceptorContext context)`
|
|
||||||
|
|
||||||
是否匹配,返回true执行拦截器,默认true
|
|
||||||
|
|
||||||
RouteInterceptorContext参数存放了各类参数信息。
|
|
||||||
|
|
||||||
参考类:
|
|
||||||
|
|
||||||
- `com.gitee.sop.gatewaycommon.interceptor.RouteInterceptor` 拦截器接口
|
|
||||||
- `com.gitee.sop.gatewaycommon.interceptor.RouteInterceptorContext` 拦截器上下文
|
|
||||||
- `com.gitee.sop.gatewaycommon.interceptor.MonitorRouteInterceptor` 默认实现的拦截器,用于收集监控数据
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -1,25 +0,0 @@
|
|||||||
# 路由授权
|
|
||||||
|
|
||||||
1.1.0版本新增了路由授权功能,采用RBAC权限管理方式实现。
|
|
||||||
|
|
||||||
- 每个ISV(appKey)对应一个或多个角色
|
|
||||||
- 每个角色分配多个路由权限
|
|
||||||
|
|
||||||
接口跟角色相关联,ISV拥有哪些角色,就具有角色对应的接口访问权限。
|
|
||||||
|
|
||||||
假设把路由a,b,c分配给了`VIP角色`,那么具有VIP角色的ISV可以访问a,b,c三个路由。
|
|
||||||
|
|
||||||
默认情况下,接口访问时公开的,ISV都能访问。如果要设置某个接口访问权限,在`@Open`注解中指定permission=true。
|
|
||||||
如:`@Open(value = "permission.story.get", permission = true)`。这样该接口是需要经过授权给ISV才能访问的。
|
|
||||||
|
|
||||||
重启服务后,登录admin,服务管理-路由列表界面中,`访问权限`列会出现一个点击授权,点击出现授权窗口,勾选对应的角色即可完成授权。
|
|
||||||
|
|
||||||
- `点击授权`,进行角色授权
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- 勾选对应角色,点击保存
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
这里演示的是:具有普通权限的ISV能够访问`permission.story.get`接口,运行`PermissionDemoPostTest`测试用例进行验证
|
|
@@ -1,80 +0,0 @@
|
|||||||
# 接口限流
|
|
||||||
|
|
||||||
SOP提供了简单的接口限流策略:
|
|
||||||
|
|
||||||
- 窗口策略:每秒处理固定数量的请求,超出请求返回错误信息。
|
|
||||||
- 令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。
|
|
||||||
|
|
||||||
如果一个接口设置了窗口策略,假设接口每秒可处理5个请求,一秒内同时有6个请求进来,前5个接口是能够访问的,第六个请求将返回错误信息。
|
|
||||||
|
|
||||||
如果设置了令牌桶策略,桶的容量是5,那么每秒中生成5个令牌,同一时间有6个请求进来,那么前5个能成功拿到令牌继续,第六个则等待,令牌重新生成了再拿。
|
|
||||||
|
|
||||||
默认情况下接口的限流功能是关闭的,可在sop admin中配置并开启。功能在`路由管理-->限流管理`下。
|
|
||||||
|
|
||||||
## 多维度限流
|
|
||||||
|
|
||||||
- 可针对接口进行限流,所有访问该接口的请求都被限流
|
|
||||||
- 可针对appKey进行限流,某个appKey请求过来后,对他限流
|
|
||||||
- 可针对IP进行限流,某个IP请求过来后,对他限流
|
|
||||||
|
|
||||||
此外还可以进行组合
|
|
||||||
|
|
||||||
- 可针对接口+appKey进行限流,这个appKey调用某个接口比较频繁,可以将它限制住
|
|
||||||
- 可针对接口+IP进行限流,某个ip在频繁调用接口,可以将它限制住
|
|
||||||
|
|
||||||
由于存在组合情况,一个接口可能会配置多个限流规则。在这种情况下会优先取排序值小的那一条,如果排序值一样,则默认取第一条。
|
|
||||||
|
|
||||||
假设有下面几个限流规则:
|
|
||||||
|
|
||||||
- 接口:`goods.get`, 排序值:1, 每秒可处理请求数:10
|
|
||||||
- 接口:`goods.get`, appKey:xxxx, 排序值:0, 每秒可处理请求数:5
|
|
||||||
- 接口:`goods.get`, ip:172.1.2.2, 排序值:2, 每秒可处理请求数:6
|
|
||||||
|
|
||||||
客户端调用接口:`http://open.domain.com?method=goods.get&app_id=xxxx`,客户端IP为`172.1.2.2`
|
|
||||||
|
|
||||||
这种情况下上面三条限流规则都命中了,由于排序值小优先执行,因此第二条规则命中.
|
|
||||||
|
|
||||||
|
|
||||||
具体设置方式可在sop admin中配置,功能在`服务管理-->限流管理`下。执行`com.gitee.sop.test.LimitTest`测试用例验证限流情况
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 分布式限流
|
|
||||||
|
|
||||||
默认的限流方式是单机的,如果要部署多台网关实例,需要使用分布式限流
|
|
||||||
|
|
||||||
SOP使用redis进行分布式限流(只支持窗口策略),操作步骤如下:
|
|
||||||
|
|
||||||
- sop-gateway/pom.xml添加redis依赖
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
- sop-gateway下的application-dev.properties文件添加redis配置
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# redis
|
|
||||||
spring.redis.database=0
|
|
||||||
spring.redis.host=127.0.0.1
|
|
||||||
spring.redis.port=6379
|
|
||||||
```
|
|
||||||
|
|
||||||
- 在`com.gitee.sop.gateway.config.MyConfig`中添加如下代码:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Autowired
|
|
||||||
private RedisTemplate redisTemplate;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void after() {
|
|
||||||
ApiConfig.getInstance().setLimitManager(new RedisLimitManager(redisTemplate));
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@@ -1,27 +0,0 @@
|
|||||||
# 路由监控
|
|
||||||
|
|
||||||
路由监控功能可以查看各个接口的调用情况,监控信息收集采用拦截器实现,前往【服务管理】-【路由监控】查看
|
|
||||||
|
|
||||||
- 后台预览
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
- 注意事项
|
|
||||||
|
|
||||||
处理完错误后,请及时`标记解决`,一个接口默认保存50条错误信息,采用LRU机制,淘汰老的。标记解决后则会空出一个位置存放新的错误信息。
|
|
||||||
|
|
||||||
重复的错误只会存放一条记录,然后累加错误次数,重复错误定义如下:
|
|
||||||
|
|
||||||
`instanceId + routeId + errorMsg`,即一个实例 + 路由id + 错误信息确定一个错误
|
|
||||||
|
|
||||||
可在网关设置`sop.monitor.error-count-capacity=50`参数调整错误容量
|
|
||||||
|
|
||||||
考虑到数据库压力,网关收到错误信息后并不会立即保存到数据库,而是先保存到内容中,然后定时保存到时间,默认时间隔为30秒
|
|
||||||
可通过`sop.monitor.flush-period-seconds=30`调整间隔时间。
|
|
||||||
|
|
||||||
相关类:
|
|
||||||
|
|
||||||
- com.gitee.sop.gateway.interceptor.MonitorRouteInterceptor
|
|
@@ -1,343 +0,0 @@
|
|||||||
# 开发SDK
|
|
||||||
|
|
||||||
开放平台把接口开发完毕后,一般需要开发对应的SDK,提供给ISV。SOP提供了一个基础的SDK开发包
|
|
||||||
|
|
||||||
开发者可以在此基础上做开发,就拿sdk-java来说,具体步骤如下:
|
|
||||||
|
|
||||||
## sdk-java
|
|
||||||
|
|
||||||
SDK依赖了三个jar包
|
|
||||||
|
|
||||||
- okhttp.jar 用于网络请求
|
|
||||||
- fastjson.jar 用于json处理
|
|
||||||
- commons-logging.jar 日志处理
|
|
||||||
|
|
||||||
### 接口封装步骤
|
|
||||||
|
|
||||||
比如获取故事信息接口
|
|
||||||
|
|
||||||
- 接口名:alipay.story.find
|
|
||||||
- 版本号:1.0
|
|
||||||
- 参数:name 故事名称
|
|
||||||
- 返回信息
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"alipay_story_find_response": {
|
|
||||||
"msg": "Success",
|
|
||||||
"code": "10000",
|
|
||||||
"name": "白雪公主",
|
|
||||||
"id": 1,
|
|
||||||
"gmtCreate": 1554193987378
|
|
||||||
},
|
|
||||||
"sign": "xxxxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
针对这个接口,封装步骤如下:
|
|
||||||
|
|
||||||
1.在`model`包下新建一个类,定义业务参数
|
|
||||||
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class GetStoryModel {
|
|
||||||
|
|
||||||
@JSONField(name = "name")
|
|
||||||
private String name;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2.在`response`包下新建一个返回类GetStoryResponse,继承`BaseResponse`
|
|
||||||
|
|
||||||
里面填写返回的字段
|
|
||||||
|
|
||||||
```
|
|
||||||
@Data
|
|
||||||
public class GetStoryResponse extends BaseResponse {
|
|
||||||
private Long id;
|
|
||||||
private String name;
|
|
||||||
private Date gmtCreate;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3.在`request`包下新建一个请求类,继承`BaseRequest`
|
|
||||||
|
|
||||||
BaseRequest中有个泛型参数,填`GetStoryResponse`类,表示这个请求对应的返回类。
|
|
||||||
重写`method()`方法,填接口名。
|
|
||||||
|
|
||||||
如果要指定版本号,可重写`version()`方法,或者后续使用`request.setVersion(version)`进行设置
|
|
||||||
|
|
||||||
```java
|
|
||||||
public class GetStoryRequest extends BaseRequest<GetStoryResponse> {
|
|
||||||
@Override
|
|
||||||
protected String method() {
|
|
||||||
return "alipay.story.find";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
可重写`getRequestMethod()`方法指定HTTP请求method,默认是POST。
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Override
|
|
||||||
protected RequestMethod getRequestMethod() {
|
|
||||||
return RequestMethod.GET;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**建议读请求用GET,写请求用POST**,
|
|
||||||
|
|
||||||
### 使用方式
|
|
||||||
|
|
||||||
```java
|
|
||||||
String url = "http://localhost:8081";
|
|
||||||
String appId = "2019032617262200001";
|
|
||||||
String privateKey = "你的私钥";
|
|
||||||
|
|
||||||
// 声明一个就行
|
|
||||||
OpenClient client = new OpenClient(url, appId, privateKey);
|
|
||||||
|
|
||||||
// 标准用法
|
|
||||||
@Test
|
|
||||||
public void testGet() {
|
|
||||||
// 创建请求对象
|
|
||||||
GetStoryRequest request = new GetStoryRequest();
|
|
||||||
// 请求参数
|
|
||||||
GetStoryModel model = new GetStoryModel();
|
|
||||||
model.setName("白雪公主");
|
|
||||||
|
|
||||||
request.setBizModel(model);
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
GetStoryResponse response = client.execute(request);
|
|
||||||
|
|
||||||
if (response.isSuccess()) {
|
|
||||||
// 返回结果
|
|
||||||
System.out.println(String.format("成功!response:%s\n响应原始内容:%s",
|
|
||||||
JSON.toJSONString(response), response.getBody()));
|
|
||||||
} else {
|
|
||||||
System.out.println("错误,subCode:" + response.getSubCode() + ", subMsg:" + response.getSubMsg());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用方式2(懒人版)
|
|
||||||
|
|
||||||
如果不想添加Request,Response,Model。可以用这种方式,返回body部分是字符串,后续自己处理
|
|
||||||
|
|
||||||
body对应的是alipay_story_find_response部分
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Test
|
|
||||||
public void testLazy() {
|
|
||||||
// 创建请求对象
|
|
||||||
CommonRequest request = new CommonRequest("alipay.story.find");
|
|
||||||
// 请求参数
|
|
||||||
Map<String, Object> bizModel = new HashMap<>();
|
|
||||||
bizModel.put("name", "白雪公主");
|
|
||||||
request.setBizModel(bizModel);
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
CommonResponse response = client.execute(request);
|
|
||||||
|
|
||||||
if (response.isSuccess()) {
|
|
||||||
// 返回结果,body对应的是alipay_story_find_response部分
|
|
||||||
String body = response.getBody();
|
|
||||||
JSONObject jsonObject = JSON.parseObject(body);
|
|
||||||
System.out.println(jsonObject);
|
|
||||||
} else {
|
|
||||||
System.out.println(response);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## sdk-.net
|
|
||||||
|
|
||||||
|
|
||||||
### 接口封装步骤
|
|
||||||
|
|
||||||
比如获取故事信息接口
|
|
||||||
|
|
||||||
- 接口名:alipay.story.find
|
|
||||||
- 版本号:1.0
|
|
||||||
- 参数:name 故事名称
|
|
||||||
- 返回信息
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"alipay_story_find_response": {
|
|
||||||
"msg": "Success",
|
|
||||||
"code": "10000",
|
|
||||||
"name": "白雪公主",
|
|
||||||
"id": 1,
|
|
||||||
"gmtCreate": 1554193987378
|
|
||||||
},
|
|
||||||
"sign": "xxxxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
针对这个接口,封装步骤如下:
|
|
||||||
|
|
||||||
1.在`Model`包下新建一个类,定义业务参数
|
|
||||||
|
|
||||||
```
|
|
||||||
namespace SDKCSharp.Model
|
|
||||||
{
|
|
||||||
public class GetStoryModel
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 故事名称
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The name.</value>
|
|
||||||
[JsonProperty("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`[JsonProperty("name")]`是Newtonsoft.Json组件中的类,用于Json序列化,括号中的是参数名称。
|
|
||||||
类似于Java中的注解,`@JSONField(name = "xx")`
|
|
||||||
|
|
||||||
2.在`Response`包下新建一个返回类GetStoryResponse,继承`BaseResponse`
|
|
||||||
|
|
||||||
里面填写返回的字段
|
|
||||||
|
|
||||||
```
|
|
||||||
namespace SDKCSharp.Response
|
|
||||||
{
|
|
||||||
public class GetStoryResponse: BaseResponse
|
|
||||||
{
|
|
||||||
[JsonProperty("id")]
|
|
||||||
public int Id { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("gmt_create")]
|
|
||||||
public string GmtCreate { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
3.在`Request`文件夹下新建一个请求类,继承`BaseRequest`
|
|
||||||
|
|
||||||
BaseRequest中有个泛型参数,填`GetStoryResponse`类,表示这个请求对应的返回类。
|
|
||||||
重写`GetMethod()`方法,填接口名。
|
|
||||||
|
|
||||||
如果要指定版本号,可重写`GetVersion()`方法,或者后续使用`request.Version = version`进行设置
|
|
||||||
|
|
||||||
```
|
|
||||||
namespace SDKCSharp.Request
|
|
||||||
{
|
|
||||||
public class GetStoryRequest : BaseRequest<GetStoryResponse>
|
|
||||||
{
|
|
||||||
public override string GetMethod()
|
|
||||||
{
|
|
||||||
return "alipay.story.find";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 使用方式
|
|
||||||
|
|
||||||
```
|
|
||||||
class MainClass
|
|
||||||
{
|
|
||||||
static string url = "http://localhost:8081";
|
|
||||||
static string appId = "2019032617262200001";
|
|
||||||
// 平台提供的私钥
|
|
||||||
static string privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=";
|
|
||||||
|
|
||||||
|
|
||||||
// 声明一个就行
|
|
||||||
static OpenClient client = new OpenClient(url, appId, privateKey);
|
|
||||||
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
TestGet();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标准用法
|
|
||||||
private static void TestGet()
|
|
||||||
{
|
|
||||||
// 创建请求对象
|
|
||||||
GetStoryRequest request = new GetStoryRequest();
|
|
||||||
// 请求参数
|
|
||||||
GetStoryModel model = new GetStoryModel();
|
|
||||||
model.Name = "白雪公主";
|
|
||||||
request.BizModel = model;
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
GetStoryResponse response = client.Execute(request);
|
|
||||||
|
|
||||||
if (response.IsSuccess())
|
|
||||||
{
|
|
||||||
// 返回结果
|
|
||||||
Console.WriteLine("故事名称:{0}", response.Name);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
|
|
||||||
response.Code, response.Msg, response.SubCode, response.SubMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 使用方式2(懒人版)
|
|
||||||
|
|
||||||
如果不想添加Request,Response,Model。可以用这种方式,返回data部分是Dictionary<string, object>,后续自己处理
|
|
||||||
|
|
||||||
```
|
|
||||||
// 懒人版,如果不想添加Request,Response,Model。可以用这种方式,返回Dictionary<string, object>,后续自己处理
|
|
||||||
private static void TestCommon()
|
|
||||||
{
|
|
||||||
// 创建请求对象
|
|
||||||
CommonRequest request = new CommonRequest("alipay.story.find");
|
|
||||||
// 请求参数
|
|
||||||
Dictionary<string, string> bizModel = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
["name"] = "白雪公主"
|
|
||||||
};
|
|
||||||
|
|
||||||
request.BizModel = bizModel;
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
CommonResponse response = client.Execute(request);
|
|
||||||
|
|
||||||
if (response.IsSuccess())
|
|
||||||
{
|
|
||||||
// 返回结果
|
|
||||||
string body = response.Body;
|
|
||||||
Dictionary<string, object> dict = JsonUtil.ParseToDictionary(body);
|
|
||||||
|
|
||||||
Console.WriteLine("Dictionary内容:");
|
|
||||||
foreach (var item in dict)
|
|
||||||
{
|
|
||||||
Console.WriteLine("{0}:{1}", item.Key, item.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
|
|
||||||
response.Code, response.Msg, response.SubCode, response.SubMsg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 发布SDK
|
|
||||||
|
|
||||||
将编写好的SDK打包后上传到云服务器,如百度网盘
|
|
||||||
|
|
||||||
前往sop-admin,点击`SDK管理`菜单,点击`发布SDK`,填写SDK语言、版本、下载地址(网盘地址)、调用示例
|
|
||||||
|
|
||||||
保存后,ISV端会看到发布的SDK
|
|
@@ -1,117 +0,0 @@
|
|||||||
# 应用授权
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
- 1、用户对开发者进行应用授权后,开发者可以帮助用户完成相应的业务逻辑。
|
|
||||||
- 2、授权采用标准的OAuth 2.0流程。
|
|
||||||
|
|
||||||
## 授权流程
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 快速接入
|
|
||||||
|
|
||||||
- 第一步:应用授权URL拼装
|
|
||||||
|
|
||||||
拼接规则:
|
|
||||||
|
|
||||||
http://openauth.yourdomain.com/oauth2/appToAppAuth?app_id=2019032617262200001&redirect_uri=http%3a%2f%2flocalhost%3a8087%2foauth2callback
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
|
|
||||||
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|
|
||||||
|--------------|-------------|--------|----|---------------|--------------------------|
|
|
||||||
| app_id | 开发者应用的AppId | String | 是 | 开发者应用的AppId | 2015101400446982 |
|
|
||||||
| redirect_uri | 回调页面 | String | 是 | 参数需要UrlEncode | http%3A%2F%2Fexample.com |
|
|
||||||
|
|
||||||
|
|
||||||
- 第二步:获取code
|
|
||||||
|
|
||||||
授权成功后,会跳转至开发者定义的回调页面(即redirect_uri参数对应的url),在回调页面请求中会带上当次授权的授权码code和开发者的app_id,示例如下:
|
|
||||||
|
|
||||||
http://www.xxx.com/oauth2callback?app_id=2015101400446982&code=ca34ea491e7146cc87d25fca24c4cD11
|
|
||||||
|
|
||||||
|
|
||||||
- 第三步:使用code换取app_auth_token
|
|
||||||
|
|
||||||
接口名称:open.auth.token.app
|
|
||||||
|
|
||||||
开发者通过code可以换取app_auth_token、授权用户的userId。
|
|
||||||
|
|
||||||
**注意**:应用授权的code唯一,code使用一次后失效,有效期24小时; app_auth_token永久有效。
|
|
||||||
|
|
||||||
**请求参数说明**
|
|
||||||
|
|
||||||
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|
|
||||||
|---------------|------|--------|----|---------------------------------------------------------------------------------|------------------------------------------|
|
|
||||||
| grant_type | 授权类型 | String | 是 | 如果使用code换取token,则为authorization_code,如果使用refresh_token换取新的token,则为refresh_token | authorization_code |
|
|
||||||
| code | 授权码 | String | 否 | 与refresh_token二选一,用户对应用授权后得到,即第一步中开发者获取到的code值 | bf67d8d5ed754af297f72cc482287X62 |
|
|
||||||
| refresh_token | 刷新令牌 | String | 否 | 与code二选一,可为空,刷新令牌时使用 | 201510BB0c409dd5758b4d939d4008a525463X62 |
|
|
||||||
|
|
||||||
**接口请求SDK示例**
|
|
||||||
|
|
||||||
```java
|
|
||||||
@GetMapping("oauth2callback")
|
|
||||||
@ResponseBody
|
|
||||||
public String callback(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
|
|
||||||
String app_id = servletRequest.getParameter("app_id");
|
|
||||||
String code = servletRequest.getParameter("code");
|
|
||||||
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest();
|
|
||||||
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel();
|
|
||||||
model.setCode(code);
|
|
||||||
model.setGrant_type("authorization_code");
|
|
||||||
request.setBizModel(model);
|
|
||||||
// 根据code获取token
|
|
||||||
OpenAuthTokenAppResponse response = openClient.execute(request);
|
|
||||||
if (response.isSuccess()) {
|
|
||||||
// 成功拿到token,开发者在这里保存token信息
|
|
||||||
// 后续使用token进行接口访问
|
|
||||||
log.info("授权成功,body:{}", response.getBody());
|
|
||||||
}
|
|
||||||
return response.getBody();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**同步响应参数说明**
|
|
||||||
|
|
||||||
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|
|
||||||
|-------------------|---------|--------|----|----------------------------------------------------------|----------------------------------|
|
|
||||||
| app_auth_token | 用户授权令牌 | String | 是 | 通过该令牌来帮助用户发起请求,完成业务 | 856faf8d77d3b985c1073557ce6ea724 |
|
|
||||||
| user_id | 授权用户的ID | String | 是 | 授权者id | 1 |
|
|
||||||
| expires_in | 令牌有效期 | Long | 是 | 负值表示永久有效 | -1 |
|
|
||||||
| re_expires_in | 刷新令牌有效期 | Long | 是 | 负值表示永久有效 | -3 |
|
|
||||||
| app_refresh_token | 刷新令牌时使用 | String | 是 | 刷新令牌后,我们会保证老的app_auth_token从刷新开始10分钟内可继续使用,请及时替换为最新token | 88e68196d2359667f0dc8136e6c86803 |
|
|
||||||
|
|
||||||
|
|
||||||
**同步响应结果示例**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"open_auth_token_app_response": {
|
|
||||||
"code": "10000",
|
|
||||||
"msg": "Success",
|
|
||||||
"app_auth_token": "88e68196d2359667f0dc8136e6c86803",
|
|
||||||
"app_refresh_token": "856faf8d77d3b985c1073557ce6ea724",
|
|
||||||
"expires_in": -1,
|
|
||||||
"re_expires_in": -3,
|
|
||||||
"user_id": "1"
|
|
||||||
},
|
|
||||||
"sign": "xxx"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**刷新token**
|
|
||||||
|
|
||||||
```java
|
|
||||||
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest();
|
|
||||||
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel();
|
|
||||||
model.setGrant_type("refresh_token");
|
|
||||||
model.setRefresh_token("856faf8d77d3b985c1073557ce6ea724");
|
|
||||||
request.setBizModel(model);
|
|
||||||
OpenAuthTokenAppResponse response = openClient.execute(request);
|
|
||||||
if (response.isSuccess()) {
|
|
||||||
// 成功拿到token,开发者在这里保存token信息
|
|
||||||
// 后续使用token进行接口访问
|
|
||||||
log.info("换取token成功,body:{}", response.getBody());
|
|
||||||
}
|
|
||||||
```
|
|
@@ -1,147 +0,0 @@
|
|||||||
# 提供restful接口
|
|
||||||
|
|
||||||
有些接口没有被开放,但是也想要通过网关来访问,SOP提供一个固定的请求格式来访问。
|
|
||||||
|
|
||||||
请求格式:
|
|
||||||
|
|
||||||
`http://ip:port/rest/服务id/your_path`,其中`http://ip:port/rest/`为固定部分,后面跟微服务请求路径。
|
|
||||||
|
|
||||||
下面是一个微服务的接口例子
|
|
||||||
|
|
||||||
```java
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("food")
|
|
||||||
public class TraditionalWebappController {
|
|
||||||
@RequestMapping(value = "getFoodById", method = RequestMethod.GET)
|
|
||||||
public Food getFoodById(Integer id) {
|
|
||||||
Food food = new Food();
|
|
||||||
food.setId(id);
|
|
||||||
food.setName("香蕉");
|
|
||||||
food.setPrice(new BigDecimal(20.00));
|
|
||||||
return food;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
这是一个`食品服务`例子,serviceId为`food-service`,假设网关ip为10.0.1.11,端口8081;食品服务ip为10.0.1.22,端口2222
|
|
||||||
|
|
||||||
1. 网关访问:`http://10.0.1.11:8081/rest/food-service/food/getFoodById?id=2`
|
|
||||||
|
|
||||||
2. 本地访问:`http://10.0.1.22:2222/food/getFoodById/?id=2`
|
|
||||||
|
|
||||||
|
|
||||||
由此可见,对于前端调用者来说,它把网关看做一个大服务,只访问网关提供的请求,不需要关心网关后面的路由转发。网关后面各个微服务独自管理,
|
|
||||||
微服务之间的调用可以使用dubbo或feign,有了版本号的管理,可以做到服务的平滑升级,对用户来说都是无感知的。结合SOP-Admin提供的上下线功能,
|
|
||||||
可实现预发布环境功能。
|
|
||||||
|
|
||||||
默认情况下,`http://ip:port/rest/`为固定部分,如果想要更改`rest`,可在网关配置文件指定:`sop.restful.path=/aaa`
|
|
||||||
|
|
||||||
请求前缀将变成:`http://ip:port/aaa/`
|
|
||||||
|
|
||||||
- 关闭restful功能
|
|
||||||
|
|
||||||
如果不想用该功能,在配置文件指定:
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# 开启restful请求,默认开启
|
|
||||||
sop.restful.enable=false
|
|
||||||
```
|
|
||||||
|
|
||||||
- 封装请求工具【可选】
|
|
||||||
|
|
||||||
封装请求,方便调用,针对vue的封装如下:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import axios from 'axios'
|
|
||||||
|
|
||||||
// 创建axios实例
|
|
||||||
const client = axios.create({
|
|
||||||
baseURL: process.env.BASE_API, // api 的 base_url
|
|
||||||
timeout: 5000, // 请求超时时间
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
})
|
|
||||||
|
|
||||||
const RequestUtil = {
|
|
||||||
/**
|
|
||||||
* 请求接口
|
|
||||||
* @param url 请求路径,如http://localhost:8081/rest/food-service/food/getFoodById
|
|
||||||
* @param data 请求数据,json格式
|
|
||||||
* @param callback 成功回调
|
|
||||||
* @param errorCallback 失败回调
|
|
||||||
*/
|
|
||||||
post: function(url, data, callback, errorCallback) {
|
|
||||||
client.post(url, data)
|
|
||||||
.then(function(response) {
|
|
||||||
const resp = response.result
|
|
||||||
const code = resp.code
|
|
||||||
// 成功,网关正常且业务正常
|
|
||||||
if (code === '10000' && !resp.sub_code) {
|
|
||||||
callback(resp)
|
|
||||||
} else {
|
|
||||||
// 报错
|
|
||||||
Message({
|
|
||||||
message: resp.msg,
|
|
||||||
type: 'error',
|
|
||||||
duration: 5 * 1000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(error) {
|
|
||||||
console.log('err' + error) // for debug
|
|
||||||
errorCallback && errorCallback(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RequestUtil
|
|
||||||
```
|
|
||||||
|
|
||||||
jQuery版本如下:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var RequestUtil = {
|
|
||||||
/**
|
|
||||||
* 请求接口
|
|
||||||
* @param url 请求路径,如http://localhost:8081/rest/food-service/food/getFoodById
|
|
||||||
* @param data 请求数据,json格式
|
|
||||||
* @param callback 成功回调
|
|
||||||
* @param errorCallback 失败回调
|
|
||||||
*/
|
|
||||||
post: function(url, data, callback, errorCallback) {
|
|
||||||
$.ajax({
|
|
||||||
url: 'http://localhost:8081' // 网关url
|
|
||||||
, type: 'post'
|
|
||||||
, headers: { 'Content-Type': 'application/json' }
|
|
||||||
, data: data
|
|
||||||
,success:function(response) {
|
|
||||||
var resp = response.result
|
|
||||||
var code = resp.code
|
|
||||||
// 成功,网关正常且业务正常
|
|
||||||
if (code === '10000' && !resp.sub_code) {
|
|
||||||
callback(resp)
|
|
||||||
} else {
|
|
||||||
// 报错
|
|
||||||
alert(resp.msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
, error: function(error) {
|
|
||||||
errorCallback && errorCallback(error)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
jQuery调用示例:
|
|
||||||
|
|
||||||
```js
|
|
||||||
$(function () {
|
|
||||||
var data = {
|
|
||||||
id: 1
|
|
||||||
,name: '葫芦娃'
|
|
||||||
}
|
|
||||||
RequestUtil.post('http://localhost:8081/rest/food-service/food/getFoodById', data, function (result) {
|
|
||||||
console.log(result)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
```
|
|
@@ -1,111 +0,0 @@
|
|||||||
# 文件上传
|
|
||||||
|
|
||||||
请求接口时带上文件
|
|
||||||
|
|
||||||
## 客户端调用
|
|
||||||
|
|
||||||
```java
|
|
||||||
DemoFileUploadRequest request = new DemoFileUploadRequest();
|
|
||||||
|
|
||||||
DemoFileUploadModel model = new DemoFileUploadModel();
|
|
||||||
model.setRemark("上传文件参数");
|
|
||||||
request.setBizModel(model);
|
|
||||||
|
|
||||||
List<UploadFile> files = new ArrayList<>();
|
|
||||||
String root = System.getProperty("user.dir");
|
|
||||||
System.out.println(root);
|
|
||||||
// 这里演示将resources下的两个文件上传到服务器
|
|
||||||
files.add(new UploadFile("file1", new File(root + "/src/main/resources/file1.txt")));
|
|
||||||
files.add(new UploadFile("file2", new File(root + "/src/main/resources/file2.txt")));
|
|
||||||
request.setFiles(files);
|
|
||||||
|
|
||||||
DemoFileUploadResponse response = client.execute(request);
|
|
||||||
|
|
||||||
System.out.println("--------------------");
|
|
||||||
if (response.isSuccess()) {
|
|
||||||
List<DemoFileUploadResponse.FileMeta> responseFiles = response.getFiles();
|
|
||||||
System.out.println("您上传的文件信息:");
|
|
||||||
responseFiles.stream().forEach(file->{
|
|
||||||
System.out.println(file);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
System.out.println("errorCode:" + response.getCode() + ",errorMsg:" + response.getMsg());
|
|
||||||
}
|
|
||||||
System.out.println("--------------------");
|
|
||||||
```
|
|
||||||
|
|
||||||
客户端使用`UploadFile`添加上传文件
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* @param name 表单名称,不能重复
|
|
||||||
* @param file 文件
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public UploadFile(String name, File file) throws IOException {
|
|
||||||
this(name, file.getName(), FileUtil.toBytes(file));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
其中`name`表示字段名称,相当于`<input type="file" name="file1"/>`中的name
|
|
||||||
|
|
||||||
源码详见sop-sdk
|
|
||||||
|
|
||||||
## 服务端接收文件
|
|
||||||
|
|
||||||
- 方式1
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 方式1:将文件写在参数中,可直接获取。好处是可以校验是否上传
|
|
||||||
* @param param
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Open("file.upload")
|
|
||||||
@RequestMapping("file1")
|
|
||||||
public FileUploadVO file1(FileUploadParam param) {
|
|
||||||
System.out.println(param.getRemark());
|
|
||||||
// 获取上传的文件
|
|
||||||
MultipartFile file1 = param.getFile1();
|
|
||||||
MultipartFile file2 = param.getFile2();
|
|
||||||
...
|
|
||||||
return vo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class FileUploadParam {
|
|
||||||
private String remark;
|
|
||||||
|
|
||||||
// 上传文件,字段名称对应表单中的name属性值
|
|
||||||
@NotNull(message = "文件1不能为空")
|
|
||||||
private MultipartFile file1;
|
|
||||||
|
|
||||||
@NotNull(message = "文件2不能为空")
|
|
||||||
private MultipartFile file2;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
方式1的好处是可以使用`JSR-303`校验文件是否上传
|
|
||||||
|
|
||||||
- 方式2
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 方式2:从request中获取上传文件
|
|
||||||
*
|
|
||||||
* @param param
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Open("file.upload2")
|
|
||||||
@RequestMapping("file2")
|
|
||||||
public FileUploadVO file2(FileUploadParam2 param, HttpServletRequest request) {
|
|
||||||
System.out.println(param.getRemark());
|
|
||||||
FileUploadVO vo = new FileUploadVO();
|
|
||||||
// 获取上传的文件
|
|
||||||
Collection<MultipartFile> uploadFiles = UploadUtil.getUploadFiles(request);
|
|
||||||
...
|
|
||||||
return vo;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
微服务端源码参考`FileUploadDemoController.java`
|
|
@@ -1,104 +0,0 @@
|
|||||||
# 配置Sleuth链路追踪
|
|
||||||
|
|
||||||
配置了Sleuth可以很方便查看微服务的调用路线图,可快速定位问题。
|
|
||||||
|
|
||||||
SOP基于SpringCloud,因此只要整合[Spring Cloud Sleuth](https://spring.io/projects/spring-cloud-sleuth)即可。
|
|
||||||
除此之外,还需要支持dubbo的链路的跟踪,Sleuth在2.0已经对dubbo做了支持,详见:[brave-instrumentation-dubbo-rpc](https://github.com/openzipkin/brave/tree/master/instrumentation/dubbo-rpc)
|
|
||||||
|
|
||||||
接入Spring Cloud Sleuth步骤如下:
|
|
||||||
|
|
||||||
- 下载zipkin服务器
|
|
||||||
|
|
||||||
以mac环境为例,执行下面命令,下载jar并启动zipkin服务
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -sSL https://zipkin.io/quickstart.sh | bash -s
|
|
||||||
java -jar zipkin.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
默认端口是9411,更多安装方式详见:[quickstart](https://zipkin.io/pages/quickstart.html)
|
|
||||||
|
|
||||||
- sop-gateway/pom.xml添加依赖
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!--开启zipkin服务链路跟踪-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-starter-zipkin</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
配置文件新增
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# zipkin服务跟踪
|
|
||||||
spring.zipkin.base-url=http://127.0.0.1:9411/
|
|
||||||
# 设置sleuth收集信息的比率,默认0.1,最大是1,数字越大越耗性能
|
|
||||||
spring.sleuth.sampler.probability=1
|
|
||||||
```
|
|
||||||
重启sop-gateway
|
|
||||||
|
|
||||||
- 打开sop-story-web/pom.xml
|
|
||||||
|
|
||||||
添加依赖:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<!--开启zipkin服务链路跟踪-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-starter-zipkin</artifactId>
|
|
||||||
</dependency>
|
|
||||||
<!-- zipkin支持dubbo -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.zipkin.brave</groupId>
|
|
||||||
<artifactId>brave-instrumentation-dubbo-rpc</artifactId>
|
|
||||||
<version>5.6.6</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
配置文件新增:
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# zipkin服务跟踪
|
|
||||||
spring.zipkin.base-url=http://127.0.0.1:9411/
|
|
||||||
# 设置sleuth收集信息的比率,默认0.1,最大是1,数字越大越耗性能
|
|
||||||
spring.sleuth.sampler.probability=1
|
|
||||||
# dubbo使用zipkin过滤器
|
|
||||||
dubbo.provider.filter=tracing
|
|
||||||
dubbo.consumer.filter=tracing
|
|
||||||
```
|
|
||||||
|
|
||||||
重启服务
|
|
||||||
|
|
||||||
- 打开sop-book/sop-book-web/pom.xml
|
|
||||||
|
|
||||||
步骤同上
|
|
||||||
|
|
||||||
- 运行DubboDemoTest.java单元测试
|
|
||||||
|
|
||||||
运行完毕看控制台,找到日志信息
|
|
||||||
|
|
||||||
```text
|
|
||||||
2019-07-18 16:22:04.438 INFO [story-service,59dae98250b276bd,60828035658f175f,true] 90553 --- [:12345-thread-2] c.g.s.s.service.DefaultDemoService : dubbo provider, param: DemoParam(id=222)
|
|
||||||
```
|
|
||||||
|
|
||||||
日志内容多了`[story-service,59dae98250b276bd,60828035658f175f,true]`部分,这些是zipkin加进去的,说明如下:
|
|
||||||
|
|
||||||
```text
|
|
||||||
story-service:服务名称
|
|
||||||
59dae98250b276bd:traceId
|
|
||||||
60828035658f175f:spanId
|
|
||||||
true:是否上传到zipkin服务器
|
|
||||||
```
|
|
||||||
|
|
||||||
查看各个服务的控制台,可以发现traceId是一致的。
|
|
||||||
|
|
||||||
- 浏览器打开:http://127.0.0.1:9411/
|
|
||||||
|
|
||||||
将traceId复制黏贴到右上角文本框进行查询,可看到服务调用链。
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -1,71 +0,0 @@
|
|||||||
# 预发布灰度发布
|
|
||||||
|
|
||||||
从1.14.0开始支持预发布、灰度发布,可登陆`SOP-Admin`,然后选择`服务列表`进行操作。
|
|
||||||
|
|
||||||
## 使用预发布
|
|
||||||
|
|
||||||
SOP中预发布的思路如下:
|
|
||||||
|
|
||||||
假设网关工程sop-gateway在阿里云负载均衡有两台服务器,域名分别为:
|
|
||||||
|
|
||||||
|域名|说明|
|
|
||||||
|:---- |:---- |
|
|
||||||
|open1.domain.com |网关服务器1 |
|
|
||||||
|openpre.domain.com | 网关服务器2,作为预发布请求入口|
|
|
||||||
|
|
||||||
SLB对外域名为:`open.domain.com`,即开放平台入口为:`http://open.domain.com`
|
|
||||||
|
|
||||||
访问`open.domain.com`会负载均衡到`open1.domain.com`和`openpre.domain.com`这两台实例
|
|
||||||
|
|
||||||
如果单独从`openpre.domain.com`访问,则会访问到预发布微服务。
|
|
||||||
|
|
||||||
SOP开启预发布步骤如下:
|
|
||||||
|
|
||||||
修改网关工程配置文件,指定预发布域名
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# 预发布网关域名,多个用英文逗号(,)隔开
|
|
||||||
pre.domain=openpre.domain.com
|
|
||||||
```
|
|
||||||
|
|
||||||
重启网关
|
|
||||||
|
|
||||||
|
|
||||||
微服务启动参数添加:`--spring.cloud.nacos.discovery.metadata.env=pre`(eureka下是:`--eureka.instance.metadata-map.env=pre`)。
|
|
||||||
建议线上配两套启动脚本,其中预发布启动脚本添加启动参数`--spring.cloud.nacos.discovery.metadata.env=pre`
|
|
||||||
|
|
||||||
登录SOP-Admin,在服务列表中点击预发布。
|
|
||||||
|
|
||||||
从`openpre.domain.com`请求进来的用户都会进预发布服务器,从SLB域名进来请求路由到非预发服务器
|
|
||||||
|
|
||||||
## 使用灰度发布
|
|
||||||
|
|
||||||
- 灰度接口定义
|
|
||||||
|
|
||||||
接口名相同,版本号不同。比如接口`user.get`,version:1.0
|
|
||||||
|
|
||||||
新建一个接口`user.get`,version:2.0
|
|
||||||
|
|
||||||
那么这个2.0接口就是灰度接口
|
|
||||||
|
|
||||||
灰度发布可允许指定的用户访问灰度服务器,其它用户还是走正常流程。
|
|
||||||
|
|
||||||
登录SOP-Admin,前往`服务列表`。
|
|
||||||
|
|
||||||
- 先设置灰度参数,指定灰度appId和灰度接口
|
|
||||||
- 服务器实例开启灰度
|
|
||||||
|
|
||||||
|
|
||||||
参考类:
|
|
||||||
|
|
||||||
- LoadBalanceServerChooser.java 预发布/灰度发布服务实例选择
|
|
||||||
|
|
||||||
### 自定义判断灰度用户
|
|
||||||
|
|
||||||
默认根据`appId`和`IP`来判断灰度用户,如果要通过其它维度来判断是否是灰度用户,可实现GrayUserBuilder接口,
|
|
||||||
然后在springboot main方法中调用如下方法
|
|
||||||
|
|
||||||
```java
|
|
||||||
ApiConfig.getInstance().addGrayUserBuilder(new XXGrayUserBuilder());
|
|
||||||
```
|
|
||||||
参考:com.gitee.sop.gatewaycommon.loadbalancer.builder.AppIdGrayUserBuilder.java
|
|
@@ -1,33 +0,0 @@
|
|||||||
# 动态修改请求参数
|
|
||||||
|
|
||||||
自1.14.0开始,zuul网关支持动态修改请求参数。即在网关修改客户端传递过来的参数,然后发送到微服务端。
|
|
||||||
|
|
||||||
```
|
|
||||||
客户端参数{"name": "jim"} --> zuul中修改为{"name": "Lucy"} --> 微服务端将收到{"name": "Lucy"}
|
|
||||||
```
|
|
||||||
|
|
||||||
使用场景:客户端请求参数经过加密,在网关解密后,再次发送明文参数给微服务端
|
|
||||||
|
|
||||||
- 如何使用
|
|
||||||
|
|
||||||
在网关springboot启动函数中添加如下代码
|
|
||||||
|
|
||||||
```java
|
|
||||||
public static void main(String[] args) {
|
|
||||||
ApiConfig.getInstance().setParameterFormatter(requestParams -> {
|
|
||||||
// 获取biz_content
|
|
||||||
JSONObject jsonObject = requestParams.getJSONObject(ParamNames.BIZ_CONTENT_NAME);
|
|
||||||
// 修改biz_content中的值
|
|
||||||
jsonObject.put("name", "name修改了111");
|
|
||||||
jsonObject.put("remark", "remark修改了222");
|
|
||||||
// 重新设置biz_content
|
|
||||||
requestParams.put(ParamNames.BIZ_CONTENT_NAME, jsonObject);
|
|
||||||
});
|
|
||||||
SpringApplication.run(SopGatewayApplication.class, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
其中requestParams是客户端传递过来的参数,可在此基础上添加修改参数。
|
|
||||||
|
|
||||||
更多参考:com.gitee.sop.gatewaycommon.zuul.filter.PreParameterFormatterFilter.java
|
|
@@ -1,3 +0,0 @@
|
|||||||
# 使用eureka
|
|
||||||
|
|
||||||
切换到`eureka`分支
|
|
@@ -1,14 +0,0 @@
|
|||||||
# 超时设置
|
|
||||||
|
|
||||||
当微服务处理业务逻辑时间过长,网关会报超时错误,默认等待时间是5秒。
|
|
||||||
|
|
||||||
可在网关指定`spring.cloud.gateway.httpclient.response-timeout`参数设置超时时间,单位毫秒
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# 设置响应超时10秒
|
|
||||||
spring.cloud.gateway.httpclient.response-timeout=10000
|
|
||||||
```
|
|
||||||
|
|
||||||
更多配置参见:`org.springframework.cloud.gateway.config.HttpClientProperties`
|
|
||||||
|
|
||||||
测试用例参见:`com.gitee.sop.test.TimeoutTest`
|
|
@@ -1,149 +0,0 @@
|
|||||||
# 网关性能测试
|
|
||||||
|
|
||||||
> 注意:记得关闭限流功能
|
|
||||||
|
|
||||||
**测试环境**
|
|
||||||
|
|
||||||
- 测试工具:[wrk](https://github.com/wg/wrk),[安装教程](https://www.cnblogs.com/quanxiaoha/p/10661650.html)
|
|
||||||
- 服务器:CentOS7(虚拟机,宿主机:macbookpro),内存:2G,CPU:1,核数:2核
|
|
||||||
- 运行环境:Java8、Mysql-5.7、Nacos-1.1.3
|
|
||||||
- 网关启动参数:
|
|
||||||
|
|
||||||
```
|
|
||||||
-verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC -Xloggc:gc-zuul.log \
|
|
||||||
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn512m -Xss256k -XX:SurvivorRatio=8 \
|
|
||||||
-XX:+UseConcMarkSweepGC
|
|
||||||
```
|
|
||||||
|
|
||||||
- zuul配置(仅针对zuul,Spring Cloud Gateway没有做优化配置):
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# 不校验时间,这样一个链接可以一直进行测试
|
|
||||||
sop.api-config.timeout-seconds=0
|
|
||||||
sop.restful.enable=true
|
|
||||||
|
|
||||||
logging.level.com.gitee=info
|
|
||||||
|
|
||||||
# zuul调优
|
|
||||||
zuul.host.max-per-route-connections=5000
|
|
||||||
zuul.host.max-total-connections=5000
|
|
||||||
zuul.semaphore.max-semaphores=5000
|
|
||||||
|
|
||||||
ribbon.ReadTimeout=5000
|
|
||||||
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=13000
|
|
||||||
|
|
||||||
logging.file=sop-gateway.log
|
|
||||||
```
|
|
||||||
|
|
||||||
CentOS允许最大连接数
|
|
||||||
```
|
|
||||||
$ ulimit -n
|
|
||||||
65535
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调用开放接口
|
|
||||||
|
|
||||||
- wrk命令:
|
|
||||||
|
|
||||||
```
|
|
||||||
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/?charset=utf-8&biz_content=%7B%22name%22%3A%22%E8%91%AB%E8%8A%A6%E5%A8%83%22%2C%22id%22%3A%221%22%7D&method=alipay.story.get&format=json&sign=RjK%2FThnzAJQOw%2BfoVLS18PgWZAR%2B25SI2XdONFhS%2BmS8vsv2jNT3rygFoh%2ByX1AJbMgIEfcBzkQyqrs29jjd5dcwHVkcf7vxXshyfcEgl0fbMF6Ihycnz7rqSqkW3lzAWx4NuWUfkPnTX8Ffuf%2BhYRaI0NCpNv%2FV300HvsvmUjS6ZzS4YHaT1peSq0agfUhwRPd97aYMnUwRZDzxNfc5wuXA7OQ1o%2FPYIuIb%2FajVfwNP5ysitc%2FKtYEqt9rNAuzkcFmsw71d2RRnrPLsDN%2BuBXnIEh482f%2FbMj2Rj4%2FMq%2B0PEtlTRbg3rYnxyfowymfX%2BNmI4gNRUt70D4a%2FL3Qiug%3D%3D&app_id=2019032617262200001&sign_type=RSA2&version=1.0×tamp=2020-01-19+13%3A34%3A12"
|
|
||||||
```
|
|
||||||
|
|
||||||
- Spring Cloud Gateway测试结果
|
|
||||||
|
|
||||||
```
|
|
||||||
8 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 139.74ms 69.39ms 617.14ms 69.82%
|
|
||||||
Req/Sec 182.12 55.74 343.00 66.24%
|
|
||||||
43391 requests in 30.09s, 11.96MB read
|
|
||||||
Requests/sec: 1441.96
|
|
||||||
Transfer/sec: 406.96KB
|
|
||||||
```
|
|
||||||
|
|
||||||
结果说明,下同:
|
|
||||||
|
|
||||||
```
|
|
||||||
8 threads and 200 connections (共8个测试线程,200个连接)
|
|
||||||
(平均值) (标准差) (最大值)(正负一个标准差所占比例)
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
(延迟)
|
|
||||||
Latency 139.74ms 69.39ms 617.14ms 69.82%
|
|
||||||
(每秒请求数)
|
|
||||||
Req/Sec 182.12 55.74 343.00 66.24%
|
|
||||||
43391 requests in 30.09s, 11.96MB read (30.09秒内处理了43391个请求,耗费流量11.96MB)
|
|
||||||
Requests/sec: 1441.96 (QPS 1441.96,即平均每秒处理请求数为1441.96)
|
|
||||||
Transfer/sec: 406.96KB (平均每秒流量406.96KB)
|
|
||||||
```
|
|
||||||
|
|
||||||
- Spring Cloud Zuul测试结果
|
|
||||||
|
|
||||||
```
|
|
||||||
8 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 230.14ms 331.27ms 2.00s 86.98%
|
|
||||||
Req/Sec 141.69 51.04 323.00 66.99%
|
|
||||||
33945 requests in 30.09s, 9.88MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 385
|
|
||||||
Requests/sec: 1128.05
|
|
||||||
Transfer/sec: 336.15KB
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调用restful请求
|
|
||||||
|
|
||||||
- wrk命令:
|
|
||||||
|
|
||||||
```
|
|
||||||
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/rest/story-service/food/getFoodById?id=2"
|
|
||||||
```
|
|
||||||
|
|
||||||
线程数为 8,模拟 200 个并发请求,持续 30 秒
|
|
||||||
|
|
||||||
- Spring Cloud Gateway测试结果
|
|
||||||
|
|
||||||
```
|
|
||||||
8 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 120.14ms 58.30ms 513.85ms 67.47%
|
|
||||||
Req/Sec 210.47 54.26 770.00 69.37%
|
|
||||||
50301 requests in 30.10s, 7.53MB read
|
|
||||||
Requests/sec: 1670.97
|
|
||||||
Transfer/sec: 256.21KB
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
- Spring Cloud Zuul测试结果
|
|
||||||
|
|
||||||
```
|
|
||||||
8 threads and 200 connections
|
|
||||||
Thread Stats Avg Stdev Max +/- Stdev
|
|
||||||
Latency 185.86ms 285.65ms 1.99s 88.55%
|
|
||||||
Req/Sec 167.75 55.60 460.00 68.05%
|
|
||||||
40070 requests in 30.09s, 6.65MB read
|
|
||||||
Socket errors: connect 0, read 0, write 0, timeout 466
|
|
||||||
Requests/sec: 1331.81
|
|
||||||
Transfer/sec: 226.50KB
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
综上所述,Spring Cloud Gateway在没有优化的情况下,压测表现比zuul好,但zuul的数据表现也不差,但是出现超时现象,总的来说还是Spring Cloud Gateway具有优势。
|
|
||||||
|
|
||||||
附启动脚本:
|
|
||||||
|
|
||||||
`restart.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo "Stopping sop-gateway-4.2.4-SNAPSHOT.jar"
|
|
||||||
pid=`ps -ef | grep sop-gateway-4.2.4-SNAPSHOT.jar | grep -v grep | awk '{print $2}'`
|
|
||||||
if [ -n "$pid" ]
|
|
||||||
then
|
|
||||||
echo "kill -9 的id:" $pid
|
|
||||||
kill -9 $pid
|
|
||||||
fi
|
|
||||||
nohup java -jar -verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC -Xloggc:gc.log \
|
|
||||||
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8\
|
|
||||||
-XX:+UseConcMarkSweepGC sop-gateway-4.2.4-SNAPSHOT.jar\
|
|
||||||
--spring.profiles.active=dev --server.port=8081 &
|
|
||||||
|
|
||||||
tail -f nohup.out
|
|
||||||
```
|
|
@@ -1,46 +0,0 @@
|
|||||||
# 原理分析之如何存储路由
|
|
||||||
|
|
||||||
SOP基于spring cloud,因此会涉及到网关路由。但是开发者不用去配置文件定义路由的隐射关系,SOP帮你解决了这个问题。
|
|
||||||
|
|
||||||
## 获取路由信息
|
|
||||||
|
|
||||||
网关启动成后会触发一个事件,代码见:`com.gitee.sop.gatewaycommon.config.AbstractConfiguration.listenEvent`
|
|
||||||
|
|
||||||
这个事件会取拉取微服务中提供的路由信息
|
|
||||||
|
|
||||||
下面以nacos为例,介绍拉取路由过程
|
|
||||||
|
|
||||||
1.从nacos中获取微服务实例
|
|
||||||
|
|
||||||
入口代码:`com.gitee.sop.bridge.route.NacosRegistryListener.onEvent`
|
|
||||||
|
|
||||||
2.拿到微服务信息,调用微服务提供的接口拉取路由数据
|
|
||||||
|
|
||||||
入口代码:`com.gitee.sop.gatewaycommon.route.BaseRegistryListener.pullRoutes`
|
|
||||||
|
|
||||||
最终落实到:`com.gitee.sop.gatewaycommon.route.ServiceRouteListener.onAddInstance`
|
|
||||||
|
|
||||||
微服务提供一个url:`http://ip:port/sop/routes`,对应Controller在:`com.gitee.sop.servercommon.route.ServiceRouteController.listRoutes`
|
|
||||||
|
|
||||||
微服务找到被`@Open`注解的方法,然后封装成一个路由对象,放到List中,最后返回给网关。
|
|
||||||
|
|
||||||
3.网关拿到路由信息,经过处理,转化成网关路由配置
|
|
||||||
|
|
||||||
关联方法:
|
|
||||||
|
|
||||||
`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.add`
|
|
||||||
|
|
||||||
`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.refresh`
|
|
||||||
|
|
||||||
|
|
||||||
路由的存储方式是一个Map,key为路由id,即接口名+版本号。
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* key:nameVersion
|
|
||||||
*/
|
|
||||||
private static final Map<String, GatewayTargetRoute> routes = synchronizedMap(new LinkedHashMap<>());
|
|
||||||
```
|
|
||||||
|
|
||||||
因为客户端调用接口都会传递一个接口名和版本号,因此通过这两个字段能够很快查询出路由信息,然后进行路由转发操作。
|
|
||||||
|
|
@@ -1,33 +0,0 @@
|
|||||||
# 原理分析之如何路由
|
|
||||||
|
|
||||||
Spring Cloud Gateway通过一系列的Filter来进行数据的传输,如下图所示:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
SOP网关在此基础上新增了几个Filter用来处理自己的逻辑,如:前置校验、结果返回。
|
|
||||||
|
|
||||||
| 过滤器 | 类型 | Order | 功能 |
|
|
||||||
| ----- | ---- | ----------------------- | ---------------------------- |
|
|
||||||
|IndexFilter| `自定义` | -2147483648 | 入口过滤器,获取参数、签名校验 |
|
|
||||||
|ParameterFormatterFilter | `自定义` | -2147482647 | 格式化参数 |
|
|
||||||
|LimitFilter|`自定义`|-2147482447|限流|
|
|
||||||
|ForwardPathFilter|系统自带|0 |设置转发的path|
|
|
||||||
|RouteToRequestUrlFilter|系统自带|10000|设置转发host|
|
|
||||||
|SopLoadBalancerClientFilter|`自定义`|10100|LoadBalance获取转发实例|
|
|
||||||
|NettyRoutingFilter|系统自带|2147483647|获取httpclient发送请求|
|
|
||||||
|ForwardRoutingFilter|系统自带|2147483647|请求分发|
|
|
||||||
|GatewayModifyResponseGatewayFilter|`自定义`|-2|处理响应结果|
|
|
||||||
|
|
||||||
一个完整的请求会自上而下经过这些Filter,下面讲解如何动态设置路由。
|
|
||||||
|
|
||||||
## 动态设置路由
|
|
||||||
|
|
||||||
网关启动后会从注册中心拉取微服务实例,然后请求微服务提供的一个接口(`/sop/routes`),获取开放接口信息(被`@Open`注解的接口)。
|
|
||||||
|
|
||||||
监听处理类在:`com.gitee.sop.bridge.route.NacosRegistryListener`
|
|
||||||
|
|
||||||
获取到路由信息后,将路由信息缓存到本地,并保存到数据库,代码在:`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.load`
|
|
||||||
|
|
||||||
然后动态设置Gateway路由,代码在:`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteRepository.refresh`
|
|
||||||
|
|
||||||
当有微服务重新启动时,网关会监听到微服务实例有变更,会重复上述步骤,确保网关存有最新的路由。
|
|
@@ -1,26 +0,0 @@
|
|||||||
# 原理分析之文档归纳
|
|
||||||
|
|
||||||
作为开放平台,必须要提供API文档。
|
|
||||||
|
|
||||||
SOP采用微服务架构实现,因此文档应该由各个微服务各自实现。难点是如何归纳各个微服务端提供的文档信息,并统一展示。
|
|
||||||
|
|
||||||
SOP的解决思路如下:
|
|
||||||
|
|
||||||
- 各微服务使用swagger定义自己的接口信息
|
|
||||||
- sop-website项目在启动时向注册中心获取所有服务实例,分别调用各个服务提供的swagger文档信息,保存到本地
|
|
||||||
- sop-website前端页面负责展示swagger提供的文档信息
|
|
||||||
|
|
||||||
由于注册中心的存在,可以很方便的获取每个微服务提供的接口,因此可以获取到swagger提供的文档信息。
|
|
||||||
|
|
||||||
如此一来的好处是,各微服务不用关心文档该怎么展示,只需要写好swagger注解即可;文档信息展示统一交给另外一个工程来维护,各司其职。
|
|
||||||
|
|
||||||
SOP设计初衷亦是如此,微服务只管写业务代码,其它的都交给SOP来处理。
|
|
||||||
|
|
||||||
文档归纳原理图:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- sop-website服务启动时向各微服务获取接口信息,保存到本地
|
|
||||||
- 用户访问website页面,website提供对应的接口文档并展示
|
|
||||||
|
|
||||||
|
|
@@ -1,92 +0,0 @@
|
|||||||
# 原理分析之预发布灰度发布
|
|
||||||
|
|
||||||
SOP网关采用`自定义负载均衡策略`来实现对预发布/灰度发布服务器实例的选择。
|
|
||||||
|
|
||||||
spring cloud gateway默认的负载均衡实现类在:`org.springframework.cloud.gateway.filter.LoadBalancerClientFilter.java`中
|
|
||||||
|
|
||||||
这个类主要做了几件事情:
|
|
||||||
|
|
||||||
1. 解析出请求路径中的scheme
|
|
||||||
2. 如果scheme不是以`lb`协议开头直接跳过
|
|
||||||
3. 如果scheme以`lb`协议开头,则说明需要进行负载均衡,选出一台微服务实例
|
|
||||||
4. 将`lb`协议解析成`http://ip:port`,继续向下请求
|
|
||||||
|
|
||||||
其中第4步是由`org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient`来完成的,
|
|
||||||
我们只要分别继承`LoadBalancerClientFilter`和`RibbonLoadBalancerClient`,然后重写其中的方法就能完成自定义负载均衡。
|
|
||||||
|
|
||||||
SOP中的重写类是:`SopLoadBalancerClientFilter`和`SopLoadBalancerClient`,核心代码委托给了`LoadBalanceServerChooser`处理,核心代码如下:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/**
|
|
||||||
* 选择服务器
|
|
||||||
*
|
|
||||||
* @param serviceId serviceId,仅gateway网关有作用
|
|
||||||
* @param exchange 请求上下文
|
|
||||||
* @param loadBalancer loadBalancer
|
|
||||||
* @param superChooser 父类默认的选择
|
|
||||||
* @param serverChooserFunction 执行选择操作
|
|
||||||
* @return 返回服务器实例,没有选到则返回null
|
|
||||||
*/
|
|
||||||
public R choose(
|
|
||||||
String serviceId
|
|
||||||
, T exchange
|
|
||||||
, ILoadBalancer loadBalancer
|
|
||||||
, Supplier<R> superChooser
|
|
||||||
, Function<List<Server>, R> serverChooserFunction) {
|
|
||||||
// 获取所有服务实例
|
|
||||||
List<Server> servers = loadBalancer.getReachableServers();
|
|
||||||
|
|
||||||
// 存放预发服务器
|
|
||||||
List<Server> preServers = new ArrayList<>(4);
|
|
||||||
// 存放灰度发布服务器
|
|
||||||
List<Server> grayServers = new ArrayList<>(4);
|
|
||||||
// 存放非预发服务器
|
|
||||||
List<Server> notPreServers = new ArrayList<>(4);
|
|
||||||
|
|
||||||
for (Server server : servers) {
|
|
||||||
// 获取实例metadata
|
|
||||||
Map<String, String> metadata = getMetadata(serviceId, server);
|
|
||||||
// 是否开启了预发模式
|
|
||||||
if (this.isPreServer(metadata)) {
|
|
||||||
preServers.add(server);
|
|
||||||
} else if (this.isGrayServer(metadata)) {
|
|
||||||
grayServers.add(server);
|
|
||||||
} else {
|
|
||||||
notPreServers.add(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notPreServers.addAll(grayServers);
|
|
||||||
// 如果没有开启预发布服务和灰度发布,直接用默认的方式
|
|
||||||
if (preServers.isEmpty() && grayServers.isEmpty()) {
|
|
||||||
return superChooser.get();
|
|
||||||
}
|
|
||||||
// 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器
|
|
||||||
if (this.isRequestFromPreDomain(exchange)) {
|
|
||||||
return serverChooserFunction.apply(preServers);
|
|
||||||
}
|
|
||||||
// 如果是灰度请求,则认为是灰度用户,选出灰度服务器
|
|
||||||
if (this.isRequestGrayServer(exchange)) {
|
|
||||||
return serverChooserFunction.apply(grayServers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 到这里说明不能访问预发/灰度服务器,则需要路由到非预发服务器
|
|
||||||
// 注意:这里允许走灰度服务器,如果不允许走,注释notPreServers.addAll(grayServers);这行
|
|
||||||
return serverChooserFunction.apply(notPreServers);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
其业务逻辑如下:
|
|
||||||
|
|
||||||
1. 选出`serviceId`对应的所有服务器实例
|
|
||||||
2. 将服务器实例进行分类,分别放进`预发布List`,`灰度List`,`非预发布List`中
|
|
||||||
3. 如果`预发布List`,`灰度List`都为空,表示没有开启任何预发/灰度服务,直接使用父类的负载均衡策略
|
|
||||||
4. 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器
|
|
||||||
5. 如果是灰度请求,则认为是灰度用户,选出灰度服务器
|
|
||||||
6. 最后剩下的是正常用户,正常用户不能走预发环境
|
|
||||||
|
|
||||||
## 参考类
|
|
||||||
|
|
||||||
- com.gitee.sop.gatewaycommon.gateway.filter.SopLoadBalancerClientFilter
|
|
||||||
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.SopLoadBalancerClient
|
|
||||||
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.GatewayLoadBalanceServerChooser
|
|
||||||
|
|
@@ -1,125 +0,0 @@
|
|||||||
# 常见问题
|
|
||||||
|
|
||||||
## Connection has been closed BEFORE send operation
|
|
||||||
|
|
||||||
压测时出现`Connection has been closed BEFORE send operation`,`java.nio.channels.ClosedChannelException`之类的错误
|
|
||||||
|
|
||||||
- 解决办法
|
|
||||||
|
|
||||||
尝试调大内存分配1G或2G,`-Xms1g -Xmx1g`
|
|
||||||
|
|
||||||
## spring cloud gateway [DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144]
|
|
||||||
|
|
||||||
如果POST请求body内容太大可能会报这个错误
|
|
||||||
|
|
||||||
- 解决版办法
|
|
||||||
|
|
||||||
网关指定配置`spring.codec.max-in-memory-size=xx`,xx默认是262144,即256K(262144=256*1024)
|
|
||||||
|
|
||||||
## primordials is not defined
|
|
||||||
|
|
||||||
node版本太高导致与gulp版本不兼容,解决方法:node退回11版本。参考https://blog.csdn.net/zxxzxx23/article/details/103000393
|
|
||||||
|
|
||||||
## Nacos指定group
|
|
||||||
|
|
||||||
可在配置文件中添加:`spring.cloud.nacos.discovery.group=xxx`指定group,不加默认是:DEFAULT_GROUP
|
|
||||||
|
|
||||||
## 在SpringCloudGateway中获取请求参数
|
|
||||||
|
|
||||||
```java
|
|
||||||
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 微服务端如何获取appId等参数
|
|
||||||
|
|
||||||
```java
|
|
||||||
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
|
|
||||||
System.out.println("app_id:" + openContext.getAppId());
|
|
||||||
System.out.println("token:" + openContext.getAppAuthToken());
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 如何关闭签名验证
|
|
||||||
|
|
||||||
- 针对某一个接口关闭签名验证
|
|
||||||
`@Open(value = "alipay.story.get", ignoreValidate = true)`
|
|
||||||
|
|
||||||
|
|
||||||
## 注册到eureka显示hostname,非ip
|
|
||||||
|
|
||||||
```properties
|
|
||||||
eureka.instance.prefer-ip-address=true
|
|
||||||
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
|
|
||||||
```
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.cloud</groupId>
|
|
||||||
<artifactId>spring-cloud-commons</artifactId>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
参考:https://www.jianshu.com/p/5ad8317961b7
|
|
||||||
|
|
||||||
## 直接访问服务的swagger-ui.html ,提示access forbidden
|
|
||||||
|
|
||||||
找到微服务的`OpenServiceConfig.java`,重写内部类Swagger2中的swaggerAccessProtected()方法,返回false。线上请设置成true
|
|
||||||
|
|
||||||
```java
|
|
||||||
// 开启文档
|
|
||||||
@Configuration
|
|
||||||
@EnableSwagger2
|
|
||||||
public static class Swagger2 extends SwaggerSupport {
|
|
||||||
@Override
|
|
||||||
protected String getDocTitle() {
|
|
||||||
return "故事API";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean swaggerAccessProtected() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 调试网关出现服务不可用
|
|
||||||
|
|
||||||
打断点调试,网关出现Read Timeout
|
|
||||||
|
|
||||||
参考:https://blog.csdn.net/qq_36872046/article/details/81058045
|
|
||||||
|
|
||||||
yml添加:
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# https://blog.csdn.net/qq_36872046/article/details/81058045
|
|
||||||
# 路由转发超时时间,毫秒,默认值1000,详见:RibbonClientConfiguration.DEFAULT_READ_TIMEOUT。
|
|
||||||
# 如果微服务端 处理时间过长,会导致ribbon read超时,解决办法将这个值调大一点
|
|
||||||
ribbon.ReadTimeout= 60000
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## 其它微服务没有开放接口,需要排除
|
|
||||||
|
|
||||||
在sop-gateway项目中配置
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# 排除服务,多个用,隔开
|
|
||||||
sop.service.exclude=your-serviceId1,your-serviceId2
|
|
||||||
```
|
|
||||||
|
|
||||||
或者使用正则:
|
|
||||||
|
|
||||||
```properties
|
|
||||||
# 排除以"test-"开头的
|
|
||||||
# 多个正则用英文分号(;)隔开
|
|
||||||
sop.service.exclude-regex=test\\-.*
|
|
||||||
```
|
|
||||||
|
|
||||||
## ISV公私钥 & 平台公私钥
|
|
||||||
|
|
||||||
```java
|
|
||||||
ISV私钥(必须):ISV保存,用来生成签名 --> ISV公钥(必须):平台保存,用来校验签名是否正确
|
|
||||||
平台私钥(非必须):平台保存,对返回结果生成签名 --> 平台公钥(非必须):ISV保存,用来校验签名是否正确
|
|
||||||
```
|
|
||||||
|
|
||||||
总结:私钥负责加密生成签名,公钥负责校验签名是否正确
|
|
Before Width: | Height: | Size: 206 KiB |
Before Width: | Height: | Size: 853 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 63 KiB |
@@ -1,68 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>SOP开发文档</title>
|
|
||||||
<link rel="icon" href="_media/favicon.ico">
|
|
||||||
<meta name="google-site-verification" content="6t0LoIeFksrjF4c9sqUEsVXiQNxLp2hgoqo0KryT-sE" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
|
||||||
<meta name="keywords" content="doc,docs,documentation,gitbook,creator,generator,github,jekyll,github-pages">
|
|
||||||
<meta name="description" content="A magical documentation generator.">
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css" title="vue" >
|
|
||||||
<style>
|
|
||||||
nav.app-nav li ul {
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
/* 显示区域宽度 */
|
|
||||||
.markdown-section{
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
.sidebar li {
|
|
||||||
margin: 0px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="app">加载中 ...</div>
|
|
||||||
<script>
|
|
||||||
window.$docsify = {
|
|
||||||
alias: {
|
|
||||||
'/.*/_sidebar.md': '/_sidebar.md?t=' + new Date().getTime()
|
|
||||||
},
|
|
||||||
auto2top: true,
|
|
||||||
coverpage: false,
|
|
||||||
executeScript: true,
|
|
||||||
loadSidebar: true,
|
|
||||||
loadNavbar: false,
|
|
||||||
mergeNavbar: true,
|
|
||||||
maxLevel: 3,
|
|
||||||
subMaxLevel: 2,
|
|
||||||
ga: 'UA-106147152-1',
|
|
||||||
name: '',
|
|
||||||
search: {
|
|
||||||
noData: {
|
|
||||||
'/zh-cn/': '没有结果!',
|
|
||||||
'/': '没有结果!'
|
|
||||||
},
|
|
||||||
paths: 'auto',
|
|
||||||
placeholder: {
|
|
||||||
'/zh-cn/': '搜索',
|
|
||||||
'/': '搜索'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
formatUpdated: '{MM}/{DD} {HH}:{mm}'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
|
|
||||||
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
|
|
||||||
<script src="//unpkg.com/docsify/lib/plugins/ga.min.js"></script>
|
|
||||||
<script src="//unpkg.com/prismjs/components/prism-bash.min.js"></script>
|
|
||||||
<script src="//unpkg.com/prismjs/components/prism-markdown.min.js"></script>
|
|
||||||
<script src="//unpkg.com/prismjs/components/prism-java.min.js"></script>
|
|
||||||
<script src="//unpkg.com/prismjs/components/prism-json.min.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
49
doc/pom.xml
@@ -1,49 +0,0 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<groupId>com.gitee.sop</groupId>
|
|
||||||
<artifactId>doc</artifactId>
|
|
||||||
<version>4.4.2-SNAPSHOT</version>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<!-- Generic properties -->
|
|
||||||
<java.version>1.8</java.version>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>junit</groupId>
|
|
||||||
<artifactId>junit</artifactId>
|
|
||||||
<version>4.8</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<!-- 打包时跳过测试 -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>2.18.1</version>
|
|
||||||
<configuration>
|
|
||||||
<skipTests>true</skipTests>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>2.3.2</version>
|
|
||||||
<configuration>
|
|
||||||
<source>${java.version}</source>
|
|
||||||
<target>${java.version}</target>
|
|
||||||
<encoding>${project.build.sourceEncoding}</encoding>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
|
|
||||||
</project>
|
|
@@ -1 +0,0 @@
|
|||||||
docsify serve docs
|
|
@@ -1,55 +0,0 @@
|
|||||||
const liveServer = require('live-server')
|
|
||||||
const isSSR = !!process.env.SSR
|
|
||||||
const middleware = []
|
|
||||||
|
|
||||||
if (isSSR) {
|
|
||||||
const Renderer = require('./packages/docsify-server-renderer/build.js')
|
|
||||||
const renderer = new Renderer({
|
|
||||||
template: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>docsify</title>
|
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="/themes/vue.css" title="vue">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!--inject-app-->
|
|
||||||
<!--inject-config-->
|
|
||||||
<script src="/lib/docsify.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>`,
|
|
||||||
config: {
|
|
||||||
name: 'docsify',
|
|
||||||
repo: 'docsifyjs/docsify',
|
|
||||||
basePath: 'https://docsify.js.org/',
|
|
||||||
loadNavbar: true,
|
|
||||||
loadSidebar: true,
|
|
||||||
subMaxLevel: 3,
|
|
||||||
auto2top: true,
|
|
||||||
alias: {
|
|
||||||
'/de-de/changelog': '/changelog',
|
|
||||||
'/zh-cn/changelog': '/changelog',
|
|
||||||
'/changelog':
|
|
||||||
'https://raw.githubusercontent.com/docsifyjs/docsify/master/CHANGELOG'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
path: './'
|
|
||||||
})
|
|
||||||
|
|
||||||
middleware.push(function(req, res, next) {
|
|
||||||
if (/\.(css|js)$/.test(req.url)) {
|
|
||||||
return next()
|
|
||||||
}
|
|
||||||
renderer.renderToString(req.url).then(html => res.end(html))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {
|
|
||||||
port: 3000,
|
|
||||||
watch: ['lib', 'docs', 'themes'],
|
|
||||||
middleware
|
|
||||||
}
|
|
||||||
|
|
||||||
liveServer.start(params)
|
|
@@ -1,99 +0,0 @@
|
|||||||
package com.gitee.sop.doc;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成_sidebar.md文件,直接运行即可
|
|
||||||
*
|
|
||||||
* @author tanghc
|
|
||||||
*/
|
|
||||||
public class SidebarTest {
|
|
||||||
|
|
||||||
static String format = " * [%s](files/%s?t=%s)\r\n";
|
|
||||||
static Map<String, Menu> levelMap = new HashMap<>(8);
|
|
||||||
|
|
||||||
static {
|
|
||||||
int i = 0;
|
|
||||||
levelMap.put("1", new Menu("* 开发文档\n", i++));
|
|
||||||
levelMap.put("9", new Menu("* 原理分析\n", i++));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
String path = SidebarTest.class.getClassLoader().getResource("").getPath();
|
|
||||||
String root = path.substring(0, path.indexOf("doc")) + "doc";
|
|
||||||
String fileDir = root + "/docs/files";
|
|
||||||
File dir = new File(fileDir);
|
|
||||||
File[] files = dir.listFiles();
|
|
||||||
Stream<File> filesStream = Stream.of(files);
|
|
||||||
Map<String, List<FileExt>> menuMap = filesStream
|
|
||||||
.sorted(Comparator.comparing(File::getName))
|
|
||||||
.map(file -> {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
FileExt fileExt = new FileExt();
|
|
||||||
fileExt.menu = file.getName().substring(0, 1);
|
|
||||||
fileExt.file = file;
|
|
||||||
return fileExt;
|
|
||||||
})
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.collect(Collectors.groupingBy(FileExt::getMenu));
|
|
||||||
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
output.append("* [首页](/?t=" + System.currentTimeMillis() + ")\n");
|
|
||||||
for (Map.Entry<String, List<FileExt>> entry : menuMap.entrySet()) {
|
|
||||||
Menu menu = levelMap.get(entry.getKey());
|
|
||||||
if (menu == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
output.append(menu.parentName);
|
|
||||||
for (FileExt fileExt : entry.getValue()) {
|
|
||||||
String filename = fileExt.file.getName();
|
|
||||||
String title = filename.substring(filename.indexOf("_") + 1, filename.length() - 3);
|
|
||||||
String line = String.format(format, title, filename, System.currentTimeMillis());
|
|
||||||
output.append(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(output);
|
|
||||||
|
|
||||||
String sidebarFilepath = root + "/docs/_sidebar.md";
|
|
||||||
|
|
||||||
FileOutputStream out = new FileOutputStream(new File(sidebarFilepath));
|
|
||||||
out.write(output.toString().getBytes());
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Menu {
|
|
||||||
String parentName;
|
|
||||||
int order;
|
|
||||||
|
|
||||||
public Menu(String parentName, int order) {
|
|
||||||
this.parentName = parentName;
|
|
||||||
this.order = order;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static class FileExt {
|
|
||||||
File file;
|
|
||||||
String menu;
|
|
||||||
|
|
||||||
public File getFile() {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMenu() {
|
|
||||||
return menu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
0
docker-build.sh
Normal file → Executable file
4
docker-entrypoint.sh
Normal file → Executable file
@@ -3,11 +3,9 @@
|
|||||||
JAVA_OPTS="-Xms128m -Xmx128m"
|
JAVA_OPTS="-Xms128m -Xmx128m"
|
||||||
|
|
||||||
# mysql, nacos配置
|
# mysql, nacos配置
|
||||||
args="--mysql.host=10.1.30.110:3306 --mysql.username=root --mysql.password=root --register.url=10.1.30.110:8848"
|
args="--mysql.host=10.1.30.110:3306 --mysql.username=root --mysql.password=root --dubbo.registry.address=10.1.30.110:8848"
|
||||||
|
|
||||||
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-gateway/sop-gateway.jar $args --logging.file.path=/sop/sop-gateway/log &
|
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-gateway/sop-gateway.jar $args --logging.file.path=/sop/sop-gateway/log &
|
||||||
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-admin/sop-admin.jar $args --logging.file.path=/sop/sop-admin/log &
|
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-admin/sop-admin.jar $args --logging.file.path=/sop/sop-admin/log &
|
||||||
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-website/sop-website.jar $args --logging.file.path=/sop/sop-website/log &
|
|
||||||
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-auth/sop-auth.jar $args --logging.file.path=/sop/sop-auth/log &
|
|
||||||
# 最后一条没有&
|
# 最后一条没有&
|
||||||
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-story/sop-story.jar $args --logging.file.path=/sop/sop-story/log
|
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-story/sop-story.jar $args --logging.file.path=/sop/sop-story/log
|
153
pom.xml
Normal file → Executable file
@@ -11,28 +11,25 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>com.gitee.sop</groupId>
|
<groupId>com.gitee.sop</groupId>
|
||||||
<artifactId>sop-parent</artifactId>
|
<artifactId>sop-parent</artifactId>
|
||||||
<version>4.4.2-SNAPSHOT</version>
|
<version>5.0.0-SNAPSHOT</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<description>一个开放平台解决方案项目,基于Spring Cloud实现,目标是能够让用户快速得搭建起自己的开放平台</description>
|
<description>一个开放平台解决方案项目,基于Dubbo实现,目标是能够让用户快速得搭建起自己的开放平台</description>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>doc</module>
|
|
||||||
<module>sop-common</module>
|
|
||||||
<module>sop-auth</module>
|
|
||||||
<module>sop-example</module>
|
<module>sop-example</module>
|
||||||
<module>sop-admin</module>
|
<module>sop-admin</module>
|
||||||
<module>sop-gateway</module>
|
<module>sop-website</module>
|
||||||
<module>sop-test</module>
|
<module>sop-test</module>
|
||||||
<module>sop-sdk</module>
|
<module>sop-sdk</module>
|
||||||
<module>sop-website</module>
|
<module>sop-gateway</module>
|
||||||
|
<module>sop-registry</module>
|
||||||
|
<module>sop-support</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>1.8</java.version>
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<maven.compiler.source>1.8</maven.compiler.source>
|
|
||||||
<maven.compiler.target>1.8</maven.compiler.target>
|
|
||||||
|
|
||||||
<!-- springboot 版本-->
|
<!-- springboot 版本-->
|
||||||
<spring-boot.version>2.6.15</spring-boot.version>
|
<spring-boot.version>2.6.15</spring-boot.version>
|
||||||
@@ -41,15 +38,10 @@
|
|||||||
<!-- spring cloud alibaba 版本 -->
|
<!-- spring cloud alibaba 版本 -->
|
||||||
<!-- 具体版本对应关系见:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
|
<!-- 具体版本对应关系见:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
|
||||||
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
|
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
|
||||||
|
<!-- dubbo版本 -->
|
||||||
|
<dubbo.version>3.2.10</dubbo.version>
|
||||||
|
|
||||||
|
|
||||||
<!-- Logging -->
|
|
||||||
<logback.version>1.2.3</logback.version>
|
|
||||||
|
|
||||||
<!-- Test -->
|
|
||||||
<junit.version>4.11</junit.version>
|
<junit.version>4.11</junit.version>
|
||||||
|
|
||||||
<fastjson.version>1.2.73</fastjson.version>
|
|
||||||
<commons-io.version>2.5</commons-io.version>
|
<commons-io.version>2.5</commons-io.version>
|
||||||
<commons-fileupload.version>1.3.3</commons-fileupload.version>
|
<commons-fileupload.version>1.3.3</commons-fileupload.version>
|
||||||
<commons-collection.version>3.2.2</commons-collection.version>
|
<commons-collection.version>3.2.2</commons-collection.version>
|
||||||
@@ -58,15 +50,7 @@
|
|||||||
<commons-logging.version>1.2</commons-logging.version>
|
<commons-logging.version>1.2</commons-logging.version>
|
||||||
<validation-api.version>2.0.1.Final</validation-api.version>
|
<validation-api.version>2.0.1.Final</validation-api.version>
|
||||||
<hibernate-validator.version>6.0.13.Final</hibernate-validator.version>
|
<hibernate-validator.version>6.0.13.Final</hibernate-validator.version>
|
||||||
<fastmybatis.version>2.4.8</fastmybatis.version>
|
<fastmybatis.version>3.0.12</fastmybatis.version>
|
||||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
|
||||||
<guava.version>29.0-jre</guava.version>
|
|
||||||
<knife4j.version>3.0.2</knife4j.version>
|
|
||||||
<swagger.version>1.5.21</swagger.version>
|
|
||||||
<springfox.version>3.0.0</springfox.version>
|
|
||||||
<easyopen.version>1.16.9</easyopen.version>
|
|
||||||
<asm.version>6.2</asm.version>
|
|
||||||
<pagehelper.version>5.2.0</pagehelper.version>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
@@ -93,60 +77,30 @@
|
|||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.guava</groupId>
|
<groupId>org.apache.dubbo</groupId>
|
||||||
<artifactId>guava</artifactId>
|
<artifactId>dubbo-bom</artifactId>
|
||||||
<version>${guava.version}</version>
|
<version>${dubbo.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>io.gitee.durcframework</groupId>
|
||||||
<artifactId>fastjson</artifactId>
|
|
||||||
<version>${fastjson.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.oschina.durcframework</groupId>
|
|
||||||
<artifactId>fastmybatis-spring-boot-starter</artifactId>
|
<artifactId>fastmybatis-spring-boot-starter</artifactId>
|
||||||
<version>${fastmybatis.version}</version>
|
<version>${fastmybatis.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.baomidou</groupId>
|
|
||||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
|
||||||
<version>${mybatis-plus.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.springfox</groupId>
|
|
||||||
<artifactId>springfox-swagger2</artifactId>
|
|
||||||
<version>${springfox.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.springfox</groupId>
|
|
||||||
<artifactId>springfox-spring-web</artifactId>
|
|
||||||
<version>${springfox.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.springfox</groupId>
|
|
||||||
<artifactId>springfox-swagger-ui</artifactId>
|
|
||||||
<version>${springfox.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>io.springfox</groupId>
|
|
||||||
<artifactId>springfox-boot-starter</artifactId>
|
|
||||||
<version>${springfox.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.xiaoymin</groupId>
|
|
||||||
<artifactId>knife4j-spring-boot-starter</artifactId>
|
|
||||||
<version>${knife4j.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.oschina.durcframework</groupId>
|
<groupId>net.oschina.durcframework</groupId>
|
||||||
<artifactId>easyopen</artifactId>
|
<artifactId>http-helper</artifactId>
|
||||||
<version>${easyopen.version}</version>
|
<version>1.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.fastjson2</groupId>
|
||||||
|
<artifactId>fastjson2</artifactId>
|
||||||
|
<version>2.0.52</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -155,12 +109,6 @@
|
|||||||
<version>3.14.7</version>
|
<version>3.14.7</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.oschina.durcframework</groupId>
|
|
||||||
<artifactId>easyopen-spring-boot-starter</artifactId>
|
|
||||||
<version>${easyopen.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.validation</groupId>
|
<groupId>javax.validation</groupId>
|
||||||
<artifactId>validation-api</artifactId>
|
<artifactId>validation-api</artifactId>
|
||||||
@@ -171,11 +119,20 @@
|
|||||||
<artifactId>hibernate-validator</artifactId>
|
<artifactId>hibernate-validator</artifactId>
|
||||||
<version>${hibernate-validator.version}</version>
|
<version>${hibernate-validator.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>com.auth0</groupId>
|
||||||
<artifactId>logback-classic</artifactId>
|
<artifactId>java-jwt</artifactId>
|
||||||
<version>${logback.version}</version>
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>transmittable-thread-local</artifactId>
|
||||||
|
<version>2.14.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
<version>32.1.3-jre</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- commons -->
|
<!-- commons -->
|
||||||
@@ -210,22 +167,10 @@
|
|||||||
<version>${commons-logging.version}</version>
|
<version>${commons-logging.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.ow2.asm</groupId>
|
|
||||||
<artifactId>asm</artifactId>
|
|
||||||
<version>${asm.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.pagehelper</groupId>
|
|
||||||
<artifactId>pagehelper</artifactId>
|
|
||||||
<version>${pagehelper.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.4</version>
|
<version>1.18.34</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.servlet</groupId>
|
<groupId>javax.servlet</groupId>
|
||||||
@@ -264,6 +209,26 @@
|
|||||||
<encoding>UTF-8</encoding>
|
<encoding>UTF-8</encoding>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||||
|
<version>3.3.1</version>
|
||||||
|
<configuration>
|
||||||
|
<configLocation>checkstyle.xml</configLocation>
|
||||||
|
<consoleOutput>true</consoleOutput>
|
||||||
|
<failsOnError>true</failsOnError>
|
||||||
|
<linkXRef>false</linkXRef>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>validate</id>
|
||||||
|
<phase>validate</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
8
sop-admin/pom.xml
Normal file → Executable file
@@ -3,18 +3,16 @@
|
|||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>com.gitee.sop</groupId>
|
<groupId>com.gitee.sop</groupId>
|
||||||
<artifactId>sop-parent</artifactId>
|
<artifactId>sop-parent</artifactId>
|
||||||
<version>4.4.2-SNAPSHOT</version>
|
<version>5.0.0-SNAPSHOT</version>
|
||||||
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
|
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
<artifactId>sop-admin</artifactId>
|
<artifactId>sop-admin</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>sop-admin-server</module>
|
<module>sop-admin-backend</module>
|
||||||
</modules>
|
</modules>
|
||||||
</project>
|
</project>
|
4
sop-admin/readme.md
Normal file → Executable file
@@ -1,5 +1,5 @@
|
|||||||
# 后台admin
|
# 后台admin
|
||||||
|
|
||||||
- sop-admin-server: admin服务端,使用方式见readme.md
|
- sop-admin-backend: admin服务端
|
||||||
- sop-admin-vue: admin前端vue实现
|
- sop-admin-frontend: admin前端实现
|
||||||
|
|
||||||
|
141
sop-admin/sop-admin-backend/admin-boot/pom.xml
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.gitee.sop</groupId>
|
||||||
|
<artifactId>sop-admin-backend</artifactId>
|
||||||
|
<version>5.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>admin-boot</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.gitee.sop</groupId>
|
||||||
|
<artifactId>admin-web</artifactId>
|
||||||
|
<version>5.0.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.dubbo</groupId>
|
||||||
|
<artifactId>dubbo-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- nacos注册中心 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.dubbo</groupId>
|
||||||
|
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- test -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- provided -->
|
||||||
|
|
||||||
|
<!-- 仅在开发中使用 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.dubbo</groupId>
|
||||||
|
<artifactId>dubbo-dependencies-zookeeper-curator5</artifactId>
|
||||||
|
<version>${dubbo.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>aliyun</id>
|
||||||
|
<name>aliyun</name>
|
||||||
|
<url>https://maven.aliyun.com/repository/public</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
<!-- 打包时跳过测试 -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>2.12.4</version>
|
||||||
|
<configuration>
|
||||||
|
<skipTests>true</skipTests>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||||
|
<version>3.3.1</version>
|
||||||
|
<configuration>
|
||||||
|
<configLocation>checkstyle.xml</configLocation>
|
||||||
|
<consoleOutput>true</consoleOutput>
|
||||||
|
<failsOnError>true</failsOnError>
|
||||||
|
<linkXRef>false</linkXRef>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>validate</id>
|
||||||
|
<phase>validate</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>check</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
</project>
|
@@ -0,0 +1,15 @@
|
|||||||
|
package com.gitee.sop.admin;
|
||||||
|
|
||||||
|
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableDubbo
|
||||||
|
public class SopAdminApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(SopAdminApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package com.gitee.sop.admin.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "admin")
|
||||||
|
@Data
|
||||||
|
public class AdminConfig {
|
||||||
|
|
||||||
|
private int jwtTimeoutDays;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,106 @@
|
|||||||
|
package com.gitee.sop.admin.config;
|
||||||
|
|
||||||
|
import com.gitee.sop.admin.common.context.SpringContext;
|
||||||
|
import com.gitee.sop.admin.common.util.SystemUtil;
|
||||||
|
import com.gitee.sop.admin.interceptor.LoginInterceptor;
|
||||||
|
import com.gitee.sop.admin.service.sys.UserCacheService;
|
||||||
|
import com.gitee.sop.admin.service.sys.impl.LocalUserCacheService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
public class SopAdminConfiguration implements ApplicationContextAware, WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Value("${front-location:}")
|
||||||
|
private String frontLocation;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
SpringContext.setApplicationContext(applicationContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(value = "user.cache.type", havingValue = "local", matchIfMissing = true)
|
||||||
|
public UserCacheService userCacheService() {
|
||||||
|
return new LocalUserCacheService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置拦截器
|
||||||
|
*
|
||||||
|
* @param registry
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
String[] excludes = {
|
||||||
|
"/", "/error",
|
||||||
|
// 排除前端资源
|
||||||
|
"/*.html", "/*.ico", "/*.png", "/*.json", "/static/**", "/assets/**"
|
||||||
|
};
|
||||||
|
registry.addInterceptor(new LoginInterceptor())
|
||||||
|
.excludePathPatterns(excludes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨域设置
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public CorsFilter corsFilter(
|
||||||
|
@Value("${torna.cors.allowed-origin-pattern:*}") String allowedOriginPattern,
|
||||||
|
@Value("${torna.cors.allowed-header:*}") String allowedHeader
|
||||||
|
) {
|
||||||
|
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||||
|
// SpringBoot升级2.4.0之后,跨域配置中的.allowedOrigins不再可用,改成addAllowedOriginPattern
|
||||||
|
corsConfiguration.addAllowedOriginPattern(allowedOriginPattern);
|
||||||
|
corsConfiguration.addAllowedHeader(allowedHeader);
|
||||||
|
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
|
||||||
|
corsConfiguration.addExposedHeader("Content-Disposition");
|
||||||
|
corsConfiguration.setAllowCredentials(true);
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", corsConfiguration);
|
||||||
|
return new CorsFilter(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置静态资源
|
||||||
|
*
|
||||||
|
* @param registry
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
String homeDir = SystemUtil.getBinPath();
|
||||||
|
String frontRoot;
|
||||||
|
if (StringUtils.hasText(frontLocation)) {
|
||||||
|
frontRoot = StringUtils.trimTrailingCharacter(frontLocation, '/');
|
||||||
|
} else {
|
||||||
|
frontRoot = homeDir + "/dist";
|
||||||
|
}
|
||||||
|
log.info("前端资源目录:{}", frontRoot);
|
||||||
|
String location = "file:" + frontRoot;
|
||||||
|
registry.addResourceHandler("/index.html").addResourceLocations(location + "/index.html");
|
||||||
|
registry.addResourceHandler("/favicon.ico").addResourceLocations(location + "/favicon.ico");
|
||||||
|
registry.addResourceHandler("/logo.png").addResourceLocations(location + "/logo.png");
|
||||||
|
registry.addResourceHandler("/platform-config.json").addResourceLocations(location + "/platform-config.json");
|
||||||
|
registry.addResourceHandler("/static/**").addResourceLocations(location + "/static/");
|
||||||
|
registry.addResourceHandler("/assets/**").addResourceLocations(location + "/assets/");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,45 @@
|
|||||||
|
package com.gitee.sop.admin.interceptor;
|
||||||
|
|
||||||
|
import com.gitee.sop.admin.common.annotation.NoToken;
|
||||||
|
import com.gitee.sop.admin.common.enums.StatusEnum;
|
||||||
|
import com.gitee.sop.admin.common.user.User;
|
||||||
|
import com.gitee.sop.admin.common.context.UserContext;
|
||||||
|
import com.gitee.sop.admin.common.exception.LoginFailureException;
|
||||||
|
import com.gitee.sop.admin.common.util.RequestUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class LoginInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||||
|
if (!(handler instanceof HandlerMethod)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||||
|
NoToken noLogin = handlerMethod.getMethodAnnotation(NoToken.class);
|
||||||
|
if (noLogin != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
noLogin = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), NoToken.class);
|
||||||
|
if (noLogin != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
User user = UserContext.getUser(request);
|
||||||
|
if (user == null || StatusEnum.of(user.getStatus()) == StatusEnum.DISABLED) {
|
||||||
|
log.error("登录失败, 客户端ip:{}, uri:{}", RequestUtil.getIP(request), request.getRequestURI());
|
||||||
|
throw new LoginFailureException("登录失败");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,8 @@
|
|||||||
|
dubbo.registry.address=zookeeper://localhost:2181
|
||||||
|
|
||||||
|
mybatis.print-sql=true
|
||||||
|
|
||||||
|
# mysql config
|
||||||
|
mysql.host=127.0.0.1:3306
|
||||||
|
mysql.username=root
|
||||||
|
mysql.password=root
|
@@ -0,0 +1,8 @@
|
|||||||
|
dubbo.registry.address=nacos://localhost:8848
|
||||||
|
|
||||||
|
mybatis.print-sql=true
|
||||||
|
|
||||||
|
# mysql config
|
||||||
|
mysql.host=127.0.0.1:3306
|
||||||
|
mysql.username=root
|
||||||
|
mysql.password=root
|
@@ -0,0 +1,48 @@
|
|||||||
|
server.port=8082
|
||||||
|
spring.profiles.active=dev
|
||||||
|
|
||||||
|
spring.application.name=sop-admin
|
||||||
|
|
||||||
|
index.path=/
|
||||||
|
|
||||||
|
####### admin config #######
|
||||||
|
# user cache
|
||||||
|
user.cache.type=local
|
||||||
|
|
||||||
|
dubbo.protocol.name=dubbo
|
||||||
|
dubbo.protocol.port=-1
|
||||||
|
dubbo.application.qos-enable=false
|
||||||
|
dubbo.consumer.check=false
|
||||||
|
# ### register config see:https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/registry/overview/
|
||||||
|
# ------
|
||||||
|
# nacos://localhost:8848 Cluster config:nacos://localhost:8848?backup=localshot:8846,localshot:8847
|
||||||
|
# zookeeper://localhost:2181 Cluster config:zookeeper://10.20.153.10:2181?backup=10.20.153.11:2181,10.20.153.12:2181
|
||||||
|
# redis://localhost:6379 Cluster config:redis://10.20.153.10:6379?backup=10.20.153.11:6379,10.20.153.12:6379
|
||||||
|
# ------
|
||||||
|
dubbo.registry.address=zookeeper://localhost:2181
|
||||||
|
|
||||||
|
####### mysql config #######
|
||||||
|
mysql.host=127.0.0.1:3306
|
||||||
|
mysql.username=
|
||||||
|
mysql.password=
|
||||||
|
mysql.db=sop
|
||||||
|
|
||||||
|
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||||
|
spring.datasource.url=jdbc:mysql://${mysql.host}/${mysql.db}?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
|
||||||
|
spring.datasource.username=${mysql.username}
|
||||||
|
spring.datasource.password=${mysql.password}
|
||||||
|
|
||||||
|
####### mybatis config #######
|
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillInsert=add_time
|
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillUpdate=update_time
|
||||||
|
mybatis.fill.com.gitee.sop.admin.common.fill.AddByFill=
|
||||||
|
mybatis.fill.com.gitee.sop.admin.common.fill.UpdateByFill=
|
||||||
|
# mybatis config file
|
||||||
|
mybatis.config-location=classpath:mybatis/mybatisConfig.xml
|
||||||
|
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
|
||||||
|
|
||||||
|
|
||||||
|
# print SQL
|
||||||
|
logging.level.com.gitee.sop.admin.dao=error
|
||||||
|
logging.level.com.gitee.fastmybatis=info
|
||||||
|
mybatis.print-sql=false
|
@@ -0,0 +1,13 @@
|
|||||||
|
package com.gitee.sop.admin;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class BaseTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
package com.gitee.sop.admin.service;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.gitee.sop.admin.BaseTest;
|
||||||
|
import com.gitee.sop.admin.service.sys.login.LoginService;
|
||||||
|
import com.gitee.sop.admin.service.sys.login.dto.LoginDTO;
|
||||||
|
import com.gitee.sop.admin.service.sys.login.dto.LoginUser;
|
||||||
|
import com.gitee.sop.admin.service.sys.login.enums.RegTypeEnum;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCrypt;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
public class LoginServiceTest extends BaseTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
LoginService loginService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void login() {
|
||||||
|
LoginDTO loginDTO = new LoginDTO();
|
||||||
|
loginDTO.setUsername("admin");
|
||||||
|
loginDTO.setPassword("123456");
|
||||||
|
loginDTO.setRegType(RegTypeEnum.BACKEND);
|
||||||
|
LoginUser loginUser = loginService.login(loginDTO);
|
||||||
|
Assert.notNull(loginUser, "not null");
|
||||||
|
System.out.println(JSON.toJSONString(loginUser));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetAdminPwd() {
|
||||||
|
// 初始密码
|
||||||
|
String defPassword = "123456";
|
||||||
|
defPassword = DigestUtils.sha256Hex(defPassword);
|
||||||
|
String encodedPassword = BCrypt.hashpw(defPassword, BCrypt.gensalt());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,36 @@
|
|||||||
|
package com.gitee.sop.admin.service;
|
||||||
|
|
||||||
|
import com.gitee.sop.admin.BaseTest;
|
||||||
|
import com.gitee.sop.admin.dao.entity.SysUser;
|
||||||
|
import com.gitee.sop.admin.service.sys.SysUserService;
|
||||||
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCrypt;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
public class PasswordTest extends BaseTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
SysUserService sysAdminUserService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置admin密码
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void resetAdminPwd() {
|
||||||
|
String username = "admin";
|
||||||
|
String defPassword = "123456";
|
||||||
|
defPassword = DigestUtils.sha256Hex(defPassword);
|
||||||
|
String encodedPassword = BCrypt.hashpw(defPassword, BCrypt.gensalt());
|
||||||
|
System.out.println("数据库保存:" + encodedPassword);
|
||||||
|
sysAdminUserService.query()
|
||||||
|
.eq(SysUser::getUsername, username)
|
||||||
|
.set(SysUser::getPassword, encodedPassword)
|
||||||
|
.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
97
sop-admin/sop-admin-backend/admin-common/pom.xml
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.gitee.sop</groupId>
|
||||||
|
<artifactId>sop-admin-backend</artifactId>
|
||||||
|
<version>5.0.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>admin-common</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>8</maven.compiler.source>
|
||||||
|
<maven.compiler.target>8</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.gitee.sop</groupId>
|
||||||
|
<artifactId>sop-service-support</artifactId>
|
||||||
|
<version>5.0.0-SNAPSHOT</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.gitee.durcframework</groupId>
|
||||||
|
<artifactId>fastmybatis-spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- json处理 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.fastjson2</groupId>
|
||||||
|
<artifactId>fastjson2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-codec</groupId>
|
||||||
|
<artifactId>commons-codec</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-fileupload</groupId>
|
||||||
|
<artifactId>commons-fileupload</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate</groupId>
|
||||||
|
<artifactId>hibernate-validator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-xml</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.auth0</groupId>
|
||||||
|
<artifactId>java-jwt</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-crypto</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.oschina.durcframework</groupId>
|
||||||
|
<artifactId>http-helper</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
@@ -0,0 +1,17 @@
|
|||||||
|
package com.gitee.sop.admin.common.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不需要登录验证
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface NoToken {
|
||||||
|
}
|
@@ -0,0 +1,45 @@
|
|||||||
|
package com.gitee.sop.admin.common.config;
|
||||||
|
|
||||||
|
import com.gitee.sop.admin.common.context.SpringContext;
|
||||||
|
import com.gitee.sop.admin.common.enums.ConfigKeyEnum;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
public class Configs {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置参数
|
||||||
|
*
|
||||||
|
* @param keyGetter 配置key
|
||||||
|
* @return 返回配参数,没有则返回null
|
||||||
|
*/
|
||||||
|
public static String getValue(ConfigKeyEnum keyGetter) {
|
||||||
|
return getValue(keyGetter, keyGetter.getDefaultValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置参数
|
||||||
|
*
|
||||||
|
* @param keyGetter 配置key
|
||||||
|
* @param defaultValue 默认值
|
||||||
|
* @return 返回配参数,没有则返回默认值
|
||||||
|
*/
|
||||||
|
public static String getValue(ConfigKeyEnum keyGetter, String defaultValue) {
|
||||||
|
return SpringContext.getBean(IConfig.class).getConfig(keyGetter.getKey(), defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置参数
|
||||||
|
*
|
||||||
|
* @param keyGetter 配置key
|
||||||
|
* @param defaultValue 默认值
|
||||||
|
* @return 返回配参数,没有则返回默认值
|
||||||
|
*/
|
||||||
|
public static String getValue(ConfigKeyEnum keyGetter, Supplier<String> defaultValue) {
|
||||||
|
return getValue(keyGetter, defaultValue.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,9 @@
|
|||||||
|
package com.gitee.sop.admin.common.config;
|
||||||
|
|
||||||
|
public interface IConfig {
|
||||||
|
|
||||||
|
String getConfig(String key);
|
||||||
|
|
||||||
|
String getConfig(String key, String defaultValue);
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
package com.gitee.sop.admin.common.constants;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
public class YesOrNo {
|
||||||
|
public static final int YES = 1;
|
||||||
|
public static final int NO = 0;
|
||||||
|
|
||||||
|
public static boolean yes(Number value) {
|
||||||
|
return value != null && value.intValue() == YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int of(Boolean b) {
|
||||||
|
return Objects.equals(b, true) ? YES : NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,31 @@
|
|||||||
|
package com.gitee.sop.admin.common.context;
|
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
public class SpringContext {
|
||||||
|
|
||||||
|
private static ApplicationContext ctx;
|
||||||
|
|
||||||
|
public static <T> T getBean(Class<T> clazz) {
|
||||||
|
return ctx.getBean(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object getBean(String beanName) {
|
||||||
|
return ctx.getBean(beanName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setApplicationContext(ApplicationContext ctx) {
|
||||||
|
SpringContext.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ApplicationContext getApplicationContext() {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void publishEvent(Object event) {
|
||||||
|
ctx.publishEvent(event);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,126 @@
|
|||||||
|
package com.gitee.sop.admin.common.context;
|
||||||
|
|
||||||
|
import com.auth0.jwt.interfaces.Claim;
|
||||||
|
import com.gitee.sop.admin.common.manager.UserCacheManager;
|
||||||
|
import com.gitee.sop.admin.common.user.User;
|
||||||
|
import com.gitee.sop.admin.common.config.Configs;
|
||||||
|
import com.gitee.sop.admin.common.enums.ConfigKeyEnum;
|
||||||
|
import com.gitee.sop.admin.common.exception.ErrorTokenException;
|
||||||
|
import com.gitee.sop.admin.common.exception.JwtErrorException;
|
||||||
|
import com.gitee.sop.admin.common.exception.JwtExpiredException;
|
||||||
|
import com.gitee.sop.admin.common.exception.LoginFailureException;
|
||||||
|
import com.gitee.sop.admin.common.util.JwtUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.math.NumberUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author tanghc
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class UserContext {
|
||||||
|
|
||||||
|
public static final String HEADER_AUTHORIZATION = "Authorization";
|
||||||
|
public static final String JWT_PREFIX = "Bearer ";
|
||||||
|
|
||||||
|
|
||||||
|
private static Supplier<String> tokenGetter = () -> {
|
||||||
|
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
|
||||||
|
return getToken(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void setTokenGetter(Supplier<String> tokenGetter) {
|
||||||
|
UserContext.tokenGetter = tokenGetter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户id
|
||||||
|
* @return 返回id,没有返回null
|
||||||
|
*/
|
||||||
|
public static Long getUserId() {
|
||||||
|
return Optional.ofNullable(getUser()).map(User::getUserId).orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户
|
||||||
|
*
|
||||||
|
* @return 返回当前登录用户,没有返回null
|
||||||
|
*/
|
||||||
|
public static User getUser() {
|
||||||
|
String token = tokenGetter.get();
|
||||||
|
try {
|
||||||
|
return getUser(token);
|
||||||
|
} catch (ErrorTokenException e) {
|
||||||
|
throw new LoginFailureException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户
|
||||||
|
*
|
||||||
|
* @return 返回当前登录用户,没有返回null
|
||||||
|
*/
|
||||||
|
public static User getUser(HttpServletRequest request) {
|
||||||
|
String token = getToken(request);
|
||||||
|
try {
|
||||||
|
return getUser(token);
|
||||||
|
} catch (ErrorTokenException e) {
|
||||||
|
throw new LoginFailureException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getToken(HttpServletRequest request) {
|
||||||
|
String token = request.getHeader(HEADER_AUTHORIZATION);
|
||||||
|
if (StringUtils.hasText(token) && token.startsWith(JWT_PREFIX)) {
|
||||||
|
return token.substring(JWT_PREFIX.length());
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取登录用户
|
||||||
|
*
|
||||||
|
* @param token 格式:<userId>:<jwt>
|
||||||
|
* @return 返回token对应的用户,没有返回null
|
||||||
|
*/
|
||||||
|
private static User getUser(String token) throws ErrorTokenException {
|
||||||
|
if (StringUtils.isEmpty(token)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String secret = Configs.getValue(ConfigKeyEnum.JWT_SECRET);
|
||||||
|
Map<String, Claim> data;
|
||||||
|
// verify jwt
|
||||||
|
try {
|
||||||
|
data = JwtUtil.verifyJwt(token, secret);
|
||||||
|
} catch (JwtExpiredException | JwtErrorException e) {
|
||||||
|
log.error("jwt verify failed, token:{}, message:{}", token, e.getMessage(), e);
|
||||||
|
throw new ErrorTokenException();
|
||||||
|
}
|
||||||
|
Claim id = data.get("id");
|
||||||
|
long userId = NumberUtils.toLong(id.asString(), 0);
|
||||||
|
if (userId == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return SpringContext.getBean(UserCacheManager.class).getUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public static Locale getLocale() {
|
||||||
|
try {
|
||||||
|
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
|
||||||
|
return request.getLocale();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Locale.SIMPLIFIED_CHINESE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,14 @@
|
|||||||
|
package com.gitee.sop.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class IdDTO {
|
||||||
|
@NotNull(message = "id不能为空")
|
||||||
|
private Long id;
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
package com.gitee.sop.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class IdsDTO {
|
||||||
|
private List<Long> ids;
|
||||||
|
}
|
@@ -0,0 +1,16 @@
|
|||||||
|
package com.gitee.sop.admin.common.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author 六如
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class StatusUpdateBatchDTO extends IdsDTO {
|
||||||
|
|
||||||
|
@NotNull(message = "状态不能为空")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
}
|