springboot自定义注解RateLimiter限流注解技术文档详解

本文主要是介绍springboot自定义注解RateLimiter限流注解技术文档详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《springboot自定义注解RateLimiter限流注解技术文档详解》文章介绍了限流技术的概念、作用及实现方式,通过SpringAOP拦截方法、缓存存储计数器,结合注解、枚举、异常类等核心组件,...

什么是限流

限流是一种控制系统访问频率的技术手段,就像高速公路的收费站控制车流量一样。

生活场景类比:

  • 银行ATM机:每张卡每天最多取款5次
  • 手机验证码:每个手机号每分钟最多发送1条
  • 网站登录:每个IP每分钟最多尝试5次

技术价值:

  1. 防止恶意攻击:阻止暴力破解、恶意爬虫
  2. 保护系统稳定:避免瞬间大量请求压垮服务器
  3. 提升用户体验:确保正常用户的访问质量
  4. 节约成本:减少不必要的资源消耗

系统架构

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   用户请求      │───→│   限流切面      │───→│   业务接口      │
│   (HTTP API)   │    │  (AOP拦截)     │    │  (Controller)   │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │   限流服务      │
                    │ (核心逻辑处理)  │
                    └─────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │   缓存存储      │
                    │ (EhCache/Redis) │
                    └─────────────────┘

工作流程:

  1. 用户发起HTTP请求
  2. Spring AOP切面拦截带有@RateLimiter注解的方法
  3. 限流服务根据注解配置生成限流键
  4. 从缓存中获取当前访问次数
  5. 判断是否超过限制,决定放行或拒绝
  6. 更新缓存中的计数器

核心组件详解

1. 限流注解 (@RateLimiter)

这是系统的核心注解,定义了限流的各种参数:

package cn.jbolt.config.anno.rateLimiter;

import org.springframework.core.annotation.AliasFor;
import Java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RateLimiter {

    /**
     * 缓存前缀 - 用于区分不同业务的限流数据
     */
    String prefix() default "jblimit:";

    /**
     * 时间窗口(秒) - 限流的时间范围
     */
    int time() default 60;

    /**
     * 允许访问次数 - 时间窗口内最大访问次数
     */
    @AliasFor(attribute = "count")
    int value() default 12;

    /**
     * 限制类型 - 决定按什么维度限流
     */
    RateLimitType limitType() default RateLimitType.DEFAULT;

    /**
     * 限制提示消息 - 触发限流时返回的错误信息
     */
    String msg() default "操作过于频繁,请稍后重试";

    /**
     * 允许访问次数 - 与value互为别名
     */
    @AliasFor(attribute = "value")
    int count() default 12;

    /**
     * 自定义键 - 当limitType为CUSTOM时使用
     */
    String customKey() default "";

    /**
     * 是否启用 - 可用于动态开关限流功能
     */
    boolean enabled() default true;

    /**
     * 额外的时间窗口限制(秒)
     * 实现双重限流:比如1秒最多1次 + 1分钟最多10次
     */
    int extraTime() default -1;

    /**
     * 额外时间窗口内的允许访问次数
     */
    int extraCount() default -1;

    /**
     * 额外限制的提示消息
     */
    String extraMsg() default "";
}

2. 限流类型枚举 (RateLimitType)

package cn.jbolt.config.anno.rateLimiter;

public enum RateLimitType {
    /**
     * 默认限制(全局)
     * 所有请求共享一个计数器
     */
    DEFAULT,
    
    /**
     * 基于IP地址限制
     * 每个IP独立计数
     */
    IP,
    
    /**
     * 基于用户ID限制
     * 每个登录用户独立计数
     */
    USER,
    
    /**
     * 基于自定义KEY限制
     * 根据业务逻辑自定义限流维度
     */
    CUSTOM
}

3. 限流异常类 (RateLimitException)

package cn.jbolt.config.exception;

public class RateLimitException extends RuntimeException {
    
    private final String message;
    private final int retryAfter;
    
    public RateLimitException(String message) {
        this(message, 0);
    }
    
    public RateLimitException(String message, int retryAfter) {
        super(message);
        this.message = message;
        this.retryAfter = retryAfter;
    }
    
    @Override
    public String getMessage() {
        return message;
    }
    
    public int getRetryAfter() {
        return retryAfter;
    }
}

4. 全局异常处理器 (RateLimitExceptionHandler)

package cn.jbolt.config.handler;

import cn.jbolt.config.exception.RateLimitException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class RateLimitExceptionHandler {
    
    @ExceptionHandler(RateLimitException.class)
    public ResponseEntity<Map<String, Object>> handleRateLimitException(
            RateLimitException e, HttpServletResponse response) {
        
        Map<String, Object> result = new HashMap<>();
        result.putphp("code", HttpStatus.TOO_MANY_REQUESTS.value());
        result.put("message", e.getMessage());
        result.put("data", null);
        
        // 设置HTTP响应头,告诉客户端多久后可以重试
        if (e.getRetryAfter() > 0) {
            response.setHeader("Retry-After", String.valueOf(e.getRetryAfter()));
        }
        response.setHeader("X-RateLimit-Window", "60");
   android     
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(result);
    }
}

5. IP工具类 (IpUtils)

package cn.jbolt.util;

import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;

public class IpUtils {
    
    private static final String[] IP_HEADER_NAMES = {
        "X-Forwarded-For",
        "X-Real-IP", 
        "Proxy-Client-IP",
        "WL-Proxy-Client-IP",
        "HTTP_CLIENT_IP",
        "HTTP_X_FORWARDED_FOR"
    };
    
    private static final String UNKNOWN = "unknown";
    private static final String LOCALHOST_IPV4 = "127.0.0.1";
    private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
    
    /**
     * 获取客户端真实IP地址
     * 处理代理服务器、负载均衡器等场景
     */
    public static String getClientIp(HttpServletRequest request) {
        if (request == null) {
            return UNKNOWN;
        }
        
        String ip = null;
        
        // 依次检查各种可能的IP头
        for (String header : IP_HEADER_NAMES) {
            ip = request.getHeader(header);
            if (isValidIp(ip)) {
                break;
            }
        }
        
        // 如果头信息中没有找到,则使用getRemoteAddr
        if (!isValidIp(ip)) {
            ip = request.getRemoteAddr();
            if (LOCALHOST_IPV6.equals(ip)) {
                ip = LOCALHOST_IPV4;
            }
        }
        
        // 处理多个IP的情况(X-Forwarded-For可能包含多个IP)
        if (StringUtils.hasText(ip) && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        
        return StringUtils.hasText(ip) ? ip : UNKNOWN;
    }
    
    /**
     * 检查IP是否有效
     */
    private static boolean isValidIp(String ip) {
        return StringUtils.hasText(ip) && !UNKNOWN.equalsIgnoreCase(ip);
    }
}

技术实现原理

1. AOP切面拦截

系统使用Spring AOP在方法执行前进行拦截,这是一个核心的限流切面类:

package cn.jbolt.config.ASPect;

import cn.jbolt.config.anno.rateLimiter.RateLimiter;
import cn.jbolt.config.anno.rateLimiter.RateLimitType;
import cn.jbolt.config.exception.RateLimitException;
import cn.jbolt.util.IpUtils;
import cn.jbolt.util.cache.RateLimiterCache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class RateLimiterAspect {
    
    private static final Logger logger = LoggerFactory.getLogger(RateLimiterAspect.class);
    
    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable {
        
        // 检查是否启用限流
        if (!rateLimiter.enabled(http://www.chinasem.cn)) {
            return point.proceed();
        }
        
        // 获取HTTP请求对象
        HttpServletRequest request = getCurrentRequest();
        if (request == null) {
            logger.warn("无法获取HttpServletRequest,跳过限流检查");
            return point.proceed();
        }
        
        // 生成限流键
        String limitKey = generateLimitKey(point, rateLimiter, request);
        
        // 执行主要限流检查
        checkRateLimit(limitKey, rateLimiter.time(), rateLimiter.count(), rateLimiter.msg());
        
        // 执行额外限流检查(如果配置了)
        if (rateLimiter.extraTime() > 0 && rateLimiter.extraCount() > 0) {
            String extraLimitKey = limitKey + ":extra";
            String extraMsg = rateLimiter.extraMsg().isEmpty() ? rateLimiter.msg() : rateLimiter.extraMsg();
            checkRateLimit(extraLimitKey, rateLimiter.extraTime(), rateLimiter.extraCount(), extraMsg);
        }
        
        // 所有限流检查通过,继续执行业务方法
        return point.proceed();
    }
    
    /**
     * 执行限流检查
     */
    private void checkRateLimit(String key, int timeWindow, int maxCount, String message) {
        try {
            // 增加计数器并获取当前访问次数
            int currentCount = RateLimiterCache.incrementAndGet(key, timeWindow, TimeUnit.SECONDS);
            
            logger.debug("限流检查: key={}, 当前次数={}, 限制次数={}", key, currentCount, maxCount);
            
            // 检查是否超过限制
            if (currentCount > maxCount) {
                long ttl = RateLimiterCache.getTtl(key);
                logger.warn("触发限流: key={}, 当前次数={}, 限制次数={}, 剩余时间={}秒", 
                          key, currentCount, maxCount, ttl);
                throw new RateLimitException(message, (int) ttl);
            }
            
        } catch (RateLimitException e) {
            throw e;
        } catch (Exception e) {
            logger.error("限流检查异常: key={}", key, e);
            // 限流服务异常时,选择放行而不是阻塞
        }
    }
    
    /**
     * 生成限流键
     */
    private String generateLimitKey(ProceedingJoinPoint point, RateLimiter rateLimiter, HttpServletRequest request) {
        StringBuilder keyBuilder = new StringBuilder();
        keyBuilder.append(rateLimiter.prefix());
        
        // 添加方法签名
        String methodSignature = point.getSignature().toShortString();
        keyBuilder.append(methodSignature);
        
        // 根据限流类型添加不同的标识
        switch (rateLimiter.limitType()) {
            case IP:
                keyBuilder.append(":ip:").append(IpUtils.getClientIp(request));
                break;
            case USER:
                String userId = getCurrentUserId(request);
                keyBuilder.append(":user:").append(userId != null ? userId : "anonymous");
                break;
            case CUSTOM:
                keyBuilder.append(":custom:").append(rateLimiter.customKey());
                break;
            case DEFAULT:
            default:
                keyBuilder.append(":default:global");
                break;
        }
        
        // 添加时间窗口,确保不同时间窗口的限流独立
        keyBuilder.append(":").append(rateLimiter.time());
        
        String finalKey = keyBuilder.toString();
        logger.debug("生成限流键: {}", finalKey);
        return finalKey;
    }
    
    /**
     * 获取当前HTTP请求
     */
    private HttpServletRequest getCurrentRequest() {
        try {
            ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            return attrs != null ? attrs.getRequest() : null;
        } catch (Exception e) {
            logger.warn("获取HttpServletRequest失败", e);
            return null;
        }
    }
    
    /**
     * 获取当前用户ID
     * 这里需要根据实际的用户认证体系来实现
     */
    private String getCurrentUserId(HttpServletRequest request) {
        // 方案1:从Session中获取
        Object userId = request.getSession().getAttribute("userId");
        if (userId != null) {
            return userId.toString();
        }
        
        // 方案2:从JWT Token中获取
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            // 解析JWT获取用户ID
            // return JwtUtils.getUserIdFromToken(token);
        }
        
        // 方案3:从请求参数中获取
        String userIdParam = request.getParameter("userId");
        if (userIdParam != null) {
            return userIdParam;
        }
        
        return null;
    }
}

2. 缓存数据结构

系统使用一个包装类来存储缓存数据:

package cn.jbolt.util.cache;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

public class CacheWrapper implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private Object value;
    private long timestamp;
    private long durationMillis;
    
    public CacheWrapper() {
    }
    
    public CacheWrapper(Object value, long duration, TimeUnit unit) {
        this.value = value;
        this.timestamp = System.currentTimeMillis();
        this.durationMillis = unit.toMillis(duration);
    }
    
    /**
     * 检查是否已过期
     */
    public boolean isExpired() {
        return System.currentTimeMillis() - timestamp > durationMillis;
    }
    
    /**
     * 获取剩余过期时间(毫秒)
     */
    public long getRemainingTime() {
        long elapsed = System.currentTimeMillis() - timestamp;
        return Math.max(0, durationMillis - elapsed);
    }
    
    // getter和setter方法
    public Object getValue() {
        return value;
    }
    
    public void setValue(Object value) {
        this.value = value;
    }
    
    public long getTimestamp() {
        return timestamp;
    }
    
    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }
    
    public long getDurationMillis() {
        return durationMillis;
    }
    
    public void setDurationMillis(long durationMillis) {
        this.durationMillis = durationMillis;
    }
}

完整代码示例

1. 控制器示例

package cn.jbolt.controller;

import cn.jbolt.config.anno.rateLimiter.RateLimiter;
import cn.jbolt.config.anno.rateLimiter.RateLimitType;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class DemoController {
    
    /**
     * 登录接口 - 防止暴力破解
     * 每个IP每分钟最多尝试5次
     */
    @PostMapping("/login")
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 60, 
        count = 5,
        msg = "登录尝试过于频繁,请1分钟后重试"
    )
    public Result login(@RequestBody LoginRequest request) {
        // 登录逻辑
        if (isValidUser(request.getUsername(), request.getPassword())) {
            return Result.success("登录成功");
        } else {
            return Result.error("用户名或密码错误");
        }
    }
    
    /**
     * 发送验证码 - 防止恶意发送
     * 每个IP每分钟最多3次
     */
    @PostMapping("/sms/send")
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 60, 
        count = 3,
        msg = "验证码发送过于频繁,请稍后重试"
    )
    public Result sendSms(@RequestBody SmsRequest request) {
        // 发送短信逻辑
        boolean success = smsService.sendCode(request.getPhone());
        return success ? Result.success("发送成功") : Result.error("发送失败");
    }
    
    /**
     * 查询接口 - 防止爬虫
     * 每个IP每分钟最多100次
     */
    @GetMapping("/products")
    @RateLimiter(
        limitType = RateLimitType.IP,
        time = 60, 
        count = 100,
        msg = "查询过于频繁,请稍后重试"
    )
    public Result getProducts(@RequestParam(defaultValue = "1") int page) {
        // 查询商品逻辑
        List<Product> products = productService.getProducts(page);
        return Result.success(products);
    }
   js 
    /**
     * 用户操作 - 防止频繁操作
     * 每个用户每分钟最多30次
     */
    @PostMapping("/user/update")
    @RateLimiter(
        limitType = RateLimitType.USER,
        time = 60, 
        count = 30,
        msg = "操作过于频繁,请稍后重试"
    )
    public Result updateUser(@RequestBody UserUpdateRequest request) {
        // 更新用户信息逻辑
        boolean success = userService.updateUser(request);
        return success ? Result.success("更新成功") : Result.error("更新失败");
    }
    
    /**
     * 关键操作 - 严格限流
     * 1秒最多1次 + 1分钟最多5次
     */
    @PostMapping("/transfer")
    @RateLimiter(
        limitType = RateLimitType.USER,
        time = 1, count = 1, msg = "操作过于频繁,请稍后再试",
        extraTime = 60, extraCount = 5, extraMsg = "您在1分钟内的操作次数已达上限"
    )
    public Result transfer(@RequestBody TransferRequest request) {
        // 转账逻辑
        boolean success = transferService.transfer(request);
        return success ? Result.success("转账成功") : Result.error("转账失败");
    }
    
    /**
     * 自定义限流 - 按商品限制
     * 每个商品每分钟最多下单20次
     */
    @PostMapping("/order/{productId}")
    @RateLimiter(
        limitType = RateLimitType.CUSTOM,
        customKey = "product_order",
        time = 60, 
        count = 20,
        msg = "该商品下单过于频繁,请稍后重试"
    )
    public Result createOrder(@PathVariable String productId, @RequestBody OrderRequest request) {
        // 创建订单逻辑
        Order order = orderService.createOrder(productId, request);
        return Result.success(order);
    }
    
    // 辅助方法
    private boolean isValidUser(String username, String password) {
        // 实际的用户验证逻辑
        return "admin".equals(username) && "123456".equals(password);
    }
}

2. 统一返回对象

package cn.jbolt.common;

public class Result {
    private int code;
    private String message;
    private Object data;
    
    public static Result success(Object data) {
        Result result = new Result();
        result.code = 200;
        result.message = "success";
        result.data = data;
        return result;
    }
    
    public static Result error(String message) {
        Result result = new Result();
        result.code = 500;
        result.message = message;
        result.data = null;
        return result;
    }
    
    // getter和setter方法
    public int getCode() {
        return code;
    }
    
    public void setCode(int code) {
        this.code = code;
    }
    
    public String getMessage() {
        return message;
    }
    
    public void setMessage(String message) {
        this.message = message;
    }
    
    public Object getData() {
        return data;
    }
    
    public void setData(Object data) {
        this.data = data;
    }
}

使用指南

1. 基本使用

// 最简单的用法 - 使用默认配置
@RateLimiter(limitType = RateLimitType.IP)
public String simpleApi() {
    return "success";
}

// 自定义时间窗口和次数
@RateLimiter(
    limitType = RateLimitType.IP,
    time = 60,    // 60秒
    count = 100   // 最多100次
)
public String customApi() {
    return "success";
}

2. 不同场景的配置建议

// 登录接口 - 严格限制
@RateLimiter(
    limitType = RateLimitType.IP,
    time = 60, count = 5,
    msg = "登录尝试过于频繁,请1分钟后重试"
)

// 查询接口 - 适中限制
@RateLimiter(
    limitType = RateLimitType.IP,
    time = 60, count = 100,
    msg = "查询过于频繁,请稍后重试"
)

// 用户操作 - 按用户限制
@RateLimiter(
    limitType = RateLimitType.USER,
    time = 60, count = 30,
    msg = "操作过于频繁,请稍后重试"
)

// 全局保护 - 系统级限制
@RateLimiter(
    limitType = RateLimitType.DEFAULT,
    time = 60, count = 200,
    msg = "系统繁忙,请稍后重试"
)

3. 双重限流配置

// 严格的双重限流:秒级 + 分钟级
@RateLimiter(
    limitType = RateLimitType.IP,
    time = 1, count = 1, msg = "请求过于频繁,请稍后再试",
    extraTime = 60, extraCount = 10, extraMsg = "您在1分钟内的请求次数已达上限"
)

// 适中的双重限流:分钟级 + 小时级
@RateLimiter(
    limitType = RateLimitType.IP,
    time = 60, count = 100, msg = "1分钟内请求过多",
    extraTime = 3600, extraCount = 1000, extraMsg = "1小时内请求过多"
)

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持China编程(wwlbbhqw.chinasem.cn)。

这篇关于springboot自定义注解RateLimiter限流注解技术文档详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1155495

相关文章

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

Redis 的 SUBSCRIBE命令详解

《Redis的SUBSCRIBE命令详解》Redis的SUBSCRIBE命令用于订阅一个或多个频道,以便接收发送到这些频道的消息,本文给大家介绍Redis的SUBSCRIBE命令,感兴趣的朋友跟随... 目录基本语法工作原理示例消息格式相关命令python 示例Redis 的 SUBSCRIBE 命令用于订

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja

Java使用Javassist动态生成HelloWorld类

《Java使用Javassist动态生成HelloWorld类》Javassist是一个非常强大的字节码操作和定义库,它允许开发者在运行时创建新的类或者修改现有的类,本文将简单介绍如何使用Javass... 目录1. Javassist简介2. 环境准备3. 动态生成HelloWorld类3.1 创建CtC

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解

《使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解》本文详细介绍了如何使用Python通过ncmdump工具批量将.ncm音频转换为.mp3的步骤,包括安装、配置ffmpeg环... 目录1. 前言2. 安装 ncmdump3. 实现 .ncm 转 .mp34. 执行过程5. 执行结

Python中 try / except / else / finally 异常处理方法详解

《Python中try/except/else/finally异常处理方法详解》:本文主要介绍Python中try/except/else/finally异常处理方法的相关资料,涵... 目录1. 基本结构2. 各部分的作用tryexceptelsefinally3. 执行流程总结4. 常见用法(1)多个e

Java实现将HTML文件与字符串转换为图片

《Java实现将HTML文件与字符串转换为图片》在Java开发中,我们经常会遇到将HTML内容转换为图片的需求,本文小编就来和大家详细讲讲如何使用FreeSpire.DocforJava库来实现这一功... 目录前言核心实现:html 转图片完整代码场景 1:转换本地 HTML 文件为图片场景 2:转换 H

Java使用jar命令配置服务器端口的完整指南

《Java使用jar命令配置服务器端口的完整指南》本文将详细介绍如何使用java-jar命令启动应用,并重点讲解如何配置服务器端口,同时提供一个实用的Web工具来简化这一过程,希望对大家有所帮助... 目录1. Java Jar文件简介1.1 什么是Jar文件1.2 创建可执行Jar文件2. 使用java

C#实现一键批量合并PDF文档

《C#实现一键批量合并PDF文档》这篇文章主要为大家详细介绍了如何使用C#实现一键批量合并PDF文档功能,文中的示例代码简洁易懂,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言效果展示功能实现1、添加文件2、文件分组(书签)3、定义页码范围4、自定义显示5、定义页面尺寸6、PDF批量合并7、其他方法