Springboot集成ElasticSearch实现minio文件内容全文检索

一、docker安装Elasticsearch

(1)springboot和Elasticsearch的版本对应关系如下,请看版本对应:

 注意安装对应版本,否则可能会出现一些未知的错误。

(2)拉取镜像

docker pull elasticsearch:7.17.6

(3)运行容器

docker run  -it  -d  --name elasticsearch -e "discovery.type=single-node"  -e "ES_JAVA_OPTS=-Xms512m -Xmx1024m"   -p 9200:9200 -p 9300:9300  elasticsearch:7.17.6

 访问http://localhost:9200/,出现如下内容表示安装成功。

(4)安装中文分词器

进入容器:

docker exec -it elasticsearch bash

然后进入bin目录执行下载安装ik分词器命令:

elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.6/elasticsearch-analysis-ik-7.17.6.zip

退出bash并重启容器:

docker restart elasticsearch

二、安装kibana

Kibana 是为 Elasticsearch设计的开源分析和可视化平台。你可以使用 Kibana 来搜索,查看存储在 Elasticsearch 索引中的数据并与之交互。你可以很容易实现高级的数据分析和可视化,以图表的形式展现出来。

(1)拉取镜像

docker pull kibana:7.17.6

(2)运行容器 

docker run --name kibana -p 5601:5601 --link elasticsearch:es  -e "elasticsearch.hosts=http://es:9200"  -d kibana:7.17.6

--link elasticsearch:es表示容器互联,即容器kibana连接到elasticsearch。

(3)使用kibana dev_tools发送http请求操作Elasticsearch

 三、后端代码

(1)引入maven依赖

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId></dependency>

(2)application.yml配置

spring:elasticsearch:uris: http://localhost:9200

(3)实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;import java.util.Date;/*** @author yangfeng*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "file")
public class File {@Idprivate String id;/*** 文件名称*/@Field(type = FieldType.Text, analyzer = "ik_max_word")private String fileName;/*** 文件分类*/@Field(type = FieldType.Keyword)private String fileCategory;/*** 文件内容*/@Field(type = FieldType.Text, analyzer = "ik_max_word")private String fileContent;/*** 文件存储路径*/@Field(type = FieldType.Keyword, index = false)private String filePath;/*** 文件大小*/@Field(type = FieldType.Keyword, index = false)private Long fileSize;/*** 文件类型*/@Field(type = FieldType.Keyword, index = false)private String fileType;/*** 创建人*/@Field(type = FieldType.Keyword, index = false)private String createBy;/*** 创建日期*/@Field(type = FieldType.Keyword, index = false)private Date createTime;/*** 更新人*/@Field(type = FieldType.Keyword, index = false)private String updateBy;/*** 更新日期*/@Field(type = FieldType.Keyword, index = false)private Date updateTime;}

(4)repository接口,继承ElasticsearchRepository

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Highlight;
import org.springframework.data.elasticsearch.annotations.HighlightField;
import org.springframework.data.elasticsearch.annotations.HighlightParameters;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;import java.util.List;/*** @author yangfeng* @date: 2024年11月9日 15:29*/
@Repository
public interface FileRepository extends ElasticsearchRepository<File, String> {/*** 关键字查询** @return*/@Highlight(fields = {@HighlightField(name = "fileName"), @HighlightField(name = "fileContent")},parameters = @HighlightParameters(preTags = {"<span style='color:red'>"}, postTags = {"</span>"}, numberOfFragments = 0))List<SearchHit<File>> findByFileNameOrFileContent(String fileName, String fileContent, Pageable pageable);
}

(5)service接口

import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;import java.util.List;/*** description: ES文件服务** @author yangfeng* @version V1.0* @date 2023-02-21*/
public interface IFileService {/*** 保存文件*/void saveFile(String filePath, String fileCategory) throws Exception;/*** 关键字查询** @return*/List<SearchHit<File>> search(FileDTO dto);/*** 关键字查询** @return*/SearchHits<File> searchPage(FileDTO dto);
}

(6)service实现类

import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.MinioUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Objects;/*** description: ES文件服务** @author yangfeng* @version V1.0* @date 2023-02-21*/
@Slf4j
@Service
public class FileServiceImpl implements IFileService {@Autowiredprivate FileRepository fileRepository;@Autowiredprivate ElasticsearchRestTemplate elasticsearchRestTemplate;/*** 保存文件*/@Overridepublic void saveFile(String filePath, String fileCategory) throws Exception {if (Objects.isNull(filePath)) {throw new JeecgBootException("文件不存在");}LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();String fileName = CommonUtils.getFileNameByUrl(filePath);String fileType = StringUtils.isNotBlank(fileName) ? fileName.substring(fileName.lastIndexOf(".") + 1) : null;InputStream inputStream = MinioUtil.getMinioFile(filePath);// 读取文件内容,上传到es,方便后续的检索String fileContent = FileUtils.readFileContent(inputStream, fileType);File file = new File();file.setId(IdUtil.getSnowflake(1, 1).nextIdStr());file.setFileContent(fileContent);file.setFileName(fileName);file.setFilePath(filePath);file.setFileType(fileType);file.setFileCategory(fileCategory);file.setCreateBy(user.getUsername());file.setCreateTime(new Date());fileRepository.save(file);}/*** 关键字查询** @return*/@Overridepublic List<SearchHit<File>> search(FileDTO dto) {Pageable pageable = PageRequest.of(dto.getPageNo() - 1, dto.getPageSize(), Sort.Direction.DESC, "createTime");return fileRepository.findByFileNameOrFileContent(dto.getKeyword(), dto.getKeyword(), pageable);}@Overridepublic SearchHits<File> searchPage(FileDTO dto) {NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();queryBuilder.withQuery(QueryBuilders.multiMatchQuery(dto.getKeyword(), "fileName", "fileContent"));// 设置高亮HighlightBuilder highlightBuilder = new HighlightBuilder();String[] fieldNames = {"fileName", "fileContent"};for (String fieldName : fieldNames) {highlightBuilder.field(fieldName);}highlightBuilder.preTags("<span style='color:red'>");highlightBuilder.postTags("</span>");highlightBuilder.order();queryBuilder.withHighlightBuilder(highlightBuilder);// 也可以添加分页和排序queryBuilder.withSorts(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)).withPageable(PageRequest.of(dto.getPageNo() - 1, dto.getPageSize()));NativeSearchQuery nativeSearchQuery = queryBuilder.build();return elasticsearchRestTemplate.search(nativeSearchQuery, File.class);}}

(7)controller

import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 文件es操作** @author yangfeng* @since 2024-11-09*/
@Slf4j
@RestController
@RequestMapping("/elasticsearch/file")
public class FileController {@Autowiredprivate IFileService fileService;/*** 保存文件** @return*/@PostMapping(value = "/saveFile")public Result<?> saveFile(@RequestBody File file) throws Exception {fileService.saveFile(file.getFilePath(), file.getFileCategory());return Result.OK();}/*** 关键字查询-repository** @throws Exception*/@PostMapping(value = "/search")public Result<?> search(@RequestBody FileDTO dto) {return Result.OK(fileService.search(dto));}/*** 关键字查询-原生方法** @throws Exception*/@PostMapping(value = "/searchPage")public Result<?> searchPage(@RequestBody FileDTO dto) {return Result.OK(fileService.searchPage(dto));}}

(8)工具类

import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;@Slf4j
public class FileUtils {private static final List<String> FILE_TYPE;static {FILE_TYPE = Arrays.asList("pdf", "doc", "docx", "text");}public static String readFileContent(InputStream inputStream, String fileType) throws Exception{if (!FILE_TYPE.contains(fileType)) {return null;}// 使用PdfBox读取pdf文件内容if ("pdf".equalsIgnoreCase(fileType)) {return readPdfContent(inputStream);} else if ("doc".equalsIgnoreCase(fileType) || "docx".equalsIgnoreCase(fileType)) {return readDocOrDocxContent(inputStream);} else if ("text".equalsIgnoreCase(fileType)) {return readTextContent(inputStream);}return null;}private static String readPdfContent(InputStream inputStream) throws Exception {// 加载PDF文档PDDocument pdDocument = PDDocument.load(inputStream);// 创建PDFTextStripper对象, 提取文本PDFTextStripper textStripper = new PDFTextStripper();// 提取文本String content = textStripper.getText(pdDocument);// 关闭PDF文档pdDocument.close();return content;}private static String readDocOrDocxContent(InputStream inputStream) {try {// 加载DOC文档XWPFDocument document = new XWPFDocument(inputStream);// 2. 提取文本内容XWPFWordExtractor extractor = new XWPFWordExtractor(document);return extractor.getText();} catch (IOException e) {e.printStackTrace();return null;}}private static String readTextContent(InputStream inputStream) {StringBuilder content = new StringBuilder();try (InputStreamReader isr = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {int ch;while ((ch = isr.read()) != -1) {content.append((char) ch);}} catch (IOException e) {e.printStackTrace();return null;}return content.toString();}}

(9)dto

import lombok.Data;@Data
public class FileDTO {private String keyword;private Integer pageNo;private Integer pageSize;}

四、前端代码

(1)查询组件封装

<template><a-input-searchv-model:value="pageInfo.keyword"placeholder="全文检索"@search="handleSearch"style="width: 220px;margin-left:30px"/><a-modal v-model:visible="showSearch" title="全文检索" width="900px" :footer="null"destroy-on-close><SearchContent :items="searchItems" :loading="loading"/><div style="padding: 10px;display: flex;justify-content: flex-end"><Pagination v-if="pageInfo.total" :pageSize="pageInfo.pageSize" :pageNo="pageInfo.pageNo":total="pageInfo.total" @pageChange="changePage" :show-total="total => `共 ${total} 条`"/></div></a-modal>
</template><script lang="ts" setup>
import {ref} from 'vue'
import {Pagination} from "ant-design-vue";
import SearchContent from "@/components/ElasticSearch/SearchContent.vue"
import {searchPage} from "@/api/sys/elasticsearch"const loading = ref<boolean>(false)
const showSearch = ref<any>(false)
const searchItems = ref<any>();const pageInfo = ref<{pageNo: number;pageSize: number;keyword: string;total: number;
}>({// 当前页码pageNo: 1,// 当前每页显示多少条数据pageSize: 10,keyword: '',total: 0,
});async function handleSearch() {if (!pageInfo.value.keyword) {return;}pageInfo.value.pageNo = 1showSearch.value = trueawait getSearchItems();
}function changePage(pageNo) {pageInfo.value.pageNo = pageNogetSearchItems();
}async function getSearchItems() {loading.value = truetry {const res: any = await searchPage(pageInfo.value);searchItems.value = res?.searchHits;debuggerpageInfo.value.total = res?.totalHits} finally {loading.value = false}
}
</script><style scoped></style>

(2)接口elasticsearch.ts

import {defHttp} from '/@/utils/http/axios';enum Api {saveFile = '/elasticsearch/file/saveFile',searchPage = '/elasticsearch/file/searchPage',
}/*** 保存文件到es* @param params*/
export const saveFile = (params) => defHttp.post({url: Api.saveFile,params
});/*** 关键字查询-原生方法* @param params*/
export const searchPage = (params) => defHttp.post({url: Api.searchPage,params
},);

(3)搜索内容组件SearchContent.vue

<template><a-spin :spinning="loading"><div class="searchContent"><div v-for="(item,index) in items" :key="index" v-if="!!items.length > 0"><a-card class="contentCard"><template #title><a @click="detailSearch(item.content)"><div class="flex" style="align-items: center"><div><img src="../../assets/images/pdf.png" v-if="item?.content?.fileType=='pdf'" style="width: 20px"/><img src="../../assets/images/word.png" v-if="item?.content?.fileType=='word'" style="width: 20px"/><img src="../../assets/images/excel.png" v-if="item?.content?.fileType=='excel'" style="width: 20px"/></div><div style="margin-left:10px"><article class="article" v-html="item.highlightFields.fileName"v-if="item?.highlightFields?.fileName"></article><span v-else>{{ item?.content?.fileName }}</span></div></div></a></template><div class="item"><article class="article" v-html="item.highlightFields.fileContent"v-if="item?.highlightFields?.fileContent"></article><span v-else>{{item?.content?.fileContent?.length > 150 ? item.content.fileContent.substring(0, 150) + '......' : item.content.fileContent}}</span></div></a-card></div><EmptyData v-else/></div></a-spin>
</template>
<script lang="ts" setup>
import {useGlobSetting} from "@/hooks/setting";
import EmptyData from "/@/components/ElasticSearch/EmptyData.vue";
import {ref} from "vue";const glob = useGlobSetting();const props = defineProps({loading: {type: Boolean,default: false},items: {type: Array,default: []},
})function detailSearch(searchItem) {const url = ref(`${glob.domainUrl}/sys/common/pdf/preview/`);window.open(url.value + searchItem.filePath + '#scrollbars=0&toolbar=0&statusbar=0', '_blank');
}</script>
<style lang="less" scoped>
.searchContent {min-height: 500px;overflow-y: auto;
}.contentCard {margin: 10px 20px;
}a {color: black;
}a:hover {color: #3370ff;
}:deep(.ant-card-body) {padding: 13px;
}
</style>

五、效果展示

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/61047.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

部分利用oracle数据字典查询对应信息的语句。

查看当前用户的缺省表空间 SQL>select username,default_tablespace from user_users; 查看当前用户的角色 SQL>select * from user_role_privs; 查看当前用户的系统权限和表级权限 SQL>select * from user_sys_privs; SQL>select * from user_tab_privs; …

#Ts篇: ts学习再梳理

ts类型梳理 类型声明的写法&#xff0c;一律为在标识符后面添加“冒号 类型”。函数参数和返回值&#xff0c;也是这样来声明类型。 function toString(num: number): string {return String(num); }上面示例中&#xff0c;函数toString()的参数num的类型是number。参数列表…

使用chrome 访问虚拟机Apache2 的默认页面,出现了ERR_ADDRESS_UNREACHABLE这个鸟问题

本地环境 主机MacOs Sequoia 15.1虚拟机Parallels Desktop 20 for Mac Pro Edition 版本 20.0.1 (55659)虚拟机-操作系统Ubuntu 22.04 服务器版本 最小安装 开发环境 编辑器编译器调试工具数据库http服务web开发防火墙Vim9Gcc13Gdb14Mysql8Apache2Php8.3Iptables 第一坑 数…

定时器的小应用

第一个项目 第一步&#xff0c;RCC开启时钟&#xff0c;这个基本上每个代码都是第一步&#xff0c;不用多想&#xff0c;在这里打开时钟后&#xff0c;定时器的基准时钟和整个外设的工作时钟就都会同时打开了 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);第二步&…

【工控】线扫相机小结 第三篇

海康软件更新 目前使用的是 MVS_STD_4.3.2_240705.exe &#xff0c;最新的已经到4.4了。 一个大的变动 在上一篇中我们提到一个问题&#xff1a; 需要注意的是&#xff0c;我们必须先设置 TriggerSelector 是 “FrameBurstStart” 还是 “LineStart” 再设置TriggerMode 是 …

零基础利用实战项目学会Pytorch

目录 pytorch简介 1.线性回归 2.数据类型 2.1数据类型检验 2.2Dimension0/Rank0 2.3 Dim1/Rank1 2.4 Dim2/Rank2 3.一些方法 4.Pytorch完成分类任务 4.1模型参数 4.2 前向传播 4.3训练以及验证 4.4 三行搞定&#xff01; 4.5 准确率 5、Pytorch完成回归任务 5.…

MySQL的游标和While循环的详细对比

MySQL游标和While循环的详细对比 在 MySQL 中&#xff0c;游标和 WHILE 循环是两种常用的处理结果集的机制。它们各自有不同的应用场景和特点。本文将详细对比这两种机制&#xff0c;并提供具体的示例代码和说明。 1. 游标&#xff08;Cursor&#xff09; 游标是一种数据库对…

【#IEEE独立出版、EI稳定检索##高录用 快见刊 稳检索#】2024健康大数据与智能医疗国际会议(ICHIH 2024,12月13-15日)

#IEEE独立出版、EI稳定检索# #往届快至会后3-4个月检索# #高录用 快见刊 稳检索# 2024健康大数据与智能医疗国际会议&#xff08;ICHIH 2024&#xff09; 2024 International Conference on Health Big Data and Intelligent Healthcare 重要信息 大会官网&#xff1a;ww…

C++网络编程之SSL/TLS加密通信

概述 在互联网时代&#xff0c;数据的安全性变得尤为重要。随着网络安全威胁的不断增加&#xff0c;确保信息传输过程中的机密性、完整性和可用性成为了开发者必须考虑的关键因素。在C网络编程中&#xff0c;使用SSL/TLS加密通信是一种常见的做法。它允许客户端和服务器之间通过…

vue3中ElementPlus引入下载icon图标不显示透明问题解决教程方法

问题&#xff1a;今天用vue3开发&#xff0c;使用ElementPlus图标引入了但是不显示&#xff0c;是空白透明 解决&#xff1a; 1、在main.js中引入element-plus/icons-vue图标库 import * as ElIcons from element-plus/icons-vue; // 引入图标库 2、注册所有图标 // 注册所有…

1+X应急响应(网络)系统加固:

系统加固&#xff1a; 数据库的重要性&#xff1a; 数据库面临的风险&#xff1a; 数据库加固&#xff1a; 业务系统加固&#xff1a; 安全设备加固&#xff1a; 网络设备加固&#xff1a;

蓝牙 HFP 协议详解及 Android 实现

文章目录 前言一、什么是蓝牙 HFP 协议&#xff1f;HFP 的核心功能HFP 的核心功能HFP 在 Android 中的典型应用场景 二、HFP 协议的工作流程HFP 的连接流程 三、HFP 在 Android 的实现1. 检查蓝牙适配器状态2. 发现并检测支持 HFP 的设备3. 获取 BluetoothHeadset 服务4. 连接设…

Prometheus面试内容整理-生态系统和集成

Prometheus 作为云原生监控的关键工具之一,拥有广泛的生态系统,并能与多种其他系统和工具集成,从而实现更加全面、灵活的监控方案。Prometheus 的生态系统不仅包括内置的各类组件(如 Alertmanager),还涉及与可视化工具、自动化服务发现平台以及持久化存储的集成。以下是关…

Web导出Excel表格

背景&#xff1a; 1. 后端主导实现 流程&#xff1a;前端调用到导出excel接口 -> 后端返回excel文件流 -> 浏览器会识别并自动下载 场景&#xff1a;大部分场景都有后端来做 2. 前端主导实现 流程&#xff1a;前端获取要导出的数据 -> 常规数据用插件处理成一个e…

【Linux】Github 仓库克隆速度慢/无法克隆的一种解决方法,利用 Gitee 克隆 Github 仓库

Github 经常由于 DNS 域名污染以及其他因素克隆不顺利。 一种办法是修改 hosts sudo gedit /etc/hosts加上一行 XXX.XXX.XXX.XXX github.comXXX 位置的 IP 可以通过网站查询 IP/服务器github.com的信息-站长工具 这种方法比较适合本身可以克隆&#xff0c;但是速度很慢的…

理解反射,学会反射:撬开私有性质(private)的属性与方法

看到这句话的时候证明&#xff1a;此刻你我都在努力 加油陌生人 个人主页&#xff1a;Gu Gu Study专栏&#xff1a;用Java学习数据结构系列喜欢的一句话&#xff1a; 常常会回顾努力的自己&#xff0c;所以要为自己的努力留下足迹喜欢的话可以点个赞谢谢了。作者&#xff1a;小…

函数式编程(4) 纯函数

纯函数&#xff1a;每次调用时结果总是一样 这个函数不纯&#xff1a;返回的值有变化 这个函数不纯:因为使用了全局变量minAge 要让它变成纯函数需要 纯函数的好处 immutability/不变性 如果创建了一个对象 js中的const阻止了重新分配一个对象给book, 而并不能阻止给更改其titl…

【Golang】——Gin 框架中间件详解:从基础到实战

中间件是 Web 应用开发中常见的功能模块&#xff0c;Gin 框架支持自定义和使用内置的中间件&#xff0c;让你在请求到达路由处理函数前进行一系列预处理操作。这篇博客将涵盖中间件的概念、内置中间件的用法、如何编写自定义中间件&#xff0c;以及在实际应用中的一些最佳实践。…

Python爬虫知识体系-----requests-----持续更新

数据科学、数据分析、人工智能必备知识汇总-----Python爬虫-----持续更新&#xff1a;https://blog.csdn.net/grd_java/article/details/140574349 文章目录 一、安装和基本使用1. 安装2. 基本使用3. response常用属性 二、get请求三、post请求四、代理 一、安装和基本使用 1.…

15分钟学 Go 第 56 天:架构设计基本原则

第56天&#xff1a;架构设计基本原则 学习目标 理解和掌握基本的架构设计原则&#xff0c;以提升软件系统的可维护性、可扩展性和可重用性。 内容提纲 架构设计原则概述常见架构设计原则 单一职责原则 (SRP)开放/封闭原则 (OCP)里氏替换原则 (LSP)接口分离原则 (ISP)依赖反…