5.0
@@ -4,10 +4,8 @@ VOLUME /sop
|
||||
|
||||
# 将所有应用放到一个镜像当中
|
||||
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-website/sop-website-server/target/*.jar sop/sop-website/sop-website.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
|
||||
ADD sop-admin/sop-admin-backend/backend-boot/target/*.jar sop/sop-admin/sop-admin.jar
|
||||
ADD sop-example/example-story/target/*.jar sop/sop-story/sop-story.jar
|
||||
|
||||
# 拷贝启动脚本
|
||||
COPY docker-entrypoint.sh /usr/local/bin/
|
||||
|
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/api";
|
||||
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/api";
|
||||
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/api";
|
||||
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>5.0.0-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 六如
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,11 +3,9 @@
|
||||
JAVA_OPTS="-Xms128m -Xmx128m"
|
||||
|
||||
# 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-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
|
1
pom.xml
@@ -16,7 +16,6 @@
|
||||
<description>一个开放平台解决方案项目,基于Dubbo实现,目标是能够让用户快速得搭建起自己的开放平台</description>
|
||||
|
||||
<modules>
|
||||
<module>doc</module>
|
||||
<module>sop-example</module>
|
||||
<module>sop-admin</module>
|
||||
<module>sop-test</module>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
sop-admin/sop-admin-frontend/public/logo.png
Normal file
After Width: | Height: | Size: 43 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>
|
Before Width: | Height: | Size: 706 B |
@@ -140,7 +140,7 @@ export function useNav() {
|
||||
|
||||
/** 获取`logo` */
|
||||
function getLogo() {
|
||||
return new URL("/logo.svg", import.meta.url).href;
|
||||
return new URL("/logo.png", import.meta.url).href;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@@ -21,13 +21,13 @@ public class PayTradeWapPayRequest {
|
||||
|
||||
@ApiModelProperty(value = "商户网站唯一订单号", required = true, example = "70501111111S001111119")
|
||||
@Length(max = 64)
|
||||
@NotBlank
|
||||
private String out_trade_no;
|
||||
@NotBlank(message = "商户网站唯一订单号必填")
|
||||
private String outTradeNo;
|
||||
|
||||
@ApiModelProperty(value = "订单总金额.单位为元,精确到小数点后两位,取值范围:[0.01,100000000] ",
|
||||
required = true, example = "9.00")
|
||||
@NotNull
|
||||
private BigDecimal total_amount;
|
||||
@NotNull(message = "订单总金额不能为空")
|
||||
private BigDecimal totalAmount;
|
||||
|
||||
|
||||
@ApiModelProperty(
|
||||
@@ -36,7 +36,7 @@ public class PayTradeWapPayRequest {
|
||||
example = "大乐透"
|
||||
)
|
||||
@Length(max = 256)
|
||||
@NotBlank
|
||||
@NotBlank(message = "订单标题不能为空")
|
||||
private String subject;
|
||||
|
||||
@ApiModelProperty(
|
||||
@@ -44,9 +44,9 @@ public class PayTradeWapPayRequest {
|
||||
required = true,
|
||||
example = "QUICK_WAP_WAY"
|
||||
)
|
||||
@NotBlank
|
||||
@NotBlank(message = "销售产品码不能为空")
|
||||
@Length(max = 64)
|
||||
private String product_code;
|
||||
private String productCode;
|
||||
|
||||
|
||||
@ApiModelProperty(
|
||||
@@ -54,7 +54,7 @@ public class PayTradeWapPayRequest {
|
||||
example = "appopenBb64d181d0146481ab6a762c00714cC27"
|
||||
)
|
||||
@Length(max = 40)
|
||||
private String auth_token;
|
||||
private String authToken;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "用户付款中途退出返回商户网站的地址",
|
||||
@@ -66,21 +66,21 @@ public class PayTradeWapPayRequest {
|
||||
@ApiModelProperty(
|
||||
value = "订单包含的商品列表信息,json格式,其它说明详见商品明细说明"
|
||||
)
|
||||
private List<GoodsDetail> goods_detail;
|
||||
private List<GoodsDetail> goodsDetail;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "绝对超时时间,格式为yyyy-MM-dd HH:mm:ss。超时时间范围:1m~15d。",
|
||||
example = "2016-12-31 10:05:00"
|
||||
)
|
||||
@Length(max = 32)
|
||||
private String time_expire;
|
||||
private String timeExpire;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "商户传入业务信息,具体值要和支付平台约定,应用于安全,营销等参数直传场景,格式为json格式",
|
||||
example = "{\"mc_create_trade_ip\":\"127.0.0.1\"}"
|
||||
)
|
||||
@Length(max = 512)
|
||||
private String business_params;
|
||||
private String businessParams;
|
||||
|
||||
|
||||
@ApiModelProperty(
|
||||
@@ -88,14 +88,14 @@ public class PayTradeWapPayRequest {
|
||||
example = "merchantBizType%3d3C%26merchantBizNo%3d2016010101111"
|
||||
)
|
||||
@Length(max = 512)
|
||||
private String passback_params;
|
||||
private String passbackParams;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "商户原始订单号,最大长度限制32位",
|
||||
example = "{\"mc_create_trade_ip\":\"127.0.0.1\"}"
|
||||
)
|
||||
@Length(max = 32)
|
||||
private String merchant_order_no;
|
||||
private String merchantOrderNo;
|
||||
|
||||
// ---
|
||||
|
||||
@@ -108,7 +108,7 @@ public class PayTradeWapPayRequest {
|
||||
)
|
||||
@NotBlank
|
||||
@Length(max = 64)
|
||||
private String goods_id;
|
||||
private String goodsId;
|
||||
|
||||
|
||||
@ApiModelProperty(
|
||||
@@ -118,7 +118,7 @@ public class PayTradeWapPayRequest {
|
||||
)
|
||||
@NotBlank
|
||||
@Length(max = 256)
|
||||
private String goods_name;
|
||||
private String goodsName;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "商品数量",
|
||||
@@ -141,21 +141,21 @@ public class PayTradeWapPayRequest {
|
||||
example = "20010001"
|
||||
)
|
||||
@Length(max = 32)
|
||||
private String alipay_goods_id;
|
||||
private String alipayGoodsId;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "商品类目",
|
||||
example = "34543238"
|
||||
)
|
||||
@Length(max = 24)
|
||||
private String goods_category;
|
||||
private String goodsCategory;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "商品类目树,从商品类目根节点到叶子节点的类目id组成,类目id值使用|分割",
|
||||
example = "124868003|126232002|126252004"
|
||||
)
|
||||
@Length(max = 128)
|
||||
private String categories_tree;
|
||||
private String categoriesTree;
|
||||
|
||||
@ApiModelProperty(
|
||||
value = "商品描述信息",
|
||||
@@ -169,7 +169,7 @@ public class PayTradeWapPayRequest {
|
||||
example = "http://www.alipay.com/xxx.jpg"
|
||||
)
|
||||
@Length(max = 400)
|
||||
private String show_url;
|
||||
private String showUrl;
|
||||
|
||||
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ package com.gitee.sop.storyweb.impl;
|
||||
|
||||
import com.gitee.sop.storyweb.message.StoryMessageEnum;
|
||||
import com.gitee.sop.storyweb.open.OpenStory;
|
||||
import com.gitee.sop.storyweb.open.req.StorySaveDTO;
|
||||
import com.gitee.sop.storyweb.open.req.StorySaveRequest;
|
||||
import com.gitee.sop.storyweb.open.resp.StoryResponse;
|
||||
import com.gitee.sop.support.context.OpenContext;
|
||||
import com.gitee.sop.support.dto.CommonFileData;
|
||||
@@ -29,13 +29,13 @@ public class OpenStoryImpl implements OpenStory {
|
||||
|
||||
|
||||
@Override
|
||||
public Integer save(StorySaveDTO storySaveDTO) {
|
||||
public Integer save(StorySaveRequest storySaveDTO) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Integer update(Integer id, StorySaveDTO storySaveDTO) {
|
||||
public Integer update(Integer id, StorySaveRequest storySaveDTO) {
|
||||
System.out.println("update, id:" + id + ", storySaveDTO=" + storySaveDTO);
|
||||
return 1;
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class OpenStoryImpl implements OpenStory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoryResponse upload(StorySaveDTO storySaveDTO, FileData file) {
|
||||
public StoryResponse upload(StorySaveRequest storySaveDTO, FileData file) {
|
||||
System.out.println("getName:" + file.getName());
|
||||
System.out.println("getOriginalFilename:" + file.getOriginalFilename());
|
||||
checkFile(Arrays.asList(file));
|
||||
@@ -97,7 +97,7 @@ public class OpenStoryImpl implements OpenStory {
|
||||
|
||||
|
||||
@Override
|
||||
public StoryResponse upload2(StorySaveDTO storySaveDTO, FileData idCardFront, FileData idCardBack) {
|
||||
public StoryResponse upload2(StorySaveRequest storySaveDTO, FileData idCardFront, FileData idCardBack) {
|
||||
List<String> list = new ArrayList<>();
|
||||
System.out.println("upload:" + storySaveDTO);
|
||||
checkFile(Arrays.asList(idCardFront, idCardBack));
|
||||
@@ -109,7 +109,7 @@ public class OpenStoryImpl implements OpenStory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StoryResponse upload3(StorySaveDTO storySaveDTO, List<FileData> files) {
|
||||
public StoryResponse upload3(StorySaveRequest storySaveDTO, List<FileData> files) {
|
||||
List<String> list = new ArrayList<>();
|
||||
list.add("upload:" + storySaveDTO);
|
||||
checkFile(files);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.gitee.sop.storyweb.open;
|
||||
|
||||
import com.gitee.sop.storyweb.open.req.StorySaveDTO;
|
||||
import com.gitee.sop.storyweb.open.req.StorySaveRequest;
|
||||
import com.gitee.sop.storyweb.open.resp.StoryResponse;
|
||||
import com.gitee.sop.support.annotation.Open;
|
||||
import com.gitee.sop.support.context.OpenContext;
|
||||
@@ -18,10 +18,10 @@ import java.util.List;
|
||||
public interface OpenStory {
|
||||
|
||||
@Open("story.save")
|
||||
Integer save(StorySaveDTO storySaveDTO);
|
||||
Integer save(StorySaveRequest storySaveRequest);
|
||||
|
||||
@Open("story.update")
|
||||
Integer update(Integer id, StorySaveDTO storySaveDTO);
|
||||
Integer update(Integer id, StorySaveRequest storySaveRequest);
|
||||
|
||||
// 演示抛出异常
|
||||
@Open("story.updateError")
|
||||
@@ -48,12 +48,12 @@ public interface OpenStory {
|
||||
|
||||
// 演示单文件上传
|
||||
@Open("story.upload")
|
||||
StoryResponse upload(StorySaveDTO storySaveDTO, FileData file);
|
||||
StoryResponse upload(StorySaveRequest storySaveRequest, FileData file);
|
||||
|
||||
// 演示多文件上传
|
||||
@Open("story.upload.more")
|
||||
StoryResponse upload2(
|
||||
StorySaveDTO storySaveDTO,
|
||||
StorySaveRequest storySaveRequest,
|
||||
@NotNull(message = "身份证正面必填") FileData idCardFront,
|
||||
@NotNull(message = "身份证背面必填") FileData idCardBack
|
||||
);
|
||||
@@ -61,7 +61,7 @@ public interface OpenStory {
|
||||
// 演示多文件上传
|
||||
@Open("story.upload.list")
|
||||
StoryResponse upload3(
|
||||
StorySaveDTO storySaveDTO,
|
||||
StorySaveRequest storySaveRequest,
|
||||
@Size(min = 2, message = "最少上传2个文件")
|
||||
List<FileData> files
|
||||
);
|
||||
|
@@ -11,7 +11,7 @@ import java.util.Date;
|
||||
* @author 六如
|
||||
*/
|
||||
@Data
|
||||
public class StorySaveDTO implements Serializable {
|
||||
public class StorySaveRequest implements Serializable {
|
||||
private static final long serialVersionUID = -1214422742659231037L;
|
||||
|
||||
@NotBlank(message = "故事名称必填")
|
@@ -88,7 +88,7 @@ public class ApiConfig {
|
||||
private String zoneId = "Asia/Shanghai";
|
||||
|
||||
/**
|
||||
* 返回结果字段小写形式
|
||||
* 字段下划线小写形式
|
||||
*/
|
||||
private Boolean fieldLowercase = false;
|
||||
private Boolean fieldSnakeCase = false;
|
||||
}
|
||||
|
@@ -1,13 +1,12 @@
|
||||
package com.gitee.sop.gateway.config;
|
||||
|
||||
import com.gitee.sop.gateway.service.ParamExecutor;
|
||||
import com.gitee.sop.gateway.service.impl.ParamExecutorImpl;
|
||||
import com.gitee.sop.gateway.service.RouteService;
|
||||
import com.gitee.sop.gateway.service.impl.RouteServiceImpl;
|
||||
import com.gitee.sop.gateway.service.Serde;
|
||||
import com.gitee.sop.gateway.service.impl.ParamExecutorImpl;
|
||||
import com.gitee.sop.gateway.service.impl.RouteServiceImpl;
|
||||
import com.gitee.sop.gateway.service.impl.SerdeGsonImpl;
|
||||
import com.gitee.sop.gateway.service.impl.SerdeImpl;
|
||||
import com.gitee.sop.gateway.service.interceptor.internal.ResultRouteInterceptor;
|
||||
import com.gitee.sop.gateway.service.manager.ApiManager;
|
||||
import com.gitee.sop.gateway.service.manager.IsvApiPermissionManager;
|
||||
import com.gitee.sop.gateway.service.manager.IsvManager;
|
||||
@@ -99,13 +98,6 @@ public class GatewayConfig {
|
||||
return new SerdeGsonImpl();
|
||||
}
|
||||
|
||||
// DEFAULT ROUTE INTERCEPTOR
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ResultRouteInterceptor resultRouteInterceptor() {
|
||||
return new ResultRouteInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ParamExecutor paramExecutor() {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.gitee.sop.gateway.service.interceptor;
|
||||
package com.gitee.sop.gateway.interceptor;
|
||||
|
||||
import com.gitee.sop.gateway.common.ApiInfoDTO;
|
||||
import com.gitee.sop.gateway.request.ApiRequestContext;
|
||||
@@ -12,6 +12,9 @@ public interface RouteInterceptor {
|
||||
|
||||
/**
|
||||
* 在路由转发前执行,签名校验通过后会立即执行此方法
|
||||
* <pre>
|
||||
* 在这个方法中抛出异常会中断接口执行,直接返回错误信息
|
||||
* </pre>
|
||||
*
|
||||
* @param context context
|
||||
* @param apiInfoDTO 接口信息
|
||||
@@ -24,7 +27,7 @@ public interface RouteInterceptor {
|
||||
*
|
||||
* @param context context
|
||||
* @param apiInfoDTO 接口信息
|
||||
* @param result 返回结果,通常是HashMap
|
||||
* @param result 业务返回结果,通常是HashMap
|
||||
* @return 返回格式化后的结果, 可对原结果进行修改
|
||||
*/
|
||||
default Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {
|
@@ -0,0 +1,10 @@
|
||||
package com.gitee.sop.gateway.interceptor;
|
||||
|
||||
/**
|
||||
* @author 六如
|
||||
*/
|
||||
public class RouteInterceptorOrders {
|
||||
|
||||
public static final int RESULT_INTERCEPTOR = -1000;
|
||||
|
||||
}
|
@@ -1,11 +1,12 @@
|
||||
package com.gitee.sop.gateway.service.interceptor.internal;
|
||||
package com.gitee.sop.gateway.interceptor.internal;
|
||||
|
||||
import com.gitee.sop.gateway.common.ApiInfoDTO;
|
||||
import com.gitee.sop.gateway.request.ApiRequestContext;
|
||||
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor;
|
||||
import com.gitee.sop.gateway.service.interceptor.RouteInterceptorOrders;
|
||||
import com.gitee.sop.gateway.interceptor.RouteInterceptor;
|
||||
import com.gitee.sop.gateway.interceptor.RouteInterceptorOrders;
|
||||
import com.gitee.sop.support.dto.CommonFileData;
|
||||
import com.gitee.sop.support.dto.FileData;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -17,6 +18,7 @@ import java.util.Objects;
|
||||
*
|
||||
* @author 六如
|
||||
*/
|
||||
@Component
|
||||
public class ResultRouteInterceptor implements RouteInterceptor {
|
||||
|
||||
private static final String CLASS = "class";
|
@@ -3,7 +3,6 @@ package com.gitee.sop.gateway.response;
|
||||
import com.gitee.sop.gateway.message.ErrorEnum;
|
||||
import com.gitee.sop.gateway.message.IError;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -53,19 +52,41 @@ import java.util.Locale;
|
||||
*
|
||||
* @author 六如
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class ApiResponse extends BaseResponse {
|
||||
public class ApiResponse implements Response {
|
||||
|
||||
public static final String SUCCESS_CODE = "0";
|
||||
public static final String SUCCESS_MSG = "success";
|
||||
|
||||
/**
|
||||
* 网关异常码,范围0~100 成功返回"0"
|
||||
*/
|
||||
private String code = SUCCESS_CODE;
|
||||
|
||||
/**
|
||||
* 网关异常信息
|
||||
*/
|
||||
private String msg = "";
|
||||
|
||||
/**
|
||||
* 返回对象
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 业务异常码
|
||||
*/
|
||||
private String subCode = "";
|
||||
private String sub_code = "";
|
||||
|
||||
/**
|
||||
* 业务异常信息
|
||||
*/
|
||||
private String subMsg = "";
|
||||
private String sub_msg = "";
|
||||
|
||||
/**
|
||||
* 解决方案
|
||||
*/
|
||||
private String solution;
|
||||
|
||||
public static ApiResponse success(Object data) {
|
||||
ApiResponse apiResponse = new ApiResponse();
|
||||
@@ -86,8 +107,8 @@ public class ApiResponse extends BaseResponse {
|
||||
ApiResponse apiResponse = new ApiResponse();
|
||||
apiResponse.setCode(error.getCode());
|
||||
apiResponse.setMsg(error.getMsg());
|
||||
apiResponse.setSubCode(subCode);
|
||||
apiResponse.setSubMsg(subMsg);
|
||||
apiResponse.setSub_code(subCode);
|
||||
apiResponse.setSub_msg(subMsg);
|
||||
apiResponse.setSolution(solution);
|
||||
return apiResponse;
|
||||
}
|
||||
@@ -99,8 +120,8 @@ public class ApiResponse extends BaseResponse {
|
||||
|
||||
public static ApiResponse error(IError error) {
|
||||
ApiResponse apiResponse = new ApiResponse();
|
||||
apiResponse.setSubCode(error.getSubCode());
|
||||
apiResponse.setSubMsg(error.getSubMsg());
|
||||
apiResponse.setSub_code(error.getSubCode());
|
||||
apiResponse.setSub_msg(error.getSubMsg());
|
||||
apiResponse.setCode(error.getCode());
|
||||
apiResponse.setMsg(error.getMsg());
|
||||
apiResponse.setSolution(error.getSolution());
|
||||
@@ -109,18 +130,7 @@ public class ApiResponse extends BaseResponse {
|
||||
|
||||
private static ApiResponse error(IError error, String subMsg) {
|
||||
ApiResponse response = error(error);
|
||||
response.setSubMsg(subMsg);
|
||||
response.setSub_msg(subMsg);
|
||||
return response;
|
||||
}
|
||||
|
||||
public Response toLower() {
|
||||
ApiResponseLower apiResponseLower = new ApiResponseLower();
|
||||
apiResponseLower.setSub_code(this.subCode);
|
||||
apiResponseLower.setSub_msg(this.subMsg);
|
||||
apiResponseLower.setCode(this.subCode);
|
||||
apiResponseLower.setMsg(this.subMsg);
|
||||
apiResponseLower.setData(this.getData());
|
||||
apiResponseLower.setSolution(this.getSolution());
|
||||
return apiResponseLower;
|
||||
}
|
||||
}
|
||||
|
@@ -1,108 +0,0 @@
|
||||
package com.gitee.sop.gateway.response;
|
||||
|
||||
import com.gitee.sop.gateway.exception.ApiException;
|
||||
import com.gitee.sop.gateway.message.ErrorEnum;
|
||||
import com.gitee.sop.gateway.message.IError;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
* 默认的结果封装类.
|
||||
* <pre>
|
||||
*
|
||||
* xml返回结果:
|
||||
* <response>
|
||||
* <code>50</code>
|
||||
* <msg>Remote service error</msg>
|
||||
* <sub_code>isv.invalid-parameter</sub_code>
|
||||
* <sub_msg>非法参数</sub_msg>
|
||||
* </response>
|
||||
* 成功情况:
|
||||
* <response>
|
||||
* <code>0</code>
|
||||
* <msg>成功消息</msg>
|
||||
* <data>
|
||||
* ...返回内容
|
||||
* </data>
|
||||
* </response>
|
||||
*
|
||||
* json返回格式:
|
||||
* {
|
||||
* "code":"50",
|
||||
* "msg":"Remote service error",
|
||||
* "sub_code":"isv.invalid-parameter",
|
||||
* "sub_msg":"非法参数"
|
||||
* }
|
||||
* 成功情况:
|
||||
* {
|
||||
* "code":"0",
|
||||
* "msg":"成功消息内容。。。",
|
||||
* "data":{
|
||||
* ...返回内容
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* 字段说明:
|
||||
* code:网关异常码 <br>
|
||||
* msg:网关异常信息 <br>
|
||||
* sub_code:业务异常码 <br>
|
||||
* sub_msg:业务异常信息 <br>
|
||||
*
|
||||
* @author 六如
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public class ApiResponseLower extends BaseResponse {
|
||||
|
||||
/**
|
||||
* 业务异常码
|
||||
*/
|
||||
private String sub_code = "";
|
||||
|
||||
/**
|
||||
* 业务异常信息
|
||||
*/
|
||||
private String sub_msg = "";
|
||||
|
||||
public static ApiResponseLower success(Object data) {
|
||||
ApiResponseLower apiResponse = new ApiResponseLower();
|
||||
apiResponse.setCode(SUCCESS_CODE);
|
||||
apiResponse.setMsg(SUCCESS_MSG);
|
||||
apiResponse.setData(data);
|
||||
return apiResponse;
|
||||
}
|
||||
|
||||
|
||||
public static ApiResponseLower error(ApiException e) {
|
||||
IError error = e.getError();
|
||||
return error(error);
|
||||
}
|
||||
|
||||
public static ApiResponseLower error(ErrorEnum errorEnum, Locale locale, String subMsg) {
|
||||
IError error = errorEnum.getError(locale);
|
||||
return error(error, subMsg);
|
||||
}
|
||||
|
||||
public static ApiResponseLower error(ErrorEnum errorEnum, Locale locale) {
|
||||
IError error = errorEnum.getError(locale);
|
||||
return error(error);
|
||||
}
|
||||
|
||||
public static ApiResponseLower error(IError error) {
|
||||
return error(error, error.getSubMsg());
|
||||
}
|
||||
|
||||
public static ApiResponseLower error(IError error, String subMsg) {
|
||||
ApiResponseLower apiResponse = new ApiResponseLower();
|
||||
apiResponse.setSub_code(error.getSubCode());
|
||||
apiResponse.setSub_msg(error.getSubMsg());
|
||||
apiResponse.setCode(error.getCode());
|
||||
apiResponse.setMsg(subMsg);
|
||||
return apiResponse;
|
||||
}
|
||||
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
package com.gitee.sop.gateway.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
/**
|
||||
* @author 六如
|
||||
*/
|
||||
@Data
|
||||
public class BaseResponse implements Response {
|
||||
|
||||
public static final String SUCCESS_CODE = "0";
|
||||
public static final String SUCCESS_MSG = "success";
|
||||
/**
|
||||
* 网关异常码,范围0~100 成功返回"0"
|
||||
*/
|
||||
private String code = SUCCESS_CODE;
|
||||
|
||||
/**
|
||||
* 网关异常信息
|
||||
*/
|
||||
private String msg = "";
|
||||
|
||||
/**
|
||||
* 返回对象
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 解决方案
|
||||
*/
|
||||
private String solution;
|
||||
}
|
@@ -5,7 +5,7 @@ package com.gitee.sop.gateway.response;
|
||||
*
|
||||
* @author 六如
|
||||
*/
|
||||
public class NoCommonResponse extends BaseResponse {
|
||||
public class NoCommonResponse extends ApiResponse {
|
||||
|
||||
public static NoCommonResponse success(Object data) {
|
||||
NoCommonResponse apiResponse = new NoCommonResponse();
|
||||
|
@@ -1,14 +1,25 @@
|
||||
package com.gitee.sop.gateway.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 序列化
|
||||
* 序列化/反序列化
|
||||
*
|
||||
* @author 六如
|
||||
*/
|
||||
public interface Serde {
|
||||
|
||||
String toJSONString(Object object);
|
||||
String toJson(Object object);
|
||||
|
||||
String toXml(Object object);
|
||||
|
||||
Map<String, Object> parseJson(String json);
|
||||
|
||||
default JSONObject parseObject(String json) {
|
||||
Map<String, Object> jsonObj = parseJson(json);
|
||||
return jsonObj instanceof JSONObject ? (JSONObject) jsonObj : new JSONObject(jsonObj);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import com.gitee.sop.gateway.request.ApiRequest;
|
||||
import com.gitee.sop.gateway.request.ApiRequestContext;
|
||||
import com.gitee.sop.gateway.request.RequestFormatEnum;
|
||||
import com.gitee.sop.gateway.request.UploadContext;
|
||||
import com.gitee.sop.gateway.response.NoCommonResponse;
|
||||
import com.gitee.sop.gateway.response.Response;
|
||||
import com.gitee.sop.gateway.service.ParamExecutor;
|
||||
import com.gitee.sop.gateway.service.Serde;
|
||||
@@ -120,7 +121,7 @@ public class ParamExecutorImpl implements ParamExecutor<HttpServletRequest, Http
|
||||
} else {
|
||||
Object responseData = apiResponse;
|
||||
// 不需要公共参数
|
||||
if (!apiResponse.needWrap()) {
|
||||
if (apiResponse instanceof NoCommonResponse || !apiResponse.needWrap()) {
|
||||
responseData = data;
|
||||
}
|
||||
this.writerText(apiRequestContext, responseData, response);
|
||||
@@ -138,7 +139,7 @@ public class ParamExecutorImpl implements ParamExecutor<HttpServletRequest, Http
|
||||
response.getWriter().write(xml);
|
||||
} else {
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
String json = serde.toJSONString(apiResponse);
|
||||
String json = serde.toJson(apiResponse);
|
||||
response.getWriter().write(json);
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@ import com.gitee.sop.gateway.common.enums.YesOrNoEnum;
|
||||
import com.gitee.sop.gateway.config.ApiConfig;
|
||||
import com.gitee.sop.gateway.request.ApiRequestContext;
|
||||
import com.gitee.sop.gateway.response.ApiResponse;
|
||||
import com.gitee.sop.gateway.response.ApiResponseLower;
|
||||
import com.gitee.sop.gateway.response.NoCommonResponse;
|
||||
import com.gitee.sop.gateway.response.Response;
|
||||
import com.gitee.sop.gateway.service.ResultWrapper;
|
||||
@@ -37,23 +36,15 @@ public class ResultWrapperImpl implements ResultWrapper {
|
||||
if (needNotWrap) {
|
||||
return NoCommonResponse.success(result);
|
||||
}
|
||||
if (Objects.equals(apiConfig.getFieldLowercase(), true)) {
|
||||
return ApiResponseLower.success(result);
|
||||
} else {
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
private Response executeApiResponse(ApiResponse apiResponse, boolean needNotWrap) {
|
||||
// 不需要公共返回参数
|
||||
if (needNotWrap) {
|
||||
return NoCommonResponse.success(apiResponse.getData());
|
||||
}
|
||||
if (Objects.equals(apiConfig.getFieldLowercase(), true)) {
|
||||
return apiResponse.toLower();
|
||||
} else {
|
||||
return apiResponse;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import com.gitee.sop.gateway.common.ApiInfoDTO;
|
||||
import com.gitee.sop.gateway.common.ParamInfoDTO;
|
||||
import com.gitee.sop.gateway.exception.ApiException;
|
||||
import com.gitee.sop.gateway.exception.ExceptionExecutor;
|
||||
import com.gitee.sop.gateway.interceptor.RouteInterceptor;
|
||||
import com.gitee.sop.gateway.message.ErrorEnum;
|
||||
import com.gitee.sop.gateway.request.ApiRequest;
|
||||
import com.gitee.sop.gateway.request.ApiRequestContext;
|
||||
@@ -15,27 +16,28 @@ import com.gitee.sop.gateway.response.Response;
|
||||
import com.gitee.sop.gateway.service.GenericServiceInvoker;
|
||||
import com.gitee.sop.gateway.service.ResultWrapper;
|
||||
import com.gitee.sop.gateway.service.RouteService;
|
||||
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor;
|
||||
import com.gitee.sop.gateway.service.Serde;
|
||||
import com.gitee.sop.gateway.service.validate.Validator;
|
||||
import com.gitee.sop.gateway.util.ClassUtil;
|
||||
import com.gitee.sop.support.dto.CommonFileData;
|
||||
import com.gitee.sop.support.context.DefaultOpenContext;
|
||||
import com.gitee.sop.support.dto.FileData;
|
||||
import com.gitee.sop.support.context.OpenContext;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.PostConstruct;
|
||||
import com.gitee.sop.support.dto.CommonFileData;
|
||||
import com.gitee.sop.support.dto.FileData;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.dubbo.common.utils.ClassUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
@@ -61,6 +63,9 @@ public class RouteServiceImpl implements RouteService {
|
||||
@Autowired
|
||||
private ResultWrapper resultWrapper;
|
||||
|
||||
@Autowired
|
||||
private Serde serde;
|
||||
|
||||
@Override
|
||||
public Response route(ApiRequestContext apiRequestContext) {
|
||||
ApiRequest apiRequest = apiRequestContext.getApiRequest();
|
||||
@@ -68,10 +73,13 @@ public class RouteServiceImpl implements RouteService {
|
||||
try {
|
||||
// 接口校验
|
||||
ApiInfoDTO apiInfoDTO = validator.validate(apiRequestContext);
|
||||
// 执行拦截器前置动作
|
||||
this.doPreRoute(apiRequestContext, apiInfoDTO);
|
||||
// 微服务结果
|
||||
Object result = doRoute(apiRequestContext, apiInfoDTO);
|
||||
// 执行拦截器后置动作
|
||||
result = this.doAfterRoute(apiRequestContext, apiInfoDTO, result);
|
||||
// 结果处理
|
||||
return resultWrapper.wrap(apiRequestContext, apiInfoDTO, result);
|
||||
} catch (Exception e) {
|
||||
log.error("接口请求报错, , ip={}, apiRequest={}", apiRequestContext.getIp(), apiRequest, e);
|
||||
@@ -120,7 +128,7 @@ public class RouteServiceImpl implements RouteService {
|
||||
}
|
||||
ApiRequest apiRequest = apiRequestContext.getApiRequest();
|
||||
String bizContent = apiRequest.getBizContent();
|
||||
JSONObject jsonObject = JSON.parseObject(bizContent);
|
||||
JSONObject jsonObject = serde.parseObject(bizContent);
|
||||
List<Object> params = new ArrayList<>();
|
||||
for (ParamInfoDTO paramInfoDTO : paramInfoList) {
|
||||
String type = paramInfoDTO.getType();
|
||||
@@ -141,10 +149,13 @@ public class RouteServiceImpl implements RouteService {
|
||||
} else {
|
||||
if (ClassUtil.isPrimitive(type)) {
|
||||
String paramName = paramInfoDTO.getName();
|
||||
Object value = null;
|
||||
try {
|
||||
Object value = jsonObject.getObject(paramName, ClassUtils.forName(type));
|
||||
params.add(value);
|
||||
if (jsonObject != null) {
|
||||
value = jsonObject.getObject(paramName, ClassUtils.forName(type));
|
||||
jsonObject.remove(paramName);
|
||||
}
|
||||
params.add(value);
|
||||
} catch (ClassNotFoundException e) {
|
||||
log.error("找不到参数class, paramInfoDTO={}, apiRequest={}", paramInfoDTO, apiRequest, e);
|
||||
throw new RuntimeException("找不到class:" + type, e);
|
||||
|
@@ -3,7 +3,12 @@ package com.gitee.sop.gateway.service.impl;
|
||||
import com.alibaba.nacos.shaded.com.google.gson.Gson;
|
||||
import com.alibaba.nacos.shaded.com.google.gson.GsonBuilder;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 序列化/反序列化 gson实现
|
||||
*
|
||||
* @author 六如
|
||||
*/
|
||||
public class SerdeGsonImpl extends SerdeImpl {
|
||||
@@ -11,12 +16,22 @@ public class SerdeGsonImpl extends SerdeImpl {
|
||||
Gson gson;
|
||||
|
||||
@Override
|
||||
public String toJSONString(Object object) {
|
||||
public String toJson(Object object) {
|
||||
return gson.toJson(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseJson(String json) {
|
||||
return gson.fromJson(json, LinkedHashMap.class);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
gson = new GsonBuilder().setDateFormat(dateFormat).create();
|
||||
gson = new GsonBuilder()
|
||||
.setDateFormat(dateFormat)
|
||||
.create();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,27 +1,35 @@
|
||||
package com.gitee.sop.gateway.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.gitee.sop.gateway.config.ApiConfig;
|
||||
import com.gitee.sop.gateway.service.Serde;
|
||||
import com.gitee.sop.gateway.util.XmlUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 六如
|
||||
*/
|
||||
public class SerdeImpl implements Serde {
|
||||
|
||||
static JSONWriter.Context context;
|
||||
static JSONWriter.Context WRITE_CONTEXT;
|
||||
|
||||
@Autowired
|
||||
protected ApiConfig apiConfig;
|
||||
|
||||
@Value("${gateway.serialize.date-format}")
|
||||
protected String dateFormat;
|
||||
|
||||
@Override
|
||||
public String toJSONString(Object object) {
|
||||
return JSON.toJSONString(object, context);
|
||||
public String toJson(Object object) {
|
||||
return JSON.toJSONString(object);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,10 +41,15 @@ public class SerdeImpl implements Serde {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> parseJson(String json) {
|
||||
return JSON.parseObject(json);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
context = new JSONWriter.Context();
|
||||
context.setDateFormat(dateFormat);
|
||||
WRITE_CONTEXT = new JSONWriter.Context();
|
||||
WRITE_CONTEXT.setDateFormat(dateFormat);
|
||||
|
||||
this.doInit();
|
||||
}
|
||||
|
@@ -1,11 +0,0 @@
|
||||
package com.gitee.sop.gateway.service.interceptor;
|
||||
|
||||
/**
|
||||
* @author 六如
|
||||
*/
|
||||
public class RouteInterceptorOrders {
|
||||
|
||||
public static final int RESULT_INTERCEPTOR = -1000;
|
||||
public static final int RESULT_WRAPPER_INTERCEPTOR = RESULT_INTERCEPTOR + 1;
|
||||
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
package com.gitee.sop.gateway.service.interceptor.internal;
|
||||
|
||||
import com.gitee.sop.gateway.common.ApiInfoDTO;
|
||||
import com.gitee.sop.gateway.config.ApiConfig;
|
||||
import com.gitee.sop.gateway.request.ApiRequestContext;
|
||||
import com.gitee.sop.gateway.response.ApiResponse;
|
||||
import com.gitee.sop.gateway.response.ApiResponseLower;
|
||||
import com.gitee.sop.gateway.service.interceptor.RouteInterceptor;
|
||||
import com.gitee.sop.gateway.service.interceptor.RouteInterceptorOrders;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
/**
|
||||
* 对结果进行包裹
|
||||
*
|
||||
* @author 六如
|
||||
*/
|
||||
public class ResultWrapperInterceptor implements RouteInterceptor {
|
||||
|
||||
@Autowired
|
||||
private ApiConfig apiConfig;
|
||||
|
||||
@Override
|
||||
public Object afterRoute(ApiRequestContext context, ApiInfoDTO apiInfoDTO, Object result) {
|
||||
if (Objects.equals(apiConfig.getFieldLowercase(), true)) {
|
||||
return ApiResponseLower.success(result);
|
||||
} else {
|
||||
return ApiResponse.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return RouteInterceptorOrders.RESULT_WRAPPER_INTERCEPTOR;
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package com.gitee.sop.gateway.util;
|
||||
|
||||
/**
|
||||
* @author 六如
|
||||
*/
|
||||
public class FieldUtil {
|
||||
|
||||
private static final String REGEX = "([a-z])([A-Z])";
|
||||
private static final String REGEX_VAL = "$1_$2";
|
||||
private static final char UNDERLINE = '_';
|
||||
|
||||
|
||||
/**
|
||||
* 驼峰转下划线
|
||||
*
|
||||
* @return 返回下划线
|
||||
*/
|
||||
public static String camelCaseToSnakeCase(String name) {
|
||||
return name.replaceAll(REGEX, REGEX_VAL).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
*
|
||||
* @param param 内容
|
||||
* @return 返回转换后的字符串
|
||||
*/
|
||||
public static String snakeCaseToCamelCase(String param) {
|
||||
if (param == null || param.trim().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
int len = param.length();
|
||||
StringBuilder sb = new StringBuilder(len);
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = param.charAt(i);
|
||||
if (c == UNDERLINE) {
|
||||
if (++i < len) {
|
||||
sb.append(Character.toUpperCase(param.charAt(i)));
|
||||
}
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@@ -34,4 +34,5 @@ public class JsonUtil {
|
||||
return JSON.parseArray(value, clazz);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
spring.profiles.active=dev
|
||||
spring.application.name=sop-index
|
||||
spring.application.name=sop-gateway
|
||||
server.port=8081
|
||||
|
||||
####### gateway config #######
|
||||
@@ -40,8 +40,6 @@ api.timeout-seconds=300
|
||||
api.timestamp-pattern=yyyy-MM-dd HH:mm:ss
|
||||
# default zone
|
||||
api.zone-id=Asia/Shanghai
|
||||
# if true, response field name all lowercase, such as : sub_code, sub_msg
|
||||
api.field-lowercase=false
|
||||
|
||||
####### dubbo config #######
|
||||
dubbo.protocol.name=dubbo
|
||||
|
@@ -177,7 +177,7 @@ namespace SDKCSharp.Client
|
||||
/// <typeparam name="T">返回的Response类</typeparam>
|
||||
/// <param name="request">请求对象</param>
|
||||
/// <returns>返回Response类</returns>
|
||||
public virtual T Execute<T>(BaseRequest<T> request) where T : BaseResponse
|
||||
public virtual Result<T> Execute<T>(BaseRequest<T> request)
|
||||
{
|
||||
return this.Execute<T>(request, null);
|
||||
}
|
||||
@@ -189,7 +189,7 @@ namespace SDKCSharp.Client
|
||||
/// <param name="request">请求对象</param>
|
||||
/// <param name="accessToken">accessToken</param>
|
||||
/// <returns>返回Response类</returns>
|
||||
public virtual T Execute<T>(BaseRequest<T> request, string accessToken) where T : BaseResponse
|
||||
public virtual Result<T> Execute<T>(BaseRequest<T> request, string accessToken)
|
||||
{
|
||||
RequestForm requestForm = request.CreateRequestForm(this.openConfig);
|
||||
Dictionary<string, string> form = requestForm.Form;
|
||||
@@ -225,18 +225,12 @@ namespace SDKCSharp.Client
|
||||
/// <param name="resp">服务器响应内容</param>
|
||||
/// <param name="request">请求Request</param>
|
||||
/// <returns>返回Response</returns>
|
||||
protected virtual T ParseResponse<T>(string resp, BaseRequest<T> request) where T: BaseResponse
|
||||
protected virtual Result<T> ParseResponse<T>(string resp, BaseRequest<T> request)
|
||||
{
|
||||
string method = request.Method;
|
||||
string rootNodeName = this.dataNameBuilder.Build(method);
|
||||
string errorRootNode = openConfig.ErrorResponseName;
|
||||
Dictionary<string, object> responseData = JsonUtil.ParseToDictionary(resp);
|
||||
bool errorResponse = responseData.ContainsKey(errorRootNode);
|
||||
if (errorResponse)
|
||||
{
|
||||
rootNodeName = errorRootNode;
|
||||
}
|
||||
object data = responseData[rootNodeName];
|
||||
object data = responseData.GetValueOrDefault(rootNodeName, null);
|
||||
responseData.TryGetValue(openConfig.SignName, out object sign);
|
||||
if (sign != null && !string.IsNullOrEmpty(publicKeyPlatform))
|
||||
{
|
||||
@@ -247,10 +241,8 @@ namespace SDKCSharp.Client
|
||||
data = JsonUtil.ToJSONString(checkSignErrorResponse);
|
||||
}
|
||||
}
|
||||
string jsonData = data == null ? "{}" : data.ToString();
|
||||
T t = JsonUtil.ParseObject<T>(jsonData);
|
||||
t.Body = jsonData;
|
||||
return t;
|
||||
Result<T> result = JsonUtil.ParseObject<Result<T>>(resp);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -7,7 +7,7 @@ namespace SDKCSharp.Common
|
||||
public class CustomDataNameBuilder: DataNameBuilder
|
||||
{
|
||||
|
||||
private string dataName = "result";
|
||||
private string dataName = "data";
|
||||
|
||||
public CustomDataNameBuilder()
|
||||
{
|
||||
|
@@ -11,7 +11,7 @@ namespace SDKCSharp.Common
|
||||
{
|
||||
public class OpenConfig
|
||||
{
|
||||
public static DataNameBuilder DATA_NAME_BUILDER = new DefaultDataNameBuilder();
|
||||
public static DataNameBuilder DATA_NAME_BUILDER = new CustomDataNameBuilder();
|
||||
|
||||
/// <summary>
|
||||
/// 返回码成功值
|
||||
|
@@ -33,9 +33,7 @@ namespace SDKTest
|
||||
{
|
||||
TestGet();
|
||||
Console.WriteLine("--------------------");
|
||||
TestCommon();
|
||||
Console.WriteLine("--------------------");
|
||||
TestUpload();
|
||||
//TestUpload();
|
||||
}
|
||||
|
||||
// 标准用法
|
||||
@@ -49,54 +47,20 @@ namespace SDKTest
|
||||
request.BizModel = model;
|
||||
|
||||
// 发送请求
|
||||
GetStoryResponse response = client.Execute(request);
|
||||
Result<GetStoryResponse> result = client.Execute(request);
|
||||
|
||||
if (response.IsSuccess())
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
// 返回结果
|
||||
Console.WriteLine("成功!response:{0}\n响应原始内容:{1}", JsonUtil.ToJSONString(response), response.Body);
|
||||
Console.WriteLine("成功!response:{0}\n响应原始内容:{1}", JsonUtil.ToJSONString(result), result.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
|
||||
response.Code, response.Msg, response.SubCode, response.SubMsg);
|
||||
result.Code, result.Msg, result.SubCode, result.SubMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// 懒人版,如果不想添加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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TestUpload()
|
||||
{
|
||||
@@ -119,10 +83,10 @@ namespace SDKTest
|
||||
request.AddFile(new UploadFile("file1", root + "/file1.txt"));
|
||||
request.AddFile(new UploadFile("file2", root + "/file2.txt"));
|
||||
|
||||
DemoFileUploadResponse response = client.Execute(request);
|
||||
if (response.IsSuccess())
|
||||
Result<DemoFileUploadResponse> result = client.Execute(request);
|
||||
if (result.IsSuccess())
|
||||
{
|
||||
List<DemoFileUploadResponse.FileMeta> responseFiles = response.Files;
|
||||
List<DemoFileUploadResponse.FileMeta> responseFiles = result.Data.Files;
|
||||
Console.WriteLine("您上传的文件信息:");
|
||||
responseFiles.ForEach(file =>
|
||||
{
|
||||
@@ -133,7 +97,7 @@ namespace SDKTest
|
||||
else
|
||||
{
|
||||
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
|
||||
response.Code, response.Msg, response.SubCode, response.SubMsg);
|
||||
result.Code, result.Msg, result.SubCode, result.SubMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace SDKCSharp.Response
|
||||
{
|
||||
public class GetStoryResponse: BaseResponse
|
||||
public class GetStoryResponse
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|