<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title><![CDATA[Heck's  Blog]]></title> 
<link>https://www.heckjj.com/index.php</link> 
<description><![CDATA[一瞬间的决定，往往可以改变很多，事实上，让自己成功的往往不是知识，是精神！ 如果你总是为自己找借口，那只好让成功推迟。执行力，今天！]]></description> 
<language>zh-cn</language> 
<copyright><![CDATA[Heck's  Blog]]></copyright>
<item>
<link>https://www.heckjj.com/post/680/</link>
<title><![CDATA[编程初学者实用代码整洁入门指南]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Mon, 02 Feb 2026 05:14:32 +0000</pubDate> 
<guid>https://www.heckjj.com/post/680/</guid> 
<description>
<![CDATA[ 
	编写干净、易于理解且可维护的代码是每位开发者必须掌握的技能。我们将探讨提高代码质量的最重要原则，并为每个原则提供代码示例。<br/><br/>如何命名变量（以及其他事物）<br/><br/>我们不用内存地址而使用名称的原因是：名称更容易记忆。更重要的是，它们能提供更多关于变量的信息，以便其他人理解其意义。<br/><br/>找到一个好的名字可能需要一些时间，但这将在未来为你和你的团队节省更多时间。而且我确信大多数读者都遇到过这种情况：几个月后再次访问自己的代码时，很难理解之前做了什么。<br/><br/><br/>如何创建有意义的名字<br/>不要用注释来解释变量的用途。如果一个名字需要注释，那么你应该花时间重命名那个变量，而不是写注释。<br/><br/>差：<br/><br/>var d; // elapsed time in days<br/>我见过这种代码无数次。人们普遍认为应该用注释来隐藏代码的混乱，但这是一种常见的误解。除非有充分理由，否则不要使用 x、y、a 或 b 等字母作为变量名（循环变量除外）。<br/><br/>好：<br/><br/>var elapsedTimeInDays;<br/>var daysSinceCreation;<br/>var daysSinceModification;<br/>这些名称要好得多。它们告诉你被测量的内容以及该测量的单位。<br/><br/><br/><br/>避免错误信息<br/>注意那些有特定含义的词语。除非其类型确实是 List，否则不要将一组账户称为 accountList。这个词有特定的含义，可能会导致错误的结论。<br/><br/>即使类型是列表，accounts 也是一个更简单、更好的名称。<br/><br/>差：<br/><br/>var accountList = [];<br/>好：<br/><br/>var accounts = [];<br/><br/><br/>避免噪音词<br/>噪声词是指那些不提供任何关于变量额外信息的词。它们是冗余的，应该被删除。<br/><br/>一些常见的噪声词有：<br/><br/>The (prefix)<br/><br/>Info<br/><br/>Data<br/><br/>Variable<br/><br/>Object<br/><br/>Manager<br/><br/>如果你的类名是 UserInfo，你只需去掉 Info，直接用 User。将类名用 BookData 代替 Book 是毋庸置疑的，因为类本来就是为了存储数据而存在的。<br/><br/><br/>使用发音友好的名称<br/>如果你不会读一个名字，讨论它时就会显得很愚蠢。<br/><br/>差：<br/><br/>const yyyymmdstr = moment().format(&quot;YYYY/MM/DD&quot;);<br/>好：<br/><br/>const currentDate = moment().format(&quot;YYYY/MM/DD&quot;);<br/><br/><br/>使用可搜索的名称<br/>避免在代码中使用魔法数字。选择可搜索的、命名的常量。不要为常量使用单个字母的名称，因为它们可能出现在很多地方，因此不易搜索。<br/><br/>差：<br/><br/>if (student.classes.length &lt; 7) &#123;<br/>&nbsp;&nbsp; // Do something<br/>&#125;<br/>好：<br/><br/>if (student.classes.length &lt; MAX_CLASSES_PER_STUDENT) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;// Do something<br/>&#125;<br/>这样更好，因为 MAX_CLASSES_PER_STUDENT 可以在代码的许多地方使用。如果将来需要将其改为 6，我们只需更改常量即可。<br/><br/>糟糕的例子会在读者脑海中产生疑问，比如 7 的重要性是什么？<br/><br/>你也应该使用你语言的常量命名和声明约定，例如 Java 中的 private static final 或 JavaScript 中的 const。<br/><br/><br/>保持一致性<br/>遵循每个概念用一个词的规则。不要在不同的类中对同一操作使用 fetch、retrieve 和 get。选择其中一个并在整个项目中统一使用，这样维护代码库的人员或你的 API 客户可以轻松找到他们正在寻找的方法。<br/><br/><br/><br/>如何编写函数<br/>保持它们简短<br/>函数应该很小，真的很小。它们很少会是 20 行长。一个函数越长，它就越可能做很多事情并产生副作用。<br/><br/><br/><br/>确保它们只做一件事<br/>你的函数应该只做一件事情。如果你遵循这个规则，它们一定会很小。函数所做的一切都应该体现在它的名字中。<br/><br/>有时候很难从函数名来判断它是否做了多件事情。一个很好的检查方法是尝试提取一个具有不同名字的另一个函数。如果你能找到，那就意味着它应该是一个不同的函数。<br/><br/>这可能是本文中最重要的概念，需要一些时间来适应。但一旦你掌握了它，你的代码看起来会更加成熟，而且肯定会更容易重构、理解和测试。<br/><br/><br/><br/>将条件语句封装在函数中<br/>重构条件并将其放入命名函数中是使你的条件语句更易读的好方法。<br/><br/>这是我从学校项目里的一段代码。这段代码负责在 Connect4 游戏的棋盘上插入芯片。<br/><br/>isValidInsertion 方法负责检查列号的有效性，让我们可以专注于插入芯片的逻辑。<br/><br/>public void insertChipAt(int column) throws Exception &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;if (isValidInsertion(column)) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;insertChip(column);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;boardConfiguration += column;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;currentPlayer = currentPlayer == Chip.RED ? Chip.YELLOW : Chip.RED;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125; else &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (!columnExistsAt(column))<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;throw new IllegalArgumentException();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else if (isColumnFull(column - 1) &#124;&#124; getWinner() != Chip.NONE)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;throw new RuntimeException();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/>如果你感兴趣，这里是 isValidInsertion 的代码。<br/><br/>&nbsp;&nbsp;private boolean isValidInsertion(int column) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;boolean columnIsAvailable = column &lt;= NUM_COLUMNS &amp;&amp; column &gt;= 1 &amp;&amp; numberOfItemsInColumn[column - 1] &lt; NUM_ROWS;<br/>&nbsp;&nbsp;&nbsp;&nbsp;boolean gameIsOver = getWinner() != Chip.NONE;<br/>&nbsp;&nbsp;&nbsp;&nbsp;return columnIsAvailable &amp;&amp; !gameIsOver;<br/>&#125;<br/>如果没有方法，if 条件将如下所示：<br/><br/>if (column &lt;= NUM_COLUMNS<br/> &amp;&amp; column &gt;= 1<br/> &amp;&amp; numberOfItemsInColumn[column - 1] &lt; NUM_ROWS <br/> &amp;&amp; getWinner() != Chip.NONE)<br/>恶心，对吧？我同意。<br/><br/><br/><br/>减少参数<br/>函数应该有两个或更少的参数，越少越好。尽量避免有三个或更多参数。<br/><br/>参数使得函数更难阅读和理解。从测试的角度来看，它们甚至更难，因为它们需要为每个参数组合编写测试用例。<br/><br/><br/><br/>不要使用标志参数<br/>标志参数是一个传递给函数的布尔值参数。根据这个参数的值，会采取两种不同的操作。<br/><br/>例如，假设有一个负责预订音乐会门票的函数，并且有两种类型的用户：高级和普通。你可以编写如下代码：<br/><br/>public Booking book (Customer aCustomer, boolean isPremium) &#123;<br/>&nbsp;&nbsp;if(isPremium) <br/>&nbsp;&nbsp; // logic for premium book<br/>&nbsp;&nbsp;else<br/>&nbsp;&nbsp; // logic for regular booking<br/>&#125;<br/>标志参数天然地违背了单一职责原则。当你看到它们时，应该考虑将函数拆分成两个。<br/><br/><br/><br/>不要有副作用<br/>副作用是你的代码的意外后果。它们可能是改变传递的参数，在按引用传递的情况下，或者可能改变一个全局变量。<br/><br/>关键点在于，它们承诺做另一件事，你需要仔细阅读代码才能注意到副作用。它们可能导致一些棘手的错误。<br/><br/>public class UserValidator &#123;<br/>&nbsp;&nbsp;private Cryptographer cryptographer;<br/>&nbsp;&nbsp;public boolean checkPassword(String userName, String password) &#123; <br/>&nbsp;&nbsp;&nbsp;&nbsp;User user = UserGateway.findByName(userName);<br/>&nbsp;&nbsp;&nbsp;&nbsp;if (user != User.NULL) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String codedPhrase = user.getPhraseEncodedByPassword();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String phrase = cryptographer.decrypt(codedPhrase, password);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (&quot;Valid Password&quot;.equals(phrase)) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Session.initialize();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return true; <br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;return false; <br/>&nbsp;&nbsp;&#125;<br/>&#125;<br/>你能看到这个函数的副作用吗？<br/><br/>它在检查密码，但当密码有效时，它还会初始化会话，这是一个副作用。<br/><br/>你可以将函数的名称改为类似 checkPasswordAndInitializeSession，以使这种效果显式化。但当你这样做时，你应该注意到你的函数实际上在处理两件事，你不应该在这里初始化会话。<br/><br/><br/><br/>不要重复自己<br/>代码重复可能是软件中所有邪恶的根源。重复的代码意味着在逻辑发生变化时需要在多个地方进行修改，而且非常容易出错。<br/><br/>使用你的 IDE 的重构功能，每当遇到重复的代码片段时，提取一个方法。<br/><br/><br/><br/>其他<br/>不要在代码注释中留下代码<br/>请勿如此。这一点很严重，因为看到代码的其他人会因为不知道它是否有什么用而不敢删除它。那些被注释掉的代码会停留很长时间。然后当变量名或方法名改变时，它变得无关紧要，但仍然没有人删除它。<br/><br/>干脆删除它。即使它很重要，也有版本控制可以找回。<br/><br/><br/><br/>了解你语言的规范<br/>你应该了解你语言在空格、注释和命名方面的规范。许多语言都有可用的风格指南。<br/><br/>例如，你应该在 Java 中使用 camelCase，但在 Python 中使用 snake_case。在 C#中，你应该将开括号放在新行，但在 Java 和 JavaScript 中，你应该将它们放在同一行。<br/><br/>这些事情因语言而异，没有统一的标准。<br/><br/><br/>结论<br/>编写干净代码并非一蹴而就的技能，它是一种需要时刻牢记这些原则并在编写代码时加以应用的习惯。<br/><br/>感谢您抽出时间阅读，希望对您有所帮助。
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/670/</link>
<title><![CDATA[解决Vue3前端上传Formdata中的file为[object Object]的问题]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Fri, 20 Dec 2024 03:31:08 +0000</pubDate> 
<guid>https://www.heckjj.com/post/670/</guid> 
<description>
<![CDATA[ 
	在使用Vue3、TypeScript和ElementUIPlus构建前端时，遇到上传表单数据为[objectObject]且后端SpringBoot报错的情况。通过排查发现前端代码中的`file`对象需要将`raw`属性值作为上传内容，解决了`MissingServletRequestPartException`。<br/><br/>项目场景：<br/>前端：Vue3 + ts + antdv<br/>后端：springboot<br/><br/><br/>打印的file：<br/>发现file内部还有一个raw属性，我们要上传的是这个raw里面的内容<br/><br/>解决方案：<br/>更改前端代码如下：<br/><br/>const file = uploadFileList.value[i];<br/>&nbsp;&nbsp;&nbsp;&nbsp;formData2.append('file', toRaw(file.raw));<br/>&nbsp;&nbsp;&nbsp;&nbsp;formData2.append('parentAdcode', formData.parentAdcode);<br/>&nbsp;&nbsp;&nbsp;&nbsp;importObjects(formData2).then(res=>&#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;this.$message.success('导入成功')<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;)
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/668/</link>
<title><![CDATA[Restful API规范]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Tue, 05 Nov 2024 02:18:56 +0000</pubDate> 
<guid>https://www.heckjj.com/post/668/</guid> 
<description>
<![CDATA[ 
	一、简介<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RESTful API 是应用程序接口 (API) 的一种架构风格，它使用 HTTP 请求来访问和使用数据。该数据可用于 GET、PUT、POST 和 DELETE 数据类型，这些数据类型是指有关资源的操作的读取、更新、创建和删除。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RESTful API（也称为 RESTful Web 服务或 REST API）基于表示状态传输 (REST)，它是一种架构风格和经常用于 Web 服务开发的通信方法。<br/><br/>二、设计原则。<br/>URL即资源，使用名词表示资源<br/>接口应直观、简洁、风格命名保持一致<br/>必须版本化，具有足够的灵活性来支持上层UI<br/>三、RFC3986标准。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RFC3986标准，简单讲就是规定了如下：除了 数字 + 字母 + -_.~ 不会被转义，其他字符都会被以百分号（%）后跟两位十六进制数 %&#123;hex&#125; 的方式进行转义。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; www 的 post form data 也就是 x-www-form-urlencode 的编码规则：除 -_.（没有 ~） 之外的所有 非字母、非数字 的字符都将被替换成 百分号（%）后跟两位十六进制数 %&#123;hex&#125;，空格（注意）则编码为加号 +。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RFC3986对 ~ 不做转码，x-www-form-urlencode 对 ~ 做转码 %7E。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RFC3986对 空格 转为 %20，x-www-form-urlencode 对 空格 转为 +。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;JS的URL编码方式：encodeURIComponent。<br/><br/>// encodeURIComponent<br/>console.log(encodeURIComponent(&quot;hello233 ~-_.&quot;))<br/>hello233%20~-_.<br/><br/>四、URI、URL和URN<br/><br/><a href="https://www.heckjj.com/attachment.php?fid=263" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=263" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0"/></a><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; URI（Uniform Resource Identifier ）：统一资源标识符，就是在某一规则下能把一个资源独一无二地标识出来。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;URL（Uniform Resource Locator）：统一资源定位符。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;URN（Uniform Resource Name）：统一资源名称。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;URI可以分为URL,URN或同时具备locators 和names特性的一个东西。URN作用就好像一个人的名字，URL就像一个人的地址。换句话说：URN确定了东西的身份，URL提供了找到它的方式。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;URL是URI的子集, 除了确定一个资源,还提供一种定位该资源的主要访问机制(如其网络“位置”)。例如： http://或 ftp://.。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;当我们替代web地址的时候，URI和URL那个更准确？<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;URI更准确。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;我们经常使用的URI不是严格技术意义上的URL。例如：你需要的文件在files.hp.com. 这是URI，但不是URL--系统可能会对很多协议和端口都做出正确的反应。http://files.hp.com 和ftp://files.hp.com可能得到完全不同的内容。这种情况可能更加普遍，想想不同谷歌域名上的不同服务啊。所以，用URI吧，这样你通常技术上是正确的，URL可不一定。<br/><br/>五、设计规范。<br/>1.版本号<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;应该将API的版本号放入URL。 版本号以字符&#039;v&#039;开头，比如：v1<br/><br/>http://demo.com/cuc/sever/v1<br/> 2.路径(Endpoint)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;接口路径，也称为端点(Endpoint)，代表一种资源的访问地址。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;①使用名词表示资源，使用HTTP Method 描述操作<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;因为&quot;资源&quot;表示一种实体，所以应该是名词，URL不应该有动词，动词应该放在HTTP协议中，CRUD的操作不要体现在URL中。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 反面教材：<br/><br/>查询员工 GET&nbsp;&nbsp;&nbsp;&nbsp; /v1/findEmployee?id=1000&nbsp;&nbsp;<br/>新增员工 POST&nbsp;&nbsp;&nbsp;&nbsp;/v1/addEmployee<br/>更新员工 POST&nbsp;&nbsp;&nbsp;&nbsp;/v1/updateEmployee?id=1000<br/>删除员工 POST&nbsp;&nbsp;&nbsp;&nbsp;/v1/deleteEmployee?id=1000<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 正确实例：<br/><br/>查询员工 GET&nbsp;&nbsp;&nbsp;&nbsp; /v1/employee/1000&nbsp;&nbsp;<br/>新增员工 POST&nbsp;&nbsp;&nbsp;&nbsp;/v1/employee<br/>更新员工 PUT&nbsp;&nbsp;&nbsp;&nbsp; /v1/employee/1000<br/>删除员工 DELETE&nbsp;&nbsp;/v1/employee/1000<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;② 资源既可以是单个，也可以是一个集合，比如：user是单个资源，users是集合资源。 由于英文名词的复数规则众多，为了保持接口命名的一致性，我们建议URL里描述集合资源的名词统一使用单数。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;反面示例：<br/><br/>查询单个员工 GET&nbsp;&nbsp;&nbsp;&nbsp; /v1/employees/1000 <br/>查询一组员工 GET&nbsp;&nbsp;&nbsp;&nbsp; /v1/employees<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;正确示例：<br/><br/>查询单个员工 GET&nbsp;&nbsp;&nbsp;&nbsp; /v1/employee/1000 <br/>查询一组员工 GET&nbsp;&nbsp;&nbsp;&nbsp; /v1/employee<br/>3.使用小写字母，多个单词用&quot;-&quot;分隔，提高URL的可读性<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;RFC3986 定义了URI是大小写敏感的(除了scheme和host部分)，为了避免歧义，接口路径应尽量使用小写字母。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;对于由多个单词构成的路径，推荐使用&#039;-&#039;(hyphen)分隔，避免使用驼峰命名（camelCase）、下划线命名(snake_case)、首字母大写的驼峰命名（PascalCase），以提高可读性。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;例子：<br/><br/>camelCase&nbsp;&nbsp;:&nbsp;&nbsp;/v1/customer/1000/shippingAddress&nbsp;&nbsp;[Bad]<br/>snake_case :&nbsp;&nbsp;/v1/customer/1000/shipping_address [Bad]<br/>PascalCase :&nbsp;&nbsp;/v1/customer/1000/ShippingAddress&nbsp;&nbsp;[Bad]<br/> <br/>hyphen&nbsp;&nbsp;&nbsp;&nbsp; :&nbsp;&nbsp;/v1/customer/1000/shipping-address [OK]<br/>4.资源嵌套层次避免过深，尽量不超过2层<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;定义REST接口时，通常使用资源嵌套（多级路径）来标识资源之间的关系，资源嵌套层次尽量不超过2层，否则很难阅读和使用。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;例子：<br/><br/>GET&nbsp;&nbsp;/v1/user/1000/company&nbsp;&nbsp;&nbsp;&nbsp;查询ID等于1000的用户的公司信息<br/>GET&nbsp;&nbsp;/v1/user/1000/company/10 查询ID等于1000的用户所属公司中ID=10的公司信息，嵌套两层<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;反面例子：<br/><br/>GET /v1/company/10/department/20302/employee/10830<br/>GET /v1/zoo/1/area/3/animal/4<br/>六. 筛选(Filtering)、排序(Sorting)、分页(Pagination)、字段选择<br/>&nbsp;&nbsp;1.筛选（Filtering)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;接口URL尽量保持简短，对于集合资源不同纬度的筛选，可通过组合不同的Query Param来实现。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;反面案例：<br/><br/>GET /v1/externalEmployees<br/>GET /v1/internalEmployees<br/>GET /v1/internalAndSeniorEmployees<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;正确案例：<br/><br/>GET /v1/employee?state=internal&amp;title=senior<br/>2.排序(Sorting)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;单个字段排序：<br/><br/>GET /v1/employee?sort=age&amp;order=asc<br/>GET /v1/employee?sort=age&amp;order=desc<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;多个字段排序：<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;字段名前缀,&#039;+&#039;：表示升序，&#039;-&#039;：降序，多个字段名以逗号分隔<br/><br/>GET /v1/employee?sort=+age,-userName<br/>3.分页(Pagination)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;对于数据量比较大的集合资源，接口实现一定要支持分页。<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;常规分页有两个重要的参数：&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/><br/>pageNo 获取那一页的记录，默认第一页。<br/>pageSize 每页返回多少条数据，推荐默认值25，必须限定此参数的最大值。<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;返回的资源列表为：[(pageNo-1)pageSize, pageNopageSize)<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;请求示例：<br/><br/>GET /v1/employee?pageNo=8&amp;pageSize=20&amp;sort=age&amp;order=desc<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;返回值可以采用以下数据格式：<br/><br/>&#123;&nbsp;&nbsp; <br/>&nbsp;&nbsp;&nbsp;&nbsp;&quot;pageSize&quot;:20,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// 页尺寸<br/>&nbsp;&nbsp;&nbsp;&nbsp;&quot;pageNo&quot;:8,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 页码<br/>&nbsp;&nbsp;&nbsp;&nbsp;&quot;totalCount&quot;:182,&nbsp;&nbsp;// 总记录数<br/>&nbsp;&nbsp;&nbsp;&nbsp;&quot;pageList&quot;:[<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;id&quot;:13483,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;name&quot;:&quot;Hehe&quot;,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;title&quot;:&quot;senior&quot;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;id&quot;:13484,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;name&quot;:&quot;haha&quot;,<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&quot;title&quot;:&quot;junior&quot;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;]<br/>&#125;<br/><br/>4.字段选择<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;调用方可能只需要资源的少量属性，接口提供方可支持客户端通过请求参数fields来选择返回的字段，多个字段以逗号分隔，示例如下：<br/><br/>GET /v1/employee?fields=id,name,age&amp;pageNo=1&amp;pageSize=20&amp;sort=age&amp;order=desc
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/665/</link>
<title><![CDATA[如何排除spring-boot-starter-parent中特定依赖项]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Thu, 16 May 2024 14:21:17 +0000</pubDate> 
<guid>https://www.heckjj.com/post/665/</guid> 
<description>
<![CDATA[ 
	我正在使用旧版Spring应用程序，并且想要迁移到Spring Boot。我的意图是使用spring-boot-starter-data- jpa。因此，我在pom.xml（管理所有spring-boot-dependencies）中添加了以下部分：<br/><br/>&lt;parent&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;version&gt;2.1.3.RELEASE&lt;/version&gt;<br/>&lt;/parent&gt;<br/>但这搞砸了我暂时需要保留的某些依赖项。我目前正在使用Selenium依赖项（版本2.53.0；从另一个项目中过渡添加），但是spring-<br/>boot正在获取的依赖项3.9.1。<br/><br/>我想排除3.9.1依赖关系，但exclusion过滤器无法按预期工作。<br/><br/>不能乱&lt;excludes&gt;然后尝试弄清楚您需要再次包含什么（在弄清楚要排除的内容之后）。只需覆盖《Spring<br/>Boot参考指南》中此处解释的版本即可。<br/><br/>假设您将spring-boot-starter- parent用作父项，则只需将a添加&lt;selenium.version&gt;到您的&lt;properties&gt;部分中即可指定所需的版本。<br/><br/>&lt;properties&gt;<br/>&nbsp;&nbsp;&lt;selenium.version&gt;2.53.0&lt;/selenium.version&gt;<br/>&lt;/properties&gt;<br/>这将使Spring Boot使用所需的版本。
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/663/</link>
<title><![CDATA[SpringBoot单元测试报错“Error creating bean with name ‘serverEndpointExporter‘ ”]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Tue, 02 Jan 2024 06:48:33 +0000</pubDate> 
<guid>https://www.heckjj.com/post/663/</guid> 
<description>
<![CDATA[ 
	在SpringBoot中使用单元测试时，出现以下报错，意思是创建名为‘serverEndpointExporter’的bean时出错。<br/>Error starting ApplicationContext. To display the conditions report re-run your application with &#039;debug&#039; enabled.<br/>[20240102 14:42:43][main][ERROR][org.springframework.boot.SpringApplication:854] Application run failed<br/>org.springframework.beans.factory.BeanCreationException: Error creating bean with name &#039;serverEndpointExporter&#039; defined in class path resource [com/nine/rivers/bms/system/config/WebSocketConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804)<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)<br/>&nbsp;&nbsp;at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:953)<br/>&nbsp;&nbsp;at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918)<br/>&nbsp;&nbsp;at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)<br/>&nbsp;&nbsp;at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:765)<br/>&nbsp;&nbsp;at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445)<br/>&nbsp;&nbsp;at org.springframework.boot.SpringApplication.run(SpringApplication.java:338)<br/>&nbsp;&nbsp;at org.springframework.boot.test.context.SpringBootContextLoader.loadContext(SpringBootContextLoader.java:121)<br/>&nbsp;&nbsp;at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContextInternal(DefaultCacheAwareContextLoaderDelegate.java:99)<br/>&nbsp;&nbsp;at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:124)<br/>&nbsp;&nbsp;at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124)<br/>&nbsp;&nbsp;at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)<br/>&nbsp;&nbsp;at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)<br/>&nbsp;&nbsp;at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:248)<br/>&nbsp;&nbsp;at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:138)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$6(ClassBasedTestDescriptor.java:350)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.executeAndMaskThrowable(ClassBasedTestDescriptor.java:355)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeTestInstancePostProcessors$7(ClassBasedTestDescriptor.java:350)<br/>&nbsp;&nbsp;at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)<br/>&nbsp;&nbsp;at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)<br/>&nbsp;&nbsp;at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384)<br/>&nbsp;&nbsp;at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)<br/>&nbsp;&nbsp;at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)<br/>&nbsp;&nbsp;at java.util.stream.StreamSpliterators$WrappingSpliterator.forEachRemaining(StreamSpliterators.java:312)<br/>&nbsp;&nbsp;at java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:743)<br/>&nbsp;&nbsp;at java.util.stream.Streams$ConcatSpliterator.forEachRemaining(Streams.java:742)<br/>&nbsp;&nbsp;at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeTestInstancePostProcessors(ClassBasedTestDescriptor.java:349)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$instantiateAndPostProcessTestInstance$4(ClassBasedTestDescriptor.java:270)<br/>&nbsp;&nbsp;at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.instantiateAndPostProcessTestInstance(ClassBasedTestDescriptor.java:269)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$2(ClassBasedTestDescriptor.java:259)<br/>&nbsp;&nbsp;at java.util.Optional.orElseGet(Optional.java:267)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$testInstancesProvider$3(ClassBasedTestDescriptor.java:258)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.execution.TestInstancesProvider.getTestInstances(TestInstancesProvider.java:31)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$prepare$0(TestMethodTestDescriptor.java:101)<br/>&nbsp;&nbsp;at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:100)<br/>&nbsp;&nbsp;at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.prepare(TestMethodTestDescriptor.java:65)<br/>&nbsp;&nbsp;at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$prepare$1(NodeTestTask.java:111)<br/>&nbsp;&nbsp;at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)<br/>&nbsp;&nbsp;at org.junit.platform.engine.support.hierarchical.NodeTestTask.prepare(NodeTestTask.java:111)<br/>&nbsp;&nbsp;at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:79)<br/>&nbsp;&nbsp;at java.util.ArrayList.forEach(ArrayList.java:1259)<br/><br/>问题分析<br/>通过报错提示可以看出，在创建命名为 ‘serverEndpointExporter’ 的bean时出错，而这个bean是webscoket中的，而webscoket是需要依赖tomcat等容器才能启动的，恰巧运行单元测试不需要依赖tomcat容器，两者互相矛盾这就是出现该问题的原因了。<br/><br/>问题解决<br/>使用tomcat容器运行单元测试，在注解 @SpringbootTest 中追加以下内容即可：<br/>@SpringbootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)<br/>
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/662/</link>
<title><![CDATA[websocket连接时前后端使用token鉴权导致连接失败的原因]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Mon, 25 Dec 2023 06:28:45 +0000</pubDate> 
<guid>https://www.heckjj.com/post/662/</guid> 
<description>
<![CDATA[ 
	前后端通过websocket通信，但是需要鉴权，可以利用websocket的子协议来传输token，代码如下：<br/>var ws = new WebSocket(&quot;ws://localhost/ws&quot;,[token]);<br/>在后面加上[这里是token内容]。这样请求头就会携带：<br/>Sec-WebSocket-Protocol:token value<br/>参数是一个数组。如果数组有多个值，后端也会接受到逗号隔开的值。<br/>后端可以获取这个token进行鉴权。<br/>重点来了！<br/>如果传递了token参数，后端响应的时候，也必须带上这个token响应！否则前端接收不到数据！<br/>我竟然忘记前端传过来的token是加密后的，首先解密后去校验，通过后再把原加密token放在响应头即可， 如果不一致也会导致前端接收不到消息。<br/>如下是java的例子“：<br/>HttpServletResponse response = (HttpServletResponse)servletResponse;<br/>response.setHeader(&quot;Sec-WebSocket-Protocol&quot;, token);
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/660/</link>
<title><![CDATA[使用cmd的Msg命令发送局域网消息]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Tue, 19 Dec 2023 08:59:06 +0000</pubDate> 
<guid>https://www.heckjj.com/post/660/</guid> 
<description>
<![CDATA[ 
	咱话不多说，正如标题所见，实践开始。<br/><br/>首先打开cmd，win键+r打开运行，输入cmd打开控制台。<br/><br/>在控制台中输入msg / server:IP地址 * &quot;要发送的内容。&quot;&nbsp;&nbsp; 注：(这里的引号要加，且*号左右必须空一格)<br/><br/>例如：我的ipv4地址是192.168.90.1**&nbsp;&nbsp;&nbsp;&nbsp;注：(用ipconfig命令可查)<br/><br/><a href="https://www.heckjj.com/attachment.php?fid=254" target="_blank"><img src="https://www.heckjj.com/attachment.php?fid=254" class="insertimage" alt="点击在新窗口中浏览此图片" title="点击在新窗口中浏览此图片" border="0"/></a><br/><br/>这样就可以了，试试吧。<br/><br/>
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/651/</link>
<title><![CDATA[上传找不到临时目录, 出现500错误，nested exception is java.lang. RuntimeException: java.nio.file. NoSuchFileException: /tmp/undertow]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Mon, 17 Jul 2023 11:53:35 +0000</pubDate> 
<guid>https://www.heckjj.com/post/651/</guid> 
<description>
<![CDATA[ 
	 Linux 系统中，Spring Boot 应用以 java -jar 命令启动时，会在操作系统的 /tmp 目录下生成一个 tomcat（或 undertow ）临时目录，上传的文件先要转换成临时文件保存在这个文件夹下面。由于临时 /tmp 目录下的文件，在长时间（10天）没有使用的情况下，系统执行了 tmp 目录清理服务（systemd-tmpfiles-clean.service）,导致 /tmp/undertow...8090 文件被清理，然而在上传的时候，undertow 服务器需要创建/tmp/undertow...8090/undertow...upload 临时文件，但是调用 Files.createFile(...) 的时候就会发现找不到父目录，才导致了以上的错误。<br/><br/>具体错误日志(参考)<br/>undertow<br/><br/>java.nio.file.NoSuchFileException: /tmp/undertow.17753558642503713859.8085/undertow7370242804103803588upload<br/><br/>Tomcat<br/><br/>The temporary upload location [/tmp/tomcat.7957874575370093230.8088/work/Tomcat/localhost/ROOT] is not valid<br/><br/>重现方法<br/>找到类 io.undertow.server.handlers.form.MultiPartParserDefinition<br/>定位到如下代码<br/><br/>@Override<br/>public void beginPart(final HeaderMap headers) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;this.currentFileSize = 0;<br/>&nbsp;&nbsp;&nbsp;&nbsp;this.headers = headers;<br/>&nbsp;&nbsp;&nbsp;&nbsp;final String disposition = headers.getFirst(Headers.CONTENT_DISPOSITION);<br/>&nbsp;&nbsp;&nbsp;&nbsp;if (disposition != null) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (disposition.startsWith(&quot;form-data&quot;)) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;currentName = Headers.extractQuotedValueFromHeader(disposition, &quot;name&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fileName = Headers.extractQuotedValueFromHeaderWithEncoding(disposition, &quot;filename&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (fileName != null &amp;&amp; fileSizeThreshold == 0) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (tempFileLocation != null) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;file = Files.createTempFile(tempFileLocation, &quot;undertow&quot;, &quot;upload&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125; else &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;file = Files.createTempFile(&quot;undertow&quot;, &quot;upload&quot;);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;createdFiles.add(file);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;fileChannel = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125; catch (IOException e) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;throw new RuntimeException(e);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/><br/>在 createdFiles.add(file); 处打断点，复制file的 path 的值找到该文件并将其删除；放开断点，错误重现；<br/><br/>解决方案<br/>方案1 (推荐)<br/>在 applicaiton.yml(applicaiton.property) 中添加配置 ：spring.servlet.multipart.location<br/>spring.servlet.multipart.location 底层就是 createMultipartConfig 中的 factory.setLocation ，见源码：org.springframework.boot.autoconfigure.web.servlet.MultipartProperties#createMultipartConfig<br/><br/>spring:<br/>&nbsp;&nbsp;servlet:<br/>&nbsp;&nbsp;&nbsp;&nbsp;multipart:<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;location: /data/tmp<br/><br/>手动指定目录后，必须保证该目录存在，并由读写的权限<br/>创建该目录 mkdir -p /data/tmp<br/><br/>方法二<br/>使用配置类配置, 类似方案1<br/><br/>@Bean<br/>public MultipartConfigElement multipartConfigElement() &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;MultipartConfigFactory factory = new MultipartConfigFactory();<br/>&nbsp;&nbsp;&nbsp;&nbsp;factory.setLocation(System.getProperty(&quot;/data/tmp&quot;));<br/>&nbsp;&nbsp;&nbsp;&nbsp;return factory.createMultipartConfig();<br/>&#125;<br/><br/>方案三(不推荐)<br/>手动创建临时目录<br/><br/>mkdir -p /tmp/undertow.17753558642503713859.8085/undertow7370242804103803588upload<br/><br/>方案四(不推荐)<br/>修改系统配置，排除该临时目录<br/><br/>vim /usr/lib/tmpfiles.d/tmp.con<br/># 文件最后添加<br/>x /tmp/undertow*
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/650/</link>
<title><![CDATA[tomcat8 url包含等特殊字符报错400的带出版本的问题]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Fri, 30 Jun 2023 15:54:14 +0000</pubDate> 
<guid>https://www.heckjj.com/post/650/</guid> 
<description>
<![CDATA[ 
	在做javaweb项目时，关于统一错误页面在开发的过程中就做过编码,并且一直都很有效,像500,404,403等常规错误码都能得到有效处理，但是400却不行,而且还暴露tomcat的版本信息,这是很严重的安全漏洞<br/>解决方法百度和测试很多，总结如下：<br/><br/>问题产生：<br/>根据rfc规范，url中不允许有 &#124;，&#123;，&#125;等特殊字符，但在实际生产中还是有些url有可能携带有这些字符，特别是&#124;还是较为常见的。在tomcat升级到7以后，对url字符的检查都变严格了，如果出现这类字符，服务器tomcat将直接返回400状态码。<br/><br/>方法一：降低tomcat的版本<br/>开发人员增加一项设置，允许配置在url可以出现的特殊字符，但也仅限于&#124;，&#123;，&#125;三种，见：http://tomcat.apache.org/tomcat-8.0-doc/config/systemprops.html#Other<br/>该项设置在以下版本的tomcat中有效：<br/><br/>- 8.5.x for 8.5.12 onwards<br/><br/>- 8.0.x for 8.0.42 onwards<br/><br/>- 7.0.x for 7.0.76 onwards<br/>- 这个只是允许出现一些特殊字符，并没有说是全部特殊字符。<br/>好像是tomcat7.9以上的版本,都不支持请求链接上带有特殊字符.否则会报400错误,<br/>tomcat请求中包含特殊字符 [] &#124; &#123;&#125; 发送get请求失败：<br/>原因：<br/>这是因为Tomcat严格按照 RFC 3986规范进行访问解析，而 RFC 3986规范定义了Url中只允许包含英文字母（a-zA-Z）、数字（0-9）、-_.~4个特殊字符以及所有保留字符(RFC3986中指定了以下字符为保留字符：! * ’ ( ) ; : @ &amp; = + $ , / ? # [ ])。传入的参数中有&quot;[]&quot;、&quot;&quot;不在RFC3986中的保留字段中，所以会报这个错。<br/><br/>方法二：修改server.xml<br/>&lt;Connector port=&quot;8080&quot; protocol=&quot;HTTP/1.1&quot; connectionTimeout=&quot;20000&quot; relaxedQueryChars=&quot;[]&#124;&#123;&#125;^&amp;#x5c;&amp;#x60;&amp;quot;&amp;lt;&amp;gt;&quot; redirectPort=&quot;8443&quot; /&gt;<br/>方法三：修改server.xml（推荐方法）<br/>在Host里加入：<br/>&lt;Valve className=&quot;org.apache.catalina.valves.ErrorReportValve&quot;&nbsp;&nbsp;showReport=&quot;false&quot; showServerInfo=&quot;false&quot; /&gt;<br/>方法三 修改后只会显示出HTTP 400 错误，不会打印堆栈和tomcat版本号信息 <br/><br/>关于方法三的解释：<br/><br/>HTTP状态代码提供有关请求的HTTP请求成功完成的信息。HTTP响应分为五个类，如下所示：<br/><br/>信息回应<br/>成功的回应<br/>重新导向<br/>客户错误<br/>服务器错误<br/>在这里，我们的目的是使用HTTP响应代码处理HTTP请求时生成的错误/异常。您可以从HTTP状态代码信息中找到有关HTTP状态代码的更多详细信息。<br/><br/>该错误报告阀是一个错误处理程序，其处理错误状态代码与错误状态码或异常和服务器信息的信息错误页上相应地重定向用户。<br/><br/>该错误报告阀用于与Tomcat服务器编写自定义的错误报告处理功能。<br/><br/>在这里，我们将在Catalina（Tomcat）中创建自定义错误报告阀。错误报告阀用于报告错误/异常。因此，如果发生任何错误（例如404,400等），它将处理该错误并为我们提供默认的tomcat页面，如下所示（400错误请求的示例）。<br/><br/>在此上方，它将为您提供状态报告信息（HTTP状态）和服务器信息。因此，您可以在Tomcat服务器的conf 中使用server.xml对其进行配置。<br/><br/>默认的Catalina错误报告阀门和配置：org.apache.catalina.valves.ErrorReportValve<br/><br/>属性&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;描述<br/>showReport&nbsp;&nbsp;如果发生错误，用于在错误页面上显示错误信息（状态代码及其信息）的标志。默认值是true<br/>showServerInfo&nbsp;&nbsp;如果发生任何错误，则用于在错误页面上显示当前服务器信息的标志。默认值是true<br/>现在，添加/更新以下内容以在 中的server.xml中进行配置。<br/><br/>&lt;Valve className=&quot;org.apache.catalina.valves.ErrorReportValve&quot; showReport=&quot;false&quot; showServerInfo=&quot;false&quot; /&gt;<br/><br/>现在，使用上述配置，您将发现以下内容。（仅HTTP状态代码）。<br/><br/><br/>注意：同时禁用showServerInfo和showReport只会返回HTTP状态代码，并从默认响应中删除所有CSS。<br/><br/>参考地址：https://aspiresoftware.in/blog/catalinatomcat-custom-error-report-valve-to-handle-errors-exceptions/<br/>
]]>
</description>
</item><item>
<link>https://www.heckjj.com/post/642/</link>
<title><![CDATA[nginx目录穿越漏洞]]></title> 
<author>Heck &lt;@hecks.tk&gt;</author>
<category><![CDATA[Web开发]]></category>
<pubDate>Wed, 22 Feb 2023 02:59:18 +0000</pubDate> 
<guid>https://www.heckjj.com/post/642/</guid> 
<description>
<![CDATA[ 
	漏洞原理<br/>nginx在配置别名(Alias)的时候忘记加/，则可能实现目录穿越<br/>配置中设置目录别名时/files配置为/home/的别名,那么当我们访问<br/>/files…/时,nginx实际处理的路径时/home/…/,从而实现了穿越目录<br/><br/> location /file&#123;<br/> autoindex on;<br/> alias /home/;<br/> &#125;<br/><br/>所以修复这个漏洞也就是将上面的/file/闭合成这样，就不支持../这些回上一层目录的操作了。
]]>
</description>
</item>
</channel>
</rss>