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

2025-12-04 0 912

大家好,我是晓凡

一、为什么要“优雅”?

产品一句话: “凡哥,接口明天上线,支持 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 map, String secret) {
        // 1. 参数名升序排列
        Map 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 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 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 白名单

配置

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 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 getOrder(@PathVariable Long id) {
    return Result.success(orderService.get(id));
}

// 限流兜底
public Result 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 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 create(@RequestBody @Validated(Create.class) OrderCreateDTO dto) {
    Long orderId = orderService.create(dto);
    return Result.success(orderId);
}

八、 统一返回值

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result implements Serializable {
    private int code;
    private String msg;
    private T data;

    public static  Result success(T data) {
        return new Result(200, \"success\", data);
    }

    public static  Result fail(String msg) {
        return new Result(500, msg, null);
    }

    /** 返回 200 但提示业务失败 */
    public static  Result bizFail(int code, String msg) {
        return new Result(code, msg, null);
    }
}

九、 统一异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /** 业务异常 */
    @ExceptionHandler(BizException.class)
    public Result handle(BizException e) {
        log.warn(\"业务异常:{}\", e.getMessage());
        return Result.bizFail(e.getCode(), e.getMessage());
    }

    /** 参数校验失败 */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValid(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(DefaultMessageSourceResolvable::getDefaultMessage)
                .collect(Collectors.joining(\",\"));
        return Result.fail(msg);
    }

    /** 兜底 */
    @ExceptionHandler(Exception.class)
    public Result 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 create(...) {}

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

思路

  1. 下单前先申请一个幂等 Token(存在 Redis,5 分钟失效)。
  2. 下单时带着 Token,后端用 Lua 脚本“判断存在并删除”,原子性保证只能用一次。
  3. 对并发极高场景,再补一层分布式锁(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 getToken() {
    return Result.success(idempotentService.createToken());
}

@PostMapping(\"/order\")
@ApiLog
public Result 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 list(OrderListDTO dto) {
    // 前端不传默认 10 条,最多 200
    long size = Math.min(dto.getPageSize(), 200);
    Page page = new Page(dto.getPageNo(), size);
    LambdaQueryWrapper w = Wrappers.lambdaQuery();
    if (StrUtil.isNotBlank(dto.getUserName())) {
        w.like(Order::getUserName, dto.getUserName());
    }
    Page po = orderMapper.selectPage(page, w);
    return po.convert(o -> BeanUtil.copyProperties(o, OrderVO.class));
}

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

  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)

十四、异步处理

下单成功后,发 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 {
    @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% 的生产场景。
祝你在领导问起接口怎么样了?的时候,可以淡淡来一句:
“接口已经准备好了,压测报告发群里了。”

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 接口开发,咱得整得“优雅”点 https://www.zuozi.net/3593.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务