news 2026/1/12 4:17:33

接口开发,咱得整得“优雅”点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
接口开发,咱得整得“优雅”点

一、为什么要“优雅”?

产品一句话: “凡哥,接口明天上线,支持 10w 并发,数据脱敏,不能丢单,不能重复,还要安全。”

优雅不是装,是为了让自己少加班、少背锅、少掉发。

今天晓凡就把压箱底的东西掏出来,手把手带你撸一套能扛生产的模板。

为方便阅读,晓凡以Java代码为例给出“核心代码 + 使用姿势”,全部亲测可直接使用。

二、项目骨架(Spring Boot 3.x)

demo-api

├── src/main/java/com/example/demo

│ ├── config // 配置:限流、加解密、日志等

│ ├── annotation // 自定义注解(幂等、日志、脱敏)

│ ├── aspect // 切面统一干活

│ ├── interceptor // 拦截器(签名、白名单)

│ ├── common // 统一返回、异常、常量

│ ├── controller // 对外暴露

│ ├── service

│ └── DemoApplication.java

└── pom.xml

三、 签名(防篡改)

对外提供的接口要做签名认证,认证不通过的请求不允许访问接口、提供服务

思路

“时间戳 + 随机串 + 业务参数”排好序,最后 APP_SECRET 拼后面,SHA256 一下。

前后端、第三方都统一,拒绝撕逼。

工具类

public class SignUtil {

/**

* 生成签名

* @param map 除 sign 外的所有参数

* @param secret 分配给你的私钥

*/

public static String sign(Map<String, String> map, String secret) {

// 1. 参数名升序排列

Map<String, String> tree = new TreeMap<>(map);

// 2. 拼成 k=v&k=v

String join = tree.entrySet().stream()

.map(e -> e.getKey() + "=" + e.getValue())

.collect(Collectors.joining("&"));

// 3. 最后拼密钥

String raw = join + "&key=" + secret;

// 4. SHA256

return DigestUtils.sha256Hex(raw).toUpperCase();

}

/** 验签:直接比对即可 */

public static boolean verify(Map<String, String> map, String secret, String requestSign) {

return sign(map, secret).equals(requestSign);

}

}

拦截器统一验签

@Component

public class SignInterceptor implements HandlerInterceptor {

@Value("${sign.secret}")

private String secret;

@Override

public boolean preHandle(HttpServletRequest request,

HttpServletResponse response,

Object handler) throws Exception {

// 只拦截接口

if (!(handler instanceof HandlerMethod)) return true;

Map<String, String> params = Maps.newHashMap();

request.getParameterMap().forEach((k, v) -> params.put(k, v[0]));

String sign = params.remove("sign"); // 签名不参与计算

if (!SignUtil.verify(params, secret, sign)) {

throw new BizException("签名错误");

}

return true;

}

}

四、 加密(防泄露)

敏感数据在网络传输过程中都应该加密处理

思路

AES对称加密,密钥放配置中心,支持一键开关。

只对敏感字段加密,别一上来全包加密,排查日志想打人。

AES 工具

public class AesUtil {

private static final String ALG = "AES/CBC/PKCS5Padding";

// 16 位

private static final String KEY = "1234567890abcdef";

private static final String IV = "abcdef1234567890";

public static String encrypt(String src) {

try {

Cipher cipher = Cipher.getInstance(ALG);

SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");

IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());

cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);

return Base64.getEncoder().encodeToString(cipher.doFinal(src.getBytes()));

} catch (Exception e) {

throw new RuntimeException("加密失败", e);

}

}

public static String decrypt(String src) {

try {

Cipher cipher = Cipher.getInstance(ALG);

SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");

IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());

cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

return new String(cipher.doFinal(Base64.getDecoder().decode(src)));

} catch (Exception e) {

throw new RuntimeException("解密失败", e);

}

}

}

五、 IP 白名单

限制请求的IP,增加IP白名单,一般在网关层处理

配置

white:

ips: 127.0.0.1,10.0.0.0/8,192.168.0.0/16

拦截器

@Component

public class WhiteListInterceptor implements HandlerInterceptor {

@Value("#{'${white.ips}'.split(',')}")

private List<String> allowList;

@Override

public boolean preHandle(HttpServletRequest request,

HttpServletResponse response,

Object handler) throws Exception {

String ip = IpUtil.getIp(request);

boolean ok = allowList.stream()

.anyMatch(rule -> IpUtil.match(ip, rule));

if (!ok) throw new BizException("IP 不允许访问");

return true;

}

}

六、 限流(Sentinel 注解版)

尤其对外提供的接口,无法保障调用频率,应该做限流处理,保障接口服务正常的提供服务

依赖

<dependency>

<groupId>com.alibaba.csp</groupId>

<artifactId>sentinel-spring-boot-starter</artifactId>

<version>1.8.6</version>

</dependency>

配置

spring:

application:

name: demo-api

sentinel:

transport:

dashboard: localhost:8080

使用姿势

@GetMapping("/order/{id}")

@SentinelResource(value = "getOrder",

blockHandler = "getOrderBlock")

public Result<OrderVO> getOrder(@PathVariable Long id) {

return Result.success(orderService.get(id));

}

// 限流兜底

public Result<OrderVO> getOrderBlock(Long id, BlockException e) {

return Result.fail("访问太频繁,稍后再试");

}

七、 参数校验(JSR303 + 分组)

即使前端做了非空,规范性校验,服务端参数校验任然是必不可少的

DTO

public class OrderCreateDTO {

@NotNull(message = "用户 ID 不能为空")

private Long userId;

@NotEmpty(message = "商品列表不能为空")

@Size(max = 20, message = "一次最多买 20 件")

private List<Item> items;

@Valid

@NotNull

private PayInfo payInfo;

@Data

public static class PayInfo {

@Min(value = 1, message = "金额必须大于 0")

private Integer amount;

}

}

分组接口

public interface Create {}

Controller

@PostMapping("/order")

public Result<Long> create(@RequestBody @Validated(Create.class) OrderCreateDTO dto) {

Long orderId = orderService.create(dto);

return Result.success(orderId);

}

八、 统一返回值

提供统一的返回结果,不应该返回值五花八门

@Data

@AllArgsConstructor

@NoArgsConstructor

public class Result<T> implements Serializable {

private int code;

private String msg;

private T data;

public static <T> Result<T> success(T data) {

return new Result<>(200, "success", data);

}

public static <T> Result<T> fail(String msg) {

return new Result<>(500, msg, null);

}

/** 返回 200 但提示业务失败 */

public static <T> Result<T> bizFail(int code, String msg) {

return new Result<>(code, msg, null);

}

}

九、 统一异常处理

系统报错信息需要提供友好的提示,避免暴露出SQL异常的信息给调用方和客户端。

@RestControllerAdvice

public class GlobalExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

/** 业务异常 */

@ExceptionHandler(BizException.class)

public Result<Void> handle(BizException e) {

log.warn("业务异常:{}", e.getMessage());

return Result.bizFail(e.getCode(), e.getMessage());

}

/** 参数校验失败 */

@ExceptionHandler(MethodArgumentNotValidException.class)

public Result<Void> handleValid(MethodArgumentNotValidException e) {

String msg = e.getBindingResult()

.getFieldErrors()

.stream()

.map(DefaultMessageSourceResolvable::getDefaultMessage)

.collect(Collectors.joining(","));

return Result.fail(msg);

}

/** 兜底 */

@ExceptionHandler(Exception.class)

public Result<Void> handleAll(Exception e) {

log.error("系统异常", e);

return Result.fail("服务器开小差");

}

}

十、 请求日志(切面 + 注解)

记录请求的入参日志和返回日志,出问题时方便快速定位。也给运维人员提供了方便

注解

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface ApiLog {}

切面

@Aspect

@Component

public class LogAspect {

private static final Logger log = LoggerFactory.getLogger("api.log");

@Around("@annotation(apiLog)")

public Object around(ProceedingJoinPoint p, ApiLog apiLog) throws Throwable {

long start = System.currentTimeMillis();

ServletRequestAttributes attr =

(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest req = attr.getRequest();

String uri = req.getRequestURI();

String params = JSON.toJSONString(p.getArgs());

Object result;

try {

result = p.proceed();

} catch (Exception e) {

log.error("【{}】params={} error={}", uri, params, e.getMessage());

throw e;

} finally {

long cost = System.currentTimeMillis() - start;

log.info("【{}】params={} cost={}ms", uri, params, cost);

}

return result;

}

}

用法

@ApiLog

@PostMapping("/order")

public Result<Long> create(...) {}

十一、幂等设计(Token & 分布式锁双保险)

对于一些涉及到数据一致性的接口一定要做好幂等设计,以防数据出现重复问题

思路

下单前先申请一个幂等 Token(存在 Redis,5 分钟失效)。

下单时带着 Token,后端用 Lua 脚本“判断存在并删除”,原子性保证只能用一次。

对并发极高场景,再补一层分布式锁(Redisson)。

代码

@Service

public class IdempotentService {

@Resource

private StringRedisTemplate redis;

/** 申请 Token */

public String createToken() {

String token = UUID.fastUUID().toString();

redis.opsForValue().set("token:" + token, "1",

Duration.ofMinutes(5));

return token;

}

/** 验证并删除 */

public boolean checkToken(String token) {

String key = "token:" + token;

// 原子删除成功才算用过

return Boolean.TRUE.equals(redis.delete(key));

}

}

Controller

@GetMapping("/token")

public Result<String> getToken() {

return Result.success(idempotentService.createToken());

}

@PostMapping("/order")

@ApiLog

public Result<Long> create(@RequestBody @Valid OrderCreateDTO dto,

@RequestHeader("Idempotent-Token") String token) {

if (!idempotentService.checkToken(token)) {

throw new BizException("请勿重复提交");

}

Long orderId = orderService.create(dto);

return Result.success(orderId);

}

十二、限制记录条数(分页 + SQL 保护)

对于批量数据接口,一定要限制返回的记录条数,不让会造成恶意攻击导致服务器宕机。

MyBatis-Plus 分页插件

@Configuration

public class MybatisConfig {

@Bean

public MybatisPlusInterceptor interceptor() {

MybatisPlusInterceptor i = new MybatisPlusInterceptor();

i.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

return i;

}

}

Service

public Page<OrderVO> list(OrderListDTO dto) {

// 前端不传默认 10 条,最多 200

long size = Math.min(dto.getPageSize(), 200);

Page<Order> page = new Page<>(dto.getPageNo(), size);

LambdaQueryWrapper<Order> w = Wrappers.lambdaQuery();

if (StrUtil.isNotBlank(dto.getUserName())) {

w.like(Order::getUserName, dto.getUserName());

}

Page<Order> po = orderMapper.selectPage(page, w);

return po.convert(o -> BeanUtil.copyProperties(o, OrderVO.class));

}

十三、 压测(JMeter + 自带脚本)

上线前,务必要对API接口进行压力测试,知道各个接口的qps情况。以便我们能够更好的预估,需要部署多少服务节点,对于API接口的稳定性至关重要。

起服务:

java -jar -Xms1g -Xmx1g demo-api.jar

JMeter 线程组:

500 线程、Ramp-up 10s、循环 20。

观测:

Sentinel 控制台看 QPS、RT

top -H 看 CPU

arthas 火焰图找慢方法

调优:

限流阈值 = 压测 80% 最高水位

发现慢 SQL 加索引

热点数据加本地缓存(Caffeine)

十四、异步处理

如果同步处理业务,耗时会非常长。这种情况下,为了提升API接口性能,我们可以改为异步处理

下单成功后,发 MQ 异步发短信/扣库存,接口 RT 直接降一半。

@Async("asyncExecutor") // 自定义线程池

public void sendSmsAsync(Long userId, String content) {

smsService.send(userId, content);

}

十五、数据脱敏

业务中对与用户的敏感数据,如密码等需要进行脱敏处理

返回前统一用 Jackson 序列化过滤器,字段加注解就行,代码零侵入。

@JsonSerialize(using = SensitiveSerializer.class)

@Target(ElementType.FIELD)

@Retention(RetentionPolicy.RUNTIME)

public @interface Sensitive {

SensitiveType type();

}

public enum SensitiveType {

PHONE, ID_CARD, BANK_CARD

}

public class SensitiveSerializer extends JsonSerializer<String> {

@Override

public void serialize(String value, JsonGenerator g, SerializerProvider p)

throws IOException {

if (StrUtil.isBlank(value)) {

g.writeString(value);

return;

}

g.writeString(DesensitizeUtil.desPhone(value));

}

}

十六、完整的接口文档(Knife4j)

提供在线接口文档,既方便开发调试接口,也方便运维人员排查错误

依赖

<dependency>

<groupId>com.github.xiaoymin</groupId>

<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>

<version>4.1.0</version>

</dependency>

配置

knife4j:

enable: true

setting:

language: zh_cn

启动后访问

http://localhost:8080/doc.html

支持在线调试、导出 PDF、Word。

十七、小结

接口开发就像炒菜:

签名、加密是“食材保鲜”

限流、幂等是“火候掌控”

日志、文档是“摆盘拍照”

每道工序做到位,才能端到桌上“色香味”俱全。

上面 13 段核心代码,直接粘过去就能跑,跑通后再按业务微调,基本能扛 90% 的生产场景。

祝你在领导问起接口怎么样了?的时候,可以淡淡来一句:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/7 3:11:08

32、深入解读 GNU 通用公共许可证

深入解读 GNU 通用公共许可证 1. Linux 与 GNU 通用公共许可证 Linux 遵循 GNU 通用公共许可证(GPL 或 copyleft),这有助于澄清 Linux 版权状态的一些混淆。Linux 既不是共享软件,也不属于公共领域。自 1993 年起,大部分 Linux 内核由 Linus Torvalds 持有版权,内核的其…

作者头像 李华
网站建设 2026/1/6 23:45:28

边缘智能新突破:LFM2-350M-ENJP-MT重塑英日实时翻译体验

边缘智能新突破&#xff1a;LFM2-350M-ENJP-MT重塑英日实时翻译体验 【免费下载链接】LFM2-350M-ENJP-MT 项目地址: https://ai.gitcode.com/hf_mirrors/LiquidAI/LFM2-350M-ENJP-MT 在全球数字化浪潮中&#xff0c;跨语言沟通已成为智能设备与企业服务的核心竞争力。L…

作者头像 李华
网站建设 2026/1/11 2:27:20

蓝易云 - CentOS7 Nacos设置开机自动重启

下面给你一套在 CentOS 7&#xff08;systemd&#xff09; 上把 Nacos 做成“开机自启 异常自动重启”的企业级落地方案&#xff08;稳、可控、可审计&#xff09;。&#x1f680;1&#xff09;前置检查&#xff08;避免“服务能起但马上挂”&#xff09;java -version作用&am…

作者头像 李华
网站建设 2026/1/9 17:12:17

[模板]st表 RMQ区间最值问题

【模板】静态区间最值_牛客题霸_牛客网 st表基于倍增的思想实现 最大值最小值思路一样 这里以最大值讲解 一个序列的子区间的个数显然有n*n个 根据倍增思想 我们首先在这个规模为n*n的状态空间中选择一些2的整数次幂的位置作为代表值 设f[i][j]表示数列中子区间[i][i2^j-…

作者头像 李华
网站建设 2026/1/9 19:01:49

Matlab COCO API终极指南:从数据处理到模型评估

Matlab COCO API终极指南&#xff1a;从数据处理到模型评估 【免费下载链接】cocoapi COCO API - Dataset http://cocodataset.org/ 项目地址: https://gitcode.com/gh_mirrors/co/cocoapi 还在为计算机视觉项目中的复杂标注数据而头疼吗&#xff1f;Matlab COCO API为…

作者头像 李华
网站建设 2026/1/10 11:19:10

14、网络PF配置的日志、监控、统计与优化

网络PF配置的日志、监控、统计与优化 日志设置与处理 设置 syslogd 处理数据步骤如下: 1. 选择日志工具( log facility )、日志级别( log level )和操作( action )。 2. 将结果行添加到 /etc/syslog.conf 文件。例如,若已设置 loghost.example.com 接收…

作者头像 李华