2026/3/30 20:07:24
网站建设
项目流程
为什么网站百度搜不到了,用flash做的ppt模板下载网站,wordpress 压缩插件,开发网站开发一、为什么要“优雅”#xff1f;产品一句话#xff1a; “凡哥#xff0c;接口明天上线#xff0c;支持 10w 并发#xff0c;数据脱敏#xff0c;不能丢单#xff0c;不能重复#xff0c;还要安全。”优雅不是装#xff0c;是为了让自己少加班、少背锅、少掉发。今天…一、为什么要“优雅”产品一句话 “凡哥接口明天上线支持 10w 并发数据脱敏不能丢单不能重复还要安全。”优雅不是装是为了让自己少加班、少背锅、少掉发。今天晓凡就把压箱底的东西掏出来手把手带你撸一套能扛生产的模板。为方便阅读晓凡以Java代码为例给出“核心代码 使用姿势”全部亲测可直接使用。二、项目骨架Spring Boot 3.xdemo-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(MapString, String map, String secret) {// 1. 参数名升序排列MapString, String tree new TreeMap(map);// 2. 拼成 kvkvString join tree.entrySet().stream().map(e - e.getKey() e.getValue()).collect(Collectors.joining());// 3. 最后拼密钥String raw join key secret;// 4. SHA256return DigestUtils.sha256Hex(raw).toUpperCase();}/** 验签直接比对即可 */public static boolean verify(MapString, String map, String secret, String requestSign) {return sign(map, secret).equals(requestSign);}}拦截器统一验签Componentpublic class SignInterceptor implements HandlerInterceptor {Value(${sign.secret})private String secret;Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {// 只拦截接口if (!(handler instanceof HandlerMethod)) return true;MapString, 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拦截器Componentpublic class WhiteListInterceptor implements HandlerInterceptor {Value(#{${white.ips}.split(,)})private ListString allowList;Overridepublic 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 注解版尤其对外提供的接口无法保障调用频率应该做限流处理保障接口服务正常的提供服务依赖dependencygroupIdcom.alibaba.csp/groupIdartifactIdsentinel-spring-boot-starter/artifactIdversion1.8.6/version/dependency配置spring:application:name: demo-apisentinel:transport:dashboard: localhost:8080使用姿势GetMapping(/order/{id})SentinelResource(value getOrder,blockHandler getOrderBlock)public ResultOrderVO getOrder(PathVariable Long id) {return Result.success(orderService.get(id));}// 限流兜底public ResultOrderVO getOrderBlock(Long id, BlockException e) {return Result.fail(访问太频繁稍后再试);}七、 参数校验JSR303 分组即使前端做了非空规范性校验服务端参数校验任然是必不可少的DTOpublic class OrderCreateDTO {NotNull(message 用户 ID 不能为空)private Long userId;NotEmpty(message 商品列表不能为空)Size(max 20, message 一次最多买 20 件)private ListItem items;ValidNotNullprivate PayInfo payInfo;Datapublic static class PayInfo {Min(value 1, message 金额必须大于 0)private Integer amount;}}分组接口public interface Create {}ControllerPostMapping(/order)public ResultLong create(RequestBody Validated(Create.class) OrderCreateDTO dto) {Long orderId orderService.create(dto);return Result.success(orderId);}八、 统一返回值提供统一的返回结果不应该返回值五花八门DataAllArgsConstructorNoArgsConstructorpublic class ResultT implements Serializable {private int code;private String msg;private T data;public static T ResultT success(T data) {return new Result(200, success, data);}public static T ResultT fail(String msg) {return new Result(500, msg, null);}/** 返回 200 但提示业务失败 */public static T ResultT bizFail(int code, String msg) {return new Result(code, msg, null);}}九、 统一异常处理系统报错信息需要提供友好的提示避免暴露出SQL异常的信息给调用方和客户端。RestControllerAdvicepublic class GlobalExceptionHandler {private static final Logger log LoggerFactory.getLogger(GlobalExceptionHandler.class);/** 业务异常 */ExceptionHandler(BizException.class)public ResultVoid handle(BizException e) {log.warn(业务异常{}, e.getMessage());return Result.bizFail(e.getCode(), e.getMessage());}/** 参数校验失败 */ExceptionHandler(MethodArgumentNotValidException.class)public ResultVoid handleValid(MethodArgumentNotValidException e) {String msg e.getBindingResult().getFieldErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(,));return Result.fail(msg);}/** 兜底 */ExceptionHandler(Exception.class)public ResultVoid handleAll(Exception e) {log.error(系统异常, e);return Result.fail(服务器开小差);}}十、 请求日志切面 注解记录请求的入参日志和返回日志出问题时方便快速定位。也给运维人员提供了方便注解Target(ElementType.METHOD)Retention(RetentionPolicy.RUNTIME)public interface ApiLog {}切面AspectComponentpublic 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;}}用法ApiLogPostMapping(/order)public ResultLong create(...) {}十一、幂等设计Token 分布式锁双保险对于一些涉及到数据一致性的接口一定要做好幂等设计以防数据出现重复问题思路下单前先申请一个幂等 Token存在 Redis5 分钟失效。下单时带着 Token后端用 Lua 脚本“判断存在并删除”原子性保证只能用一次。对并发极高场景再补一层分布式锁Redisson。代码Servicepublic class IdempotentService {Resourceprivate 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));}}ControllerGetMapping(/token)public ResultString getToken() {return Result.success(idempotentService.createToken());}PostMapping(/order)ApiLogpublic ResultLong 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 分页插件Configurationpublic class MybatisConfig {Beanpublic MybatisPlusInterceptor interceptor() {MybatisPlusInterceptor i new MybatisPlusInterceptor();i.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return i;}}Servicepublic PageOrderVO list(OrderListDTO dto) {// 前端不传默认 10 条最多 200long size Math.min(dto.getPageSize(), 200);PageOrder page new Page(dto.getPageNo(), size);LambdaQueryWrapperOrder w Wrappers.lambdaQuery();if (StrUtil.isNotBlank(dto.getUserName())) {w.like(Order::getUserName, dto.getUserName());}PageOrder po orderMapper.selectPage(page, w);return po.convert(o - BeanUtil.copyProperties(o, OrderVO.class));}十三、 压测JMeter 自带脚本上线前务必要对API接口进行压力测试知道各个接口的qps情况。以便我们能够更好的预估需要部署多少服务节点对于API接口的稳定性至关重要。起服务java -jar -Xms1g -Xmx1g demo-api.jarJMeter 线程组500 线程、Ramp-up 10s、循环 20。观测Sentinel 控制台看 QPS、RTtop -H 看 CPUarthas 火焰图找慢方法调优限流阈值 压测 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 JsonSerializerString {Overridepublic void serialize(String value, JsonGenerator g, SerializerProvider p)throws IOException {if (StrUtil.isBlank(value)) {g.writeString(value);return;}g.writeString(DesensitizeUtil.desPhone(value));}}十六、完整的接口文档Knife4j提供在线接口文档既方便开发调试接口也方便运维人员排查错误依赖dependencygroupIdcom.github.xiaoymin/groupIdartifactIdknife4j-openapi3-spring-boot-starter/artifactIdversion4.1.0/version/dependency配置knife4j:enable: truesetting:language: zh_cn启动后访问http://localhost:8080/doc.html支持在线调试、导出 PDF、Word。十七、小结接口开发就像炒菜签名、加密是“食材保鲜”限流、幂等是“火候掌控”日志、文档是“摆盘拍照”每道工序做到位才能端到桌上“色香味”俱全。上面 13 段核心代码直接粘过去就能跑跑通后再按业务微调基本能扛 90% 的生产场景。祝你在领导问起接口怎么样了的时候可以淡淡来一句