java 抓取对接第三方API平台数据 完整实现 | 先获取登录Token + Token鉴权查询 + 缓存重试机

Markdown 教程:单类通用Token接口请求工具(无继承、纯独立类、大白话易懂)

一、功能说明

市面上大部分第三方接口流程固定:

  1. 调用登录接口,拿到鉴权Token
  2. 存下Token和过期时间,重复查询不用反复登录
  3. 查数据时带上Token请求头
  4. Token失效自动重新获取再重试一次
  5. 解析返回的列表数据

本代码单独一个类,不继承任何父类,所有配置集中在最顶部,换接口只改常量,无硬编码文字,新手直接复制就能用。

二、所需Maven依赖

xml 复制代码
<!-- http请求工具okhttp -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.0</version>
</dependency>
<!-- json解析工具fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.74</version>
</dependency>

三、完整独立工具类(无继承,全部逻辑写在一类)

java 复制代码
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import okhttp3.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 通用Token鉴权接口请求工具
 * 执行流程:登录拿token → 缓存token复用 → 带token查询数据 → 失效自动重试 → 解析列表
 * 全部配置写在顶部常量,更换接口只修改常量即可
 * 不继承任何类,完全独立,可直接复制使用
 */
public class CommonTokenApiUtil {
    // ====================== 配置区(只修改这里,下方逻辑不用动) ======================
    // 获取token的登录接口地址
    private static final String LOGIN_URL = "https://xxxx.com/api/IPLogin";
    // 分页查询数据的业务接口地址
    private static final String SEARCH_URL = "https://xxxx.com/api/search";
    // 每页查询条数
    private static final int PAGE_SIZE = 50;
    // 提前5分钟刷新token,避免查询中途过期
    private static final long REFRESH_TOKEN_TIME = 5 * 60 * 1000;
    // 详情页面拼接前缀
    private static final String DETAIL_URL_PREFIX = "https://xxxx.com/asset/searchingDetail?id=";
    // ==============================================================================

    // 缓存token,全局复用
    private static String cacheToken;
    // token过期时间戳(毫秒)
    private static long tokenExpireTime;
    // 全局统一http请求对象,复用节省资源
    private static final OkHttpClient HTTP_CLIENT = new OkHttpClient();
    // json请求固定头部类型
    private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");

    /**
     * 对外暴露的统一查询入口
     * @param keyword 搜索关键词
     * @param pageNum 当前页码
     * @return 解析后的列表数据
     */
    public List<ItemData> searchData(String keyword, int pageNum) {
        System.out.println("开始执行接口查询,关键词:" + keyword + ",页码:" + pageNum);
        List<ItemData> resultList = new ArrayList<>();
        try {
            // 1. 获取可用token,过期自动刷新
            String token = getAvailableToken();
            if (token == null) {
                System.out.println("获取token失败,终止查询");
                return resultList;
            }

            // 2. 构造查询接口请求参数
            JSONObject searchParam = buildSearchParam(keyword, pageNum);
            // 3. 第一次发起查询
            String responseJson = sendSearchRequest(token, searchParam);

            // 4. 返回为空说明token失效,刷新token重试一次
            if (responseJson == null || responseJson.isBlank()) {
                System.out.println("token失效,重新刷新后重试查询");
                String newToken = getAvailableToken();
                if (newToken != null) {
                    responseJson = sendSearchRequest(newToken, searchParam);
                }
            }

            // 5. 解析返回的json数据,封装成实体
            if (responseJson != null && !responseJson.isBlank()) {
                resultList = parseResponseJson(responseJson);
                System.out.println("查询完成,本次返回数据条数:" + resultList.size());
            }
        } catch (Exception e) {
            System.out.println("查询接口出现异常:" + e.getMessage());
            e.printStackTrace();
        }
        return resultList;
    }

    /**
     * 获取有效的token,线程安全,未过期直接复用,过期重新登录
     */
    private synchronized String getAvailableToken() {
        long nowTime = System.currentTimeMillis();
        // 判断token存在且未到刷新时间,直接返回缓存token
        if (cacheToken != null && nowTime < tokenExpireTime - REFRESH_TOKEN_TIME) {
            return cacheToken;
        }
        // 重新调用登录接口获取新token
        return refreshToken();
    }

    /**
     * 调用登录接口,刷新全新token
     */
    private String refreshToken() {
        try {
            // 登录接口请求体,无参数传空json对象
            JSONObject loginBody = new JSONObject();
            Request request = new Request.Builder()
                    .url(LOGIN_URL)
                    .post(RequestBody.create(JSON_TYPE, loginBody.toJSONString()))
                    .addHeader("Content-Type", "application/json")
                    .build();

            // 执行请求
            try (Response response = HTTP_CLIENT.newCall(request).execute()) {
                if (!response.isSuccessful() || response.body() == null) {
                    System.out.println("登录接口请求失败,状态码:" + response.code());
                    return null;
                }
                String respStr = response.body().string();
                JSONObject respJson = JSONObject.parseObject(respStr);
                // 判断登录是否成功
                if (!respJson.getBooleanValue("succeeded")) {
                    System.out.println("登录获取token失败,提示:" + respJson.getString("message"));
                    return null;
                }
                JSONObject data = respJson.getJSONObject("data");
                // 缓存token
                cacheToken = data.getString("token");
                // 计算过期时间戳
                int expireSeconds = data.getIntValue("expiredOn");
                tokenExpireTime = System.currentTimeMillis() + expireSeconds * 1000L;
                System.out.println("token刷新成功,有效时长:" + expireSeconds + "秒");
                return cacheToken;
            }
        } catch (Exception e) {
            System.out.println("刷新token发生异常:" + e.getMessage());
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 构造查询接口需要的json参数
     */
    private JSONObject buildSearchParam(String keyword, int pageNum) {
        JSONObject body = new JSONObject();
        body.put("sort", "");
        body.put("pageIndex", pageNum);
        body.put("pageSize", PAGE_SIZE);
        body.put("keywords", keyword);
        return body;
    }

    /**
     * 携带token发送查询POST请求
     */
    private String sendSearchRequest(String token, JSONObject paramJson) throws Exception {
        Request request = new Request.Builder()
                .url(SEARCH_URL)
                .post(RequestBody.create(JSON_TYPE, paramJson.toJSONString()))
                .addHeader("Content-Type", "application/json")
                .addHeader("Authorization", "Bearer " + token)
                .build();
        try (Response response = HTTP_CLIENT.newCall(request).execute()) {
            if (response.isSuccessful() && response.body() != null) {
                return response.body().string();
            }
            System.out.println("数据查询接口请求失败,状态码:" + response.code());
            return null;
        }
    }

    /**
     * 解析接口返回的json,封装成自定义实体列表
     */
    private List<ItemData> parseResponseJson(String jsonStr) {
        List<ItemData> list = new ArrayList<>();
        JSONObject rootJson = JSONObject.parseObject(jsonStr);
        // 判断接口返回成功
        if (!rootJson.getBooleanValue("succeeded")) {
            System.out.println("查询接口业务失败,提示:" + rootJson.getString("message"));
            return list;
        }
        JSONObject dataObj = rootJson.getJSONObject("data");
        JSONObject hitsObj = dataObj.getJSONObject("hits");
        JSONArray sourceArr = hitsObj.getJSONArray("source");
        if (sourceArr == null || sourceArr.isEmpty()) {
            System.out.println("当前页码无数据");
            return list;
        }
        // 循环每一条数据封装
        for (int i = 0; i < sourceArr.size(); i++) {
            JSONObject item = sourceArr.getJSONObject(i);
            ItemData data = new ItemData();
            data.setTitle(item.getString("title"));
            data.setAuthor(item.getString("author"));
            data.setSource(item.getString("journal_name"));
            data.setAbstractText(item.getString("abstract"));
            data.setPublishDate(item.getString("pub_date_display"));
            data.setKeyword(item.getString("keyword"));
            // 拼接详情链接
            String id = item.getString("id");
            if (id != null) {
                data.setDetailUrl(DETAIL_URL_PREFIX + id);
            }
            list.add(data);
        }
        return list;
    }

    // 内部实体:封装每条返回的数据
    public static class ItemData {
        private String title;
        private String author;
        private String source;
        private String abstractText;
        private String publishDate;
        private String keyword;
        private String detailUrl;

        // getter setter
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
        public String getAuthor() { return author; }
        public void setAuthor(String author) { this.author = author; }
        public String getSource() { return source; }
        public void setSource(String source) { this.source = source; }
        public String getAbstractText() { return abstractText; }
        public void setAbstractText(String abstractText) { this.abstractText = abstractText; }
        public String getPublishDate() { return publishDate; }
        public void setPublishDate(String publishDate) { this.publishDate = publishDate; }
        public String getKeyword() { return keyword; }
        public void setKeyword(String keyword) { this.keyword = keyword; }
        public String getDetailUrl() { return detailUrl; }
        public void setDetailUrl(String detailUrl) { this.detailUrl = detailUrl; }
    }

    // 测试main方法,直接运行就能测试
    public static void main(String[] args) {
        CommonTokenApiUtil util = new CommonTokenApiUtil();
        // 查询关键词,第一页
        List<ItemData> dataList = util.searchData("测试关键词", 1);
        for (ItemData item : dataList) {
            System.out.println("标题:" + item.getTitle() + " 作者:" + item.getAuthor());
        }
    }
}

四、代码大白话讲解

1. 顶部常量配置区

所有接口地址、分页大小、刷新时间全部写在最上面,以后换别的第三方接口,只改这几行,不用动下面业务代码。

2. 核心变量说明

  • cacheToken:存登录拿到的令牌,不用每次搜索都登录一次
  • tokenExpireTime:记录token什么时候过期,提前5分钟自动换新
  • HTTP_CLIENT:全局只用一个请求工具,频繁调用不会创建大量连接

3. 方法功能拆解

  1. searchData:对外调用入口,传入关键词和页码直接拿结果
  2. getAvailableToken:判断token是否过期,过期自动刷新(加了同步锁,多线程不会同时刷新)
  3. refreshToken:调用登录接口获取新token,更新缓存和过期时间
  4. buildSearchParam:组装查询接口需要的一长串json参数,单独抽出来方便修改字段
  5. sendSearchRequest:带上token请求头发送查询
  6. parseResponseJson:把后端返回的复杂json,转换成简单实体对象,方便业务使用

4. 容错逻辑

  • 拿不到token直接返回空列表,打印日志
  • 查询返回空自动刷新token重试一次,解决token中途失效问题
  • 所有网络、解析异常全部捕获打印,不会直接崩溃程序

五、使用方法

  1. 复制整个类到项目,无需引入父类、无需实现接口
  2. 修改顶部常量里的接口地址
  3. 在业务代码调用:
java 复制代码
CommonTokenApiUtil api = new CommonTokenApiUtil();
List<CommonTokenApiUtil.ItemData> data = api.searchData("人工智能", 1);
  1. 自带main函数,右键运行可直接调试接口,快速验证是否能正常拿到数据