Deprecated: Creation of dynamic property Typecho\Widget\Request::$feed is deprecated in /www/wwwroot/blog.iletter.top/var/Widget/Archive.php on line 246
白荼日记 - java https://blog.iletter.top/index.php/tag/java/ 建筑类网站爬虫 https://blog.iletter.top/index.php/archives/364.html 2025-07-04T20:26:41+08:00 最近帮我同学写相关建筑类网站的爬虫以及前后端搜索界面功能,其实技术要点一个没有,涉及到加密的网站我也是放弃爬虫,解密太麻烦了。简单的网站都是一套的逻辑爬虫,大家可以参考一下。有兴趣的话帮忙点个start支持一下前后端系统以及数据库https://gitee.com/wonder19991209/mohurd\_search\_sys爬虫脚本https://gitee.com/wonder19991209/mohurd-spider Centos7配置java环境 https://blog.iletter.top/index.php/archives/345.html 2024-12-27T10:59:30+08:00 下载:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html解压文件tar -zxvf jdk-8u431-linux-x64.tar.gz -C /home/env/java/设置环境变量vim /etc/profile在文件最末尾加上export JAVA_HOME=/home/env/java/jdk1.8.0_431 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH=${JAVA_HOME}/bin:$PATH使环境变量生效source /etc/profile配置软连接ln -s /home/env/java/jdk1.8.0_431/bin/java /usr/bin/java检查java -version删除软连接并重新配置确认现有软连接ls -l /usr/bin/java删除rm /usr/bin/java重新配置ln -s /env/java/jdk-21.0.3/bin/java /usr/bin/java java项目引入resources下面的文件 https://blog.iletter.top/index.php/archives/173.html 2024-09-26T23:57:00+08:00 好久没写java,差点都忘了。。。起因是一个资源文件在resources目录下面,转换后的地址类型是string类型不是file,于是用类加载器获取文件资源路径String dbPath = GetIpAddr.class.getClassLoader().getResource("ip2region/ip2region.xdb").getPath();但是这样在编译部署完毕后会报错Generating unique operation named: userInfoUsingGET_1 2024-09-26 17:15:40.345 INFO 3992 --- [nio-8081-exec-1] o.a.c.c.C.[.[localhost].[/sys_api] : Initializing Spring DispatcherServlet 'dispatcherServlet' failed to load vector index from file:/C:/Users/Administrator/Desktop/jar/api.jar!/BOOT-INF/classes!/ip2region/ip2region.xdb: java.io.FileNotFoundException: file:\C:\Users\Administrator\Desktop\jar\api.jar!\BOOT-INF\classes!\ip2region\ip2region.xdb (文件名、目录名或卷标语法不正确。) 2024-09-26 17:15:40.664 ERROR 3992 --- [nio-8081-exec-1] i.d.exception.RenExceptionHandler : ### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'ip_addr' at row 1 ### The error may exist in URL [jar:file:/C:/Users/Administrator/Desktop/jar/api.jar!/BOOT-INF/classes!/mapper/MarkVisiter.xml] ### The error may involve io.dellevin.dao.MarkVisiterDao.insertVisitRecord-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO mark_visiter ( visit_url, ip, ip_addr, create_time) VALUES ( ?, ?, ?, NOW() ) ### Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'ip_addr' at row 1 ; Data truncation: Data too long for column 'ip_addr' at row 1; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'ip_addr' at row 1 org.springframework.dao.DataIntegrityViolationException:只能用流方式加载文件,于是采用如下方式:可以使用一个临时文件来处理这个问题。首先读取输入流,然后将其写入一个临时文件,最后将该临时文件的路径作为 dbPathString dbPath = null; // 1、从 dbPath 中预先加载 VectorIndex 缓存 try (InputStream inputStream = GetIpAddr.class.getClassLoader().getResourceAsStream("ip2region/ip2region.xdb")) { if (inputStream == null) { throw new FileNotFoundException("Resource not found: ip2region/ip2region.xdb"); } // 创建临时文件 File tempFile = Files.createTempFile("ip2region", ".xdb").toFile(); try (FileOutputStream outputStream = new FileOutputStream(tempFile)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } // 获取临时文件路径 dbPath = tempFile.getAbsolutePath(); } catch (IOException e) { System.out.printf("failed to load vector index: %s\n", e); return "failed to load vector index: " + e + " "; } 简单优化-针对大数据查询的一些小思路 https://blog.iletter.top/index.php/archives/156.html 2024-04-10T23:50:00+08:00 我的member_user里面有352599条数据,gp_project里面有1211974条数据,请问该如何优化这个sql的查询效率SELECT gp.*, mu.linkmanName , mu.linkmanPhone, mu.legalPersonName, mu.legalPersonPhone, mu.address, mu.registerArea FROM gp_project gp LEFT JOIN member_user mu ON mu.supplierId = gp.supplier_id WHERE DATE(gp.publicity_time) >= '2024-04-03' AND DATE(gp.publicity_time) <= '2024-04-07' and mu.deleted = 0针对这样的数据,我的数据表里面数据太多,导致了查询需要512秒的时间,而且这仅仅是两个表的关联,之后还需要关联四五个表组成一条查询语句,如果仅在mysql里面查,未免有些太为难服务器了。因为平常找数据用的sql里面的in语法比较多,所以尝试了一下用关联字段in数据的情况,突然发现,这样还挺快,主要是因为关联字段是索引,所以比较快,通过单表查出数据,然后将关键字段提取出来,in到另一个表里面快速查询,最后拼接字段,来实现数据的查询结果。另外因为涉及到多个循环,如果仅仅遵循 循环的外小内大原则,这样未免要写多个循环,降低了程序的运行效率多个O(n^2)的程序跑的,这样未免太致命了。改用map的key和value对照关系这样通过一个循环+map的方式降低时间复杂度为O(1) ,这样就会快很多下面是操作案例mybaits部分(单个案例) <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.xyjq.mapper.db1.LoginMemberUserDao"> <!--根据供应商id判断是否为注册用户 --> <select id="isRegisterSupplier" resultType="com.xyjq.entity.db1.LoginMemberUserEntity"> SELECT supplierId,createTime FROM `login_member_user` WHERE supplierId in <foreach collection="supplierIdList" item="supplierId" open="(" separator="," close=")"> #{supplierId} </foreach> </select> </mapper> 业务层部分List<GpProjectEntity> list = baseDao.queryList(params); // 提取查询结果里面的supplierID的set集合 Set<Integer> supplierIdList = list.stream() .map(GpProjectEntity::getSupplierId) .filter(Objects::nonNull) .collect(Collectors.toSet()); SimpleDateFormat inputFormat = new SimpleDateFormat("yyyyMMddHHmmss"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 以下遍历中防止嵌套for循环O(n^2),使用map对象存储降低时间复杂度为O(1) // 优化设置供应商信息 Map<String, MemberUserEntity> memberUserInfoMap = new HashMap<>(); memberUserDao.getMemberUserInfo(supplierIdList).forEach(memberUser -> memberUserInfoMap.put(memberUser.getSupplierId().toString(), memberUser)); // 设置供应商的工商地址信息 Map<String, SupplierBussinessInformationEntity> supplierBusinessInfoMap = new HashMap<>(); supplierBussinessInformationDao.getSupplierBusinessInfo(supplierIdList).forEach(supplierBusinessInfo -> supplierBusinessInfoMap.put(supplierBusinessInfo.getSupplierId().toString(), supplierBusinessInfo)); // 设置是否是注册用户 Map<String, LoginMemberUserEntity> loginMemberUserMap = new HashMap<>(); loginMemberUserDao.isRegisterSupplier(supplierIdList).forEach(loginMemberUser -> loginMemberUserMap.put(loginMemberUser.getSupplierId().toString(), loginMemberUser)); // 设置是否有融资需求 Map<String, String> financingNeedsMap = new HashMap<>(); // 根据搜索结果,如果key一样就拼接value financingNeedsDao.getCompFinancingNeeds(supplierIdList).forEach(financingNeeds -> { String key = financingNeeds.getSupplierId(); String value = FinancingStatus.getDescriptionByCode(financingNeeds.getFinancingProgress()) + "(" + financingNeeds.getCreateTime() + ")"; financingNeedsMap.merge(key, value, (oldValue, newValue) -> oldValue + ", " + newValue); }); for (GpProjectEntity project : list) { // 设置项目名称 if (project.getAgName() != null && !project.getAgName().isEmpty()) { project.setProjectNameMergeAgBid(project.getAgName()); } else if (project.getJatTpProName() != null && !project.getJatTpProName().isEmpty()) { project.setProjectNameMergeAgBid(project.getJatTpProName()); } // 设置项目金额 if (project.getAgTotalMoney() != null) { project.setProjectMoneyMergeAgBid(project.getAgTotalMoney()); } else { project.setProjectMoneyMergeAgBid(project.getWinPriceRevised()); } // 设置项目的日期 if (project.getAgSignDate() != null) { Date date = null; try { date = inputFormat.parse(project.getAgSignDate()); } catch (ParseException e) { throw new RuntimeException(e); } project.setProjectDateMergeAgBid(dateFormat.format(date)); } else { project.setProjectDateMergeAgBid( dateFormat.format(project.getJatWinTime()) ); } String supplierId = project.getSupplierId().toString(); // 设置供应商信息 MemberUserEntity memberUser = memberUserInfoMap.get(supplierId); if (memberUser != null) { project.setProjectLinkManName(memberUser.getLinkmanName()); project.setProjectLinkManPhone(memberUser.getLinkmanPhone()); project.setProjectLegalPersonName(memberUser.getLegalPersonName()); project.setProjectLegalPersonPhone(memberUser.getLegaPersonPhone()); project.setProjectAddress(memberUser.getAddress()); project.setProjectRegisterArea(memberUser.getRegisterArea()); } // 设置工商信息省市区 SupplierBussinessInformationEntity supplierBussinessInformation = supplierBusinessInfoMap.get(supplierId); if (supplierBussinessInformation != null) { project.setProjectProvince(supplierBussinessInformation.getProvince()); project.setProjectCity(supplierBussinessInformation.getCity()); project.setProjectDistrict(supplierBussinessInformation.getDistrict()); } // 设置是否是注册用户 LoginMemberUserEntity loginMemberUser = loginMemberUserMap.get(supplierId); if (loginMemberUser != null) { project.setProjectIsRegisterSupplier("已注册(" + dateFormat.format(loginMemberUser.getCreateTime()) + ")"); } // 设置融资历史记录 String financingNeeds = financingNeedsMap.get(supplierId); if (financingNeeds != null) { project.setProjectFinancingNeeds(financingNeeds); } } 判断两个事件是否存在冲突 https://blog.iletter.top/index.php/archives/132.html 2023-05-17T23:48:00+08:00 给你两个字符串数组 event1 和 event2 ,表示发生在同一天的两个闭区间时间段事件,其中:event1 = [startTime1, endTime1]event2 = [startTime2, endTime2]事件的时间为有效的 24 小时制且按 HH:MM 格式给出。当两个事件存在某个非空的交集时(即,某些时刻是两个事件都包含的),则认为出现 冲突 。如果两个事件之间存在冲突,返回 true ;否则,返回 false 。示例 1:输入:event1 = ["01:15","02:00"], event2 = ["02:00","03:00"] 输出:true 解释:两个事件在 2:00 出现交集。示例 2:输入:event1 = ["01:00","02:00"], event2 = ["01:20","03:00"] 输出:true 解释:两个事件的交集从 01:20 开始,到 02:00 结束。示例 3:输入:event1 = ["10:00","11:00"], event2 = ["14:00","15:00"] 输出:false 解释:两个事件不存在交集。提示:evnet1.length == event2.length == 2.event1[i].length == event2[i].length == 5startTime1 <= endTime1startTime2 <= endTime2所有事件的时间都按照 HH:MM 格式给出代码class Solution { public boolean haveConflict(String[] event1, String[] event2) { int event1StartTime = strToIntTime(event1[0]); int event1EndTime = strToIntTime(event1[1]); int event2StartTime = strToIntTime(event2[0]); int event2EndTime = strToIntTime(event2[1]); if(event1EndTime < event2StartTime || event2EndTime <event1StartTime ){ return false; } return true; } public static int strToIntTime(String str) { String[] ss = str.split(":"); int hours = Integer.parseInt(ss[0])*60; return hours+Integer.parseInt(ss[1]); } }思路比对时间不在另一个区间内就可以了,但是string不能进行比较,只能使用int类型比较。 负二进制数相加 https://blog.iletter.top/index.php/archives/145.html 2023-05-07T23:48:00+08:00 给出基数为 -2 的两个数 arr1 和 arr2,返回两数相加的结果。数字以 数组形式 给出:数组由若干 0 和 1 组成,按最高有效位到最低有效位的顺序排列。例如,arr = [1,1,0,1] 表示数字 (-2)^3 + (-2)^2 + (-2)^0 = -3。数组形式 中的数字 arr 也同样不含前导零:即 arr == [0] 或 arr[0] == 1。返回相同表示形式的 arr1 和 arr2 相加的结果。两数的表示形式为:不含前导零、由若干 0 和 1 组成的数组。示例 1:输入:arr1 = [1,1,1,1,1], arr2 = [1,0,1] 输出:[1,0,0,0,0] 解释:arr1 表示 11,arr2 表示 5,输出表示 16 。示例 2:输入:arr1 = [0], arr2 = [0] 输出:[0]示例 3:输入:arr1 = [0], arr2 = [1] 输出:[1]提示:1 <= arr1.length, arr2.length <= 1000arr1[i] 和 arr2[i] 都是 0 或 1arr1 和 arr2 都没有前导0代码:class Solution { public int[] addNegabinary(int[] arr1, int[] arr2) { // 定义规则:0+1=1,进位向前边减一,够减则减,不够减则借位 int[] outCome = null; if (arr1.length > arr2.length) { for (int i = 1; i <= arr2.length; i++) { arr1[arr1.length - i] += arr2[arr2.length - i]; } outCome = arr1; } else { for (int i = 1; i <= arr1.length; i++) { arr2[arr2.length - i] += arr1[arr1.length - i]; } outCome = arr2; } // System.out.println(Arrays.toString(outCome)); int add = 0; for (int i = outCome.length - 1; i > 0; i--) { if (outCome[i] + add >= 2) { add = -1; outCome[i] -= 2; } else if (outCome[i] + add < 0) { outCome[i - 1] += 1; outCome[i] = 1; add = 0; } else { outCome[i] += add; add = 0; } } outCome[0] += add; //System.out.println(Arrays.toString(outCome)); if (outCome[0] >= 2) { outCome[0] -= 2; int[] newOutCome = new int[outCome.length + 2]; newOutCome[0] = 1; newOutCome[1] = 1; for (int i = 0; i < outCome.length; i++) { newOutCome[2 + i] = outCome[i]; } outCome = newOutCome; } // System.out.println(Arrays.toString(outCome)); if (outCome[0] == 0) { int zlength = 0; while (zlength < outCome.length && outCome[zlength] == 0) { zlength++; } System.out.println(zlength); if (zlength == outCome.length) { outCome = new int[1]; outCome[0] = 0; } else { int[] newOutCome = new int[outCome.length - zlength]; for (int i = 0; i < newOutCome.length; i++) { newOutCome[i] = outCome[zlength + i]; } outCome = newOutCome; } } return outCome; } } 错误的办法:这里主要错误的原因是因为忽略了数组之间的长度,因为长度是1000也就是最大的数是-2的1000次幂。远远的超出了bigint,int,以及long类型的计算长度。所以下面的办法是妥妥的不对的关键字所占位数范围int32−231−231 ~ 2 ^{31} - 1long64−263−263 ~ 2 ^{63} - 1class Solution { public int[] addNegabinary(int[] arr1, int[] arr2) { int xx = lowBinaryToInt(arr1) + lowBinaryToInt(arr2); String ss= intToLowBinaryString(xx); String[] ssArr = ss.split(""); int[] res = new int[ss.length()]; for (int i = 0; i <= ss.length()-1; i++) { res[i] = Integer.parseInt(ssArr[i]); } return res; } public static int lowBinaryToInt(int[] arr) { double res=0; int xx = 0; for (int i = arr.length - 1; i>=0; i--) { if (arr[i] == 1) { res = res+Math.pow(-2, xx); xx++; }else{ xx++; } } return (int)res; } public static String intToLowBinaryString(int N) { StringBuilder sb = new StringBuilder(); while (N != 0) { int r = N % -2; N = N / -2; if (r < 0) { r = r + 2; N = N+1; } sb.append(r); } return sb.length() > 0 ? sb.reverse().toString() : "0"; } } vue+springboot实现预览word https://blog.iletter.top/index.php/archives/112.html 2023-04-29T23:48:00+08:00 其实这个有很多办法可以实现。利用office的在线预览,vue的依赖,转换成pdf进行预览。三个办法我都试了。微软官方的office在线预览的话需要一个公网ip,同时有的时候还需要你科学上网,这个直接寄。word官方预览链接(支持三种格式,部分word带特殊符号或流程图无法显示) const routeUrl = file.url; // 文件路径 const url = encodeURIComponent(routeUrl); const officeUrl = "https://view.xdocin.com/view?src=" + url;​ office官方预览(pdf不能展示)let officeUrl = 'http://view.officeapps.live.com/op/view.aspx?src='+urlvue的依赖的话,GitHub上搜索一下会有很多。vue-office这个项目很棒,可惜我引入包和标签的时候会报错,人家用的是js,而我用的是ts,这个也pass了。这个是项目地址,非常棒的一个项目,可以参考学习一下:https://github.com/501351981/vue-office到最后,还要回到word转换成office的破办法,这是我最难接受的一个,用的人一多,后端压力就大了,万一流量上来了,服务器直接开摆,笑死。但是实在是没办法了,退退退而求其次,只能这样了。主要思路是前端把文件id获取到,去数据库搜索到存放word的url地址,后端通过url地址把doc文档转换成输入流,然后通过输出流存放到一个专属的pdf文件夹,再把这个pdf生成链接给前端,前端显示的话就用这一个链接进行显示。前端代码<template> <el-dialog v-model="visible" :title="$t('文件预览')" :close-on-click-modal="false" :close-on-press-escape="false"> <div v-if="fileMessage.fileType == 'jpg' || fileMessage.fileType == 'png' || fileMessage.fileType == 'ico' || fileMessage.fileType == 'gif' || fileMessage.fileType == 'webp' "> <el-image style="width: 100%" :src="fileMessage.getFilePath"> </el-image> </div> <div v-else-if="fileMessage.fileType == 'docx' || fileMessage.fileType == 'doc'"> <embed :src="result.url" style="width: 100%; height: 600px" /> <!-- <h2 style="text-align: center;color: brown; ">暂不支持doc、docx格式!</h2> --> </div> <div v-else-if="fileMessage.fileType == 'xlsx' || fileMessage.fileType == 'xls'"> <h2 style="text-align: center;color: brown; ">暂不支持该xlsx、xls格式!</h2> </div> <div v-else-if="fileMessage.fileType == 'pdf'"> <embed :src="fileMessage.getFilePath" style="width: 100%; height: 600px" /> </div> <div v-else> <h2 style="text-align: center;color: brown; ">暂不支持该文件格式!</h2> </div> <template v-slot:footer> <el-button @click="visible = false">{{ $t("退出预览") }}</el-button> </template> </el-dialog> </template> <script lang="ts" setup> import { reactive, ref } from "vue"; import baseService from "@/service/baseService"; import { ElMessage } from "element-plus"; const visible = ref(false); const dataFormRef = ref(); const fileMessage = reactive({ getFilePath: "", fileType: "" }); const dataForm = reactive({ uploadFileId: "", uploadCompanyName: "", uploadFileName: "", uploadFilePath: "", uploadUserName: "", uploadFileTime: "", }); const init = (uploadFileId?: number) => { visible.value = true; dataForm.uploadFileId = ""; //重置表单数据 if (dataFormRef.value) { dataFormRef.value.resetFields(); } if (uploadFileId) { getInfo(uploadFileId); } }; const result = reactive({ url: "", msg: "" }); //var showType: string; // 获取信息 const getInfo = (uploadFileId: number) => { baseService.get("/uploadFile/xyjqfileupload/" + uploadFileId).then((res) => { Object.assign(dataForm, res.data); if (dataForm.uploadFilePath != "") { fileMessage.getFilePath = dataForm.uploadFilePath; //获取到图片的路径,写在这里面才管用,写在外面,第一次请求时候没有数据 //console.log(getFilePath.substring(getFilePath.lastIndexOf(".") + 1, getFilePath.length)); //console.log(getFilePath); fileMessage.fileType = fileMessage.getFilePath.substring(fileMessage.getFilePath.lastIndexOf(".") + 1, fileMessage.getFilePath.length); //showType = fileType; console.log("文件类型:" + fileMessage.fileType); console.log(fileMessage.getFilePath); if (fileMessage.fileType == "doc" || fileMessage.fileType == "docx") { baseService .post(`/uploadFile/xyjqfileupload/pdfUrl/${uploadFileId}`) .then((res) => { if (res.code === 0) { result.url = res.data; console.log(res.data); } else { ElMessage.error(res.msg); result.msg = res.msg; } }) .catch((err) => { ElMessage.error(err.message); }); } } else { ElMessage.error("链接地址为空,无法打开"); } }); }; defineExpose({ init }); </script> 主要核心代码是这里// 获取信息 const getInfo = (uploadFileId: number) => { baseService.get("/uploadFile/xyjqfileupload/" + uploadFileId).then((res) => { Object.assign(dataForm, res.data); if (dataForm.uploadFilePath != "") { fileMessage.getFilePath = dataForm.uploadFilePath; //获取到图片的路径,写在这里面才管用,写在外面,第一次请求时候没有数据 //console.log(getFilePath.substring(getFilePath.lastIndexOf(".") + 1, getFilePath.length)); //console.log(getFilePath); fileMessage.fileType = fileMessage.getFilePath.substring(fileMessage.getFilePath.lastIndexOf(".") + 1, fileMessage.getFilePath.length); //showType = fileType; console.log("文件类型:" + fileMessage.fileType); console.log(fileMessage.getFilePath); if (fileMessage.fileType == "doc" || fileMessage.fileType == "docx") { baseService .post(`/uploadFile/xyjqfileupload/pdfUrl/${uploadFileId}`) .then((res) => { if (res.code === 0) { result.url = res.data; console.log(res.data); } else { ElMessage.error(res.msg); result.msg = res.msg; } }) .catch((err) => { ElMessage.error(err.message); }); } } else { ElMessage.error("链接地址为空,无法打开"); } }); };主要就是获取到文件之后看看文件后缀,欸!是doc或者docx格式的,那就继续post后端,把文件id发过去,这样后端就能通过id在数据库里面找到文件的在线链接。然后进行下载解析word。后端部分代码 @PostMapping("/pdfUrl/{fileId}") @ApiOperation("WORD转换PdF") @LogOperation("WORD转换pdf") public Result wordToPdf(@PathVariable("fileId") String fileId) throws Exception { XyjqFileUploadEntity xyjqFileUploadEntity = xyjqFileUploadService.selectById(fileId); //获取到url地址 String url = xyjqFileUploadEntity.getUploadFilePath(); //给新保存的文件起一个新的名字 String strFileNameAndPoint = url.substring(url.lastIndexOf("/")+1); String newFileName = strFileNameAndPoint.substring(0,strFileNameAndPoint.lastIndexOf("."))+"_pdf.pdf"; System.out.println(newFileName); String paths = "D:\\Archive\\Desktop\\uploadFile\\WordToPdf\\"+newFileName; //先提前构建一个生成的pdf保存路径 File outputFile = new File(paths); //先把获取到的文件转换成输入流的模式 URL receiveUrl = new URL(url); HttpURLConnection conn = (HttpURLConnection)receiveUrl.openConnection(); //设置超时间为3秒 conn.setConnectTimeout(3*1000); //将传输过来的doc文件转换成流 InputStream docxInputStream = conn.getInputStream(); try { OutputStream outputStream = new FileOutputStream(outputFile); IConverter converter = LocalConverter.builder().build(); converter. convert(docxInputStream).as(DocumentType.DOCX).//作为doc格式 to(outputStream).as(DocumentType.PDF). execute(); outputStream.close(); System.out.println("Word To Pdf 转换完成"); } catch (Exception e) { e.printStackTrace(); } //将file格式转换成MultipartFile格式 File file = new File(paths); MultipartFile cMultiFile = getMultipartFile(file); //获取到新的url返还给前端进行预览 String newUrl = OSSFactory.build().uploadSuffix(cMultiFile.getBytes(), "pdf"); System.out.println(newUrl); return new Result().ok(newUrl); } public static MultipartFile getMultipartFile(File file) { DiskFileItem item = new DiskFileItem("file" , MediaType.MULTIPART_FORM_DATA_VALUE , true , file.getName() , (int)file.length() , file.getParentFile()); try { OutputStream os = item.getOutputStream(); os.write(FileUtils.readFileToByteArray(file)); } catch (IOException e) { e.printStackTrace(); } return new CommonsMultipartFile(item); }我开始是想传输url的,但是post之后,发现失败,原来前端不承认这样的链接啊,我也是太天真了,所以只能这么做了。哦对,还需要引入两个依赖 <!-- 转换成PDF--> <dependency> <groupId>com.documents4j</groupId> <artifactId>documents4j-local</artifactId> <version>1.0.3</version> </dependency> <dependency> <groupId>com.documents4j</groupId> <artifactId>documents4j-transformer-msoffice-word</artifactId> <version>1.0.3</version> </dependency>基本情况是这个样子,中间还少了一些判断,比如第二次打开的时候,这个链接其实以及生成一个pdf了,再次预览的话,又要生成一个一样的就覆盖了,实在是太占用资源了,还不如去mysql里面做个表去保存这个生成的预览pdf链接,然后直接返还,这样就能再一定程度节省服务器资源了。目前没写主要是太困了,我需要睡觉了。。。想起来再写吧。 Java截取字符 https://blog.iletter.top/index.php/archives/108.html 2023-04-26T23:48:00+08:00 今天脑子突然抽了,截取字符的时候想把[1651114259328487426]截取成1651114259328487426我知道是substring从1开始截取,但是为什么最后是length()-1呢,一开始-2我还很疑惑,看了一些文档才反应过来。beginIndex -- 起始索引(包括), 索引从 0 开始。endIndex -- 结束索引(不包括)。原来是最后的索引是不包括的呀。。。自己真的蠢到家了,自己把自己蠢到了,说到底基础还是不扎实。Java截取最后一个/后面的所有字符String imgUrl = "http://127.0.0.1:8080/cms/ReadAddress/1479805098158.jpg"; String image = imgUrl.substring(imgUrl.lastIndexOf("/")+1); Failed to start bean documentationPluginsBootstrapper https://blog.iletter.top/index.php/archives/84.html 2023-04-03T23:48:00+08:00 关于报错org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper' Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2023-04-03 00:08:57.651 ERROR 15032 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.22.jar:5.3.22] at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.22.jar:5.3.22] at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.22.jar:5.3.22] at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na] at org.springframework.context.support.DefaultLifecycleProcessor.startBeans(DefaultLifecycleProcessor.java:155) ~[spring-context-5.3.22.jar:5.3.22] at org.springframework.context.support.DefaultLifecycleProcessor.onRefresh(DefaultLifecycleProcessor.java:123) ~[spring-context-5.3.22.jar:5.3.22] at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:935) ~[spring-context-5.3.22.jar:5.3.22] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.22.jar:5.3.22] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:145) ~[spring-boot-2.6.10.jar:2.6.10] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:745) ~[spring-boot-2.6.10.jar:2.6.10] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:420) ~[spring-boot-2.6.10.jar:2.6.10] at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) ~[spring-boot-2.6.10.jar:2.6.10] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317) ~[spring-boot-2.6.10.jar:2.6.10] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) ~[spring-boot-2.6.10.jar:2.6.10] at com.wonder.UselessToolsApplication.main(UselessToolsApplication.java:14) ~[classes/:na] Caused by: java.lang.NullPointerException: null at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:112) ~[springfox-spi-2.9.2.jar:null] at springfox.documentation.spi.service.contexts.Orderings$8.compare(Orderings.java:109) ~[springfox-spi-2.9.2.jar:null] at com.google.common.collect.ComparatorOrdering.compare(ComparatorOrdering.java:37) ~[guava-20.0.jar:na] at java.base/java.util.TimSort.countRunAndMakeAscending(TimSort.java:355) ~[na:na] at java.base/java.util.TimSort.sort(TimSort.java:220) ~[na:na] at java.base/java.util.Arrays.sort(Arrays.java:1441) ~[na:na] at com.google.common.collect.Ordering.sortedCopy(Ordering.java:855) ~[guava-20.0.jar:na] at springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider.requestHandlers(WebMvcRequestHandlerProvider.java:57) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:138) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper$2.apply(DocumentationPluginsBootstrapper.java:135) ~[springfox-spring-web-2.9.2.jar:null] at com.google.common.collect.Iterators$7.transform(Iterators.java:750) ~[guava-20.0.jar:na] at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na] at com.google.common.collect.TransformedIterator.next(TransformedIterator.java:47) ~[guava-20.0.jar:na] at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:52) ~[guava-20.0.jar:na] at com.google.common.collect.MultitransformedIterator.hasNext(MultitransformedIterator.java:50) ~[guava-20.0.jar:na] at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:249) ~[guava-20.0.jar:na] at com.google.common.collect.ImmutableList.copyOf(ImmutableList.java:209) ~[guava-20.0.jar:na] at com.google.common.collect.FluentIterable.toList(FluentIterable.java:614) ~[guava-20.0.jar:na] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.defaultContextBuilder(DocumentationPluginsBootstrapper.java:111) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.buildContext(DocumentationPluginsBootstrapper.java:96) ~[springfox-spring-web-2.9.2.jar:null] at springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper.start(DocumentationPluginsBootstrapper.java:167) ~[springfox-spring-web-2.9.2.jar:null] at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ~[spring-context-5.3.22.jar:5.3.22] ... 14 common frames omitted问题分析因为Springfox 使用的路径匹配是基于AntPathMatcher的,而Spring Boot 2.6.X使用的是PathPatternMatcher。修复方案修复方案一:降低Spring Boot 版本到2.6.x以下版本比如下面版本组合是兼容的Spring Boot版本Swagger 版本2.5.62.9.2修复方案二: SpringBoot版本不降级解决方案比如下面版本组合是兼容的Spring Boot版本Swagger 版本2.6.53.0.0修复方案三:设置application配置文件spring: mvc: pathmatch: matching-strategy: ant_path_matcher修复方案四,修改spring源文件我没找到这个java文件。。。。没试过,估计应该可以。。参考地址:https://github.com/springfox/springfox/issues/3462revert matching strategy spring.mvc.pathmatch.matching-strategy to ant-path-matcherhack springfox WebMvcRequestHandlerProvider to filter out actuator controllers which don't respect spring.mvc.pathmatch.matching-strategy public WebMvcRequestHandlerProvider(Optional<ServletContext> servletContext, HandlerMethodResolver methodResolver, List<RequestMappingInfoHandlerMapping> handlerMappings) { this.handlerMappings = handlerMappings.stream().filter(mapping -> mapping.getPatternParser() == null) .collect(Collectors.toList());swagger配置类.apis(RequestHandlerSelectors.basePackage("com.wonder.controller")) 这里填写扫描包,对应类添加@APIpackage com.wonder.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import javax.servlet.ServletContext; import java.util.Optional; @Configuration // 标明是配置类 @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) // DocumentationType.SWAGGER_2 固定的,代表swagger2 // .groupName("分布式任务系统") // 如果配置多个文档的时候,那么需要配置groupName来分组标识 .apiInfo(apiInfo()) // 用于生成API信息 .select() // select()函数返回一个ApiSelectorBuilder实例,用来控制接口被swagger做成文档 .apis(RequestHandlerSelectors.basePackage("com.wonder.controller")) // 用于指定扫描哪个包下的接口 .paths(PathSelectors.any())// 选择所有的API,如果你想只为部分API生成文档,可以配置这里 .build(); } /** * 用于定义API主界面的信息,比如可以声明所有的API的总标题、描述、版本 * @return */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("项目API") // 可以用来自定义API的主标题 .description("项目SwaggerAPI管理") // 可以用来描述整体的API //.termsOfServiceUrl("") // 用于定义服务的域名 .version("1.0") // 可以用来定义版本。 .build(); // } }访问地址:http://localhost:8080/swagger-ui.html java获取客户端的ip地址以及获取位置 https://blog.iletter.top/index.php/archives/82.html 2023-04-02T23:48:00+08:00 主要依赖: <!--通过ip查询客户端位置--> <dependency> <groupId>org.lionsoul</groupId> <artifactId>ip2region</artifactId> <version>2.7.0</version> </dependency> <!-- Alibaba Fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.76</version> </dependency>首先先获取到访问者的ip地址访问必须是通过公网访问,要是内网互ip访问相测试的话,会返回127.0.0.1,或者访问者的内网ip地址。通过X-FORWARDED-FOR等信息。跟踪原有的客户端IP地址和原来客户端请求的服务器地址。package com.wonder.utils; import javax.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; public class getIPutils { public static String resClientIP(HttpServletRequest request) { String ip = getClientIpAddress(request); boolean isInner = isInnerIP(ip); if (!isInner) { return ip; }else { return "内网ip"; } } public static String getClientIpAddress(HttpServletRequest request) { // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址 String headerName = "x-forwarded-for"; String ip = request.getHeader(headerName); if (null != ip && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) { // 多次反向代理后会有多个IP值,第一个IP才是真实IP,它们按照英文逗号','分割 if (ip.indexOf(",") != -1) { ip = ip.split(",")[0]; } } if (checkIp(ip)) { headerName = "Proxy-Client-IP"; ip = request.getHeader(headerName); } if (checkIp(ip)) { headerName = "WL-Proxy-Client-IP"; ip = request.getHeader(headerName); } if (checkIp(ip)) { headerName = "HTTP_CLIENT_IP"; ip = request.getHeader(headerName); } if (checkIp(ip)) { headerName = "HTTP_X_FORWARDED_FOR"; ip = request.getHeader(headerName); } if (checkIp(ip)) { headerName = "X-Real-IP"; ip = request.getHeader(headerName); } if (checkIp(ip)) { headerName = "remote addr"; ip = request.getRemoteAddr(); // 127.0.0.1 ipv4, 0:0:0:0:0:0:0:1 ipv6 if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { //根据网卡取本机配置的IP InetAddress inet = null; try { inet = InetAddress.getLocalHost(); } catch (UnknownHostException e) { e.printStackTrace(); } ip = inet.getHostAddress(); } } System.out.println("getClientIp IP is " + ip + ", headerName = " + headerName); return ip; } private static boolean checkIp(String ip) { if (null == ip || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { return true; } return false; } /** * 判断IP是否是内网地址 * @param ipAddress ip地址 * @return 是否是内网地址 */ public static boolean isInnerIP(String ipAddress) { boolean isInnerIp; long ipNum = getIpNum(ipAddress); /** 私有IP:A类 10.0.0.0-10.255.255.255 B类 172.16.0.0-172.31.255.255 C类 192.168.0.0-192.168.255.255 还有127这个网段是环回地址 **/ long aBegin = getIpNum("10.0.0.0"); long aEnd = getIpNum("10.255.255.255"); long bBegin = getIpNum("172.16.0.0"); long bEnd = getIpNum("172.31.255.255"); long cBegin = getIpNum("192.168.0.0"); long cEnd = getIpNum("192.168.255.255"); isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd) || ipAddress.equals("127.0.0.1"); return isInnerIp; } private static long getIpNum(String ipAddress) { String[] ip = ipAddress.split("\\."); long a = Integer.parseInt(ip[0]); long b = Integer.parseInt(ip[1]); long c = Integer.parseInt(ip[2]); long d = Integer.parseInt(ip[3]); return a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d; } private static boolean isInner(long userIp, long begin, long end) { return (userIp >= begin) && (userIp <= end); } public static String getRealIP(HttpServletRequest request){ // 获取客户端ip地址 String clientIp = request.getHeader("x-forwarded-for"); if (clientIp == null || clientIp.length() == 0 || "unknown".equalsIgnoreCase(clientIp)) { clientIp = request.getRemoteAddr(); } String[] clientIps = clientIp.split(","); if(clientIps.length <= 1) return clientIp.trim(); // 判断是否来自CDN if(isComefromCDN(request)){ if(clientIps.length>=2) return clientIps[clientIps.length-2].trim(); } return clientIps[clientIps.length-1].trim(); } private static boolean isComefromCDN(HttpServletRequest request) { String host = request.getHeader("host"); return host.contains("www.189.cn") ||host.contains("shouji.189.cn") || host.contains( "image2.chinatelecom-ec.com") || host.contains( "image1.chinatelecom-ec.com"); } }相关请求头X-Forwarded-For 记录一个请求从客户端出发到目标服务器过程中经历的代理,或者负载平衡设备的IP。这是由缓存代理软件 Squid 引入,用来表示 HTTP 请求端真实 IP,现在已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器的ip。现在大部分的代理都会加上这个请求头。Proxy-Client-IP/WLProxy-Client-IP 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。HTTP_CLIENT_IP 有些代理服务器会加上此请求头。X-Real-IP nginx代理一般会加上此请求头。通过ip获取位置通过上面返还的ip地址进行解析,具体方式可以参考Ip2region的官方文档。package com.wonder.utils; import com.alibaba.fastjson.JSONObject; import org.lionsoul.ip2region.xdb.Searcher; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; public class FromIpToCtiy { //https://github.com/lionsoul2014/ip2region/tree/master/binding/java public static String resLocation(String ip) { String dbPath = "src/main/resources/ip2region.xdb"; // 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。 byte[] vIndex; try { vIndex = Searcher.loadVectorIndexFromFile(dbPath); } catch (Exception e) { System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e); return "{\"error\":\"加载索引失败\"}"; } // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。 Searcher searcher; try { searcher = Searcher.newWithVectorIndex(dbPath, vIndex); } catch (Exception e) { System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e); return "{\"error\":\"创建缓存搜索器失败\"}"; } // 3、查询 long sTime; String region = null; long cost = 0; try { sTime= System.nanoTime(); region= searcher.search(ip); cost= TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime)); System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost); // 4、关闭资源 searcher.close(); } catch (Exception e) { System.out.printf("failed to search(%s): %s\n", ip, e); } Map<String,String> resmap = new HashMap<>(); resmap.put("location",region); resmap.put("ioCount", String.valueOf(searcher.getIOCount())); resmap.put("cost", cost+"μs"); return JSONObject.toJSONString(resmap); // 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。 } }说实在的,其实没啥技术含量,就是互相调用查看的,通过客户端的请求进行解析。然后查询地址的,也就是通过一个人家写的库进行查询。很简单的。欸。。目前没啥好想法了。