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

大家好,我是晓凡。

一、为什么要"优雅"?

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

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

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

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

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

arduino 复制代码
demo-api
├── src/main/java/com/example/demo
│   ├── config          // 配置:限流、加解密、日志等
│   ├── annotation      // 自定义注解(幂等、日志、脱敏)
│   ├── aspect          // 切面统一干活
│   ├── interceptor     // 拦截器(签名、白名单)
│   ├── common          // 统一返回、异常、常量
│   ├── controller      // 对外暴露
│   ├── service
│   └── DemoApplication.java
└── pom.xml

三、 签名(防篡改)

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

思路

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

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

工具类

java 复制代码
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);
    }
}

拦截器统一验签

java 复制代码
@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 工具

java 复制代码
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白名单,一般在网关层处理

配置

yaml 复制代码
white:
  ips: 127.0.0.1,10.0.0.0/8,192.168.0.0/16

拦截器

java 复制代码
@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 注解版)

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

依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-boot-starter</artifactId>
    <version>1.8.6</version>
</dependency>

配置

yaml 复制代码
spring:
  application:
    name: demo-api
sentinel:
  transport:
    dashboard: localhost:8080

使用姿势

java 复制代码
@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

java 复制代码
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;
    }
}

分组接口

java 复制代码
public interface Create {}

Controller

java 复制代码
@PostMapping("/order")
public Result<Long> create(@RequestBody @Validated(Create.class) OrderCreateDTO dto) {
    Long orderId = orderService.create(dto);
    return Result.success(orderId);
}

八、 统一返回值

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

java 复制代码
@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异常的信息给调用方和客户端。

java 复制代码
@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("服务器开小差");
    }
}

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

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

注解

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLog {}

切面

java 复制代码
@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;
    }
}

用法

java 复制代码
@ApiLog
@PostMapping("/order")
public Result<Long> create(...) {}

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

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

思路

  1. 下单前先申请一个幂等 Token(存在 Redis,5 分钟失效)。
  2. 下单时带着 Token,后端用 Lua 脚本"判断存在并删除",原子性保证只能用一次。
  3. 对并发极高场景,再补一层分布式锁(Redisson)。

代码

java 复制代码
@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

java 复制代码
@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 分页插件

java 复制代码
@Configuration
public class MybatisConfig {
    @Bean
    public MybatisPlusInterceptor interceptor() {
        MybatisPlusInterceptor i = new MybatisPlusInterceptor();
        i.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return i;
    }
}

Service

java 复制代码
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接口的稳定性至关重要。

  1. 起服务:
    java -jar -Xms1g -Xmx1g demo-api.jar

  2. JMeter 线程组:

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

  3. 观测:

    • Sentinel 控制台看 QPS、RT
    • top -H 看 CPU
    • arthas 火焰图找慢方法
  4. 调优:

    • 限流阈值 = 压测 80% 最高水位
    • 发现慢 SQL 加索引
    • 热点数据加本地缓存(Caffeine)

十四、异步处理

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

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

java 复制代码
@Async("asyncExecutor")   // 自定义线程池
public void sendSmsAsync(Long userId, String content) {
    smsService.send(userId, content);
}

十五、数据脱敏

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

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

java 复制代码
@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)

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

依赖

xml 复制代码
<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
    <version>4.1.0</version>
</dependency>

配置

yaml 复制代码
knife4j:
  enable: true
  setting:
    language: zh_cn

启动后访问
http://localhost:8080/doc.html

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

十七、小结

接口开发就像炒菜:

  • 签名、加密是"食材保鲜"
  • 限流、幂等是"火候掌控"
  • 日志、文档是"摆盘拍照"

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

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

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

"接口已经准备好了,压测报告发群里了。"

相关推荐
无限进步_几秒前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神1 分钟前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe4 分钟前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿5 分钟前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记16 分钟前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson17 分钟前
CAS的底层实现
java
九英里路28 分钟前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串
YDS82932 分钟前
大营销平台 —— 抽奖前置规则过滤
java·spring boot·ddd
仍然.37 分钟前
多线程---CAS,JUC组件和线程安全的集合类
java·开发语言
不懂的浪漫42 分钟前
mqtt-plus 架构解析(五):错误处理与 ErrorAction 聚合策略
java·spring boot·后端·物联网·mqtt·架构