SpringBoot对接外部接口,一步一步性能调实战篇
需求分析:
本平台对接某某平台的接口,保证接口的稳定性和安全性 实战:
首先我们初始化一个Demo,SpringBoot初始化教程略,初始化后的效果如下:
1.引入依赖
这里我们使用 commons-httpclient 3 commons-httpclient commons-httpclient 3.1 com.google.code.gson gson 2.8.6 org.projectlombok lombok 2.编写工具类(HttpClientUtils):
Get请求:
根据需求,这里我们需要两个参数,一个是token,一个是url参数 + url地址,权限验证采用的是Bearer Tokenpublic 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 Tokenpublic static String sendPost(String urlParam, Map 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 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>(){}.getType(); HashMap 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.javapackage 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 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 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>(){}.getType(); HashMap 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 implements Serializable { private static final long serialVersionUID = 1L; /** * 编码:0表示成功,其他值表示失败 */ private int code = 0; /** * 消息内容 */ private String msg = "success"; /** * 响应数据 */ private T data; public Result ok(T data) { this.setData(data); return this; } public Result 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 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>(){}.getType(); HashMap 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 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().ok(res); } catch (Exception e) { e.printStackTrace(); return new Result().error("请求失败!"); } }
模拟Post请求:/** * 模拟POST请求 * 请求地址:http://localhost:8080/identity/getDetail_post */ @PostMapping("/getDetail_post") public Result getDataPost(@RequestBody HashMap requestBody) { String url = BASE_URL + "/****"; try { String res = sendPost(url, requestBody, TOKEN);; return new Result().ok(res); } catch (Exception e) { e.printStackTrace(); return new Result().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 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>(){}.getType(); HashMap 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 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().ok(res); } catch (Exception e) { e.printStackTrace(); return new Result().error("请求失败!"); } } /** * 模拟POST请求 * 请求地址:http://localhost:8080/identity/getDetail_post */ @PostMapping("/getDetail_post") public Result getDataPost(@RequestBody HashMap requestBody) { String url = BASE_URL + "/****"; try { String res = sendPost(url, requestBody, TOKEN);; return new Result().ok(res); } catch (Exception e) { e.printStackTrace(); return new Result().error("请求失败!"); } } } 优化:
模拟场景:在尽可能的不破坏源代码的情况下,不喜勿喷优化一:属性通过配置文件读取
新建application.yml文件api: baseUrl: http://***** username: **** password: ****
新建配置文件读取类:
ApiConfig.javapackage 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 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>(){}.getType(); HashMap resMap = gson.fromJson(res, mapType); LinkedTreeMap data = (LinkedTreeMap) resMap.get("data"); TOKEN = (String) data.get("token"); log.info("token获取成功:{}", TOKEN); return TOKEN; }
由于Token会存在过期时间,所以我们这里引用Redis引入依赖: org.springframework.boot spring-boot-starter-data-redis 在application.yml添加redis配置: spring: # redis 配置 redis: # 地址 host: xxxxx # 端口,默认为xxx port: xxxx # 数据库索引(db0,db1,db2...不同业务可以放在不同数据库中) database: 0 # 密码 password: "xxxx" 注入RedisTemplate,并优化 @Autowired RedisTemplate redisTemplate; private String getToken() { ValueOperations operations = redisTemplate.opsForValue(); // 0.查询Redis if(!StringUtils.isEmpty(operations.get("token"))) { return operations.get("token"); } // 1.调用获取token接口 String url = BASE_URL + "/***/token"; Map 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>(){}.getType(); HashMap 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; } 进一步优化
场景:如果有大量请求同时访问一个正好过期的缓存数据,可能会出现缓存击穿,所以我们的解决方案是添加分布式锁加入依赖: org.redisson redisson 3.11.0 org.springframework.boot spring-boot-starter-data-redis 创建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); } } 注入并编写读锁、写锁: @Autowired RedisTemplate 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 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>(){}.getType(); HashMap 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; } 再次优化:
互联网系统经常会遇到高并发大流量的请求,在突发情况下(如秒杀、抢购),瞬间大流量会直接把系统打垮,为了防止出现这种情况最常见的解决方案之一就是限流,当请求达到一定的并发数或速率,就进行等待、排队、降级、拒绝服务等。
基于Guava工具类【令牌桶算法】,借助自定义注解+AOP实现接口限流
令牌桶算法的原理也比较简单:系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。单机模式模拟:添加依赖: com.google.guava guava 30.1-jre org.springframework.boot spring-boot-starter-aop 自定义限流注解:
Limit.javapackage 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 "系统繁忙,请稍后再试."; } 使用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 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().error(msg).toString()); } } 给需要限流的接口加上注解 /** * Get请求 * @return */ @GetMapping("/getDetail_get") @Limit(key = "limit1", permitsPerSecond = 1, timeout = 1000, timeunit = TimeUnit.MILLISECONDS, msg = "当前排队人数较多,请稍后再试!") public Result 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().ok(res); } catch (Exception e) { e.printStackTrace(); return new Result().error("请求失败!"); } }
多次请求时:
来源:blog.csdn.net/m0_51517236/article/details/126855104