java 项目内接口限流

webService接口限流

近期公司有个webservice接口在高并被调用时发生了read time out一场,经过一番查阅发现这是由于jdk低版本jdk1.7.0_51版本之下,解析soap协议的接口时创建XmlReader存在数量限制(默认64000),并且创建时不会进行重置,引起的read time out,官方给出的建议时升级到1.7.0_51(参考链接:https://bugs.openjdk.java.net/browse/JDK-8028111).
废话不多说,先上测试对比图.
这事没加限流前的测试图,39个请求失败
在这里插入图片描述
这是加了限流后的测试结果,7个请求失败
在这里插入图片描述
测试结果与运行系统的性能相关(我自己的电脑快卡爆了,tps连1都没有,之前测试的50并发线程,3000个请求,全部成功).由于机器性能可能不一样,加了限流队列的配置文件,实际使用中可根据机器的性能做测试,使用合适的大小.
下面是代码.

1.web.xml

做限流的时候考虑到webservice接口和controller中的接口分别由webservice容器和spring Ioc容器托管,所以使用了过滤器,需要在web.xml中配置,代码如下:

    <filter-name>limitFilter</filter-name>
    <!-- 过滤器文件包路径 -->
    <filter-class>com.shcmcc.poms.limit.filter.LimitFilter</filter-class>
    <init-param>
        <param-name>limitConfigLocation</param-name>
        <!-- 配置文件位置 -->
        <param-value>classpath:./etc/limit.properties</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>limitFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

2.相关代码

2.1 Limit.java

import java.lang.annotation.*;

/**
 * @author MQ
 * @desc 限流注解
 *  limitPath 限流的url
 *  limitCount 限流队列的大小
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Limit {

    String limitPath() default "";

    int limitCount() default 0;

}

2.2PropertiesLoader.java

import org.apache.commons.lang.StringUtils;

import java.io.*;
import java.util.Properties;

/**
 * @author MQ
 * @desc 配置文件加载器,读取.properties文件
 */
public class PropertiesLoader {

    public static Properties loadProperties(String filePath) throws IOException {
        // Construct BufferedReader from FileReader
        BufferedReader br = new BufferedReader(new FileReader(new File(filePath)));
        String line = null;
        Properties properties = new Properties();
        // 循环读取行内容
        while ((line = br.readLine()) != null) {
            line = new String(line.getBytes("utf-8"));
            // 跳过注释
            if (!line.startsWith("#") && StringUtils.isNotBlank(line.trim())) {
                String[] keyValue = line.split("=");
                properties.setProperty(keyValue[0].trim(), keyValue[1].trim());
            }
        }
        br.close();
        return properties;
    }

2.3limit.properties

#包名,数组,以'|'号分隔
scanPackage=com.shcmcc.poms.webservice

#接口默认队列限制数量
defalutLimitCount=5

2.4 LimitDefinition.java

import org.javatuples.Pair;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * @author MQ
 * @desc 限流定义器
 */
public class LimitDefinition {

	// 请求队列的大小
    protected int limit = 10;

	// 使用了阻塞队列,当请求队列满的时候,阻塞当前请求
	// LinkedBlockingQueue提供了阻塞/非阻塞的存/取方法,相关方法
	/**
     * take():首选。当队列为空时阻塞
     * poll():弹出队顶元素,队列为空时,返回空
     * peek():和poll烈性,返回队队顶元素,但顶元素不弹出。队列为空时返回null
     * remove(Object o):移除某个元素,队列为空时抛出异常。成功移除返回true
     * 
     * 添加数据
     * put():首选。队满是阻塞
     * offer():队满时返回false
     */
    protected LinkedBlockingQueue<Pair<ServletRequest, ServletResponse>> requestQueue;

    public LimitDefinition(int limit) {
        this.limit = limit;
        this.requestQueue = new LinkedBlockingQueue<Pair<ServletRequest, ServletResponse>>(limit);
    }

    public int getLimit() {
        return limit;
    }

    public void setLimit(int limit) {
        this.limit = limit;
    }

    public LinkedBlockingQueue<Pair<ServletRequest, ServletResponse>> pollRequest() {
        return requestQueue;
    }

	// 将请求放入队列的时候执行doFilter()方法处理请求
    public void putRequest(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException, InterruptedException {
            this.requestQueue.put(new Pair<ServletRequest, ServletResponse>(req, resp));
            // 只要队列中存在请求,就遍历并处理请求
            while (requestQueue.size() > 0) {
                Pair<ServletRequest, ServletResponse> polled = requestQueue.poll();
                doFilter(polled.getValue0(), polled.getValue1(), chain);
            }

    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        chain.doFilter(req, resp);
    }
}

上面的是辅助用的一些java类和配置文件,注释已加好

3.初始化及调用

3.1初始化容器

3.1.1过滤器中初始化

自定义过滤器需要实现javax.servlet.Filter接口,需要重写并实现init()初始化、doFilter()过滤请求、destroy()销毁方法,因此初始化filter容器选择放在init()方法中,代码如下

// 请求队列定义Map,key为接口路径,value为接口的限流参数(LimitDefinition)
private Map<String, LimitDefinition> requestQueueDefinitionCache = new HashMap<String, LimitDefinition>();
@Override
public void init(FilterConfig filterConfig) {
    logger.info("==================================================================init limit filter=======================================================");
    // 从webservice容器中直接获取代理的接口信息
    delegate = (WSServletDelegate) filterConfig.getServletContext().getAttribute(WSServlet.JAXWS_RI_RUNTIME_INFO);
    // 将webservice代理和filterConfig一同传入初始化方法
    initLimitDefinition(delegate, filterConfig);
}

// 初始化容器
private void initLimitDefinition(WSServletDelegate delegate, FilterConfig config) {

    // 扫描并注册webservice容器注册好的路径,给定limit默认值
    if (delegate != null) {
        Properties properties = null;
        try {
        	// 读取配置文件的配置信息(包名,请求队列默认大小)
            properties = PropertiesLoader.loadProperties(getConfigPath(config));
        } catch (IOException e) {
            e.printStackTrace();
            logger.error(e.getMessage(),e);
        }
        if (properties != null) {
        	// 获取请求队列默认大小值
            String defalutLimitCount = properties.getProperty("defalutLimitCount");
            if (StringUtils.isNotBlank(defalutLimitCount)) {
            	// 获取webservice代理接口的信息
                List<ServletAdapter> adapters = delegate.adapters;
                for (ServletAdapter adapter : adapters) {
                    String requestPath = adapter.getValidPath();
                    if (requestQueueDefinitionCache.get(requestPath) == null) {
                    	// 将接口信息放入请求定义队列中
                        requestQueueDefinitionCache.put(requestPath, new LimitDefinition(new Integer(defalutLimitCount)));
                    }
                }
            }

        }
    }
    // LimitReader扫描包并读取接口上的注解参数
    LimitReader limitReader = new LimitReader(config);
    // 将扫描的数据全部放入缓存
    requestQueueDefinitionCache.putAll(limitReader.getLimitDefinitionCache());
}

private String getConfigPath(FilterConfig config) {
    // 获取配置文件路径
    String configLocation = config.getInitParameter("limitConfigLocation")
            .replace("classpath:", "").trim()
            .replace("./", "/");
    String realFilePath = this.getClass().getResource(configLocation).getFile();
    return realFilePath;
}

filter中仅对webservice接口做了初始化,controller中的接口在LimitReader中初始化

3.1.2扫描controller接口并注册到限流容器

由于考虑到后期可能增加其他的功能,所以将扫包的代码抽取出来,放在了LimitReader中,这样便于以后维护(修补bug)或增加新功能,废话不多说,上代码

import javax.servlet.FilterConfig;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import com.shcmcc.poms.limit.annotation.Limit;

/**
 * @author MQ
 * @desc 配置读取器
 */
public class LimitReader {

    private Map<String, LimitDefinition> requestQueueDefinitionCache = new HashMap<String, LimitDefinition>();

    public LimitReader(FilterConfig config) {
        Properties properties = loadProperties(config);
        // 读取包名
        String scanPackageStr = properties.getProperty("scanPackage");
        if (StringUtils.isBlank(scanPackageStr)) return;

        String[] packageUrls = scanPackageStr.replaceAll("\\.", "/").split("\\|");
        try {
        	// 扫包(骚包?)
            doScan(packageUrls);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

	// 这一块代码是为了读取使用了@Limit注解的接口需要配置的信息,并注册到限流容器
	private void doScan(String[] scanPackages) throws FileNotFoundException {
		// 遍历包名数组
        for (int i = 0; i < scanPackages.length; i++) {
            String scanPackage = scanPackages[i].trim();
            URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.","/"));
            // 将包路径转换成url,作为根目录
            File classPath = new File(url.getFile());
            if (!classPath.exists()) throw new FileNotFoundException("scanPackage is invalid");

            //当成是一个ClassPath文件夹
            for (File file : classPath.listFiles()) {
            	// 如果file是文件夹,则递归继续扫包
                if(file.isDirectory()){
                    doScan(new String[]{scanPackage + "." + file.getName()});
                }else {
                	// 如果不是文件夹,需要判断是否.class文件,否则Class.forName()会报错
                    if(!file.getName().endsWith(".class")){continue;}
                    //全类名 = 包名.类名
                    String className = (scanPackage + "." + file.getName().replace(".class", "")).replaceAll("/",".");
                    try {
                    	// 加载扫描到的class文件
                        Class clazz = Class.forName(className);
                        readLimitAnnotation(clazz);
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

	// 这里读取方法上的Limit注解
    private void readLimitAnnotation(Class clazz) {
    	// 获取并遍历Class中定义的所有方法
        Method[] methods = clazz.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            // 判断是否使用了Limit注解
            if (!method.isAnnotationPresent(Limit.class)) continue;

            Limit limit = method.getAnnotation(Limit.class);
            // 读取注解上的配置数据
            String limitPath = limit.limitPath();
            int limitCount = limit.limitCount();
            // 放入缓存,配置文件中存在为默认队列大小,这里允许覆盖
            requestQueueDefinitionCache.put(limitPath, new LimitDefinition(limitCount));
        }
    }

    public Map<String, LimitDefinition> getLimitDefinitionCache() {
        return this.requestQueueDefinitionCache;
    }

    private Properties loadProperties(FilterConfig config) {
        // 获取配置文件路径
        String configLocation = config.getInitParameter("limitConfigLocation")
                .replace("classpath:", "").trim()
                .replace("./", "/");
        String realFilePath = this.getClass().getResource(configLocation).getFile();
        Properties properties = null;
        try {
            properties = PropertiesLoader.loadProperties(realFilePath);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return properties;
        // jdk1.8可以直接使用这种方式进行文件内容读取
        // Properties properties = new Properties(FileReader);
    }

}

3.2阻塞请求

对于请求的阻塞在LimitDefinition.putRequest()方法中执行,使用的是LinkedBlockingQueue.put()方法的阻塞机制,当队列满的时候会阻塞当前线程,
而当队列中存在请求未处理时,就会循环遍历请求队列处理请求,取请求的方法使用的是LinkedBlockingQueue.poll()方法,该方法非阻塞,但是LinkedBlockingQueue中的元素都会单独加锁,所以不会存在线程安全问题

4使用方法

webservice接口在filter容器初始化时自动加载了.基于springmvc的接口只需要在方法上加Limit注解,并定义好相关参数就可以做到接口限流

@Controller
@RequestMapping("controller")
public class controller {
	
	@Limit(limitPath="/itemProject/method", limitCount = 10)
    @RequestMapping("method")
    public void method() {
        // execute xxx
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 中的 AOP (Aspect Oriented Programming) 可以用来实现接口级别的限流功能,通常我们会结合第三方库如 Spring Cloud Gateway、Webrx 或者自定义拦截器来完成这个任务。以下是一个简单的概述: 1. 引入依赖:首先,在Spring Boot项目中添加限流库的依赖,比如 Spring Cloud Gateway 提供了 WebFlux 基于令牌桶(Token Bucket)的限流支持。 ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> ``` 2. 定义限流规则:在配置类中设置限流策略,例如限制某个接口每秒的请求次数。Spring Cloud Gateway 使用 `RateLimiter` 来控制。 ```java @Bean public RateLimiter myLimit(RateLimiterConfig config) { // 设置限流参数,如每秒50次请求 return RateLimiter.of("my-limit", config.limitForPeriod(1, TimeUnit.SECONDS), false); } ``` 3. AOP 配置:创建一个切面(Aspect),利用 `@Around` 注解和 `RateLimiter` 对目标方法进行拦截,并在调用之前检查是否达到限流阈值。 ```java @Aspect @Component public class ApiRateLimitingAspect { @Autowired private RateLimiter myLimit; @Around("@annotation(api)") public Object limitApi(ProceedingJoinPoint joinPoint, Api api) throws Throwable { if (!myLimit.tryAcquire()) { throw new RateLimiterRejectedException("Exceeded rate limit"); } // 执行原方法 return joinPoint.proceed(); } // 如果你需要为每个接口定义不同的限流规则,可以使用注解来标记 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Api { String value() default ""; } } ``` 在这个例子中,我们假设有一个 `Api` 注解用于标记接口,然后在 `limitApi` 方法中对被该注解修饰的方法进行限流
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值