飞道的博客

项目实战之——开放接口简单封装

355人阅读  评论(0)

项目由来

最近项目重构过程中、因为业务需要将一个项目拆分成两个项目,这两项目中有业务交互。所以需要通过开放接口实现交互,所以参考了一些相关行业的系统做了简单的封装所以有了这次的开放接口简单封装

设计目标

简单来说就是一个工具集

  • 简化工作量
  • 可插拔式的
  • 快速集成

项目结构

example-open-base

  • 主要用为公用的对象及一些基础工具

example-open-api

  • 对服务提供方实现的一些简化功能

example-open-sdk

  • 对外开放的快速集成工具集

开放文档工具

  1. 这里使用的是showdoc

常用工具

这里通过接口将相关的基础工具抽象出来作为一个服务提供出来给业务进行复用。这些工具都提供了默认的实现,就是说可以使用默认的实现也可以自己重新实现进行复盖。

aes 数据加密算法,抽象与默认实现

package org.nm.south.open.base.api.service;

public interface AesEncryptionProvider {
    /**
     * 加密
     *
     * @param encData
     * @param openApiProperties
     * @return
     */
    String encrypt(String encData, OpenApiProperties openApiProperties);

    /**
     * 解密
     *
     * @param decData
     * @param openApiProperties
     * @return
     */
    String decrypt(String decData, OpenApiProperties openApiProperties);
}

sgin 签名算法,抽象与默认实现

package org.nm.south.open.base.api.service;

/**
 * sign 签名加密方式提供者
 */
public interface SignEncryptionProvider {

    /**
     * 签名
     *
     * @param content
     * @return
     */
    String sign(String content);


    /**
     * 获取签名加密方式
     *
     * @return
     */
    String getSignMethod();

}

签名服务提供,抽象与默认实现

package org.nm.south.open.base.api.service;

import java.util.Map;

/**
 * 签名服务提供
 */
public interface SignProvider {

    /**
     * 出签
     *
     * @param params               签名参数
     * @param openApiSignConfigure 签名keygen接口
     * @return
     */
    String outSign(Map<String, Object> params, OpenApiSignConfigure openApiSignConfigure);

    /**
     * 验签
     *
     * @param sign                 原签名
     * @param params               签名参数
     * @param openApiSignConfigure 签名keygen接口
     * @return
     */
    boolean checkSign(String sign, Map<String, Object> params, OpenApiSignConfigure openApiSignConfigure);


    /**
     * 获取签名加密方式
     *
     * @return
     */
    String getSignMethod();
}

数据模型

这里的数据模型是由指服务方接收第三方的请求与下发给第三方的通知;相对于第三方来说也相应在存在接收服务方的请求发服务方的通知

服务方

/***
 * 通知对象
 *
 * @Auther: guzhangwen
 * @BusinessCard https://blog.csdn.net/gu_zhang_w
 * @Date: 2019/7/19
 * @Time: 10:32
 * @version : V1.0
 */
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class SouthOpenNotify extends SouthOpenBase implements Serializable {

    /**
     * 每个通知的唯一id
     */
    private Long id;

    /**
     * API接口名称
     */
    private String methodName;

    /**
     * 商户ID
     */
    private String accountId;

}

/***
 *
 * @Auther: guzhangwen
 * @BusinessCard https://blog.csdn.net/gu_zhang_w
 * @Date: 2019/7/17
 * @Time: 19:17
 * @version : V1.0
 */
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class SouthOpenRequest extends SouthOpenBase implements Serializable {

    /**
     * 每个通知的唯一id
     */
    private Long id;

    /**
     * API接口名称
     */
    private String methodName;

    /**
     * 供应商帐号id
     */
    private String accountId;

    /**
     * 版本号,默认1.00
     */
    private String version;

}

第三方

@Data
public class SouthOpenNotify extends SouthOpenBase {


    /**
     * 每个通知的唯一id
     */
    private Long id;

    /**
     * API接口名称
     */
    private String methodName;

    /**
     * 商户ID
     */
    private String accountId;

    /**
     * 版本号,默认1.00
     */
    private String version;
}
/***
 *
 * @Auther: guzhangwen
 * @BusinessCard https://blog.csdn.net/gu_zhang_w
 * @Date: 2019/7/17
 * @Time: 19:17
 * @version : V1.0
 */
@Data
public class SouthOpenRequest extends SouthOpenBase {

    /**
     * 请求ID
     */
    private Long id;

    /**
     * API接口名称
     */
    private String methodName;

    /**
     * 商户ID
     */
    private String accountId;
}

公用部分


/***
 *
 * @Auther: guzhangwen
 * @BusinessCard https://blog.csdn.net/gu_zhang_w
 * @Date: 2020/4/18
 * @Time: 23:21
 * @version : V1.0
 */
@Data
public class SouthOpenBase {

    /**
     * 时间戳,格式为yyyy-MM-dd HH:mm:ss,时区为GMT+8
     */
    private String timestamp;

    /**
     * 签名
     */
    private String sign;

    /**
     * 签名的摘要算法:hmac,md5
     */
    private String signMethod;

    /**
     * 操作人
     */
    private String operator;

    /**
     * 请求|响应内容密文
     */
    private String body;

}

/***
 *
 * @Auther: guzhangwen
 * @BusinessCard https://blog.csdn.net/gu_zhang_w
 * @Date: 2019/7/17
 * @Time: 19:41
 * @version : V1.0
 */
@Data
public class SouthOpenResponse extends SouthOpenBase {


    /**
     * 响应状态码
     */
    private Integer code;

    /**
     * 响应状态的描述
     */
    private String message;
}

对外提供的接口

最终对外提供的接口,实际上无论是api 还是 sdk 都是使用如下接口,进行数据包装、数据解密、签名、验签以关注到实际的业务中去

api ApiRequestProcessor

package org.nm.south.open.api.service;

import org.nm.south.open.api.bo.SouthOpenNotify;
import org.nm.south.open.api.bo.SouthOpenRequest;
import org.nm.south.open.base.api.service.OpenApiProperties;
import org.nm.south.open.base.bo.SouthOpenResponse;
import org.nm.south.open.exception.ErrorCode;

public interface ApiRequestProcessor {
    /**
     * 获取开放接口配置文件
     *
     * @param accountId
     * @return
     */
    OpenApiProperties getOpenApiProperties(String accountId);

    /**
     * 内容加密
     *
     * @param request
     * @return
     */
    String decrypt(SouthOpenRequest request);

    /**
     * 校验请求
     *
     * @param request
     */
    Boolean verifyRequest(SouthOpenRequest request);

    /**
     * 校验响应请求
     *
     * @param response
     * @param accountId
     * @return
     */
    String verifyResponse(SouthOpenResponse response, String accountId);


    /**
     * 构建响应对象
     *
     * @param state
     * @param content
     * @param operator
     * @param accountId
     * @return
     */
    SouthOpenResponse builderResponse(ErrorCode state, String content, String operator, String accountId);


    /**
     * 构建通知对象
     *
     * @param notifyId
     * @param notifyApi
     * @param content
     * @param operator
     * @param accountId
     * @return
     */
    SouthOpenNotify builderNotify(Long notifyId, NotifyApi notifyApi, String content, String operator, String accountId);
}

sdk ApiRequestProcessor

package com.lvbang.pt.open.sdk.processor;

import com.lvbang.pt.open.sdk.api.NotifyApi;
import com.lvbang.pt.open.sdk.bo.SouthOpenNotify;
import com.lvbang.pt.open.sdk.bo.SouthOpenRequest;
import com.lvbang.pt.open.sdk.exception.ErrorCode;
import lombok.NonNull;
import org.nm.south.open.base.api.service.OpenApiProperties;
import org.nm.south.open.base.bo.SouthOpenResponse;

public interface ApiRequestProcessor {
    /**
     * 获取开放接口配置文件
     *
     * @param accountId
     * @return
     */
    OpenApiProperties getOpenApiProperties(String accountId);

    /**
     * 内容加密
     *
     * @param request
     * @return
     */
    String decrypt(SouthOpenRequest request);

    /**
     * 校验请求
     *
     * @param request
     */
    Boolean verifyRequest(SouthOpenRequest request);

    /**
     * 校验响应请求
     *
     * @param accountId
     * @param response
     * @return
     */
    String verifyResponse(@NonNull String accountId, @NonNull SouthOpenResponse response);


    /**
     * 构建响应对象
     *
     * @param accountId
     * @param state
     * @param content
     * @param operator
     * @return
     */
    SouthOpenResponse builderResponse(@NonNull String accountId, @NonNull ErrorCode state, String content, String operator);


    /**
     * 构建通知对象
     *
     * @param accountId
     * @param notifyApi
     * @param content
     * @param operator
     * @return
     */
    SouthOpenNotify builderNotify(@NonNull String accountId, Long notifyId, @NonNull NotifyApi notifyApi, String content, String operator);
}

常用工具被覆盖与Eanble 注解实现

作为一个实用的工具包,应该具备可插拔、可覆盖、可替换等特性。所以针对常用工具,这里使用到了 @ConditionalOnMissingBean 这个注解的特性是如果指定的bean 不存在时就创建这个bean 到spring 容器吗。针对可插拔的实现,我们可以随时地使这个包的功能开户或者关闭,因为我们都知道spring boot 你需要指定包路径或者在Application 启动类下的所有包都会被加载到容器中,所以我希望使用是可以不用关心这个包内部的实现也路径,所以这里模仿了spring boot 里面的 @Enablexxx

可覆盖、可替换的实现

  • 所有常用工具都不需要打上 @Service 或者 @Component 注解,所有都是通过 OpenApiConfigure 中注册完成,对外开放的只有 OpenApiConfigure ,将spring 扫描到这个类时,会为这个类下的所有Bean 进行注册,另外当遇上 @ConditionalOnMissingBean 注解时,就会判定当前容器上下文中是否存在 @ConditionalOnMissingBean 中指定的bean 类型或者name。如果存在则不初始化,反之就初始化到容器中

  • OpenBaseConfigure

    package org.nm.south.open.base.config;
    
    import org.nm.south.open.base.api.service.AesEncryptionProvider;
    import org.nm.south.open.base.api.service.SignEncryptionProvider;
    import org.nm.south.open.base.api.service.SignProvider;
    import org.nm.south.open.base.api.service.provider.DefaultAesEncryptionProvider;
    import org.nm.south.open.base.api.service.provider.DefaultSignEncryptionProvider;
    import org.nm.south.open.base.api.service.provider.DefaultSignProvider;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /***
     *
     * @Auther: guzhangwen
     * @BusinessCard https://blog.csdn.net/gu_zhang_w
     * @Date: 2020/4/21
     * @Time: 11:20
     * @version : V1.0
     */
    @Configuration
    public class OpenBaseConfigure {
    
    
        /**
         * aes 加密服务提供
         *
         * @return
         */
        @Bean
        @ConditionalOnMissingBean(value = AesEncryptionProvider.class)
        public AesEncryptionProvider aesEncryptionProvider() {
            return new DefaultAesEncryptionProvider();
        }
    
        /**
         * 签名加密服务提供
         *
         * @return
         */
        @Bean
        @ConditionalOnMissingBean(value = SignEncryptionProvider.class)
        public SignEncryptionProvider signEncryptionProcessor() {
            return new DefaultSignEncryptionProvider();
        }
    
        /**
         * 签名服务提供
         *
         * @param signEncryptionProcessor
         * @return
         */
        @Bean
        @ConditionalOnMissingBean(value = SignProvider.class)
        public SignProvider signProvider(SignEncryptionProvider signEncryptionProcessor) {
            return new DefaultSignProvider(signEncryptionProcessor);
        }
    
    }
    
    
  • OpenApiConfigure

    package org.nm.south.open.api.config;
    
    import org.nm.south.open.api.service.ApiRequestProcessor;
    import org.nm.south.open.api.service.provider.DefaultApiRequestProcessor;
    import org.nm.south.open.api.service.provider.DefaultOpenApiProperties;
    import org.nm.south.open.base.api.service.AesEncryptionProvider;
    import org.nm.south.open.base.api.service.OpenApiProperties;
    import org.nm.south.open.base.api.service.SignProvider;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /***
     *
     * @Auther: guzhangwen
     * @BusinessCard https://blog.csdn.net/gu_zhang_w
     * @Date: 2020/4/17
     * @Time: 17:34
     * @version : V1.0
     */
    @Configuration
    public class OpenApiConfigure {
    
        /**
         * 开放接口api配置
         *
         * @return
         */
        @Bean
        @ConditionalOnMissingBean(value = OpenApiProperties.class)
        public OpenApiProperties openApiProperties() {
            return new DefaultOpenApiProperties();
        }
    
        /**
         * opi 请求、响应信息处理器
         *
         * @param openApiProperties
         * @param signProvider
         * @param aesEncryptionProvider
         * @return
         */
        @Bean
        @ConditionalOnMissingBean(value = ApiRequestProcessor.class)
        public ApiRequestProcessor apiRequestProcessor(OpenApiProperties openApiProperties, SignProvider signProvider, AesEncryptionProvider aesEncryptionProvider) {
            return new DefaultApiRequestProcessor(aesEncryptionProvider, signProvider, openApiProperties);
        }
    }
    
    

可插拔的实现

使用的人是不应该需要知道扫描那些包路径的,所以这里我们需要简化这一步。他们只需要使用 @EnableSouthApi 注解就可以获得对应的服务,这里使用到了 ImportBeanDefinitionRegistrar 接口,这里我们可以通过 AnnotationMetadata 获取到 @EnableSouthApi 中注解的packages 属性的值;然后再通过 ClassPathBeanDefinitionScanner 进行包扫描加载bean 到容器中

  • @EnableSouthAp

    package org.nm.south.open.annotations;
    	import org.nm.south.open.api.config.EnableSouthOpenApiConfigureRegistrar;
    	import org.springframework.context.annotation.Import;
    	import java.lang.annotation.*;
    	
    	@Retention(RetentionPolicy.RUNTIME)
    	@Target(ElementType.TYPE)
    	@Documented
    	@Import(EnableSouthOpenApiConfigureRegistrar.class)
    	public @interface EnableSouthOpenApi {
    	    /**
    	     * 需要扫描的包名
    	     *
    	     * @return
    	     */
    	    String[] packages() default {"org.nm.south.open.api", "org.nm.south.open.base"};
    	}
    
  • EnableSouthOpenSdkConfigureRegistrar

    package com.lvbang.pt.open.sdk.config;
    
    import com.lvbang.pt.open.sdk.annotations.EnableSouthOpenSdk;
    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
    import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
    import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
    import org.springframework.core.type.AnnotationMetadata;
    
    /***
     *
     * @Auther: guzhangwen
     * @BusinessCard https://blog.csdn.net/gu_zhang_w
     * @Date: 2020/4/17
     * @Time: 17:34
     * @version : V1.0
     */
    public class EnableSouthOpenSdkConfigureRegistrar implements ImportBeanDefinitionRegistrar {
    
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            //获得注解中的packages 值
            String[] arra = (String[]) importingClassMetadata.getAnnotationAttributes(EnableSouthOpenSdk.class.getName()).get("packages");
            //扫描指定包的所有bean
            ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
            scanner.scan(arra);
        }
    }
    
    

关于使用

  1. 引用依赖
  2. 使用 @EnableSouthOpenApi
  3. 继承 AbstractApiRequestProcessor 抽象类
  4. 实现 OpenApiProperties 接口
  5. 就可以开展具体的业务了

项目地址

  1. https://gitee.com/97wx/example/tree/master/example-open

关于我

  • 关注不迷路,点赞走一波~ 转载请标注~
  • 公众号

转载:https://blog.csdn.net/gu_zhang_w/article/details/105654253
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场