6.2 集成平台(改)

企业在数字化转型过程中内外部集成是一个必然需求、也是趋势

集成的诉求主要来自两个方面:1.企业的数字化改造是由外而内逐步进行的(内部异构集成)、2.企业数字化方向是朝越来越开放的方向发展(外部平台、工具集成)。总的来说企业在数字化转型过程中内外部集成是一个必然需求、也是趋势。所以我们不能简单地去理解做个API对接就结束了,而是要统一规划构建成企业的集成门户对API定义,安全、控制、记录等做全方位管理。oinone在下个版本规则中也纳入了基于集成平台之上做产品化配置的需求

概述

pamirs-eip为平台提供企业集成门户的相关功能,如请求外部接口使用的【集成接口】和对外开放被其他系统请求调用的【开放接口】功能。在请求外部接口时,还支持了多个接口调用(路由定义)、分页控制(paging)、增量控制(incremental)等功能。

准备工作

Step1 POM与模块依赖

pamirs-demo-api 和 pamirs-second-api 的pom文件中引入pamirs-eip2-api包依赖

<dependency>
    <groupId>pro.shushi.pamirs.core</groupId>
    <artifactId>pamirs-eip2-api</artifactId>
</dependency>

DemoModule和SecondModule 增加对EipModule的依赖

@Module(dependencies = {EipModule.MODULE_MODULE})

pamirs-demo-boot和pamirs-second-boot工程的pom文件中引入pamirs-eip2-core包依赖

<dependency>
    <groupId>pro.shushi.pamirs.core</groupId>
    <artifactId>pamirs-eip2-core</artifactId>
</dependency>

Step2 yaml配置文件参考

pamirs-demo-boot和pamirs-second-boot工程的application-dev.yml文件中增加配置pamirs.boot.modules增加eip,即在启动模块中增加eip模块

pamirs:
    boot:
    modules:
        - eip

pamirs-demo-boot和pamirs-second-boot工程的application-dev.yml文件中增加eip模块的数据源与路由配置

pamirs:
    framework:
    data:
        ds-map:
        eip: eip
    datasource:
    eip:
      driverClassName: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://127.0.0.1:3306/eip_v3?useSSL=false&allowPublicKeyRetrieval=true&useServerPrepStmts=true&cachePrepStmts=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
      username: root
      password: oinone
      initialSize: 5
      maxActive: 200
      minIdle: 5
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      asyncInit: true

pamirs-demo-boot工程的application-dev.yml文件中修改eip的配置

pamirs:
    eip:
    open-api:
      enabled: false

pamirs-second-boot工程的application-dev.yml文件中修改eip的配置

pamirs:
  eip:
    enabled: true
    open-api:
      enabled: true
      route:
        host: 127.0.0.1
        port: 8094
        aes-key: Nj5Thnxz4rV8Yy1FLGA2hUym3RepB8MKgafEaYC4GKo=

注:

hosts配置在远程调用时不能使用127.0.0.1,可配置为0.0.0.0进行自动识别。若自动识别仍无法访问,请准确配置其他已知的可访问IP地址。

aes-key:用下面代码生成

附录:AES Key生成

pro.shushi.pamirs.core.common.EncryptHelper加解密帮助类,默认支持AES、RSA类型的数据加解密方法,也可自定义其他类型的加解密方法。

System.out.println(EncryptHelper.getKey(EncryptHelper.getAESKey()));

Step3 在pamirs-second-api新建一个SessionTenantApi实现类

只要在我们公共的jar包中构建类似DemoSessionTenant类就可以了,之所以要构建SessionTenantApi实现类是因为EIP是以租户信息做路由的。所以这里我们写死返回一个“pamirs”租户就好了。

记得要重新mvn install second工程,再刷新demo工程

package pro.shushi.pamirs.second.api.tenant;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.framework.session.tenant.api.SessionTenantApi;
import pro.shushi.pamirs.meta.api.core.session.SessionClearApi;
import pro.shushi.pamirs.meta.common.spi.SPI;

@Order(99)
@Component
@SPI.Service
public class DemoSessionTenant implements SessionTenantApi, SessionClearApi {
    public String getTenant() {
        return "pamirs";
    }

    public void setTenant(String tenant) {
    }

    public void clear() {
    }
}

开放接口(举例)

Step1 用于演示的模型定义

package pro.shushi.pamirs.second.api.model;

import pro.shushi.pamirs.meta.annotation.Field;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.base.IdModel;

@Model.model(TestOpenApiModel.MODEL_MODEL)
@Model(displayName = "演示开放接口模型")
public class TestOpenApiModel extends IdModel {

    public static final String MODEL_MODEL = "demo.second.TestOpenApiModel";

    @Field.String
    @Field(displayName = "名称")
    private String name;

    @Field.Integer
    @Field(displayName = "年龄")
    private Integer age;
}
package pro.shushi.pamirs.second.api.tmodel;

import pro.shushi.pamirs.meta.annotation.fun.Data;

import java.io.Serializable;

@Data
public class TestOpenApiResponse implements Serializable {

    private Long id;
    private String name;
    private Integer age;

}

Step2 用于演示的接口定义

  1. TestOpenApiModelService接口定义了内部访问函数queryById

  2. TestOpenApiModelServiceImpl 实现TestOpenApiModelService接口,并定义了两个开放接口

a. queryById4Open函数用@Open进行注解,请求路径是:openapi/pamirs/方法名?tenant=pamirs,返回对象结构为函数返回值类型的JSON格式

b. queryById4OpenError函数用@Open(config = TestEipConfig.class,path = "error")进行注解,请求路径为:openapi/pamirs/error?tenant=pamirs,返回对象结构为函数返回值类型的JSON格式

c. config = TestEipConfig.class 设置通用配置类,可以在TestEipConfig增加@Open.Advanced。优先级低于方法上的注解。TestEipConfig见集成接口举例的集成配置模型定义。配置模型在开放接口定义中非必须的,在集成接口定义中时是必须。

d更多注解配置见Eip注解说明

package pro.shushi.pamirs.second.api.service;

import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
public interface TestOpenApiModelService {

    String FUN_NAMESPACE ="demo.second.TestOpenApiModelService";
    @Function
    TestOpenApiModel queryById(Long id);

}
package pro.shushi.pamirs.second.core.service;

import org.apache.camel.ExtendedExchange;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.annotation.Open;
import pro.shushi.pamirs.eip.api.enmu.EipExpEnumerate;
import pro.shushi.pamirs.eip.api.entity.openapi.OpenEipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.common.exception.PamirsException;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;
import pro.shushi.pamirs.second.api.service.TestOpenApiModelService;
import pro.shushi.pamirs.second.api.tmodel.TestOpenApiResponse;

import java.util.Optional;

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {

    @Override
    @Function
    public TestOpenApiModel queryById(Long id) {
        return new TestOpenApiModel().queryById(id);
    }

    @Function
    @Open
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        String id = Optional.ofNullable(String.valueOf(context.getInterfaceContext().getIteration("id"))).orElse("");
        TestOpenApiModel temp  = queryById(Long.valueOf(id));
        TestOpenApiResponse response = new TestOpenApiResponse();
        if(temp != null ) {
            response.setAge(temp.getAge());
            response.setId(temp.getId());
            response.setName(temp.getName());
        }else{
            response.setAge(1);
            response.setId(1L);
            response.setName("oinone eip test");
        }
        OpenEipResult<TestOpenApiResponse> result = new OpenEipResult<TestOpenApiResponse>(response);
        return result;
    }

    @Function
    // @Open(config = TestEipConfig.class,path = "error")
    @Open(path = "error")
    @Open.Advanced(
            httpMethod = "post"
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}

Step3 用于演示的请求协议

请求方法:POST

{
  "id":111
}

请求参数格式:

{
  "success":true,
  "errorCode":0,
  "result":{
    "age":1,
    "id":1,
    "name":"oinone eip test"
  }
}

成功响应格式:

{
  "success":true,
  "errorCode":0,
  "result":{
    "age":1,
    "id":1,
    "name":"oinone eip test"
  }
}

异常响应格式:

{
  "success":false,
    "errorCode":"20140000",
  "errorMsg":"系统异常, 测试异常"
}

Step4 初始化EIP

构建SecondModuleBizInit 实现InstallDataInit, UpgradeDataInit, ReloadDataInit接口,调用Eip注解模式的初始化方法:EipResolver.resolver(SecondModule.MODULE_MODULE,null)

package pro.shushi.pamirs.second.core.init;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.boot.common.api.command.AppLifecycleCommand;
import pro.shushi.pamirs.boot.common.api.init.InstallDataInit;
import pro.shushi.pamirs.boot.common.api.init.ReloadDataInit;
import pro.shushi.pamirs.boot.common.api.init.UpgradeDataInit;
import pro.shushi.pamirs.eip.api.annotation.EipResolver;
import pro.shushi.pamirs.second.api.SecondModule;

import java.util.Collections;
import java.util.List;

@Component
public class SecondModuleBizInit implements InstallDataInit, UpgradeDataInit, ReloadDataInit {

    @Override
    public boolean init(AppLifecycleCommand command, String version) {
        initEip();
        return Boolean.TRUE;
    }

    @Override
    public boolean reload(AppLifecycleCommand command, String version) {
        initEip();
        return Boolean.TRUE;
    }

    @Override
    public boolean upgrade(AppLifecycleCommand command, String version, String existVersion) {
        initEip();
        return Boolean.TRUE;
    }

    @Override
    public List<String> modules() {
        return Collections.singletonList(SecondModule.MODULE_MODULE);
    }

    @Override
    public int priority() {
        return 0;
    }

    private void initEip() {
        EipResolver.resolver(SecondModule.MODULE_MODULE,null);
    }
}

Step5 重启看效果

Second模块重新 mvn install

重启pamris-second-boot工程

请求地址一:http://localhost:8094/openapi/pamirs/queryById4Open?tenant=pamirs

curl --location --request POST 'http://localhost:8094/openapi/pamirs/queryById4Open?tenant=pamirs' \
--header 'Content-Type: application/json' \
--data-raw '{
    "id": "111",
}'

返回结果:

{"errorCode":0,"result":{"age":1,"id":1,"name":"oinone eip test"},"success":true}

请求地址二:http://localhost:8094/openapi/pamirs/error?tenant=pamirs

curl --location --request POST 'http://localhost:8094/openapi/pamirs/error?tenant=pamirs' \
--header 'Content-Type: application/json' \
--data-raw '{
    "id": "111",
}'

返回结果:

{"success":false,"errorCode":"20140000","errorMsg":"系统异常, 测试异常"}

集成接口(举例)

下面的例子我们将用集成接口调用开放接口,集成接口配置在Demo模块

Step1 用于演示的集成配置模型定义

  1. 配置模型必须继承IEipAnnotationSingletonConfig接口

  2. 自定义construct函数,达到修改配置时可以自动刷新对应的集成接口和开放接口

  3. host为服务端:域名+端口,如www.oinone.top:80

  4. shcema为请求协议:http或https

  5. 类上可以增加@Open.Advanced 和 @Integrate.Advanced 配置达到通用配置的作用,优先级低于方法上的注解

package pro.shushi.pamirs.demo.api.model;

import pro.shushi.pamirs.eip.api.annotation.IEipAnnotationSingletonConfig;
import pro.shushi.pamirs.meta.annotation.Field;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.base.IdModel;
import pro.shushi.pamirs.meta.enmu.FunctionOpenEnum;
import pro.shushi.pamirs.meta.enmu.FunctionTypeEnum;

@Model.model(TestEipConfig.MODEL_MODEL)
@Model(displayName = "演示集成接口配置模型")
public class TestEipConfig extends IdModel implements IEipAnnotationSingletonConfig<TestEipConfig> {

    public static final String MODEL_MODEL = "demo.TestEipConfig";

    @Field(displayName = "服务端域名")
    private String host;
    @Field(displayName = "请求协议Http或Https")
    private String schema;

    @Function(openLevel = FunctionOpenEnum.API,summary = "演示集成接口配置模型")
    @Function.Advanced(type = FunctionTypeEnum.QUERY)
    public TestEipConfig construct(TestEipConfig config){
        TestEipConfig config1 = config.singletonModel();
        if(config1!=null){
            return config1;
        }
        return config.construct();
    }
}

Step2 用于演示的接口定义

  1. TestIntegrateService接口定义了内部访问函数callQueryById、callQueryByData、callQueryByIdError

  2. TestIntegrateServiceImpl 实现TestIntegrateService接口,并把方法定义为集成接口

a. config = TestEipConfig.class 设置通用配置类,可以在TestEipConfig增加@Integrate.Advanced。优先级低于方法上的注解。它在集成接口定义中时是必须。

b. @Integrate.Advanced(path=""),集成接口最终请求地址:TestEipConfig.schema+"://"+TestEipConfig.host+path

c. 方法体返回空就可以,实际执行时候该方法被拦截并不会执行

d. @Integrate.ConvertParam

ⅰ. 请求从方法入参数会以参数名为key放在上下文中,@Integrate.ConvertParam(inParam="data.id",outParam="id")代表把参数名为data的id属性值,放到上下文key为id上。

ⅱ. url和head的参数转化参见paramConverter的特殊转换

e. finalResultKey是指最终的请求参数key,会从上下文中找对应的key值作为请求参数

f. 更多注解配置见Eip注解说明

package pro.shushi.pamirs.demo.api.service;

import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Fun(TestIntegrateService.FUN_NAMESPACE)
public interface TestIntegrateService {
    String FUN_NAMESPACE ="demo.TestIntegrateService";
    @Function
    EipResult<SuperMap> callQueryById(String id);
    @Function
    EipResult<SuperMap> callQueryByData(TestOpenApiModel data);
    @Function
    EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data);

}
package pro.shushi.pamirs.demo.core.service;

import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.api.service.TestIntegrateService;
import pro.shushi.pamirs.eip.api.annotation.Integrate;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Fun(TestIntegrateService.FUN_NAMESPACE)
@Component
public class TestIntegrateServiceImpl implements TestIntegrateService {

    @Override
    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
    public EipResult<SuperMap> callQueryById(String id) {
        return null;
    }

    @Override
    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
    @Integrate.RequestProcessor(
            convertParams={
                    @Integrate.ConvertParam(inParam="data.id",outParam="id")
            }
    )
    public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
        return null;
    }

    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
    @Integrate.RequestProcessor(
            finalResultKey = "out",
            convertParams={
                    @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
            }
    )
    public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
        return null;
    }

}

Step3 新建TestEipAction

package pro.shushi.pamirs.demo.core.action;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.api.service.TestIntegrateService;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Action;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.enmu.ActionContextTypeEnum;
import pro.shushi.pamirs.meta.util.JsonUtils;
import pro.shushi.pamirs.second.api.model.TestOpenApiModel;

@Model.model(TestEipConfig.MODEL_MODEL)
@Component
@Slf4j
public class TestEipAction {

    @Autowired
    private TestIntegrateService testIntegrateService;

    @Action(displayName = "调用集成接口callQueryById",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryById(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryById("111");
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByIdError",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdError(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByIdError((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByData",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdSuccess(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByData((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }
}

Step4 构建测试入口

@UxMenu("集成测试")@UxRoute(TestEipConfig.MODEL_MODEL) class TestEipConfigMenu{}

Step5 初始化Eip

在DemoModuleBizInit 类中增加私有方法initEip,并在init 、reload 、upgrade 中调用该私有化

private void initEip() {
    EipResolver.resolver(DemoModule.MODULE_MODULE,null);
}

Step6 重启看效果

  1. 点击【集成测试】菜单,点击【新增】按钮,配置集成接口信息

image.png

  1. 点击【集成测试】菜单,依次点击三个方法

image.png

  1. 模块切换进入【集成接口】,点击【日志】菜单。比对请求不同点

a. 设置finalResultKey的表达从接口上下文中取最终对应的value作为请求参数,不然会传全部接口上下文过去

b. callQueryByIdError,集成接口和对应的开放接口对结果认知差别

ⅰ. 开放接口返回异常信息

ⅱ. 集成接口没有对返回信息作判定,直接当成功。集成接口如何判定对方返还的是错误,请见自定义异常判定

自定义异常判定(举例)

Step1 新建异常判定函数

新建异常判定处理类TestExceptionPredictFunction,实现IEipExceptionPredict接口。从context.getInterfaceContextValue(DEFAULT_ERROR_CODE_KEY)中获取成功与否的值,并进行判断

package pro.shushi.pamirs.demo.core.service;

import pro.shushi.pamirs.core.common.StringHelper;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.IEipExceptionPredict;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;

import static pro.shushi.pamirs.eip.api.IEipContext.DEFAULT_ERROR_CODE_KEY;

@Fun(TestExceptionPredictFunction.FUN_NAMESPACE)
public class TestExceptionPredictFunction implements IEipExceptionPredict<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.TestExceptionPredictFunction";
    public static final String FUN ="testFunction";

    @Override
    public boolean test(IEipContext<SuperMap> context) {
        return !Boolean.TRUE.toString().equals(StringHelper.valueOf(context.getExecutorContextValue(DEFAULT_ERROR_CODE_KEY)));
    }

    @Function
    @Function.fun(FUN)
    public Boolean testFunction(IEipContext<SuperMap> context) {
        return test(context);
    }
}

Step2 修改TestIntegrateServiceImpl类

修改TestIntegrateServiceImpl的callQueryByIdError函数的注解,增加@Integrate.ExceptionProcessor注解

  1. errorCode ="success", 从返回结果中拿键值为“success”的值,作为接口上下文对应键值为IEipContext.DEFAULT_ERROR_CODE_KEY的值

  2. exceptionPredictFun 指定函数名

  3. exceptionPredictNamespace 指定函数命名空间

    
    …… 依赖类引用

@Fun(TestIntegrateService.FUN_NAMESPACE)
@Component
public class TestIntegrateServiceImpl implements TestIntegrateService {

……其他代码

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        }
)
@Integrate.ExceptionProcessor(
        errorCode ="success",
        exceptionPredictFun = TestExceptionPredictFunction.FUN,
        exceptionPredictNamespace = TestExceptionPredictFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

}

### Step3 重启看效果

1. 点击【集成测试】菜单,点击【调用集成接口callQueryByIdError 】

2. 模块切换进入【集成接口】,点击【日志】菜单。集成接口是否成功调用判定为false

![image.png](https://doc.oinone.top/wp-content/uploads/2024/05/1665041696328-76259be4-72ed-47c2-b3ea-4f7f3c9e6417.png)

## 自定义安全策略(AccessToken)

开发接口提供默认三种实现方式,例子采用DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN

 认证模式,最简单的安全模式:是有accessToken

1. DEFAULT_AUTHENTICATION_PROCESSOR_FUN:根据EipApplication配置的加密类型进行加解密处理的认证

2. DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN:不进行加解密处理的认证。

3. DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN:使用MD5简单验签的认证。

### Step1 给开放接口加上认证处理配置
```java
……依赖类引用

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {

    @Override
    @Function
    public TestOpenApiModel queryById(Long id) {
        return new TestOpenApiModel().queryById(id);
    }

    @Function
    @Open
    @Open.Advanced(
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        String id = Optional.ofNullable(String.valueOf(context.getInterfaceContext().getIteration("id"))).orElse("");
        TestOpenApiModel temp  = queryById(Long.valueOf(id));
        TestOpenApiResponse response = new TestOpenApiResponse();
        if(temp != null ) {
            response.setAge(temp.getAge());
            response.setId(temp.getId());
            response.setName(temp.getName());
        }else{
            response.setAge(1);
            response.setId(1L);
            response.setName("oinone eip test");
        }
        OpenEipResult<TestOpenApiResponse> result = new OpenEipResult<TestOpenApiResponse>(response);
        return result;
    }

    @Function
    @Open(path = "error")
    @Open.Advanced(
        httpMethod = "post",
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_NO_ENCRYPT_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}
</code></pre>
<h3>Step2 修改TestEipAction,增加返回结果判断</h3>
<pre><code class="language-java">@Model.model(TestEipConfig.MODEL_MODEL)
@Component
@Slf4j
public class TestEipAction {

    @Autowired
    private TestIntegrateService testIntegrateService;

    @Action(displayName = "调用集成接口callQueryById",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryById(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryById("111");
        if(!eipResult.getSuccess()){
            throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryById 失败").errThrow();
        }
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));-
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByIdError",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdError(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByIdError((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        if(!eipResult.getSuccess()){
            throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryByIdError 失败").errThrow();
        }
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }

    @Action(displayName = "调用集成接口callQueryByData",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig callQueryByIdSuccess(TestEipConfig data){
        EipResult<SuperMap> eipResult = testIntegrateService.callQueryByData((TestOpenApiModel)new TestOpenApiModel().setId(111L));
        if(!eipResult.getSuccess()){
            throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryByData 失败").errThrow();
        }
        PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
        return data;
    }
}</code></pre>
<h3>Step3 重启看效果</h3>
<p>期待效果,因为开放接口增加了安全相关校验,集成接口这边并没有调整,所以都会抛错</p>
<p><img src="https://doc.oinone.top/wp-content/uploads/2024/05/1665028192254-4f96bda0-d2c2-4b23-a7be-268c84a920f1.png" alt="image.png" /></p>
<h3>Step4 为集成接口定义auth处理类</h3>
<h3>Step4_1 修改 TestEipConfig</h3>
<p>增加了appkey和appSecret两个字段,用于存储开放接口的appkey和appSecret。大家注意我们是用集成接口去访问开放接口,大家可以要想象成两个应用。</p>
<p>站在开放接口角度,但凡需要访问开放接口,就需要先申请appkey和appSecret,并通过appkey和appSecret来换取accessToken,再用accessToken来请求。</p>
<p>站在集成接口角度,我们需要访问对方的接口,必须遵循对方接口安全规范。</p>
<pre><code class="language-java">package pro.shushi.pamirs.demo.api.model;

import pro.shushi.pamirs.eip.api.annotation.IEipAnnotationSingletonConfig;
import pro.shushi.pamirs.eip.api.annotation.Integrate;
import pro.shushi.pamirs.meta.annotation.Field;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.annotation.Model;
import pro.shushi.pamirs.meta.base.IdModel;
import pro.shushi.pamirs.meta.enmu.FunctionOpenEnum;
import pro.shushi.pamirs.meta.enmu.FunctionTypeEnum;

@Model.model(TestEipConfig.MODEL_MODEL)
@Model(displayName = "演示集成接口配置模型")
@Integrate.ExceptionProcessor(
        errorCode ="success",
        exceptionPredictFun = "testFunction",
        exceptionPredictNamespace = "demo.TestExceptionPredictFunction"
)
public class TestEipConfig extends IdModel implements IEipAnnotationSingletonConfig<TestEipConfig> {

    public static final String MODEL_MODEL = "demo.TestEipConfig";

    @Field(displayName = "服务端域名")
    private String host;
    @Field(displayName = "请求协议Http或Https")
    private String schema;

    @Field(displayName = "appkey")
    private String appKey;

    @Field(displayName = "appSecret")
    @Field.String(size = 4096)
    private String appSecret;

    @Function(openLevel = FunctionOpenEnum.API,summary = "演示集成接口配置模型")
    @Function.Advanced(type = FunctionTypeEnum.QUERY)
    public TestEipConfig construct(TestEipConfig config){
        TestEipConfig config1 = config.singletonModel();
        if(config1!=null){
            return config1;
        }
        return config.construct();
    }
}
</code></pre>
<h3>Step4_2 修改TestIntegrateService和TestIntegrateServiceImpl</h3>
<ol>
<li>
<p>定义和实现fetchOinoneAccessToken</p>
</li>
<li>
<p>修改集成注解增加auth处理器</p>
</li>
</ol>
<h3>Step4_3 新建TestAuthFunction</h3>
<ol>
<li>
<p>获取TestEipConfig的配置信息</p>
</li>
<li>
<p>先从缓存中拿accessToken</p>
<pre><code class="language-java">
@Function
EipResult<SuperMap> fetchOinoneAccessToken(String appkey,String appSecret) ;</code></pre>
</li>
</ol>
<pre><code>
```java

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN
)
public EipResult<SuperMap> callQueryById(String id) {
    return null;
}

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN
)
public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN

)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/get/access-token?tenant=pamirs")
@Integrate.RequestProcessor(
        convertParams={
                @Integrate.ConvertParam(inParam="appkey",outParam= IEipContext.HEADER_PARAMS_KEY+".appkey"),
                @Integrate.ConvertParam(inParam="appSecret",outParam=IEipContext.HEADER_PARAMS_KEY+".appSecret")
        }
)
@Integrate.ResponseProcessor(
        convertParams={
                @Integrate.ConvertParam(inParam="access_token",outParam="accessToken"),
        }
)
public EipResult<SuperMap> fetchOinoneAccessToken(String appkey,String appSecret) {
    return null;
}

a. 拿到直接放到请求头中

b. 拿不到调用对方接口获取accessToken(这里对方接口也就是我们自己的开放平台的接口)获取accessToken

package pro.shushi.pamirs.demo.core.service;

import org.apache.camel.ExtendedExchange;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.api.service.TestIntegrateService;
import pro.shushi.pamirs.eip.api.IEipAuthenticationProcessor;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.entity.EipResult;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.common.spring.BeanDefinitionUtils;

import java.util.concurrent.TimeUnit;

@Fun(TestAuthFunction.FUN_NAMESPACE)
@Component
public class TestAuthFunction implements IEipAuthenticationProcessor<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.TestAuthFunction";
    public static final String FUN ="testAuthentication";
    @Function
    @Function.fun(FUN)
    public Boolean testAuthentication(IEipContext<SuperMap> context,ExtendedExchange exchange) {
        return authentication(context,exchange);
    }

    @Override
    public boolean authentication(IEipContext<SuperMap> context, ExtendedExchange exchange) {
        //获取第三方配置
        TestEipConfig testEipConfig = new TestEipConfig().singletonModel();
        TestIntegrateService testIntegrateService =  BeanDefinitionUtils.getBean(TestIntegrateService.class);
        RedisTemplate redisTemplate = (RedisTemplate)BeanDefinitionUtils.getBean("redisTemplate");
        String accessToken = (String)redisTemplate.opsForValue().get(testEipConfig.getAppKey());
        if(StringUtils.isNotBlank(accessToken)){
            context.putInterfaceContextValue(IEipContext.HEADER_PARAMS_KEY+".accessToken",accessToken);
        }else {
            //调用获取accessToken接口
            EipResult<SuperMap> eipResult = testIntegrateService.fetchOinoneAccessToken(testEipConfig.getAppKey(), testEipConfig.getAppSecret());
            if (eipResult.getSuccess()) {
                accessToken = String.valueOf(eipResult.getContext().getInterfaceContextValue("accessToken"));
                if (StringUtils.isBlank(accessToken)) {
                    return Boolean.FALSE;
                }
                context.putInterfaceContextValue(IEipContext.HEADER_PARAMS_KEY + ".accessToken", accessToken);
                //oinone的accessToken默认失效时间为15分钟
                redisTemplate.opsForValue().set(testEipConfig.getAppKey(), accessToken,13*60, TimeUnit.SECONDS);
            } else {
                return Boolean.FALSE;
            }
        }
        return Boolean.TRUE;
    }
}

Step4_4 修改TestEipAction

借用TestEipConfig 模型的列表页提供一个新增EipApplication的入口

……引用依赖类
@Model.model(TestEipConfig.MODEL_MODEL)
@Component
@Slf4j
public class TestEipAction {
    ……其他代码

    @Action(displayName = "创建一个集成应用",contextType = ActionContextTypeEnum.CONTEXT_FREE)
    public TestEipConfig createEipApplication(TestEipConfig data){

        EipApplication eipApplication = new EipApplication().setName("测试的集成应用").queryOne();
        if(eipApplication ==null){
            eipApplication = new EipApplication().setName("测试的集成应用");
            eipApplication.create();
        }else {
            eipApplication.updateById();
        }
        return data;
    }
}

Step5 重启看效果

  1. 点击【集成测试】菜单,点击【创建一个集成应用】按钮,数据库查看创建出来的EipApplication记录和EipAuthentication记录

image.png

image.png

  1. 点击【集成测试】菜单,编辑记录,填写appkey和appSecret

image.png

image.png

  1. 点击【集成测试】菜单,依次点击恢复原有的效果,只有【调用集成接口callQueryByIdError】报错其他不再报错了,也可以进入集成接口模块的日志菜单查看请求日志

image.png

自定义安全策略(AccessToken+加密)

例子采用DEFAULT_AUTHENTICATION_PROCESSOR_FUN安全策略,根据EipApplication配置的加密类型进行加解密处理的认证。安全模式:accessToken+加密

当开放接口采用DEFAULT_AUTHENTICATION_PROCESSOR_FUN的策略,需要外部调用方请求参数为{result:"加密后的请求体内容"}的结构,result的值为加密后的请求体内容。

Step1 更改开放接口认证方式

认证方式换成DEFAULT_AUTHENTICATION_PROCESSOR_FUN,@Open.Advanced(authenticationProcessorFun= EipFunctionConstant.DEFAULT_AUTHENTICATION_PROCESSOR_FUN)

小伙伴们可以把@Open.Advanced(authenticationProcessorFun)的配置挪到TestEipConfig中去,这样就不用为每个接口配置了,自己动手吧。

//…… 包引用
@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {

//…… 其他代码

    @Function
    @Open
    @Open.Advanced(
            authenticationProcessorFun= EipFunctionConstant.DEFAULT_AUTHENTICATION_PROCESSOR_FUN,
            authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
       //…… 其他代码
    }

    @Function
    @Open(path = "error")
    @Open.Advanced(
            httpMethod = "post",
            authenticationProcessorFun= EipFunctionConstant.DEFAULT_AUTHENTICATION_PROCESSOR_FUN,
            authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}

Step2 重启看效果

期待效果,因为开放接口修改了安全相关校验,集成接口这边并没有调整,所以都会抛错

image.png

Step3 为集成接口定义新的auth处理类

Step3_1 修改 TestEipConfig

增加两个字段encryptType和publicKey,这里加密类型为RSA,我们为集成接口申请的EipApplication没有指定加密类型,默认RSA。oinone的开放接口总共支持两种:RSA、AES


    @Field(displayName = "encryptType")
    private EncryptTypeEnum encryptType;

    @Field(displayName = "publicKey")
    @Field.String(size = 4096)
    private String publicKey;

Step3_2 新建TestInOutConvertFunction

新建TestInOutConvertFunction实现IEipInOutConverter接口

package pro.shushi.pamirs.demo.core.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import org.apache.camel.ExtendedExchange;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.EncryptHelper;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.eip.api.IEipInOutConverter;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.util.JsonUtils;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

@Fun(TestInOutConvertFunction.FUN_NAMESPACE)
@Component
public class TestInOutConvertFunction implements IEipInOutConverter {

    public static final String FUN_NAMESPACE ="demo.third.TestInOutConvertFunction";
    public static final String FUN ="exchangeObject";

    @Function
    @Function.fun(FUN)
    @Override
    public Object exchangeObject(ExtendedExchange exchange, Object inObject) throws Exception {
        //获取第三方配置
        TestEipConfig testEipConfig = new TestEipConfig().singletonModel();
        //数据加密
        String data;
        if (inObject instanceof InputStream) {
            data = (String) JSON.parseObject((InputStream)inObject, String.class, new Feature[0]);
        } else if (inObject instanceof String) {
            data = (String)inObject;
        } else {
            data = JsonUtils.toJSONString(inObject);
        }
        switch (testEipConfig.getEncryptType()) {
            case RSA:
                try {
                    data = EncryptHelper.encryptByKey(EncryptHelper.getPublicKey(testEipConfig.getEncryptType().value(), testEipConfig.getPublicKey()), data);
                } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) {
                    e.printStackTrace();
                    data = null;
                }
                break;
            case AES:
                try {
                    data = EncryptHelper.encryptByKey(EncryptHelper.getSecretKeySpec(testEipConfig.getEncryptType().value(), testEipConfig.getPublicKey()), data);
                } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
                    e.printStackTrace();
                    data = null;
                }
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + testEipConfig.getEncryptType());
        }
        //放到指定的路径中,因为集成接口的finalResultKey为"out",这样刚好构建{result:""}的请求结构,根据不同公司不一样
        return "{result:\""+data+"\"}";
    }
}

Step3_3 修改TestIntegrateServiceImpl

为集成方法增加inOutConverter的注解

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestInOutConvertFunction.FUN,
        inOutConverterNamespace = TestInOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryById(String id) {
    return null;
}

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestInOutConvertFunction.FUN,
        inOutConverterNamespace = TestInOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestInOutConvertFunction.FUN,
        inOutConverterNamespace = TestInOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

Step4 重启看效果

  1. 点击【集成测试】菜单,点击【创建一个集成应用】按钮,数据库查看创建出来的EipApplication记录和EipAuthentication记录,为记录填写publicKey

image.png

image.png

  1. 再次点击【集成测试】菜单,依次点击恢复原有的效果,只有【调用集成接口callQueryByIdError】报错其他不再报错了,也可以进入集成接口模块的日志菜单查看请求日志

image.png

自定义安全策略(AccessToken+签名)

例子采用DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN

安全策略,安全模式:accessToken+签名

签名支持:md5和hmac

签名方式和签名摘要:请求方指定,在请求体中设置signatureMethod、signature

Step1 更改开放接口认证方式

认证方式换成DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,@Open.Advanced(authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN)

//…… 包引用

@Fun(TestOpenApiModelService.FUN_NAMESPACE)
@Component
public class TestOpenApiModelServiceImpl implements TestOpenApiModelService {
//…… 其他代码

    @Function
    @Open
    @Open.Advanced(
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4Open(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        //…… 其他代码
    }

    @Function
    @Open(config = TestEipConfig.class,path = "error")
    @Open.Advanced(
        httpMethod = "post",
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
    )
    public OpenEipResult<TestOpenApiResponse> queryById4OpenError(IEipContext<SuperMap> context , ExtendedExchange exchange) {
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("测试异常").errThrow();
    }

}

Step2 重启看效果

期待效果,因为开放接口修改了安全相关校验,集成接口这边并没有调整,所以都会抛错

image.png

Step3 为集成接口定义新的auth处理类

Step3_1 新建SignUtil

新建SignUtil签名工具类

package pro.shushi.pamirs.demo.core.util;

import org.apache.commons.lang3.StringUtils;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

public class SignUtil {

    public static String signTopRequest(Map<String, String> params, String secret, String signMethod) throws IOException {
        // 第一步:检查参数是否已经排序
        String[] keys = params.keySet().toArray(new String[0]);
        Arrays.sort(keys);

        // 第二步:把所有参数名和参数值串在一起
        StringBuilder query = new StringBuilder();
        if ("md5".equals(signMethod)) { //签名的摘要算法,可选值为:hmac,md5,hmac-sha256
            query.append(secret);
        }
        for (String key : keys) {
            String value = params.get(key);
            if (StringUtils.isAllBlank(new CharSequence[]{key, value})) {
                query.append(key).append(value);
            }
        }

        // 第三步:使用MD5/HMAC加密
        byte[] bytes;
        if ("hmac".equals(signMethod)) {
            bytes = encryptHMAC(query.toString(), secret);
        } else {
            query.append(secret);
            bytes = encryptMD5(query.toString());
        }

        // 第四步:把二进制转化为大写的十六进制(正确签名应该为32大写字符串,此方法需要时使用)
        return byte2hex(bytes);
    }

    public static byte[] encryptHMAC(String data, String secret) throws IOException {
        byte[] bytes = null;
        try {
            SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacMD5");
            Mac mac = Mac.getInstance(secretKey.getAlgorithm());
            mac.init(secretKey);
            bytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        } catch (GeneralSecurityException gse) {
            throw new IOException(gse.toString());
        }
        return bytes;
    }

    private static byte[] encryptMD5(String data) throws IOException {
        return encryptMD5(data.getBytes(StandardCharsets.UTF_8));
    }

    private static byte[] encryptMD5(byte[] data) throws IOException {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            return md.digest(data);
        } catch (NoSuchAlgorithmException var3) {
            throw new IOException(var3.toString(), var3);
        }
    }

    public static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }
}

Step3_2 新建TestMd5InOutConvertFunction

  1. 跟加密一样,必须在所有内容都处理完成后进行处理,要不然参数个数对不上的。

  2. 例子中新增一种设置Header的方法,exchange.getMessage().setHeader前面,在前面我们学习AccessToken的时候用的方法是:context.putInterfaceContextValue(IEipContext.HEADER_PARAMS_KEY+".accessToken"

    
    package pro.shushi.pamirs.demo.core.service;

import org.apache.camel.ExtendedExchange;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.demo.api.model.TestEipConfig;
import pro.shushi.pamirs.demo.core.util.SignUtil;
import pro.shushi.pamirs.eip.api.IEipInOutConverter;
import pro.shushi.pamirs.eip.api.converter.DefaultInOutConverter;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Fun(TestMd5InOutConvertFunction.FUN_NAMESPACE)
@Component
public class TestMd5InOutConvertFunction extends DefaultInOutConverter implements IEipInOutConverter {

public static final String FUN_NAMESPACE ="demo.third.TestMd5InOutConvertFunction";
public static final String FUN ="exchangeObject";

@Function
@Function.fun(FUN)
public Object exchangeObject(ExtendedExchange exchange, Object inObject) throws Exception {
    if (inObject instanceof Map) {
        Map<String, Object> map = (Map)inObject;
        if (MapUtils.isNotEmpty(map)) {

            TestEipConfig testEipConfig = new TestEipConfig().singletonModel();
            String signatureMethod ="md5";
            Map<String, String> params = new HashMap(map.size());
            for(Map.Entry<String ,Object> entry: map.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                params.put(key, String.valueOf(value));
            }
            try {
                String signature = SignUtil.signTopRequest(params, testEipConfig.getAppKey(), signatureMethod);
                exchange.getMessage().setHeader("signatureMethod",signatureMethod);
                exchange.getMessage().setHeader("signature",signature);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return super.exchangeObject(exchange, inObject);
}

}


### Step3_3 修改TestIntegrateServiceImpl

更换inOutConverter的注解为TestMd5InOutConvertFunction
```JAVA
@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestMd5InOutConvertFunction.FUN,
        inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryById(String id) {
    return null;
}

@Override
@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/queryById4Open?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestMd5InOutConvertFunction.FUN,
        inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE
)
public EipResult<SuperMap> callQueryByData(TestOpenApiModel data) {
    return null;
}

@Function
@Integrate(config = TestEipConfig.class)
@Integrate.Advanced(path = "/openapi/pamirs/error?tenant=pamirs")
@Integrate.RequestProcessor(
        finalResultKey = "out",
        convertParams={
                @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
        },
        authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
        authenticationProcessorFun =  TestAuthFunction.FUN,
        inOutConverterFun = TestMd5InOutConvertFunction.FUN,
        inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE

)
public EipResult<SuperMap> callQueryByIdError(TestOpenApiModel data) {
    return null;
}

Step4 重启看效果

点击【集成测试】菜单,依次点击恢复原有的效果,只有【调用集成接口callQueryByIdError】报错其他不再报错了,也可以进入集成接口模块的日志菜单查看请求日志

image.png

自定义序列化方式

Step1 新增一个xml的开放接口

为TestOpenApiModelServiceImpl新增一个方法queryById4ReturnXml,使用@Open注解,路径配置为xml。同时记得重启second应用,发布这个openApi

@Function
@Open(path = "xml")
@Open.Advanced(
        httpMethod = "post",
        authenticationProcessorFun= EipFunctionConstant.DEFAULT_MD5_SIGNATURE_AUTHENTICATION_PROCESSOR_FUN,
        authenticationProcessorNamespace = EipFunctionConstant.FUNCTION_NAMESPACE
)
public String queryById4ReturnXml(IEipContext<SuperMap> context , ExtendedExchange exchange) {
    return "<result><id>12</id><success>true</success></result>";
}

Step2 Demo工程引入dom4j的依赖包

在总工程中修改pom.xml增加对dom4j的依赖管理

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.1</version>
</dependency>

在pamirs-demo-core工程中修改pom.xml增加对dom4j的依赖

<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
</dependency>

Step3 自定义序列化函数TestSerializableFunction

新建TestSerializableFunction实现 IEipSerializable<SuperMap>接口

package pro.shushi.pamirs.demo.core.service;

import com.alibaba.fastjson.JSONArray;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipSerializable;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;

@Fun(TestSerializableFunction.FUN_NAMESPACE)
@Component
public class TestSerializableFunction implements IEipSerializable<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.third.TestSerializableFunction";
    public static final String FUN ="serializable";

    @Override
    @Function.Advanced(displayName = "自定义xml序列化方式")
    @Function.fun(FUN)
    public SuperMap serializable(Object inObject) {
        if (inObject == null) {
            return new SuperMap();
        } else {
            SuperMap result;
            if (inObject instanceof String) {
                String inObjectString = (String)inObject;
                if (StringUtils.isNotBlank(inObjectString)) {
                    result = this.stringToSuperMap(inObjectString);
                } else {
                    result = new SuperMap();
                }
            } else if (inObject instanceof InputStream) {
                result = this.inputStreamToString((InputStream)inObject);
            } else if (inObject instanceof SuperMap) {
                result = (SuperMap) inObject;
            }
            else{
                result = new SuperMap();
            }
            return result;
        }
    }

    protected SuperMap inputStreamToString(InputStream inputStream) {
        StringBuilder sb = new StringBuilder();
        String line;
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
            while ((line = bufferedReader.readLine()) != null) {
                sb.append(line);
            }
            return serializable(sb.toString());
        } catch (IOException e) {
            return new SuperMap();
        }
    }

    protected SuperMap stringToSuperMap(String s) {
        SuperMap result = new SuperMap();
        try {
            Document document = DocumentHelper.parseText(s);
            Element root = document.getRootElement();
            iterateNodes(root, result);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    public static void iterateNodes(Element node, SuperMap superMap){
        //获取当前元素的名称
        String nodeName = node.getName();
        if(superMap.containsKey(nodeName)){
            //该元素在同级下有多个
            Object object = superMap.getIteration(nodeName);
            List<Object> list = Lists.newArrayList();
            if(object instanceof JSONArray){
                list = (List) object;
            }else {
                list = Lists.newArrayList();
                list.add(object);
            }
            //获取该元素下所有子元素
            List<Element> listElement = node.elements();
            if(listElement.isEmpty()){
                //该元素无子元素,获取元素的值
                String nodeValue = node.getTextTrim();
                list.add(nodeValue);
                superMap.putIteration(nodeName, list);
                return;
            }
            //有子元素
            SuperMap subMap = new SuperMap();
            //遍历所有子元素
            for(Element e:listElement){
                //递归
                iterateNodes(e, subMap);
            }
            list.add(subMap);
            subMap.putIteration(nodeName, list);
            return;
        }
        List<Element> listElement = node.elements();
        if(listElement.isEmpty()){
            //该元素无子元素,获取元素的值
            String nodeValue = node.getTextTrim();
            superMap.putIteration(nodeName, nodeValue);
            return;
        }
        //有子节点,新建一个JSONObject来存储该节点下子节点的值
        SuperMap subMap = new SuperMap();
        //遍历所有一级子节点
        for(Element e:listElement){
            //递归
            iterateNodes(e, subMap);
        }
        superMap.putIteration(nodeName, subMap);
    }

}

Step4 新增一个异常判定类

新增TestXmlExceptionPredictFunction异常判定类,因为原来的异常判断类配置在了TestEipConfig,所以所有方法都会默认使用TestEipConfig类上定义的异常判断类。

package pro.shushi.pamirs.demo.core.service;

import pro.shushi.pamirs.core.common.SuperMap;
import pro.shushi.pamirs.eip.api.IEipContext;
import pro.shushi.pamirs.eip.api.IEipExceptionPredict;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;

@Fun(TestXmlExceptionPredictFunction.FUN_NAMESPACE)
public class TestXmlExceptionPredictFunction implements IEipExceptionPredict<SuperMap> {

    public static final String FUN_NAMESPACE ="demo.TestXmlExceptionPredictFunction";
    public static final String FUN ="testFunction";

    @Override
    public boolean test(IEipContext<SuperMap> context) {
        return Boolean.FALSE;
    }

    @Function
    @Function.fun(FUN)
    public Boolean testFunction(IEipContext<SuperMap> context) {
        return test(context);
    }
}

Step5 修改TestIntegrateService和TestIntegrateServiceImpl

TestIntegrateService增加方法定义

@Function
EipResult<SuperMap> callQueryReturnXml(TestOpenApiModel data) ;

TestIntegrateServiceImpl实现callQueryReturnXml 并定义为集成接口,同时配置自定义序列化函数

……引用依赖类

@Fun(TestIntegrateService.FUN_NAMESPACE)
@Component
public class TestIntegrateServiceImpl implements TestIntegrateService {
        ……其他代码

    @Function
    @Integrate(config = TestEipConfig.class)
    @Integrate.Advanced(
            path = "/openapi/pamirs/xml?tenant=pamirs",
            httpMethod = "post"
    )
    @Integrate.RequestProcessor(
            finalResultKey = "out",
            convertParams={
                    @Integrate.ConvertParam(inParam="data.id",outParam="out.id")
            },
            authenticationProcessorNamespace = TestAuthFunction.FUN_NAMESPACE,
            authenticationProcessorFun =  TestAuthFunction.FUN,
            inOutConverterFun = TestMd5InOutConvertFunction.FUN,
            inOutConverterNamespace = TestMd5InOutConvertFunction.FUN_NAMESPACE

    )
    @Integrate.ResponseProcessor(
            finalResultKey = "result",
            serializableFun = TestSerializableFunction.FUN,
            serializableNamespace = TestSerializableFunction.FUN_NAMESPACE
    )
    @Integrate.ExceptionProcessor(
            errorCode ="success",
            exceptionPredictFun = TestXmlExceptionPredictFunction.FUN,
            exceptionPredictNamespace = TestXmlExceptionPredictFunction.FUN_NAMESPACE
    )
    public EipResult<SuperMap> callQueryReturnXml(TestOpenApiModel data) {
        return null;
    }

}

Step6 增加测试入口

修改TestEipAction 类增加一个callQueryByXml的Action定义,调用testIntegrateService集成接口的callQueryReturnXml方法

@Action(displayName = "调用集成接口callQueryReturnXml",contextType = ActionContextTypeEnum.CONTEXT_FREE)
public TestEipConfig callQueryByXml(TestEipConfig data){
    EipResult<SuperMap> eipResult = testIntegrateService.callQueryReturnXml((TestOpenApiModel)new TestOpenApiModel().setName("cpc").setId(111L));
    if(!eipResult.getSuccess()){
        throw PamirsException.construct(EipExpEnumerate.SYSTEM_ERROR).appendMsg("调用集成接口callQueryReturnXml 失败").errThrow();
    }
    PamirsSession.getMessageHub().error(JsonUtils.toJSONString(eipResult));
    return data;
}

Step7 重启看效果

image.png

Eip注解说明

开放接口注解

Open
├── name 显示名称
├── config 配置类
├── path 路径
├── Advanced 更多配置
│ ├── httpMethod 请求方法,默认:post
│ ├── inOutConverterFun 输入输出转换器函数名称
│ ├── inOutConverterNamespace 输入输出转换器函数命名空间
│ ├── authenticationProcessorFun 认证处理器函数名称
│ ├── authenticationProcessorNamespace 认证处理器函数命名空间
│ ├── serializableFun 序列化函数名称
│ ├── serializableNamespace 序列化函数命名空间
│ ├── deserializationFun 反序列化函数名称
│ └── deserializationNamespace 反序列化函数命名空间

集成接口注解

Integrate
├── name 显示名称
├── config 配置类
├── Advanced 更多配置
│ ├── host 请求域名+端口
│ ├── path 请求路径 以“/”开头
│ ├── schema 请求协议 http或者https
│ └── httpMethod 请求方法,默认post
├── ExceptionProcessor 异常配置
│ ├── exceptionPredictFun 异常判定函数名
│ ├── exceptionPredictNamespace 异常判定函数命名空间
│ ├── errorMsg 异常判定Msg的键值
│ └── errorCode 异常判定errorCode的键值
├── RequestProcessor 请求处理配置
│ ├── finalResultKey 请求的最终结果键值
│ ├── inOutConverterFun 输入输出转换器函数名称
│ ├── inOutConverterNamespace 输入输出转换器函数命名空间
│ ├── paramConverterCallbackFun 参数转换回调函数名称
│ ├── paramConverterCallbackNamespace 参数转换回调函数命名空间
│ ├── authenticationProcessorFun 认证处理器函数名称
│ ├── authenticationProcessorNamespace 认证处理器函数命名空间
│ ├── serializableFun 序列化函数名称
│ ├── serializableNamespace 序列化函数命名空间
│ ├── deserializationFun 反序列化函数名称
│ ├── deserializationNamespace 反序列化函数命名空间
│ └── convertParams 参数转化集合
│ └── ConvertParam 参数转化
│ ├── inParam 输入参数的键值
│ └── outParam输出参数的键值
├── ResponseProcessor 请求处理配置
│ ├── finalResultKey 响应的最终结果键值
│ ├── inOutConverterFun 输入输出转换器函数名称
│ ├── inOutConverterNamespace 输入输出转换器函数命名空间
│ ├── paramConverterCallbackFun 参数转换回调函数名称
│ ├── paramConverterCallbackNamespace 参数转换回调函数命名空间
│ ├── authenticationProcessorFun 认证处理器函数名称
│ ├── authenticationProcessorNamespace 认证处理器函数命名空间
│ ├── serializableFun 序列化函数名称
│ ├── serializableNamespace 序列化函数命名空间
│ ├── deserializationFun 反序列化函数名称
│ ├── deserializationNamespace 反序列化函数命名空间
│ └── convertParams 参数转化集合
│ └── ConvertParam 参数转化
│ ├── inParam 输入参数的键值
│ └── outParam 输出参数的键值

Eip处理函数说明

Eip处理函数 作用 需实现接口 说明
inOutConverter 输入输出转换器函数 IEipInOutConverter inOutConverter 核心处理请求体的构造或响应体的构造
paramConverter 参数转换回调函数 IEipParamConverter 如果自定义paramConverter,会让默认ConvertParam配置的(inParam,outParam)转化失效了,需要自行处理
authenticationProcessor 认证处理器函数 IEipAuthenticationProcessor<SuperMap>
serializable 序列化函数 IEipSerializable<SuperMap> 序列化方法将任意对象转换为上下文承载对象
deserialization 反序列化函数 IEipDeserialization<SuperMap> 目前这个意义不大,因为如果设置了finalResultKey,就会不走反序列化。现在都是通过inoutconverter处理的。
exceptionProcessor 异常判定函数 IEipExceptionPredict<SuperMap>

paramConverter的特殊转换

paramConverter的特殊转换
URL_QUERY_PARAMS_KEY IEipContext.URL_QUERY_PARAMS_KEY.xxx是请求参数,会拼接在url的问号后面 convertParams={@Integrate.ConvertParam(inParam="id",outParam=IEipContext.URL_QUERY_PARAMS_KEY+".id") }
HEADER_PARAMS_KEY IEipContext.HEADER_PARAMS_KEY.xxx是http请求头参数 convertParams={@Integrate.ConvertParam(inParam="id",outParam=IEipContext.HEADER_PARAMS_KEY+".id") }

调用流程

请求调用流程

6.2 集成平台(改)

响应调用流程

6.2 集成平台(改)

Oinone社区 作者:史, 昂原创文章,如若转载,请注明出处:https://doc.oinone.top/oio4/9326.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
史, 昂的头像史, 昂数式管理员
上一篇 2024年5月23日 am8:12
下一篇 2024年5月23日 am8:14

相关推荐

  • 3.3.4 模型的继承

    在我们的很多项目中,客户都是有个性化需求的,就像我们不能找到两件一模一样的东西,何况是企业的经营与管理思路,多少都会有差异。常规的方式只能去修改标准产品的逻辑来适配客户的需求。导致后续标品维护非常困难。而在介绍完这节以后是不是让你更加清晰认知到我们2.4.2【oinone独特性之每一个需求都可以是一个模块】一文中所表达的特性带来的好处呢? 一、继承方式 继承方式可以分为五种: 抽象基类ABSTRACT,只保存不希望为每个子模型重复键入的信息的模型,抽象基类模型不生成数据表存储数据,只供其他模型继承模型可继承域使用,抽象基类可以继承抽象基类。 扩展继承EXTENDS,子模型与父模型的数据表相同,子模型继承父模型的字段与函数。存储模型之间的继承默认为扩展继承。 多表继承MULTI_TABLE,父模型不变,子模型获得父模型的可继承域生成新的模型;父子模型不同表,子模型会建立与父模型的一对一关联关系字段(而不是交叉表),使用主键关联,同时子模型会通过一对一关联关系引用父模型的所有字段。多表继承父模型需要使用@Model.MultiTable来标识,子模型需要使用@Model.MultiTableInherited来标识。 代理继承PROXY,为原始模型创建代理,可以增删改查代理模型的实体数据,就像使用原始(非代理)模型一样。不同之处在于代理继承并不关注更改字段,可以更改代理中的元信息、函数和动作,而无需更改原始内容。一个代理模型必须仅能继承一个非抽象模型类。一个代理模型可以继承任意数量的没有定义任何模型字段的抽象模型类。一个代理模型也可以继承任意数量继承相同父类的代理模型。 临时继承TRANSIENT,将父模型作为传输模型使用,并可以添加传输字段。 二、继承约束 通用约束 对于扩展继承,查询的时候,父模型只能查询到父模型字段的数据,子模型可以查询出父模型及子模型的字段数据(因为派生关系所以子模型复刻了一份父模型的字段到子模型中)。 系统不会为抽象基类创建实际的数据库表,它们也没有默认的数据管理器,不能被实例化也无法直接保存,它们就是用来被继承的。抽象基类完全就是用来保存子模型们共有的内容部分,达到重用的目的。当它们被继承时,它们的字段会全部复制到子模型中。 系统不支持非jar包依赖模型的继承。 多表继承具有阻断效应,子模型无法继承多表继承父模型的存储父模型的字段,需要使用@Model.Advanced注解的inherited属性显示声明继承父模型的父模型。但是可以继承多表继承父模型的抽象父模型的字段。 可以使用@Model.Advanced的unInheritedFields和unInheritedFunctions属性设置不从父类继承的字段和函数。 跨模块继承约束 如果模型间的继承是跨模块继承,应该与模型所属模块建立依赖关系;如果模块间有互斥关系,则不允许建立模块依赖关系,同理模型间也不允许存在继承关系。 跨模块代理继承,对代理模型的非inJvm函数调用将使用远程调用方式;跨模块扩展(同表)继承将使用本地调用方式,如果是数据管理器函数,将直连数据源。 模型类型与继承约束 抽象模型可继承:抽象模型(Abstract) 临时模型可继承:抽象模型(Abstract)、传输模型(Transient) 存储模型可继承:抽象模型(Abstract)、存储模型(Store)、存储模型(多表,Multi-table Store),不可继承多个Store或Multi-table Store 多表存储模型(父)可继承:同扩展继承 多表存储模型(子)在继承单个Multi-table Store后可继承:抽象模型(Abstract)、存储模型(Store),不可继承多个Store 代理模型可继承: 抽象模型(Abstract),须搭配继承Store、Multi-table Store或Proxy 存储模型(Store),不可继承多个Store或Multi-table Store 存储模型(多表,Multi-table Store),不可继承多个Store或Multi-table Store 代理模型(Proxy),可继承多个Proxy,但多个父Proxy须继承自同一个Store或Multi-table Store,且不能再继承其他Store或Multi-table Store 同名字段以模型自身字段为有效配置,若模型自身不存在该字段,继承字段以第一个加载的字段为有效配置,所以在多重继承的情况下,未避免继承同名父模型字段的不确定性,在自身模型配置同名字段来确定生效配置。 三、继承的使用场景 模型的继承可以继承父模型的元信息、字段、数据管理器和函数 抽象基类 解决公用字段问题 扩展继承 解决开放封闭原则、跨模块扩展等问题 多表继承 解决多型派生类字段差异问题和前端多存储模型组合外观问题 代理继承 解决同一模型在不同场景下的多态问题(一表多态) 临时继承 解决使用现有模型进行数据传输问题 举例,前端多存储模型组合外观问题可通过多表继承的子模型,并一对一关联到关联模型,同时使用排除继承字段去掉不需要继承的字段。子模型通过默认模型管理器提供查询功能给前端,默认查询会查询子模型数据列表并在列表行内根据一对一关系查出关联模型数据合并,关联模型数据展现形态在行内是平铺还是折叠,在详情是分组还是选项卡可以自定义view进行配置 扩展继承 父子同表,模型在所有场景都有一致化的表现,意味着原模型被扩展成了新模型,父子模型的表名一致,模型编码不同,可覆盖父模型的模型管理器、数据排序规则、函数 多表继承 父子多表,父子间有隐式一对一关系,即父子模型都增加了一对一关联关系字段,同时父模型的字段被引用到子模型,且引用字段为只读字段,意味着子模型不可以直接更改父模型的字段值,子模型不继承父模型的模型管理器、数据排序规则、函数,子模型拥有自己的默认模型管理器、数据排序规则、函数。多表继承具有阻断效应,子模型无法自动多表继承父模型的存储父模型,需要显式声明多表继承父模型的存储父模型。 代理继承 代理模型继承并可覆盖父模型的模型管理器、数据排序规则、函数,同时可以使用排除继承字段和函数来达到不同场景不同视觉交互的效果。 图3-3-4-1 继承的使用场景 四、抽象基类(举例) 参考前文中3.3.2【模型的类型】一文中关于抽象模型的介绍 五、多表继承(举例) 场景设计如下 图3-3-4-2 多表继承设计场景 Step1 新建宠物品种、宠狗品种和萌猫品种模型 新建宠物品种模型,用@Model.MultiTable(typeField = "kind"),申明为可多表继承父类,typeField指定为kind字段 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.base.IdModel; @Model.MultiTable(typeField = "kind") @Model.model(PetType.MODEL_MODEL) @Model(displayName="品种",labelFields = {"name"}) public class PetType extends IdModel { public static final String MODEL_MODEL="demo.PetType"; @Field(displayName = "品种名") private String name; @Field(displayName = "宠物分类") private String kind; } 图3-3-4-3 多表继承示例代码 新建宠狗品种模型,用@Model.MultiTableInherited(type = PetDogType.KIND_DOG),申明以多表继承模式继承PetType,覆盖kind字段(用defaultValue设置默认值,用invisible = true设置为前端不展示),更多模块元数据以及模型字段元数据配置详见4.1.6【模型之元数据详解】一文 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; @Model.MultiTableInherited(type = PetDogType.KIND_DOG) @Model.model(PetDogType.MODEL_MODEL) @Model(displayName="宠狗品种",labelFields = {"name"}) public class PetDogType extends PetType { public static final String MODEL_MODEL="demo.PetDogType"; public static final String KIND_DOG="DOG"; @Field(displayName = "宠物分类",defaultValue = PetDogType.KIND_DOG,invisible = true) private String kind; } 图3-3-4-4 多表继承示例代码 新建萌猫品种模型,用@Model.MultiTableInherited(type = PetCatType.KIND_CAT),申明以多表继承模式继承PetType,覆盖kind字段(用defaultValue设置默认值,用invisible = true设置为前端不展示),并新增一个CatShapeEnum枚举类型的字段shape package pro.shushi.pamirs.demo.api.enumeration; import pro.shushi.pamirs.meta.annotation.Dict; import pro.shushi.pamirs.meta.common.enmu.BaseEnum; @Dict(dictionary = CatShapeEnum.DICTIONARY,displayName = "萌猫体型") public class CatShapeEnum extends BaseEnum<CatShapeEnum,Integer>…

    2024年5月23日
    1.0K00
  • 7.1 设计器总览

    设计器转为非专业研发设计,在Oinone3.0版本中已经完成元数据完整在线化,真正做到低无一体。对于设计器的定位我们开篇就介绍过,它是LCDP的产品化呈现,是冰山露在外面大家看得到的,核心还是在LCDP本身。我们先目睹下设计器的一些产品页面,如您有想体验,可以在Oinone官网注册 模型设计器 Oinone以模型为驱动,当有模型、数据字典、数据编码等设计功能,我们就可以完整地定义产品数据模型,模型设计器整体呈现区别于普通ER图,以当前模型为核心视角展开,可以点击关联模型切换主视角。这样的好处在于突出当前设计,聚焦设计本身。同时模型上预留了几个核心入口如:分类管理、继承拓扑图、页面设计、逻辑设计等。另外我们在体验上区分了专家模式和经典模式,顾名思义,专家模式的功能会更加丰富,对专业知识的要求也会更高。专家模式下一般会增加一些跟业务无关的配置如:索引设置等调优行为 逻辑设计器 从图灵完备的角度上说,要支持功能越完备,使用越复杂。我们优先从图灵完备的角度出发,所以我们第一版逻辑设计器相对比较复杂,第二版本规划中会类似模型设计器推出专家版和经典版。 界面设计器 界面设计器第一版会先支撑后端页面在线自定义,后边将陆续推出前端页面、多端能力。为了支持多端和2C页面的设计,我们对前后端协议做了比较大的改造。目前设计器已经支持完全基于V3的前后端协议。 数据可视化 数据可视化支持从内部系统模型获取数据内容后,根据业务需求自定义图表,目的是为企业提供更高效的数据分析工具。 与市场同类产品相比,我们的数据可视化产品:不需要前置维护数据源、进行数据转换;可智取业务系统模型,系统自动解析选择的模型、接口、表格中的字段后进行数据分析;降低对数据分析人员研发能力要求的同时,也提升了数据分析的效率。 流程设计器 Oinone流程设计器为业务流程和审批流程提供了可自动执行的流程模型:通过定义流转过程中的各个动作、规则,以此实现流程自动化。在Oinone流程设计器中,流程可以跨应用设计,不同应用的模型之间可以通过同一流程执行。

    2024年5月23日
    82600
  • 4.1.18 框架之网关协议-Variables变量

    我们在应用开发过程有一种特殊情况在后端逻辑编写的时候需要知道请求的发起入口,平台利用GQL协议中的Variables属性来传递信息,本文就介绍如何获取。 一、前端附带额外变量 属性名 类型 说明 scene String 菜单入口 表4-1-18-1 前端附带额外变量 图4-1-18-1 variables信息中的scene 二、后端如何接收variables信息 通过PamirsSession.getRequestVariables()可以得到PamirsRequestVariables对象。 三、第一个variable(举例) Step1 修改PetTalentAction,获取得到前端传递的Variables package pro.shushi.pamirs.demo.core.action; ……类引用 @Model.model(PetTalent.MODEL_MODEL) @Component public class PetTalentAction { ……其他代码 @Function.Advanced(type= FunctionTypeEnum.QUERY) @Function.fun(FunctionConstants.queryPage) @Function(openLevel = {FunctionOpenEnum.API}) public Pagination<PetTalent> queryPage(Pagination<PetTalent> page, IWrapper<PetTalent> queryWrapper){ String scene = (String)PamirsSession.getRequestVariables().getVariables().get("scene"); System.out.println("scene: "+ scene); ……其他代码 } ……其他代码 } 图4-1-18-2 修改PetTalentAction Step2 重启验证 点击宠物达人不同菜单入口,查看效果 图4-1-18-3 示例效果(一) 图4-1-18-4 示例效果(二)

    2024年5月23日
    70000
  • 3.4.3.3 SPI机制-扩展点

    扩展点结合拦截器的设计,oinone可以点、线、面一体化管理Function 扩展点用于扩展函数逻辑。扩展点类似于SPI机制(Service Provider Interface),是一种服务发现机制。这一机制为函数逻辑的扩展提供了可能。 一、构建第一个扩展点 自定义扩展点(举例) 在我们日常开发中,随着对业务理解的深入,往往还在一些逻辑中会预留扩展点,以便日后应对不同需求时可以灵活替换某一小块逻辑。 在3.3.4【模型的继承】一文中的PetCatItemQueryService,是独立新增函数只作公共逻辑单元。现在我们给它的实现类增加一个扩展点。在PetCatItemQueryServiceImpl的queryPage方法中原本会先查询PetCatType列表,我们这里假设这个逻辑随着业务发展未来会发生变化,我们可以预先预留【查询萌猫类型扩展点】 Step1 新增扩展点定义PetCatItemQueryCatTypeExtpoint 扩展点命名空间:在接口上用@Ext声明扩展点命名空间。会优先在本类查找@Ext,若为空则往接口向上做遍历查找,返回第一个查找到的@Ext.value注解值,使用该值再获取函数的命名空间;如果未找到,则返回扩展点全限定类名。所以我们这里扩展点命名空间为:pro.shushi.pamirs.demo.api.extpoint.PetCatItemQueryCatTypeExtpoint 扩展点技术名称:先取@ExtPoint.name,若为空则取扩展点接口方法名。所以我们这里技术名为queryCatType package pro.shushi.pamirs.demo.api.extpoint; import pro.shushi.pamirs.demo.api.model.PetCatType; import pro.shushi.pamirs.meta.annotation.Ext; import pro.shushi.pamirs.meta.annotation.ExtPoint; import java.util.List; @Ext public interface PetCatItemQueryCatTypeExtpoint { @ExtPoint(displayName = "查询萌猫类型扩展点") List<PetCatType> queryCatType(); } 图3-4-3-11 新增扩展点定义PetCatItemQueryCatTypeExtpoint Step2 修改PetCatItemQueryServiceImpl(用Ext.run模式调用) 修改queryPage,增加扩展点的使用代码。扩展点的使用有两种方式 方法一,使用命名空间和扩展点名称调用Ext.run(namespace, fun, 参数); 方法二,使用函数式接口调用Ext.run(函数式接口, 参数); 我们这里用了第二种方式 用PetCatItemQueryCatTypeExtpoint的全限定类名作为扩展点的命名空间(namespace) 用queryCatType的方法名作为扩展点的技术名称(name) 根据namespace+name去找到匹配扩展点实现,并根据规则是否匹配,以及优先级唯一确定一个扩展点实现去执行逻辑 package pro.shushi.pamirs.demo.core.service; ……省略依赖包 @Model.model(PetCatItem.MODEL_MODEL) @Component public class PetCatItemAction extends DataStatusBehavior<PetCatItem> { @Override protected PetCatItem fetchData(PetCatItem data) { return data.queryById(); } @Action(displayName = "启用") public PetCatItem dataStatusEnable(PetCatItem data){ data = super.dataStatusEnable(data); data.updateById(); return data; } @Function.Advanced(displayName = "查询模型数据的默认过滤条件", type = FunctionTypeEnum.QUERY, managed = true) @Function(openLevel = {LOCAL}) public String queryFilters() { StringBuilder sqlWhereCondition = new StringBuilder(); // List<PetCatType> typeList = new PetCatType().queryList(); List<PetCatType> typeList = Ext.run(PetCatItemQueryCatTypeExtpoint::queryCatType, new Object[]{}); if(!CollectionUtils.isEmpty(typeList)){ // sqlWhereCondition.append("type_id"); sqlWhereCondition.append(PStringUtils.fieldName2Column(LambdaUtil.fetchFieldName(PetCatItem::getTypeId))); sqlWhereCondition.append(StringUtils.SPACE).append(SqlConstants.IN).append(CharacterConstants.LEFT_BRACKET); for(PetCatType petCatType: typeList){ sqlWhereCondition.append(petCatType.getId()).append(CharacterConstants.SEPARATOR_COMMA); } sqlWhereCondition.deleteCharAt(sqlWhereCondition.lastIndexOf(CharacterConstants.SEPARATOR_COMMA)); sqlWhereCondition.append(StringUtils.SPACE).append(CharacterConstants.RIGHT_BRACKET); } return sqlWhereCondition.toString(); } ……省略其他函数 } 图3-4-3-12 修改PetCatItemQueryServiceImpl Step3 新增扩展点实现PetCatItemQueryCatTypeExtpointOne 扩展点命名空间要与扩展点定义一致,用@Ext(PetCatItemQueryCatTypeExtpoint.class) @ExtPoint.Implement声明这是在@Ext声明的命名空间下,且技术名为queryCatType的扩展点实现 package pro.shushi.pamirs.demo.core.extpoint; import pro.shushi.pamirs.demo.api.extpoint.PetCatItemQueryCatTypeExtpoint; import pro.shushi.pamirs.demo.api.model.PetCatType; import pro.shushi.pamirs.meta.annotation.Ext; import pro.shushi.pamirs.meta.annotation.ExtPoint; import pro.shushi.pamirs.meta.api.session.PamirsSession; import java.util.List; @Ext(PetCatItemQueryCatTypeExtpoint.class) public class PetCatItemQueryCatTypeExtpointOne implements PetCatItemQueryCatTypeExtpoint { @Override @ExtPoint.Implement(displayName = "查询萌猫类型扩展点的默认实现") public List<PetCatType> queryCatType() { PamirsSession.getMessageHub().info("走的是第一个扩展点"); List<PetCatType> typeList = new PetCatType().queryList(); return typeList; } } 图3-4-3-13 新增扩展点实现PetCatItemQueryCatTypeExtpointOne Step4…

    2024年5月23日
    98700
  • 2.3 Oinone独特性之源,元数据与设计原则

    让我们来揭开Oinone元数据的神秘面纱,了解它的核心组成、获取方式、面向对象特性以及带来的好处。您或许会想,这些特性能否解决企业数字化转型中互联网架构遇到的挑战呢? 元数据是本文多次提到的重要概念。作为LCDP的基础,元数据支持企业所有研发范式。它数字化描述了软件本身,包括数据、行为和视图等方面。在描述数据时,元数据本身就是数据的数据;在描述行为时,它就是行为的数据;在描述视图时,它就是视图的数据。只有深入理解元数据,才能全面了解Oinone的其他特性。 本章节将介绍元数据的整体概览(如下图2-3所示),带领您了解其核心组成、面向对象特性以及组织方式。请注意,本章节将不会详细展开元数据的细节,这些细节将在后续的相关章程中深入介绍。 图2-3 元数据整体视图 一:以下是元数据的核心组成介绍: 模块(Module):它是将程序划分成若干个子功能,每个模块完成了一个子功能,再把这些模块总起来组成一个整体。它是按业务领域划分和管理的最小单元,是一组功能、界面的集合。 模型(Model):Oinone一切从模型出发,是数据及对行为的载体。它是对所需要描述的实体进行必要的简化,并用适当的变现形式或规则把它的主要特征描述出来所得到的系统模仿品。它包括元信息、字段、数据管理器和自定义函数。同时遵循面向对象设计原则,包括封装、继承和多态。 交互组件(UI Componment):它用菜单、视图和Action来勾绘出模块的前端交互拓扑,并且用组件化的方式统一管理、布局和视图。它用Action来描述所有可操作行为。 函数(Function):它是Oinone可执行逻辑单元,跟模型绑定则对应模型的方法。它描述满足数学领域函数定义,含有三个要素:定义域A、值域C{f(x),x属于A}和对应法则f。其中核心是对应法则f,它是函数关系的本质特征。它满足面向对象原则,可以设置不同开放级别,本地与远程智能切换。 元数据注册表:它以模块为单位的安装记录,在模块安装时,相关的元数据都会在元数据注册表中记录。 二:元数据的产生方式,既可以通过代码注解扫描获取,也可以通过可视化编辑器直接添加。 从代码注解中扫描获取,示例如下代码(如下图2-4所示)。 @Model.model(ResourceBank.MODEL_MODEL) @Model(displayName = "银行",labelFields = "name") public class ResourceBank extends IdModel { public static final String MODEL_MODEL = "resource.ResourceBank"; @Field.String @Field(required = true, displayName = "名称") private String name; @Field.String @Field(required = true, displayName = "银行识别号码", summary = "Bank Identifier Code, BIC 或者 Swift") private String bicCode; …… } 图2-4 从代码注解中扫描获取元数据 可视化的编辑器添加元数据,具体介绍详见7.1《Oinone的设计器》章节 三:Oinone是一种通用低代码开发平台,其元数据设计满足应用开发所需的所有元素,并支持所有研发范式。 它基于元数据的具体实现秉承以下原则: 部署与研发无关; 以模型驱动,符合面向对象设计原则; 代码与数据相互融合,编辑器产生的元数据以面向对象的方式继承扩展标准产品的元数据。 这些原则的集合使整个平台能够实现以下功能特性: 开发分布式应用与单体应用一样简单,部署方式由后期决定。如果要部署为分布式应用,则需要在boot工程中引入Oinone的rpc包。详见4.3《Oinone的分布式体验》一章节; 面向对象的特性使得每个需求都可以是独立模块,独立安装与卸载,让系统像乐高积木一样搭建; 支持两种元数据产生方式,融合的原则确保标准产品迭代与个性化保持独立,真正做到低无一体。 四:这些特性刚好也解决了2.2《互联网架构作为最佳实践为何失效》一章节中客户挑战的三个刺眼问题 互联网架构落地企业数字化转型面临的问题 Oinone应对的策略 不是说敏捷响应吗?为什么改个需求这么慢,不单时间更长,付出的成本也更高了? 特性1、特性2、特性3 不是说能力中心吗?当引入新供应商或有新场景开发的时候,为什么前期做的能力中心不能支撑了? 特性2、特性3 不是说性能好吗?为什么我投入的物理资源更多了? 特性1 表2-2互联网架构落地企业数字化转型面临的问题及Oinone应对策略

    2024年5月23日
    79700

Leave a Reply

登录后才能评论