【实战】SpringBoot对接外部API并一步一步性能调优

需求分析:

本平台对接某某平台的接口,保证接口的稳定性和安全性

实战:

首先我们初始化一个Demo,SpringBoot初始化教程略,初始化后的效果如下:
【实战】SpringBoot对接外部API并一步一步性能调优_第1张图片
1.引入依赖:[这里我们使用 commons-httpclient 3]

<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
        <dependency>
            <groupId>commons-httpclient</groupId>
            <artifactId>commons-httpclient</artifactId>
            <version>3.1</version>
        </dependency>
<!-- 使用goole的json转化工具 -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

2.编写工具类(HttpClientUtils):

Get请求:
根据需求,这里我们需要两个参数,一个是token,一个是url参数 + url地址,权限验证采用的是Bearer Token

 public static String sendGet(String urlParam, String token) {
        // 1.创建httpClient实例对象
        HttpClient httpClient = new HttpClient();
        // 设置httpClient连接主机服务器超时时间:15000毫秒
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        // 2.创建GetMothod实例对象
        GetMethod getMethod = new GetMethod(urlParam);
        // 3.设置post请求超时时间、请求头
        getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        getMethod.addRequestHeader("Content-Type", "application/json");
        if(!StringUtils.isEmpty(token)) {
            Header header = new Header("Authorization", "Bearer " + token);
            getMethod.addRequestHeader(header);
        }
        try {
            // 4.执行getMethod,调用http接口
            httpClient.executeMethod(getMethod);
            // 5.读取内容[流的形式读取]
            InputStream is = getMethod.getResponseBodyAsStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            // 采用线程安全的StringBuffer
            StringBuffer res = new StringBuffer();
            String str= "";
            while((str = br.readLine()) != null){
                res.append(str);
            }
            return res.toString();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 6.释放连接
            getMethod.releaseConnection();
        }
        return null;
    }

Post请求:
根据需求,这里我们需要三个参数,一个是token,一个是url参数 + url地址,还有一个是请求体,权限验证采用的是Bearer Token

public static String sendPost(String urlParam, Map<String, Object> jsonMap, String token) {
        // 1.创建httpClient实例对象
        HttpClient httpClient = new HttpClient();
        // 设置httpClient连接主机服务器超时时间:15000毫秒
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        // 2.创建PostMethod实例对象
        PostMethod postMethod = new PostMethod(urlParam);
        // 设置post请求超时时间、请求头
        postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        postMethod.addRequestHeader("Content-Type", "application/json;charset=utf-8");
        if(!StringUtils.isEmpty(token)) {
            Header header = new Header("Authorization", "Bearer " + token);
            postMethod.addRequestHeader(header);
        }
        // 3.设置请求体
        Gson gson = new Gson();
        String jsonStr = gson.toJson(jsonMap);
        postMethod.setRequestBody(jsonStr);
        try {
            // 4.执行postMethod,调用http接口
            httpClient.executeMethod(postMethod);
            // 5.读取内容[流的形式读取]
            InputStream is = postMethod.getResponseBodyAsStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            // 采用线程安全的StringBuffer
            StringBuffer res = new StringBuffer();
            String str= "";
            while((str = br.readLine()) != null){
                res.append(str);
            }
            return res.toString();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 7.释放连接
            postMethod.releaseConnection();
        }
        return null;
    }

Main方法测试:

// 1.调用获取token接口
        String baseUrl = "http://*****/";
        String url = baseUrl + "/****/token";
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("username", "***");
        jsonMap.put("password", "***");
        String res = sendPost(url, jsonMap, null);
        log.info("获得的请求结果:{}", res);
获得的请求结果:
{"message":"success","status":1,"data":{"token":"eyJh****"}}

我们请求得到JSON字符串后,使用GSON来解析JSON,提取有用的信息,如token

 // 2.解析JSON,得到token
        Gson gson = new Gson();
        // 克服泛型类型擦除问题
        // 具体查阅https://zditect.com/main-advanced/java/gson-json-to-map.html
        Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
        HashMap<String, Object> resMap = gson.fromJson(res, mapType);
        log.info("请求结果解析:{}", resMap);
        LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
        String token = (String) data.get("token");
        log.info("token为:{}", token);

获取得到token后我们开始使用,模拟一次Get请求:

url = baseUrl + "******";
log.info("获得的请求结果:{}", sendGet(url, token));


获得的请求结果:
{"message":"success","status":1,"data":{****}

完整工具类,方便大家拿来直接使用:
HttpClientUtils.java

package com.example.demo;

import com.google.gson.Gson;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.springframework.util.StringUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @author xh
 * @Date 2022/9/14
 */
@Slf4j
public class HttpClientUtils {
    public static String sendPost(String urlParam, Map<String, Object> jsonMap, String token) {
        // 1.创建httpClient实例对象
        HttpClient httpClient = new HttpClient();
        // 设置httpClient连接主机服务器超时时间:15000毫秒
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        // 2.创建PostMethod实例对象
        PostMethod postMethod = new PostMethod(urlParam);
        // 设置post请求超时时间、请求头
        postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        postMethod.addRequestHeader("Content-Type", "application/json;charset=utf-8");
        if(!StringUtils.isEmpty(token)) {
            Header header = new Header("Authorization", "Bearer " + token);
            postMethod.addRequestHeader(header);
        }
        // 3.设置请求体
        Gson gson = new Gson();
        String jsonStr = gson.toJson(jsonMap);
        postMethod.setRequestBody(jsonStr);
        try {
            // 4.执行postMethod,调用http接口
            httpClient.executeMethod(postMethod);
            // 5.读取内容[流的形式读取]
            InputStream is = postMethod.getResponseBodyAsStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            // 采用线程安全的StringBuffer
            StringBuffer res = new StringBuffer();
            String str= "";
            while((str = br.readLine()) != null){
                res.append(str);
            }
            return res.toString();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 7.释放连接
            postMethod.releaseConnection();
        }
        return null;
    }

    public static String sendGet(String urlParam, String token) {
        // 1.创建httpClient实例对象
        HttpClient httpClient = new HttpClient();
        // 设置httpClient连接主机服务器超时时间:15000毫秒
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        // 2.创建GetMothod实例对象
        GetMethod getMethod = new GetMethod(urlParam);
        // 3.设置post请求超时时间、请求头
        getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        getMethod.addRequestHeader("Content-Type", "application/json");
        if(!StringUtils.isEmpty(token)) {
            Header header = new Header("Authorization", "Bearer " + token);
            getMethod.addRequestHeader(header);
        }
        try {
            // 4.执行getMethod,调用http接口
            httpClient.executeMethod(getMethod);
            // 5.读取内容[流的形式读取]
            InputStream is = getMethod.getResponseBodyAsStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
            // 采用线程安全的StringBuffer
            StringBuffer res = new StringBuffer();
            String str= "";
            while((str = br.readLine()) != null){
                res.append(str);
            }
            return res.toString();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 6.释放连接
            getMethod.releaseConnection();
        }
        return null;
    }

    public static void main(String[] args) {
        // 1.调用获取token接口
        String baseUrl = "http://****";
        String url = baseUrl + "/*****";
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("username", "****");
        jsonMap.put("password", "*****");
        String res = sendPost(url, jsonMap, null);
        log.info("获得的请求结果:{}", res);
        // 2.解析JSON,得到token
        Gson gson = new Gson();
        // 克服泛型类型擦除问题
        // 具体查阅https://zditect.com/main-advanced/java/gson-json-to-map.html
        Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
        HashMap<String, Object> resMap = gson.fromJson(res, mapType);
        log.info("请求结果解析:{}", resMap);
        LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
        String token = (String) data.get("token");
        log.info("token为:{}", token);
        // 3.模拟Get请求
        // TODO 需要使用URL编码
        url = baseUrl + "****";
        log.info("获得的请求结果:{}", sendGet(url, token));
    }
}

为前端提供接口并测试:
首先我们统一返回风格:
Result.java

package com.example.demo;

import lombok.Data;

import java.io.Serializable;

/**
 * @author xh
 * @Date 2022/9/14
 */
@Data
public class Result<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 编码:0表示成功,其他值表示失败
     */
    private int code = 0;
    /**
     * 消息内容
     */
    private String msg = "success";
    /**
     * 响应数据
     */
    private T data;

    public Result<T> ok(T data) {
        this.setData(data);
        return this;
    }

    public Result<T> error(String msg) {
        this.code = 500;
        this.msg = msg;
        return this;
    }
}

新建ApiController:
首先我们将公共变量做一个提取:

public static String TOKEN = "";

    public static final String BASE_URL = "http://****";

    public static final String USERNAME = "****";

    public static final String PASSWORD = "****";

// 静态代码块
    static {
        // 1.调用获取token接口
        String url = BASE_URL + "/****";
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("username", USERNAME);
        jsonMap.put("password", PASSWORD);
        String res = sendPost(url, jsonMap, null);
        // 2.解析JSON,得到token
        Gson gson = new Gson();
        Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
        HashMap<String, Object> resMap = gson.fromJson(res, mapType);
        LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
        TOKEN = (String) data.get("token");
        log.info("token获取成功:{}", TOKEN);
    }

模拟Get请求:

/**
     * Get请求
     * 请求地址:http://localhost:8080/identity/getDetail_get?handle=xxx
     * @return
     */
    @GetMapping("/getDetail_get")
    public Result<String> getDataGet(@RequestParam String handle) {
        log.info("开始发起Get请求, token为:{}", TOKEN);
        Assert.notNull(handle);
        String url = BASE_URL + "/xxx=" + handle;
        try {
            String res = sendGet(url, TOKEN);
            return new Result<String>().ok(res);
        } catch (Exception e) {
            e.printStackTrace();
            return new Result<String>().error("请求失败!");
        }
    }

模拟Post请求:

/**
     * 模拟POST请求
     * 请求地址:http://localhost:8080/identity/getDetail_post
     */
    @PostMapping("/getDetail_post")
    public Result<String> getDataPost(@RequestBody HashMap<String, Object> requestBody) {
        String url = BASE_URL + "/****";
        try {
            String res = sendPost(url, requestBody, TOKEN);;
            return new Result<String>().ok(res);
        } catch (Exception e) {
            e.printStackTrace();
            return new Result<String>().error("请求失败!");
        }
    }

整体代码:

package com.example.demo;

import com.google.gson.Gson;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import static com.example.demo.HttpClientUtils.sendGet;
import static com.example.demo.HttpClientUtils.sendPost;

/**
 * @author xh
 * @Date 2022/9/14
 */
@RestController
@RequestMapping("/identity/")
@Slf4j
public class ApiController {
    public static String TOKEN = "";

    public static final String BASE_URL = "http://*****";

    public static final String USERNAME = "****";

    public static final String PASSWORD = "****";

    // 静态代码块
    static {
        // 1.调用获取token接口
        String url = BASE_URL + "/identity/token";
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("username", USERNAME);
        jsonMap.put("password", PASSWORD);
        String res = sendPost(url, jsonMap, null);
        // 2.解析JSON,得到token
        Gson gson = new Gson();
        Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
        HashMap<String, Object> resMap = gson.fromJson(res, mapType);
        LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
        TOKEN = (String) data.get("token");
        log.info("token获取成功:{}", TOKEN);
    }

    /**
     * Get请求
     * 请求地址:http://localhost:8080/identity/getDetail_get?handle=****
     * @return
     */
    @GetMapping("/getDetail_get")
    public Result<String> getDataGet(@RequestParam String handle) {
        log.info("开始发起Get请求, token为:{}", TOKEN);
        Assert.notNull(handle);
        String url = BASE_URL + "/****" + handle;
        try {
            String res = sendGet(url, TOKEN);
            return new Result<String>().ok(res);
        } catch (Exception e) {
            e.printStackTrace();
            return new Result<String>().error("请求失败!");
        }
    }

    /**
     * 模拟POST请求
     * 请求地址:http://localhost:8080/identity/getDetail_post
     */
    @PostMapping("/getDetail_post")
    public Result<String> getDataPost(@RequestBody HashMap<String, Object> requestBody) {
        String url = BASE_URL + "/****";
        try {
            String res = sendPost(url, requestBody, TOKEN);;
            return new Result<String>().ok(res);
        } catch (Exception e) {
            e.printStackTrace();
            return new Result<String>().error("请求失败!");
        }
    }
}

优化:


模拟场景:在尽可能的不破坏源代码的情况下,不喜勿喷
优化一:属性通过配置文件读取
新建application.yml文件

api:
  baseUrl: http://*****
  username: ****
  password: ****

新建配置文件读取类:
ApiConfig.java

package com.example.demo;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


/**
 * @author xh
 * @Date 2022/9/14
 */
@Component
@ConfigurationProperties(prefix = "api")
@Data
public class ApiConfig {

    /**
     * API地址
     */
    private String baseUrl;

    /**
     * 代理用户名
     */
    private String username;

    /**
     * 代理密码
     */
    private String password;
}

ApiController进行微调:

	@Autowired
    ApiConfig apiConfig;

    public static String TOKEN = "";

    public static String BASE_URL = "";

    public static String USERNAME = "";

    public static String PASSWORD = "";

    @PostConstruct
    private void getBaseInfo() {
        BASE_URL = apiConfig.getBaseUrl();
        USERNAME = apiConfig.getUsername();
        PASSWORD = apiConfig.getPassword();
    }

    private String getToken() {
        if(!StringUtils.isEmpty(TOKEN)) {
            return TOKEN;
        }
        // 1.调用获取token接口
        String url = BASE_URL + "/***/token";
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("username", USERNAME);
        jsonMap.put("password", PASSWORD);
        String res = sendPost(url, jsonMap, null);
        // 2.解析JSON,得到token
        Gson gson = new Gson();
        Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
        HashMap<String, Object> resMap = gson.fromJson(res, mapType);
        LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
        TOKEN = (String) data.get("token");
        log.info("token获取成功:{}", TOKEN);
        return TOKEN;
    }

由于Token会存在过期时间,所以我们这里引用Redis
1.引入依赖:


        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

2.在application.yml添加redis配置:

spring:
  # redis 配置
  redis:
    # 地址
    host: xxxxx
    # 端口,默认为xxx
    port: xxxx
    # 数据库索引(db0,db1,db2...不同业务可以放在不同数据库中)
    database: 0
    # 密码
    password: 'xxxx'

2.注入RedisTemplate,并优化

	@Autowired
    RedisTemplate<String, String> redisTemplate;

    private String getToken() {
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        // 0.查询Redis
        if(!StringUtils.isEmpty(operations.get("token"))) {
            return operations.get("token");
        }
        // 1.调用获取token接口
        String url = BASE_URL + "/***/token";
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("username", USERNAME);
        jsonMap.put("password", PASSWORD);
        String res = sendPost(url, jsonMap, null);
        // 2.解析JSON,得到token
        Gson gson = new Gson();
        Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
        HashMap<String, Object> resMap = gson.fromJson(res, mapType);
        LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
        String token = (String) data.get("token");
        // 设置TOKEN 6小时过期
        operations.set("token", token, 6, TimeUnit.HOURS);
        log.info("token获取成功:{}", token);
        return token;
    }

进一步优化,场景:如果有大量请求同时访问一个正好过期的缓存数据,可能会出现缓存击穿,所以我们的解决方案是添加分布式锁

1.加入依赖:


        
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.11.0version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        

2.创建RedissionConfig.java 配置RedissionClient

package com.example.demo;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

/**
 * @author xh
 * @Date 2022/9/14
 */
@Component
public class RedissionConfig {
    /**
     * 所有对redisson的使用都是通过RedissonClient对象
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() {
        //创建配置
        Config config = new Config();
        //可以用"rediss://"来启用SSL连接,useSingleServer表示单例模式
        config.useSingleServer().setAddress("redis://xxxx:xxxx").setDatabase(0).setPassword("xxxx");
        //根据config创建出RedissonClient实例
        return Redisson.create(config);
    }

}

3.注入并编写读锁、写锁:

	@Autowired
    RedisTemplate<String, String> redisTemplate;

    @Autowired
    RedissonClient redisson;

    private String getToken() {
        // 0.查询Redis
        String token = readToken();
        if(!StringUtils.isEmpty(token)) {
            return token;
        }
        // 1.调用获取token接口
        String url = BASE_URL + "/xxx/token";
        Map<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("username", USERNAME);
        jsonMap.put("password", PASSWORD);
        String res = sendPost(url, jsonMap, null);
        // 2.解析JSON,得到token
        Gson gson = new Gson();
        Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
        HashMap<String, Object> resMap = gson.fromJson(res, mapType);
        LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
        token = (String) data.get("token");
        // 设置TOKEN
        Assert.isTrue(setToken(token));
        log.info("token获取成功:{}", token);
        return token;
    }

    private String readToken() {
        RReadWriteLock lock = redisson.getReadWriteLock("token-lock");
        RLock rLock = lock.readLock();
        String token = "";
        try {
            //加读锁
            rLock.lock();
            token = redisTemplate.opsForValue().get("token");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return token;
    }

    private boolean setToken(String token) {
        RReadWriteLock lock = redisson.getReadWriteLock("token-lock");
        RLock rLock = lock.writeLock();
        try {
            // 改数据加写锁,读数据加读锁
            rLock.lock();
            redisTemplate.opsForValue().set("token", token, 6, TimeUnit.HOURS);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return true;
    }

使用JMeter测试:


再次优化:

互联网系统经常会遇到高并发大流量的请求,在突发情况下(如秒杀、抢购),瞬间大流量会直接把系统打垮,为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。

基于Guava工具类【令牌桶算法】,借助自定义注解+AOP实现接口限流

【实战】SpringBoot对接外部API并一步一步性能调优_第2张图片
令牌桶算法的原理也比较简单:系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。

单机模式模拟:

1.添加依赖:

	<!-- Guava: 限流工具类RateLimiter -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>30.1-jre</version>
        </dependency>
        <!-- 加入AOP依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.自定义限流注解:
Limit.java

package com.example.demo;

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * @author xh
 * @Date 2022/9/15
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {
    /**
     * 资源的key,唯一
     * 作用:不同的接口,不同的流量控制
     */
    String key() default "";

    /**
     * 最多的访问限制次数
     */
    double permitsPerSecond () ;

    /**
     * 获取令牌最大等待时间
     */
    long timeout();

    /**
     * 获取令牌最大等待时间,单位(例:分钟/秒/毫秒) 默认:毫秒
     */
    TimeUnit timeunit() default TimeUnit.MILLISECONDS;

    /**
     * 得不到令牌的提示语
     */
    String msg() default "系统繁忙,请稍后再试.";
}

3.使用AOP切面拦截限流注解

package com.example.demo;

import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * @author xh
 * @Date 2022/9/15
 */
@Slf4j
@Aspect
@Component
public class LimitAop {
    /**
     * 不同的接口,不同的流量控制
     * map的key为 Limiter.key
     */
    private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();

    @Around("@annotation(com.example.demo.Limit)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //拿limit的注解
        Limit limit = method.getAnnotation(Limit.class);
        if (limit != null) {
            //key作用:不同的接口,不同的流量控制
            String key=limit.key();
            RateLimiter rateLimiter = null;
            //验证缓存是否有命中key
            if (!limitMap.containsKey(key)) {
                // 创建令牌桶
                rateLimiter = RateLimiter.create(limit.permitsPerSecond());
                limitMap.put(key, rateLimiter);
                log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
            }
            rateLimiter = limitMap.get(key);
            // 拿令牌
            boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
            // 拿不到命令,直接返回异常提示
            if (!acquire) {
                log.debug("令牌桶={},获取令牌失败",key);
                this.responseFail(limit.msg());
                return null;
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 直接向前端抛出异常
     * @param msg 提示信息
     */
    private void responseFail(String msg)  {
        HttpServletResponse resp=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        resp.setCharacterEncoding("utf-8");
        resp.setContentType("application/json; charset=utf-8");
        PrintWriter writer = null;
        try {
            writer = resp.getWriter();
        } catch (IOException e) {
            e.printStackTrace();
        }
        writer.write(new Result<String>().error(msg).toString());
    }
}

4.给需要限流的接口加上注解

/**
     * Get请求     * @return
     */
    @GetMapping("/getDetail_get")
    @Limit(key = "limit1", permitsPerSecond = 1, timeout = 1000, timeunit = TimeUnit.MILLISECONDS, msg = "当前排队人数较多,请稍后再试!")
    public Result<String> getDataGet(@RequestParam String handle) {
        log.info("开始发起Get请求, token为:{}", getToken());
        Assert.notNull(handle);
        String url = BASE_URL + "/****" + handle;
        try {
            String res = sendGet(url, getToken());
            return new Result<String>().ok(res);
        } catch (Exception e) {
            e.printStackTrace();
            return new Result<String>().error("请求失败!");
        }
    }

多次请求时:
【实战】SpringBoot对接外部API并一步一步性能调优_第3张图片

你可能感兴趣的:(#,SpringBoot,spring,boot,java,服务器)