Java 中如何优雅的实现对外接口,需要注意哪些事项?

news2024/10/7 20:36:56

博主之前做过恒丰银行代收付系统(相当于支付接口),包括现在的oltpapi交易接口和虚拟业务的对外提供数据接口。总之,当你做了很多项目写了很多代码的时候,就需要回过头来,多总结总结,这样你会看到更多之前写代码的时候看不到的东西,也能更明白为什么要这样做。

做接口需要考虑的问题

什么是接口

接口无非就是客户端请求你的接口地址,并传入一堆该接口定义好的参数,通过接口自身的逻辑处理,返回接口约定好的数据以及相应的数据格式。

接口怎么开发

接口由于本身的性质,由于和合作方对接数据,所以有以下几点需要在开发的时候注意:

1、定义接口入参:写好接口文档

2、定义接口返回数据类型:一般都需要封装成一定格式,确定返回json还是xml报文等

见如下返回数据定义格式:

package com.caiex.vb.model;

import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Result", propOrder = { "resultCode", "resultMsg" })
public class Result implements Serializable {
private static final long serialVersionUID = 10L;
protected int resultCode;
protected String resultMsg;

public int getResultCode() {
return this.resultCode;
}

public void setResultCode(int value) {
this.resultCode = value;
}

public String getResultMsg() {
return this.resultMsg;
}

public void setResultMsg(String value) {
this.resultMsg = value;
}
}

package com.caiex.vb.model;

import java.io.Serializable;

public class Response implements Serializable {

private static final long serialVersionUID = 2360867989280235575L;

private Result result;

private Object data;

public Result getResult() {
if (this.result == null) {
this.result = new Result();
}
return result;
}

public void setResult(Result result) {
this.result = result;
}

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

}

3、确定访问接口的方式,get or post等等,可以根据restful接口定义规则RESTful API。

4、定义一套全局统一并通用的返回码,以帮助排查问题;

public static int NO_AGENT_RATE = 1119;  //未找到兑换率

public static int SCHEME_COMMIT_FAIL = 4000;  //方案提交失败

public static int SCHEME_CONFIRMATION = 4001;  //方案确认中

public static int SCHEME_NOT_EXIST = 4002;  //方案不存在

public static int SCHEME_CANCEL= 4005;  //方案不存在

//。。。。

5、统一的异常处理:应该每个系统都需要一套统一的异常处理

package com.caiex.vb.interceptor;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.caiex.vb.model.Response;

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

private  Logger  logger = LoggerFactory.getLogger(this.getClass()); 

/**
     * 所有异常报错
     * @param request
     * @param exception
     * @return
     * @throws Exception
     */
@ExceptionHandler(value=Exception.class)  
public Response allExceptionHandler(HttpServletRequest request,  
Exception exception) throws Exception  
{  
    	logger.error("拦截到异常:", exception);
Response response = new Response();
        response.setData(null);
        response.getResult().setResultCode(9999);
        response.getResult().setResultMsg("系统繁忙");
return response;  
}  

}

6、拦截器链设置

合作方访问接口的时候,会根据你接口定义好的传参访问你的接口服务器,但是会存在接口参数类型错误或者格式不对,必传参数没传的问题,甚至一些恶意请求,都可以通过拦截器链进行前期拦截,避免造成接口服务的压力。还有很重要的一点,加签验签也可以在拦截器设置。继承WebMvcConfigurerAdapter实现springboot的拦截器链。实现HandlerInterceptor方法编写业务拦截器。

package com.caiex.vb.interceptor;


import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.alibaba.fastjson.JSON;
import com.caiex.redis.service.api.RedisApi;
import com.caiex.vb.model.Response;
import com.caiex.vb.utils.CaiexCheckUtils;

@Component
public class SignInterceptor extends BaseValidator implements HandlerInterceptor{

private Logger logger = LogManager.getLogger(this.getClass());

@Resource
private RedisApi redisApi;


public void afterCompletion(HttpServletRequest arg0,
HttpServletResponse arg1, Object arg2, Exception arg3)
throws Exception {
// TODO Auto-generated method stub

}

public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1,
Object arg2, ModelAndView arg3) throws Exception {
// TODO Auto-generated method stub

}

public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1,
Object arg2) throws Exception {
if(isTestIpAddr(arg0)){
return true;
}
String securityKey = redisApi.hGet("securityKey", arg0.getParameter("agentid"));
if(StringUtils.isEmpty(securityKey)){
Response response = new Response();
			response.setData(null);
			response.getResult().setResultCode(8001);
			response.getResult().setResultMsg("缺少私钥, 渠道号:" + arg0.getParameter("agentid"));
			logger.error("缺少私钥, 渠道号:" + arg0.getParameter("agentid"));
InterceptorResp.printJson(arg1, response);
return false;
}

if(StringUtils.isEmpty(arg0.getParameter("sign")) || !arg0.getParameter("sign").equals(CaiexCheckUtils.getSign(arg0.getParameterMap(), securityKey))){
Response response = new Response();
			response.setData(null);
			response.getResult().setResultCode(3203);
			response.getResult().setResultMsg("参数签名认证失败");
			logger.error("参数签名认证失败:" + JSON.toJSONString(arg0.getParameterMap()) + " securityKey = " + securityKey);
InterceptorResp.printJson(arg1, response);
return false;
}else{
return true;
}

}

}

package com.caiex.oltp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.caiex.oltp.interceptor.APILimitRateValidator;
import com.caiex.oltp.interceptor.CommonValidator;
import com.caiex.oltp.interceptor.DDSAuthValidator;
import com.caiex.oltp.interceptor.QueryPriceParamsValidator;
import com.caiex.oltp.interceptor.TradeParamsValidator;


@EnableWebMvc
@Configuration
@ComponentScan
public class WebAppConfigurer extends WebMvcConfigurerAdapter {

@Bean
CommonValidator commonInterceptor() {
return new CommonValidator();
}

@Bean
DDSAuthValidator ddsAuthInterceptor() {
return new DDSAuthValidator();
}

@Bean
QueryPriceParamsValidator queryPriceParamsInterceptor() {
return new QueryPriceParamsValidator();
}

@Bean
TradeParamsValidator tradeParamsInterceptor() {
return new TradeParamsValidator();
}

@Bean
APILimitRateValidator aPILimitRateInterceptor() {
return new APILimitRateValidator();
}


@Override
public void addInterceptors(InterceptorRegistry registry) {

//访问速率限制
	    	registry.addInterceptor(aPILimitRateInterceptor())
.addPathPatterns("/*/*");
//.addPathPatterns("/price/getPriceParam");

//参数签名认证
	        registry.addInterceptor(ddsAuthInterceptor())
.addPathPatterns("/tradeState/*")
.addPathPatterns("/recycle/*")
.addPathPatterns("/matchInfo/*")
.addPathPatterns("/price/tradeTicketParam");

//公共参数检查
	        registry.addInterceptor(commonInterceptor())
.addPathPatterns("/price/tradeTicketParam")
.addPathPatterns("/tradeState/*")
.addPathPatterns("/recycle/*");

//询价参数校验
	        registry.addInterceptor(queryPriceParamsInterceptor())
.addPathPatterns("/price/getPriceParam");

//交易参数检查
	        registry.addInterceptor(tradeParamsInterceptor())
.addPathPatterns("/price/tradeTicketParam");

super.addInterceptors(registry);
}
}

7、token令牌和sign数字签名实现数据保密性。

创建令牌(Token)

为保证请求的合法性,我们提供第三方创建令牌接口,某些接口需要通过token验证消息的合法性,以免遭受非法攻击。

token过期时间目前暂时定为1天,由于考虑到合作方往往是分布式环境,多台机器都有可能申请token,为了降低合作方保证token一致性的难度,调用接口创建token成功以后一分钟以内,再次请求token返回的数据是一样的。

获取私钥

获取用于数字签名的私钥,第三方获取的私钥需妥善保存,并定期更新,私钥只参与数字签名,不作为参数传输。

数字签名方式:

参数签名;签名方式:所有值不为null的参数(不包括本参数)均参与数字签名,按照“参数名+参数值+私钥”的格式得到一个字符串,再将这个字符串MD5一次就是这个参数的值。(示例:h15adc39y9ba59abbe56e057e60f883g),所以需要先获取私钥。

验签方式:

将用户的所有非null参数放入定义好排序规则的TreeSet中进行排序,再用StringBuilder按照按照“参数名+参数值+私钥”的格式得到一个字符串(私钥从redis拿),再将这个字符串MD5一次就是这个参数的值。将这个值与用户传来的sign签名对比,相同则通过,否则不通过。

private String createToken(){
String utk = "Msk!D*"+System.currentTimeMillis()+"UBR&FLP";
	logger.info("create token   --- "+Md5Util.md5(utk));
return Md5Util.md5(utk);
}

8、接口限流

有时候服务器压力真的太大,以防交易接口被挤死,就可以对一些其他不影响主要业务功能并且计算量大的接口做限流处理。RateLimit--使用guava来做接口限流,当接口超过指定的流量时,就不处理该接口的请求。详细可看RateLimit。也可参考其他限流框架。

9、协议加密,http升级成https;

为什么要升级呢,为了保证数据的安全性。当使用https访问时,数据从客户端到服务断,服务端到客户端都加密,即使黑客抓包也看不到传输内容。当然还有其他好处,这里不多讲。但这也是开发接口项目需要注意的一个问题。

如何提高接口的高并发和高可用

接口开发好了,接下来就讨论接口的可用性问题。首先我们要将高并发和高可用区分一下,毕竟高可用是在可用的情况,只是很慢或者效率不高。其实也可以归为一类问题,但是不重要啦,重要的是怎么提高你写的接口的访问速度和性能。

1、接口的高并发解决方案(其实没有唯一答案,业界针对不同业务也有很多不同的方法)

当访问一个接口获取数据时,发现返回很慢,或者总是超时,如果排除网络的原因,那就是接口服务器压力太大,处理不过来了。在世界杯期间,我们查看后台日志总是connection by reset和borker pipe和一些超时问题。这时候,你可能遇到了高并发和高可用问题。但是,不管遇到什么问题,都不能臆断和乱改,你得需要找到慢的原因,才能对症下药,乱改可能会导致其他问题的出现。首先,解决高并发问题的三个方向是负载均衡,缓存和集群。

1)负载均衡

我们使用的是阿里云服务器的负载均衡,后台分布式服务管理,我们运维小哥哥搭建了一套k8s,可以自由在k8s上扩展服务节点,各个服务结点也能随内存的使用自动漂移,不用多说,k8s真的很厉害,感兴趣的同学可以详细去学。那么问题来了,阿里云的负载均衡怎么对应到k8s的负载均衡呢?这个涉及到了k8s的service暴露的一些特点,简单说就是k8s把所有集群的服务都通过指定的内部负载均衡,在指定的服务器上暴露,然后我们又把这几个服务器接在阿里云负载均衡下,这个涉及的细节和配置很多。当然,除nginx外,还有其他负载均衡解决方案,软件硬件都有,硬件如f5等。

阿里云的nginx负载均衡,我们使用的是加权轮询策略,其实轮询是最低效的方式;

这就是最基本的负载均衡实例,但这不足以满足实际需求;目前Nginx服务器的upstream模块支持6种方式的分配:

负载均衡策略

2)集群

首先,通过排查问题,发现是oltpapi接口服务处理请求很慢,大量请求过来,总是超时和中断连接,这时候,我们想着最简单的方法就是加机器,给oltp接口服务多加几台机器。嗯,一切都很完美,如预期进行,但是加到一定数量,你发现,怎么不起效果,异步响应还是很慢,或者更直观的说,消息队列出现了严重的消息堆积。这时候,你发现出现了新的问题或者瓶颈,这个问题已经不是说加oltp服务器能解决了,那么,就需要去重新定位问题。发现是消息堆积,消息堆积就是生产者过快,导致消费者消费不过来,这时候,你就需要增加消费者的消费数量。给风控系统多加几台机器,让消费者和生产者达到一定平衡。这里有个误区,你可能以为是rocketmq的broker数量过少,增加broker数量,其实当消费者和生产者保持一样的速度时,消息肯定不对堆积,按照原始的broker数量就足够。但是增加broker也会使得消息得到尽快的处理,提升一定效率。

3)缓存

当加机器不能解决问题时,或者说没那么多服务器可使用时,那么就要重代码层面解决高并发问题。Redis 是一个高性能的key-value数据库,当获取数据从数据库拿很慢时,就可以存储到redis,从redis取值。

1、用ConcurrentHashMap缓存对象,并设置过期时间
2、redis缓存数据,结合spring定时任务定时获取不会经常改动的key
3、提高使用redis的效率:比如使用mGet一次获取多个key
4、…等

2、接口高可用问题

高可用问题应该上升到整个服务的架构问题上,就是说在搭建整体系统是就应该考虑到。高可用问题是以单点故障,访问速度慢的问题为主导。见 服务高可用

1、redis主从分布式(redis的单点故障和访问速度的提高和主从备份)
2、分布式dubbo服务的zookeeper主从集群
3、strom的主从集群
4、…等

总结

下面对接口开发服务做一些总结:

1、是拉还是推:

当接口作为数据源时,还要考虑数据是让合作方主动过来拉还是数据有变化就推送呢,当然是推的效果更好,但是如何有效的推数据,不推重复数据等都是需要根据实际业务考虑的问题。

2、多台分布式服务器上,怎么保证交易的幂等和订单的唯一性

当接口服务和合作方都处于分布式情况下,就很容易出现一个订单号申请多次交易请求,但是根据幂等性,一张彩票只能交易一次,并且每次不管何时请求,结果都应该一样不会改变。这种情况下,我们怎么保证唯一性呢,我们需要把该订单和订单状态存redis,每次请求时去看是否订单已存在。但可能这次交易不成功,下次这张票还可以继续交易,可以生成新的订单号啊。redis的setNX是一个很好的解决方案,意思是当存在该key时,返回false,当没有时,该key和value插入成功。用作检查订单是否正在提交,如果是,则阻塞本次请求,避免重复提交 ,可以设置过期时间3s。提交之前锁定订单,防止重复提交。

3、处理时间超过10s,自动返回该订单交易失败

总之,博主发现,在高并发场景下,导致服务崩溃的原因还是redis和数据库,可能是redis读写太慢,或者数据库的一些sql使用不当,或者没建索引导致读写很慢。总之,这是一条很漫长的路,我们都需要慢慢积累经验和学习前人更优秀的解决办法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/859338.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

react中使用路由起手式,一些思路和细节。

一.安装并配置 我们选择使用react-router实现路由效果 yarn add react-router-dom下载后需要对Route进行引入,是个内置的组件。该组件是有两个属性一个是path,一个是component,path是组件对应的路由,component是对应的组件 二.…

pytest 用例运行方式

一、命令行方式运行 执行某个目录下所有的用例,符合规范的所有用例 进入到对应的目录,直接执行pytest; 例如需要执行testcases 下的所有用例; 可以进入testcases 目录; 然后执行pytest 进入对应目录的上级目录,执行pytest 目录名称/ ; ; 例如需要执行testcases 下…

kafka 分布式的情况下,如何保证消息的顺序消费?

目录 一、什么是分布式 二、kafka介绍 三、消息的顺序消费 四、如何保证消息的顺序消费 一、什么是分布式 分布式是指将计算任务分散到多个计算节点上进行并行处理的一种计算模型。在分布式系统中,多台计算机通过网络互联,共同协作完成任务。每个计…

LinearAlgebraMIT_8_TheRankOfMatrix

这节课中主要讲解根据秩来判断方程组/矩阵的(solvability)解情况,即通过秩来判断(aumented matrix)增广矩阵的解。我们需要直接求解方程组的解就是求解矩阵的解。 x.1 判断(非齐次线性方程组)Axb是否有解 我们以下面这个方程组为例,它具有3个约束条件和…

innodb buffer pool

buffer pool是主存中的一个区域,InnoDB 在访问时缓存表和索引数据。缓冲池允许直接从内存访问频繁使用的数据,这加快了处理速度。在专用服务器上,高达80% 的物理内存通常分配给缓冲池。为了提高大容量读取操作的效率,将缓冲池划分…

fetch异步上传图片(附html+JavaScript+php代码)

效果 index.html <!DOCTYPE html> <html><head><title>图片上传示例</title><meta charset"utf-8"><script src"upload.js"></script><style>*{padding: 0;margin: 0;}#app{width: 500px;margin: …

什么是媒体代发布?媒体代发布注意事项

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 媒体代发布是指将新闻稿或其他宣传内容委托给专业的媒体代理机构或公司进行发布和推广的活动。这些机构通常拥有丰富的媒体资源、人脉和经验&#xff0c;能够更好地将信息传递给目标受众…

MySQL5.7保姆级安装教程

环境 Linux版本Mysql版本(待安装)CentOS 75.7 1、配置YUM源 在MySQL官网中下载YUM源rpm安装包&#xff1a;http://dev.mysql.com/downloads/repo/yum/ 目前MySQL官网下载的MySQL源安装后yum下载的MySQL是8.0版本&#xff0c;为了非必要的麻烦&#xff0c;直接提供MySQL5.7的…

MNIST数据集知识合集

MNIST数据集知识合集 认识MNIST数据集通过本地文件加载MNIST数据集torchvision.datasets加载MNIST数据集可视化&#xff08;即转换成.jpg/.png之类的文件&#xff09;疑惑—datasets.mnist和datasets.MNIST问题—downloadFalse运行报错 搭建CNN用于数字识别 认识MNIST数据集 M…

MODBUS TCP转CCLINK IE协议网关cclink通讯异常的处理方法

你是否曾经遇到过需要将不同的设备连接到一个统一的网络中&#xff1f;或者你是否曾经遇到过设备之间的通讯协议不兼容的问题&#xff1f;捷米的JM-CCLKIE-TCP通讯网关就是为解决这些问题而设计的。 JM-CCLKIE-TCP通讯网关是一款自主研发的CCLINK IE FIELD BASIC从站功能的通讯…

Appium2 安装教程

目录 第一步&#xff1a;安装 node.js 第二步&#xff1a;安装 Android Studio 第三步&#xff1a;下载 JDK 第四步&#xff1a;安装 Appium 卸载旧版本appium1.X 安装新版Appium2.0 安装appium驱动与插件 第五步&#xff1a;安装appium inspector 第六步&#xff1a;…

VSCode-Python传参数进行Debug

新建demo.py import argparse def parse_args():description "debug example" parser argparse.ArgumentParser(descriptiondescription) help "The path of address"parser.add_argument(--host,help help) parser.add_ar…

2023-08-05力扣今日五题-好题

链接&#xff1a; 剑指 Offer 52. 两个链表的第一个公共节点 题意&#xff1a; 如题 解&#xff1a; 非常有趣的双指针 首先我们不管他们是否有公共段啊&#xff0c;我们要知道一个指针从A出发走到A结尾&#xff0c;再从B出发走到B结尾&#xff0c;和从B出发最终到A结尾是…

小程序逆向之源码获取

背景&#xff1a;小程序使用越来越多&#xff0c;很多时候&#xff0c;我们工作中需要用到对小程序的研究&#xff0c;那么就出现了一个课题&#xff0c;小程序如何逆向&#xff0c;如何获取源码&#xff0c;今天这篇文章就来讲一下如何获取源码&#xff08;pc端&#xff09;。…

Amplifier--Transistor amplify introduce

#1, 功率放大器分类&#xff1a;甲类&#xff0c;乙类&#xff0c;甲乙类 #2&#xff0c; 甲类放大器&#xff1a; 当静态工作点Q设在负载线线段的中点&#xff0c; 在整个信号周期内都有电流Ic通过时&#xff0c;称为甲类放大器&#xff1b; #3&#xff0c; 乙类放大器&…

年至年的选择仿elementui的样式

组件&#xff1a;<!--* Author: liuyu liuyuxizhengtech.com* Date: 2023-02-01 16:57:27* LastEditors: wangping wangpingxizhengtech.com* LastEditTime: 2023-06-30 17:25:14* Description: 时间选择年 - 年 --> <template><div class"yearPicker"…

轻松批量自定义重命名,为文件夹加上个性化编号!

在日常生活和工作中&#xff0c;我们经常需要对大量的文件夹进行重命名&#xff0c;以更好地管理和整理文件。然而&#xff0c;手动逐个修改文件夹名称费时费力&#xff0c;效率低下。现在&#xff0c;我们为您推荐一款简单易用的工具&#xff0c;可以帮助您快速批量自定义重命…

Unity-UGUI优化策略

界面出栈规则&#xff1a; 界面目录导航、策划界面回退需求造成界面套娃问题&#xff0c;夹带一系列层级问题&#xff0c;应该和策划进行友好沟通&#xff0c;避免界面不合理的出栈入栈规则 overdraw&#xff1a; 尽量减少同屏 半透明物体渲染 Unity 之 UGUI优化&#xff08;…

C++入门--string类的实现

目录 1.string类常用函数实现&#xff08;1&#xff09;string类成员变量定义&#xff08;2&#xff09; string类默认构造函数实现&#xff08;3&#xff09; string类拷贝构造函数实现&#xff08;4&#xff09;string类析构函数&#xff08;5&#xff09;string类c_str()函数…

linux umask:文件访问权限控制预设值

1. umask 定义 在 linux 系统中&#xff0c;umask 被定义在 /etc/profile 配置文件中&#xff0c;有一段 shell 脚本对 umask 是这么定义的。在 shell 会话输入命令&#xff1a; $ cat /etc/profile # 查看 /etc/profile 配置文件的内容 if [ $UID -gt 199 ] &&…