-
在 Java 中,按钮(Button)是 GUI 编程中最常用的组件之一,主要用于触发用户操作。根据使用的 GUI 框架不同,Button 的实现方式也有所不同。下面我将介绍 Swing 和 JavaFX 两种主要框架中的 Button 使用方法。1. Swing 中的 JButton基本介绍JButton 是 Swing 包中的按钮组件,位于 javax.swing 包中。常用构造方法JButton() // 创建一个无文本、无图标的按钮JButton(String text) // 创建带有指定文本的按钮JButton(Icon icon) // 创建带有指定图标的按钮JButton(String text, Icon icon) // 创建带有指定文本和图标的按钮常用方法设置属性void setText(String text) // 设置按钮文本String getText() // 获取按钮文本void setIcon(Icon icon) // 设置按钮图标Icon getIcon() // 获取按钮图标void setEnabled(boolean enabled) // 启用/禁用按钮boolean isEnabled() // 检查按钮是否启用void setToolTipText(String text) // 设置鼠标悬停提示文本外观控制void setBackground(Color bg) // 设置背景色void setForeground(Color fg) // 设置前景色(文本颜色)void setFont(Font font) // 设置字体事件处理void addActionListener(ActionListener l) // 添加动作监听器void removeActionListener(ActionListener l) // 移除动作监听器基本使用示例import javax.swing.*;import java.awt.*;import java.awt.event.*;public class SwingButtonExample { public static void main(String[] args) { // 创建主窗口 JFrame frame = new JFrame("JButton 示例"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300, 200); frame.setLayout(new FlowLayout()); // 创建按钮 JButton button = new JButton("点击我"); // 添加事件监听器 button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog(frame, "按钮被点击了!"); } }); // 将按钮添加到窗口 frame.add(button); // 显示窗口 frame.setVisible(true); }}2. JavaFX 中的 Button基本介绍Button 是 JavaFX 中的按钮组件,位于 javafx.scene.control 包中。常用构造方法Button() // 创建一个无文本的按钮Button(String text) // 创建带有指定文本的按钮Button(String text, Node graphic) // 创建带有文本和图标的按钮常用方法设置属性void setText(String text) // 设置按钮文本String getText() // 获取按钮文本void setGraphic(Node graphic) // 设置按钮图标Node getGraphic() // 获取按钮图标void setDisable(boolean disable) // 禁用/启用按钮boolean isDisabled() // 检查按钮是否禁用void setTooltip(Tooltip tooltip) // 设置鼠标悬停提示外观控制void setStyle(String style) // 设置CSS样式void setId(String id) // 设置ID用于CSS选择器void setPrefSize(double width, double height) // 设置首选大小事件处理void setOnAction(EventHandler<ActionEvent> value) // 设置动作事件处理器基本使用示例import javafx.application.Application;import javafx.scene.Scene;import javafx.scene.control.Button;import javafx.scene.control.Alert;import javafx.scene.control.Alert.AlertType;import javafx.scene.layout.StackPane;import javafx.stage.Stage;public class JavaFXButtonExample extends Application { @Override public void start(Stage primaryStage) { // 创建按钮 Button btn = new Button(); btn.setText("点击我"); // 设置按钮事件 btn.setOnAction(event -> { Alert alert = new Alert(AlertType.INFORMATION); alert.setTitle("信息"); alert.setHeaderText(null); alert.setContentText("按钮被点击了!"); alert.showAndWait(); }); // 创建布局并添加按钮 StackPane root = new StackPane(); root.getChildren().add(btn); // 创建场景 Scene scene = new Scene(root, 300, 250); // 设置舞台 primaryStage.setTitle("JavaFX Button 示例"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); }}3. 高级功能Swing JButton 高级功能设置按钮图标状态:button.setPressedIcon(pressedIcon); // 按下时显示的图标button.setRolloverIcon(rolloverIcon); // 鼠标悬停时显示的图标button.setDisabledIcon(disabledIcon); // 禁用时显示的图标设置按钮边框:button.setBorder(BorderFactory.createLineBorder(Color.RED));设置快捷键:button.setMnemonic(KeyEvent.VK_C); // 设置Alt+C为快捷键JavaFX Button 高级功能使用CSS样式:btn.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white;");添加图标:ImageView imageView = new ImageView(new Image("file:icon.png"));btn.setGraphic(imageView);设置按钮效果:btn.setEffect(new DropShadow());总结:对于新的Java GUI项目,建议使用JavaFX,因为它是Oracle推荐的现代GUI框架对于维护旧项目或需要轻量级解决方案时,可以使用SwingJavaFX提供了更丰富的视觉效果和现代化的API,而Swing则更成熟稳定无论是使用Swing还是JavaFX,Button都是创建交互式用户界面的基础组件,掌握其使用方法对于Java GUI开发至关重要。
-
cit服务器ccivr icdpub中localcode改成utf-8了之前tts放音改的
-
【问题来源】 内部测试环境功能测试 【问题简要】 在ICD V300R008C25版本的SCE开发GSL流程中,在外接功能类CELL中有一个外接JAVA接口调用CELL控件,目前调用之后查看javadll.log日志,显示Error! Can not find Java Class,Class name = com/huawei/ivr/ivrTest, CaseCode = 9,请问下这种情况怎么解决呢?麻烦了谢谢!【问题类别】 IVR(gsl) 【AICC解决方案版本】 AICC 版本:AICC 23.200 SCE 版本: ICD V300R008C25 【期望解决时间】 尽快 【日志或错误截图】
-
1. 使用 String 类的 hashCode() 方法Java 的 String 类自带一个 hashCode() 方法,该方法返回一个 int 类型的哈希值。这个哈希值是基于字符串的内容计算得出的。String str = "Hello, World!"; int hash = str.hashCode(); System.out.println("Hash code: " + hash); 2. 使用 MessageDigest 类进行更复杂的哈希如果你需要更复杂的哈希(如 MD5、SHA-1、SHA-256 等),可以使用 java.security.MessageDigest 类。这些哈希算法返回的是字节数组,你可以将其转换为十六进制字符串表示,但本质上哈希计算的结果是字节数组。import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class HashExample { public static void main(String[] args) { try { String str = "Hello, World!"; MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hashBytes = md.digest(str.getBytes()); // 将字节数组转换为十六进制字符串(如果需要字符串表示) StringBuilder hexString = new StringBuilder(); for (byte b : hashBytes) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } System.out.println("SHA-256 Hash: " + hexString.toString()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } } 总结String.hashCode() 返回一个 int 类型的哈希值。使用 MessageDigest 可以计算更复杂的哈希(如 MD5、SHA-256),返回字节数组,可以转换为十六进制字符串表示。如果你只需要一个简单的整数哈希值,String.hashCode() 是一个直接且方便的选择。如果你需要更安全的哈希(如用于密码存储),则应使用 MessageDigest 或类似的库来计算更复杂的哈希。
-
【问题来源】 内部测试环境功能测试 【问题简要】 在ICD V300R008C25版本的SCE开发GSL流程中,在外接功能类CELL中有一个外接JAVA接口调用CELL控件,请问一下,我看文档介绍只支持jdk1.2、1.4、1.5,目前的在本地环境中使用的是openjdk1.8版本开发的java应用打包成的jar包。可否支持1.8版本呢?或者有其他的SCE版本支持吗?【问题类别】 IVR(gsl) 【AICC解决方案版本】 AICC 版本:AICC 23.200 SCE 版本: ICD V300R008C25 【期望解决时间】 尽快 【日志或错误截图】
-
在V300R008C25版本的SCE开发GSL流程中,在外接功能类CELL中有一个外接JAVA接口调用CELL控件,请问一下,我看文档介绍只支持jdk1.2、1.4、1.5,目前的环境中使用的是openjdk1.8版本的,可否支持1.8版本呢?或者有其他的SCE支持吗?
-
复赛名次在哪公布?复赛名次在哪公布
-
1. 理解DOM和页面结构在开始获取和解析页面内容之前,我们需要理解DOM(Document Object Model)的概念。DOM是将HTML或XML文档表示为树状结构的编程接口,其中每个节点都是文档的一部分,如元素、属性或文本。1.1 DOM树结构DOM将文档表示为节点树,其中:文档节点是整个文档的根节点元素节点代表HTML元素属性节点代表HTML属性文本节点包含元素内的文本内容<!DOCTYPE html><html><head> <title>示例页面</title></head><body> <h1>欢迎</h1> <p class="intro">这是一个示例段落。</p></body></html>对应的DOM树结构:Documenthtmlheadtitle“示例页面” (文本节点)bodyh1“欢迎” (文本节点)p (class属性为"intro")“这是一个示例段落。” (文本节点)1.2 为什么需要解析页面内容解析页面内容有许多实际应用:网页抓取(Web Scraping)内容分析自动化测试浏览器扩展开发数据提取和转换2. 获取整个页面的HTML代码2.1 使用document.documentElement.outerHTML获取整个页面HTML代码的最简单方法是使用document.documentElement.outerHTML属性:const fullPageHTML = document.documentElement.outerHTML;console.log(fullPageHTML); // 输出完整的HTML文档原理:document.documentElement代表HTML文档的根元素(通常是<html>元素)outerHTML属性获取元素及其所有子元素的HTML表示2.2 使用document.documentElement.innerHTML如果只需要<html>元素内部的内容(不包括<html>标签本身),可以使用:const htmlContent = document.documentElement.innerHTML;console.log(htmlContent); // 输出<html>内部的所有内容2.3 使用document.getElementsByTagName(‘html’)[0]另一种获取整个HTML内容的方式:const htmlElement = document.getElementsByTagName('html')[0];const fullHTML = htmlElement.outerHTML;console.log(fullHTML);2.4 获取DOCTYPE声明如果需要包含DOCTYPE声明,可以组合使用:const doctype = document.doctype;const doctypeString = doctype ? `<!DOCTYPE ${doctype.name}${doctype.publicId ? ` PUBLIC "${doctype.publicId}"` : ''}${doctype.systemId ? ` "${doctype.systemId}"` : ''}>` : '';const fullDocument = doctypeString + document.documentElement.outerHTML;console.log(fullDocument);3. 解析页面内容获取HTML代码后,下一步是解析其中的内容。JavaScript提供了多种方法来选择和操作DOM元素。3.1 使用DOM选择器方法3.1.1 getElementById通过元素的ID获取单个元素:const header = document.getElementById('header');console.log(header.textContent);3.1.2 getElementsByClassName通过类名获取元素集合:const items = document.getElementsByClassName('item');Array.from(items).forEach(item => { console.log(item.textContent);});3.1.3 getElementsByTagName通过标签名获取元素集合:const paragraphs = document.getElementsByTagName('p');Array.from(paragraphs).forEach(p => { console.log(p.innerHTML);});3.1.4 querySelector和querySelectorAll使用CSS选择器语法选择元素:// 获取第一个匹配的元素const firstItem = document.querySelector('.list-item');console.log(firstItem.textContent);// 获取所有匹配的元素const allItems = document.querySelectorAll('.list-item');allItems.forEach(item => { console.log(item.textContent);});3.2 遍历DOM树3.2.1 父节点和子节点const parent = document.querySelector('.parent');const children = parent.children; // 获取所有子元素// 遍历子节点Array.from(children).forEach(child => { console.log(child.tagName);});// 获取父节点const child = document.querySelector('.child');const parentNode = child.parentNode;console.log(parentNode.tagName);3.2.2 兄弟节点const item = document.querySelector('.item');const nextSibling = item.nextElementSibling;const previousSibling = item.previousElementSibling;console.log('下一个兄弟节点:', nextSibling);console.log('上一个兄弟节点:', previousSibling);3.2.3 递归遍历整个DOM树function traverseDOM(node, depth = 0) { // 打印当前节点信息 console.log(`${' '.repeat(depth * 2)}${node.nodeName}${node.nodeValue ? `: ${node.nodeValue.trim()}` : ''}`); // 如果有子节点,递归遍历 if (node.childNodes && node.childNodes.length > 0) { Array.from(node.childNodes).forEach(child => { traverseDOM(child, depth + 1); }); }}// 从body开始遍历traverseDOM(document.body);3.3 提取元素属性和内容3.3.1 获取元素属性const link = document.querySelector('a');console.log('href:', link.getAttribute('href'));console.log('class:', link.className);console.log('id:', link.id);console.log('所有属性:', link.attributes);3.3.2 获取元素文本内容const paragraph = document.querySelector('p');console.log('textContent:', paragraph.textContent); // 包括隐藏元素的文本console.log('innerText:', paragraph.innerText); // 仅显示文本,受CSS影响console.log('innerHTML:', paragraph.innerHTML); // 包含HTML标签3.3.3 获取表单元素值const input = document.querySelector('input[type="text"]');console.log('输入值:', input.value);const checkbox = document.querySelector('input[type="checkbox"]');console.log('是否选中:', checkbox.checked);const select = document.querySelector('select');console.log('选择的值:', select.value);console.log('选择的文本:', select.options[select.selectedIndex].text);4. 高级解析技术4.1 使用XPath解析XPath提供了一种在XML/HTML文档中导航和选择节点的强大方式:// 评估XPath表达式function evaluateXPath(xpath, context = document) { const result = []; const query = document.evaluate(xpath, context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); for (let i = 0; i < query.snapshotLength; i++) { result.push(query.snapshotItem(i)); } return result;}// 使用示例:获取所有h2标题的文本const headings = evaluateXPath('//h2');headings.forEach(h2 => { console.log(h2.textContent);});4.2 使用TreeWalker遍历DOMTreeWalker接口提供了更灵活的DOM遍历方式:const treeWalker = document.createTreeWalker( document.body, // 根节点 NodeFilter.SHOW_ELEMENT, // 只显示元素节点 { acceptNode: function(node) { return node.tagName === 'P' ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }}, // 只接受<p>元素 false);const paragraphs = [];let currentNode = treeWalker.nextNode();while (currentNode) { paragraphs.push(currentNode); currentNode = treeWalker.nextNode();}console.log('找到的段落:', paragraphs);4.3 使用DOMParser解析HTML字符串如果需要解析HTML字符串而不是现有文档:const htmlString = `<html><body><h1>标题</h1><p>段落内容</p></body></html>`;const parser = new DOMParser();const doc = parser.parseFromString(htmlString, 'text/html');// 现在可以像普通DOM一样操作const title = doc.querySelector('h1');console.log(title.textContent); // 输出"标题"4.4 使用MutationObserver监听DOM变化如果需要监控DOM的变化:const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { console.log('DOM发生了变化:', mutation); if (mutation.addedNodes.length) { console.log('添加的节点:', mutation.addedNodes); } if (mutation.removedNodes.length) { console.log('移除的节点:', mutation.removedNodes); } });});// 开始观察body元素及其子元素的变化observer.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true});// 停止观察// observer.disconnect();5. 实际应用示例5.1 提取所有链接function extractAllLinks() { const links = document.querySelectorAll('a[href]'); const urls = Array.from(links).map(link => { return { text: link.textContent.trim(), href: link.getAttribute('href'), title: link.getAttribute('title') || '' }; }); console.log('页面中的所有链接:', urls); return urls;}extractAllLinks();5.2 提取文章内容function extractArticleContent() { // 尝试找到可能包含文章内容的元素 const potentialSelectors = [ 'article', '.article', '.post', '.content', 'main', '#main' ]; let articleElement = null; for (const selector of potentialSelectors) { const element = document.querySelector(selector); if (element) { articleElement = element; break; } } // 如果没有找到特定元素,尝试启发式方法 if (!articleElement) { // 查找包含多个段落的最长元素 const allElements = document.querySelectorAll('body *'); let maxLength = 0; allElements.forEach(el => { const textLength = el.textContent.trim().length; const paragraphCount = el.querySelectorAll('p').length; if (textLength > maxLength && paragraphCount > 1) { maxLength = textLength; articleElement = el; } }); } if (articleElement) { const title = document.querySelector('h1') || document.querySelector('title') || { textContent: '无标题' }; const paragraphs = Array.from(articleElement.querySelectorAll('p')) .map(p => p.textContent.trim()) .filter(text => text.length > 0); const images = Array.from(articleElement.querySelectorAll('img')) .map(img => img.getAttribute('src')); return { title: title.textContent.trim(), paragraphs, images }; } return null;}console.log('提取的文章内容:', extractArticleContent());5.3 提取表格数据function extractTableData() { const tables = document.querySelectorAll('table'); const tableData = []; tables.forEach((table, index) => { const rows = table.querySelectorAll('tr'); const data = []; rows.forEach(row => { const cells = row.querySelectorAll('td, th'); const rowData = Array.from(cells).map(cell => cell.textContent.trim()); data.push(rowData); }); tableData.push({ tableIndex: index + 1, rows: data }); }); console.log('提取的表格数据:', tableData); return tableData;}extractTableData();5.4 提取元数据function extractMetaData() { const metaTags = document.querySelectorAll('meta'); const metadata = {}; metaTags.forEach(tag => { const name = tag.getAttribute('name') || tag.getAttribute('property') || tag.getAttribute('itemprop'); const content = tag.getAttribute('content'); if (name && content) { metadata[name] = content; } }); // 获取标题 metadata.title = document.title; // 获取描述(优先从meta标签获取) if (!metadata.description) { const firstParagraph = document.querySelector('p'); if (firstParagraph) { metadata.description = firstParagraph.textContent.trim().substring(0, 150) + '...'; } } // 获取关键词 if (!metadata.keywords) { metadata.keywords = []; } else if (typeof metadata.keywords === 'string') { metadata.keywords = metadata.keywords.split(',').map(k => k.trim()); } console.log('页面元数据:', metadata); return metadata;}extractMetaData();6. 处理动态内容现代网页经常使用JavaScript动态加载内容,这给内容提取带来了挑战。6.1 检测动态加载的内容// 使用MutationObserver检测动态加载的内容function watchForDynamicContent(callback) { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { callback(mutation.addedNodes); } }); }); observer.observe(document.body, { childList: true, subtree: true }); return observer;}// 示例:检测新加载的内容并提取其中的链接const dynamicLinks = new Set();const observer = watchForDynamicContent(nodes => { nodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const links = node.querySelectorAll('a[href]'); links.forEach(link => { const href = link.getAttribute('href'); if (!dynamicLinks.has(href)) { dynamicLinks.add(href); console.log('发现新链接:', href); } }); } });});// 停止观察// observer.disconnect();6.2 等待特定元素出现function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(mutations => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); reject(new Error(`等待元素 "${selector}" 超时`)); }, timeout); });}// 使用示例waitForElement('.dynamic-content') .then(element => { console.log('元素已加载:', element); }) .catch(error => { console.error(error); });6.3 模拟滚动以加载更多内容async function scrollToLoadAllContent() { let lastHeight = document.body.scrollHeight; let attempts = 0; const maxAttempts = 10; while (attempts < maxAttempts) { // 滚动到底部 window.scrollTo(0, document.body.scrollHeight); // 等待内容加载 await new Promise(resolve => setTimeout(resolve, 2000)); // 检查高度是否变化 const newHeight = document.body.scrollHeight; if (newHeight === lastHeight) { break; } lastHeight = newHeight; attempts++; } console.log('完成滚动,最终高度:', lastHeight);}// 使用示例scrollToLoadAllContent().then(() => { console.log('所有内容已加载(或达到最大尝试次数)');});7. 性能优化和最佳实践7.1 批量操作减少重绘// 不推荐的方式(每次循环都会导致重绘)const items = document.querySelectorAll('.item');items.forEach(item => { item.style.color = 'red';});// 推荐的方式(使用文档片段批量操作)const fragment = document.createDocumentFragment();const newItems = Array(10).fill().map((_, i) => { const div = document.createElement('div'); div.className = 'item'; div.textContent = `项目 ${i + 1}`; fragment.appendChild(div); return div;});document.body.appendChild(fragment);7.2 使用事件委托提高性能// 不推荐的方式(为每个元素添加事件监听器)document.querySelectorAll('.clickable-item').forEach(item => { item.addEventListener('click', handleClick);});// 推荐的方式(事件委托)document.body.addEventListener('click', event => { if (event.target.closest('.clickable-item')) { handleClick(event); }});function handleClick(event) { console.log('点击的项目:', event.target);}7.3 缓存DOM查询结果// 不推荐的方式(多次查询相同的元素)function updateElements() { document.querySelector('.item').style.color = 'red'; document.querySelector('.item').style.fontSize = '16px'; document.querySelector('.item').textContent = '更新后的文本';}// 推荐的方式(缓存查询结果)function updateElementsOptimized() { const item = document.querySelector('.item'); item.style.color = 'red'; item.style.fontSize = '16px'; item.textContent = '更新后的文本';}7.4 使用更高效的选择器// 不高效的选择器(过于通用)const allDivs = document.querySelectorAll('div div div');// 更高效的选择器(更具体)const specificDivs = document.querySelectorAll('.container > .wrapper > .content');8. 安全考虑8.1 防止XSS攻击当处理动态内容时,要注意防范XSS(跨站脚本)攻击:// 不安全的方式(直接插入HTML)function unsafeInsert(content) { document.querySelector('.output').innerHTML = content;}// 安全的方式(使用textContent或DOMPurify)function safeInsert(content) { // 方法1:仅插入文本 document.querySelector('.output').textContent = content; // 方法2:使用DOMPurify清理HTML // const clean = DOMPurify.sanitize(content); // document.querySelector('.output').innerHTML = clean;}8.2 处理用户生成的内容function sanitizeUserInput(input) { // 移除脚本标签 let sanitized = input.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); // 移除危险的属性 sanitized = sanitized.replace(/\son\w+="[^"]*"/g, ''); // 其他清理逻辑... return sanitized;}const userInput = '<script>alert("XSS")</script><img src="x" onerror="alert(1)">';console.log('清理后的输入:', sanitizeUserInput(userInput));9. 跨域限制和解决方案9.1 同源策略限制浏览器出于安全考虑实施了同源策略,限制了从不同源(协议、域名、端口)加载和操作内容的能力。9.2 使用CORS如果目标服务器支持CORS(跨源资源共享),可以直接请求:fetch('https://api.example.com/data', { method: 'GET', mode: 'cors', headers: { 'Content-Type': 'application/json' }}).then(response => response.json()).then(data => console.log(data)).catch(error => console.error('错误:', error));9.3 使用代理服务器对于不支持CORS的网站,可以通过自己的服务器代理请求:// 前端代码fetch('/proxy?url=' + encodeURIComponent('https://example.com')) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 解析文档... });// 服务器端(Node.js示例)/*app.get('/proxy', async (req, res) => { const { url } = req.query; try { const response = await axios.get(url); res.send(response.data); } catch (error) { res.status(500).send('代理请求失败'); }});*/9.4 浏览器扩展解决方案如果是开发浏览器扩展,可以使用chrome.webRequest API绕过某些限制:// 在manifest.json中声明权限/*"permissions": [ "webRequest", "webRequestBlocking", "<all_urls>"]*/// 在background.js中/*chrome.webRequest.onBeforeSendHeaders.addListener( details => { // 修改请求头 details.requestHeaders.push({ name: 'Origin', value: 'https://your-extension-id.chromiumapp.org' }); return { requestHeaders: details.requestHeaders }; }, { urls: ['<all_urls>'] }, ['blocking', 'requestHeaders']);*/10. 完整的页面解析工具示例下面是一个完整的示例,展示如何构建一个功能丰富的页面解析工具:class PageParser { constructor() { this.parsedData = { metadata: {}, structure: {}, content: {}, resources: {} }; } // 解析页面元数据 parseMetadata() { // 标题 this.parsedData.metadata.title = document.title; // meta标签 this.parsedData.metadata.metaTags = {}; document.querySelectorAll('meta').forEach(tag => { const name = tag.getAttribute('name') || tag.getAttribute('property') || tag.getAttribute('itemprop'); if (name) { this.parsedData.metadata.metaTags[name] = tag.getAttribute('content'); } }); // 链接标签 this.parsedData.metadata.links = []; document.querySelectorAll('link').forEach(link => { this.parsedData.metadata.links.push({ rel: link.getAttribute('rel'), href: link.getAttribute('href'), type: link.getAttribute('type') }); }); return this; } // 分析页面结构 analyzeStructure() { // 统计各类元素数量 this.parsedData.structure.elementCounts = {}; const allElements = document.querySelectorAll('*'); Array.from(allElements).forEach(el => { const tag = el.tagName.toLowerCase(); this.parsedData.structure.elementCounts[tag] = (this.parsedData.structure.elementCounts[tag] || 0) + 1; }); // 获取主要内容区域 this.parsedData.structure.mainContent = this.findMainContent(); return this; } // 查找主要内容区域 findMainContent() { const contentSelectors = [ 'main', 'article', '.main-content', '.content', '#content', '.article', '.post' ]; for (const selector of contentSelectors) { const element = document.querySelector(selector); if (element) { return { selector, textLength: element.textContent.length, paragraphCount: element.querySelectorAll('p').length }; } } // 启发式方法:查找包含最多文本的元素 let maxLength = 0; let mainElement = null; document.querySelectorAll('body > div, body > section').forEach(el => { const length = el.textContent.length; if (length > maxLength) { maxLength = length; mainElement = el; } }); return mainElement ? { selector: this.generateSelector(mainElement), textLength: mainElement.textContent.length, paragraphCount: mainElement.querySelectorAll('p').length } : null; } // 生成元素选择器 generateSelector(element) { if (element.id) { return `#${element.id}`; } const path = []; let current = element; while (current && current !== document.body) { let selector = current.tagName.toLowerCase(); if (current.className && typeof current.className === 'string') { const classes = current.className.split(/\s+/).filter(c => c); if (classes.length) { selector += `.${classes.join('.')}`; } } // 如果有兄弟元素,添加:nth-child const siblings = Array.from(current.parentNode.children); const index = siblings.indexOf(current); if (siblings.length > 1) { selector += `:nth-child(${index + 1})`; } path.unshift(selector); current = current.parentNode; } return path.join(' > '); } // 提取页面内容 extractContent() { // 提取所有文本段落 this.parsedData.content.paragraphs = Array.from(document.querySelectorAll('p')) .map(p => p.textContent.trim()) .filter(text => text.length > 0); // 提取所有标题 this.parsedData.content.headings = {}; ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(tag => { this.parsedData.content.headings[tag] = Array.from(document.querySelectorAll(tag)) .map(el => el.textContent.trim()); }); // 提取图片 this.parsedData.content.images = Array.from(document.querySelectorAll('img')) .map(img => ({ src: img.getAttribute('src'), alt: img.getAttribute('alt') || '', width: img.width, height: img.height })); return this; } // 收集页面资源 collectResources() { // 脚本 this.parsedData.resources.scripts = Array.from(document.querySelectorAll('script[src]')) .map(script => script.getAttribute('src')); // 样式表 this.parsedData.resources.stylesheets = Array.from(document.querySelectorAll('link[rel="stylesheet"]')) .map(link => link.getAttribute('href')); // 图片 this.parsedData.resources.images = Array.from(document.querySelectorAll('img[src]')) .map(img => img.getAttribute('src')); // 外部链接 this.parsedData.resources.links = Array.from(document.querySelectorAll('a[href]')) .filter(a => { const href = a.getAttribute('href'); return href && !href.startsWith('#') && !href.startsWith('javascript:'); }) .map(a => a.getAttribute('href')); return this; } // 获取解析结果 getResult() { return this.parsedData; } // 静态方法:完整解析页面 static parseFullPage() { return new PageParser() .parseMetadata() .analyzeStructure() .extractContent() .collectResources() .getResult(); }}// 使用示例document.addEventListener('DOMContentLoaded', () => { const pageData = PageParser.parseFullPage(); console.log('完整页面分析结果:', pageData); // 可以将结果发送到服务器或保存 // fetch('/api/save-analysis', { // method: 'POST', // body: JSON.stringify(pageData) // });});11. 总结本文详细介绍了如何使用JavaScript获取和解析页面内容,涵盖了从基础到高级的各种技术。我们学习了:获取页面HTML:使用outerHTML、innerHTML等方法获取完整或部分的HTML代码DOM遍历和选择:使用各种选择器方法和遍历技术定位特定元素内容提取:从元素中提取文本、属性和结构化数据高级技术:XPath、TreeWalker、MutationObserver等高级API的使用动态内容处理:监控和等待动态加载的内容性能优化:批量操作、事件委托等提高性能的技术安全考虑:防范XSS攻击和正确处理用户输入跨域限制:理解和解决同源策略带来的限制完整工具实现:构建一个功能全面的页面解析工具通过这些技术,你可以构建强大的网页抓取工具、内容分析系统或浏览器扩展,满足各种实际应用需求。记住在实际应用中要考虑性能、安全和合法性,确保你的代码既高效又负责任。————————————————原文链接:https://blog.csdn.net/sixpp/article/details/146491098
-
今天学习到了Java中的String,String是Java中一个非常重要的类,在我们做字符串操作的时候,需要使用到String。一、什么是StringString是JDK提供的一个类,是引用类型,并不是基本的数据类型,String用于字符串操作,在之前学习c语言的时候,对于一些字符串,会初始化字符数组表示。char str [] = {'a', 'b', 'c'};在Java中,String像是一个很大的char数组,我们在定义字符串的时候,不需要再去像上方代码一样去定义,而是直接可以使用String去定义。String str = "abc";String就像是很大的char数组,但相比于char数组而言,String可以做字符串拼接操作,而char数组并不能直接去做字符串的拼接,如下代码。String str = "abc" + "def";二、如何定义一个String既然知道String是一个类,那么类一定有他的初始化方法,在Java中,String的初始化有以下几种常用的方式。这是官方给出的一些介绍。 1. 用双引号定义String字符串,可以直接通过双引号来定义,把需要的字符串内容用双引号包裹,可以直接对String类型的对象赋值。public class StringDemo { public static void main(String[] args) { String str = "abc"; }}2. 通过构造函数定义String作为一个类,可以通过new关键字初始化,在Java中可以通过构造函数对String类型的对象赋值。public class StringDemo { public static void main(String[] args) { String str = new String("abc"); System.out.println(str); }}这是一种基本的构造字符串的方式,除此之外还有一些其他的构造方式。通过无参构造函数初始化这样的方式初始化的String的值为空,也就是什么都没有。public String() { this.value = "".value; this.coder = "".coder; }通过char数组进行初始化String可以传入一个char数组进行初始化,String会拼接char数组当中的所有字符。public String(char value[]) { this(value, 0, value.length, null); }带位移的方式通过char数组进行初始化这样的方式和上边的方式其实是一样的,只不过在初始化字符串的时候,会根据传入的offset作为char数组开始拼接的起始索引,并且拼接上count个字符。public String(char value[], int offset, int count) { this(value, offset, count, rangeCheck(value, offset, count)); }除了以上几种常用的方式外,还有一些其他的方式,如通过int数组进行初始化,通过byte数组进行初始化,大家感兴趣的话可以自己研究一下。 三、String中的一些常用方法了解到什么是String,下面就要理解一些String中常用的方法。1 字符串比较字符串之间的比较又分为很多不同的方法,下边是一些常用的字符串比较方法。 1.1 字符串使用 ==字符串使用 == 操作,其实是一个有坑的点,一般不用,这里不在多讲,有兴趣的可以尝试一下以下代码。 1.2 字符串使用equals()字符串的比较,使用这个方法比较多一点。 1.3 使用 equalsIgnoreCase()这个方法相比于普通的equals方法的区别是,这个方法是忽略大小写的。 除此之外,字符串比较还有这两个方法。1.4 cpmpareTo和compareToIgnoreCase这两个方法和equals方法的区别是,equals返回的是boolean类型的变量,而compare方法返回的是int类型的变量。具体比较方法如下:两个字符串按照单个字符从前向后作比较,遇到不同的字符,返回两个字符的差值如果两个字符比较完成了,没有发现不同的字符,返回两个字符串的长度差。2 字符串大小写转换 String中还提供了一些字符串大小写转换的方法。 2.1 toUpperCase()这个方法就是把字符串全部转换为大写。 2.2 toLowerCase()这个方法把字符串全部转换为小写 3 字符串长度获取3.1 length()字符串长度的获取通过以下方法获取。 4 判断是否包含一段字符串4.1 contains()String是有子字符串的概念的,比如我们要查看某个字符串中是否包含一小段的字符串,我们可以用以下方法。 5 字符串切割和拼接 5.1 split()字符串的切割和拼接是非常实用的方法,现在我们有这样的一个字符串。String students = "zhangsan,lisi,wangwu,liuliu";我们想要把这些姓名拆分出来,就需要用到下边的方法。 对于字符串切割,我们需要传入一个作为分割的字符,在上方的代码当中传入的是一个",",当然在具体应用的时候,还要根据具体的场景做分析。5.2 join()除了字符串分割外,当然也有字符串拼接的操作,如果我们想把studentArr中的学生姓名用横杠拼接起来,像这样zhangsan-lisi-wangwu-liuliu需要以下代码完成,这里只介绍基本的使用。 6 字符串寻找字串起始位置我们可以判断字串是否存在,也可以获取子串在字符串的起始索引。 6.1 indexOf()看下方代码,我们试图在字符串中寻找有没有值为"lisi"的字符串,我们调用indexOf方法就可以,indexOf方法有两种,一种是直接传匹配字串,另外一种是传入匹配子串的同时传入开始匹配的起始下边,比如我们从索引10开始寻找,因为lisi的开始索引为9,当我们把开始匹配的下标放到10的时候,就没有办法在匹配到"lisi"了。 6.2 lastIndexOf() 和indexOf()是一样的,不同的是,lastIndexOf()是判断的结尾,是从后往前找的。7 获取字串7.1 substring()字串可以判断存不存在,可以获取起始下标,当然也可以获取子串。通过起始下标和结束下标来截取子串。8 字符串替换 replace:有两个重载形式,replace(char oldChar, char newChar)用于字符替换,replace(CharSequence target, CharSequence replacement) 可用于字符串替换。这里的CharSequence是字符串序列,简单理解就是字符串 ,该方法不会将参数解析为正则表达式。replaceAll:方法签名为replaceAll(String regex, String replacement),参数regex要求是一个正则表达式字符串,它会按照正则表达式规则去匹配字符串中的子串并替换 。如果传入的不是正则表达式,也会当作普通字符串处理。replaceFirst:方法签名为replaceFirst(String regex, String replacement) ,和replaceAll一样,第一个参数regex也是基于正则表达式的,不过它只替换第一次匹配到的子串。当传入非正则表达式的普通字符串时,也能进行字符串替换操作。 对于一些不想要的字符,也是可以替换的,拿上方的这个字符串来说。String students = "zhangsan,lisi,wangwu,liuliu";如果不想要逗号了,想要用横线分割,下方代码可以实现。8.1 replace()8.2 replaceAll()与replace()不同的是,replaceAll()是可以传入正则表达式的,这里不在讲正则表达式,以下的方式也是可以替换的。 以上就是一些常用的String的使用方法。————————————————原文链接:https://blog.csdn.net/2201_76027234/article/details/146368113
-
1. 关键字定义:被Java语言赋予了特殊含义,用做专门用途的字符串(或单词)HelloWorld案例中,出现的关键字有 class、public 、 static 、 void 等,这些单词已经被Java定义好了。特点:全部关键字都是小写字母。关键字比较多,不需要死记硬背,学到哪里记到哪里即可。 说明: 关键字一共50个,其中const和goto是保留字(reserved word)。true,false,null不在其中,它们看起来像关键字,其实是字面量,表示特殊的布尔值和空值。2. 标识符Java中变量、方法、类等要素命名时使用的字符序列,称为标识符。 技巧:凡是自己可以起名字的地方都叫标识符。 标识符的命名规则(必须遵守的硬性规定): 由26个英文字母大小写,0-9 ,_或 $ 组成数字不可以开头。不可以使用关键字和保留字,但能包含关键字和保留字。Java中严格区分大小写,长度无限制。标识符不能包含空格。 3. 变量3.1 初识变量变量的概念: 内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化 变量的构成包含三个要素:数据类型、变量名、存储的值 Java中变量声明的格式:数据类型 变量名 = 变量值 变量的作用:用于在内存中保存数据。 使用变量注意: Java中每个变量必须先声明,后使用。使用变量名来访问这块区域的数据。变量的作用域:其定义所在的一对{ }内。变量只有在其作用域内才有效。出了作用域,变量不可以再被调用。同一个作用域内,不能定义重名的变量。3.2 Java中变量的数据类型Java中变量的数据类型分为两大类: 基本数据类型:包括 整数类型、浮点数类型、字符类型、布尔类型。 引用数据类型:包括数组、 类、接口、枚举、注解、记录。 3.3 变量的使用3.3.1 步骤1:变量的声明格式:数据类型 变量名;1//例如://存储一个整数类型的年龄int age; //存储一个小数类型的体重double weight; //存储一个单字符类型的性别 char gender; //存储一个布尔类型的婚姻状态boolean marry; //存储一个字符串类型的姓名String name; //声明多个同类型的变量int a,b,c; //表示a,b,c三个变量都是int类型。 注意:变量的数据类型可以是基本数据类型,也可以是引用数据类型。 3.3.2 步骤2:变量的赋值给变量赋值,就是把“值”存到该变量代表的内存空间中。同时,给变量赋的值类型必须与变量声明的类型一致或兼容。 变量赋值的语法格式: 变量名 = 值; 举例1:可以使用合适类型的常量值给已经声明的变量赋值 age = 18;weight = 109;gender = '女'; 举例2:可以使用其他变量或者表达式给变量赋值 int m = 1;int n = m; int x = 1;int y = 2;int z = 2 * x + y; 3:变量可以反复赋值 //先声明,后初始化char gender;gender = '女'; //给变量重新赋值,修改gender变量的值gender = '男';System.out.println("gender = " + gender);//gender = 男 举例4:也可以将变量的声明和赋值一并执行 boolean isBeauty = true;String name = "迪丽热巴"; 内存结构如图: 4. 基本数据类型介绍4.1 整数类型:byte、short、int、longJava各整数类型有固定的表数范围和字段长度,不受具体操作系统的影响,以保证Java程序的可移植性。 定义long类型的变量,赋值时需要以"l"或"L"作为后缀。 Java程序中变量通常声明为int型,除非不足以表示较大的数,才使用long。 Java的整型常量默认为 int 型。 4.1.1 补充:计算机存储单位**字节(Byte):**是计算机用于计量存储容量的基本单位,一个字节等于8 bit。 **位(bit):**是数据存储的最小单位。二进制数系统中,每个0或1就是一个位,叫做bit(比特),其中8 bit 就称为一个字节(Byte)。 转换关系: 8 bit = 1 Byte1024 Byte = 1 KB1024 KB = 1 MB1024 MB = 1 GB1024 GB = 1 TB4.2 浮点类型:float、double与整数类型类似,Java 浮点类型也有固定的表数范围和字段长度,不受具体操作系统的影响。 浮点型常量有两种表示形式:十进制数形式。如:5.12 512.0f .512 (必须有小数点)科学计数法形式。如:5.12e2 512E2 100E-2float:单精度,尾数可以精确到7位有效数字。很多情况下,精度很难满足需求。double:双精度,精度是float的两倍。通常采用此类型。定义float类型的变量,赋值时需要以"f"或"F"作为后缀。Java 的浮点型常量默认为double型。4.2.1 关于浮点型精度的说明并不是所有的小数都能可以精确的用二进制浮点数表示。二进制浮点数不能精确的表示0.1、0.01、0.001这样10的负次幂。 浮点类型float、double的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数字计算或保留指定位数的精度,需要使用BigDecimal类。 测试用例: //测试1:(解释见章末企业真题:为什么0.1 + 0.2不等于0.3)System.out.println(0.1 + 0.2);//0.30000000000000004 //测试2:float ff1 = 123123123f;float ff2 = ff1 + 1;System.out.println(ff1);System.out.println(ff2);System.out.println(ff1 == ff2); 4.3 字符类型:charchar 型数据用来表示通常意义上“字符”(占2字节) Java中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其他书面语的一个字符。 字符型变量的三种表现形式: **形式1:**使用单引号(’ ')括起来的单个字符。 例如:char c1 = ‘a’; char c2 = ‘中’; char c3 = ‘9’; **形式2:**直接使用 Unicode值来表示字符型常量:‘\uXXXX’。其中,XXXX代表一个十六进制整数。 例如:\u0023 表示 ‘#’。 **形式3:**Java中还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量。 例如:char c3 = ‘\n’; // '\n’表示换行符 转义字符 说明 Unicode表示方式\n 换行符 \u000a\t 制表符 \u0009\" 双引号 \u0022\' 单引号 \u0027\\ 反斜线 \u005c\b 退格符 \u0008\r 回车符 \u000dchar类型是可以进行运算的。因为它都对应有Unicode码,可以看做是一个数值。 4.4 布尔类型:booleanboolean 类型用来判断逻辑条件,一般用于流程控制语句中: if条件控制语句;while循环控制语句;for循环控制语句;do-while循环控制语句;boolean类型数据只有两个值:true、false,无其它。 不可以使用0或非 0 的整数替代false和true,这点和C语言不同。拓展:Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达所操作的boolean值,在编译之后都使用java虚拟机中的int数据类型来代替:true用1表示,false用0表示。——《java虚拟机规范 8版》举例: boolean isFlag = true; if(isFlag){ //true分支}else{ //false分支} 经验之谈: Less is More!建议不要这样写:if ( isFlag = = true ),只有新手才如此。关键也很容易写错成if(isFlag = true),这样就变成赋值isFlag为true而不是判断!老鸟的写法是if (isFlag)或者if ( !isFlag)。 5. 基本数据类型变量间运算规则在Java程序中,不同的基本数据类型(只有7种,不包含boolean类型)变量的值经常需要进行相互转换。 转换的方式有两种:自动类型提升和强制类型转换。 5.1 自动类型提升规则:将取值范围小(或容量小)的类型自动提升为取值范围大(或容量大)的类型 。 基本数据类型的转换规则如图所示: (1)当把存储范围小的值(常量值、变量的值、表达式计算的结果值)赋值给了存储范围大的变量时 int i = 'A';//char自动升级为int,其实就是把字符的编码值赋值给i变量了double d = 10;//int自动升级为doublelong num = 1234567; //右边的整数常量值如果在int范围呢,编译和运行都可以通过,这里涉及到数据类型转换 //byte bigB = 130;//错误,右边的整数常量值超过byte范围long bigNum = 12345678912L;//右边的整数常量值如果超过int范围,必须加L,显式表示long类型。否则编译不通过 (2)当存储范围小的数据类型与存储范围大的数据类型变量一起混合运算时,会按照其中最大的类型运算。 int i = 1;byte b = 1;double d = 1.0; double sum = i + b + d;//混合运算,升级为double (3)当byte,short,char数据类型的变量进行算术运算时,按照int类型处理。 byte b1 = 1;byte b2 = 2;byte b3 = b1 + b2;//编译报错,b1 + b2自动升级为int char c1 = '0';char c2 = 'A';int i = c1 + c2;//至少需要使用int类型来接收System.out.println(c1 + c2);//113 5.2 强制类型转换将3.14 赋值到int 类型变量会发生什么?产生编译失败,肯定无法赋值。 int i = 3.14; // 编译报错 想要赋值成功,只有通过强制类型转换,将double 类型强制转换成int 类型才能赋值。 规则:将取值范围大(或容量大)的类型强制转换成取值范围小(或容量小)的类型。 自动类型提升是Java自动执行的,而强制类型转换是自动类型提升的逆运算,需要我们自己手动执行。 转换格式: 数据类型1 变量名 = (数据类型1)被强转数据值; //()中的数据类型必须<=变量值的数据类型 (1)当把存储范围大的值(常量值、变量的值、表达式计算的结果值)强制转换为存储范围小的变量时,可能会损失精度或溢出。 int i = (int)3.14;//损失精度 double d = 1.2;int num = (int)d;//损失精度 int i = 200;byte b = (byte)i;//溢出 (2)当某个值想要提升数据类型时,也可以使用强制类型转换。这种情况的强制类型转换是没有风险的,通常省略。 int i = 1;int j = 2;double bigger = (double)(i/j); (3)声明long类型变量时,可以出现省略后缀的情况。float则不同。 long l1 = 123L;long l2 = 123;//如何理解呢? 此时可以看做是int类型的123自动类型提升为long类型 //long l3 = 123123123123; //报错,因为123123123123超出了int的范围。long l4 = 123123123123L; //float f1 = 12.3; //报错,因为12.3看做是double,不能自动转换为float类型float f2 = 12.3F;float f3 = (float)12.3; 6 基本数据类型与String的运算6.1 字符串类型:StringString不是基本数据类型,属于引用数据类型使用一对""来表示一个字符串,内部可以包含0个、1个或多个字符。声明方式与基本数据类型类似。例如:String str = “尚硅谷”;6.2 运算规则1、任意八种基本数据类型的数据与String类型只能进行连接“+”运算,且结果一定也是String类型 System.out.println("" + 1 + 2);//12 int num = 10;boolean b1 = true;String s1 = "abc"; String s2 = s1 + num + b1;System.out.println(s2);//abc10true //String s3 = num + b1 + s1;//编译不通过,因为int类型不能与boolean运算String s4 = num + (b1 + s1);//编译通过 2、String类型不能通过强制类型()转换,转为其他的类型 String str = "123";int num = (int)str;//错误的 int num = Integer.parseInt(str);//正确的,后面才能讲到,借助包装类的方法才能转 7. 运算符运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。 运算符的分类: 按照功能分为:算术运算符、赋值运算符、比较(或关系)运算符、逻辑运算符、位运算符、条件运算符、Lambda运算符分类 运算符算术运算符(7个) +、-、*、/、%、++、–赋值运算符(12个) =、+=、-=、*=、/=、%=、>>=、<<=、>>>=、&=、|=、^=等比较(或关系)运算符(6个) >、>=、<、<=、==、!=逻辑运算符(6个) &、|、^、!、&&、||位运算符(7个) &、|、^、~、<<、>>、>>>条件运算符(1个) (条件表达式)?结果1:结果2Lambda运算符(1个) ->(第18章时讲解)按照操作数个数分为:一元运算符(单目运算符)、二元运算符(双目运算符)、三元运算符 (三目运算符)分类 运算符一元运算符(单目运算符) 正号(+)、负号(-)、++、–、!、~二元运算符(双目运算符) 除了一元和三元运算符剩下的都是二元运算符三元运算符 (三目运算符) (条件表达式)?结果1:结果27.1 算术运算符7.1.1 基本语法 举例1:加减乘除模 public class ArithmeticTest1 {public static void main(String[] args) {int a = 3;int b = 4; System.out.println(a + b);// 7System.out.println(a - b);// -1System.out.println(a * b);// 12System.out.println(a / b);// 计算机结果是0,为什么不是0.75呢?System.out.println(a % b);// 3 //结果与被模数符号相同 System.out.println(5%2);//1System.out.println(5%-2);//1System.out.println(-5%2);//-1System.out.println(-5%-2);//-1//商*除数 + 余数 = 被除数//5%-2 ==>商是-2,余数时1 (-2)*(-2)+1 = 5//-5%2 ==>商是-2,余数是-1 (-2)*2+(-1) = -4-1=-5}} 举例2:“+”号的两种用法 第一种:对于+两边都是数值的话,+就是加法的意思第二种:对于+两边至少有一边是字符串的话,+就是拼接的意思public class ArithmeticTest2 {public static void main(String[] args) {// 字符串类型的变量基本使用// 数据类型 变量名称 = 数据值;String str1 = "Hello";System.out.println(str1); // Hello System.out.println("Hello" + "World"); // HelloWorld String str2 = "Java";// String + int --> StringSystem.out.println(str2 + 520); // Java520// String + int + int// String + int// StringSystem.out.println(str2 + 5 + 20); // Java520}} 举例3:自加自减运算 理解:++ 运算,表示自增1。同理,-- 运算,表示自减1,用法与++ 一致。 1、单独使用 变量在单独运算的时候,变量前++和变量后++,是没有区别的。变量前++ :例如 ++a 。变量后++ :例如 a++ 。public class ArithmeticTest3 {public static void main(String[] args) {// 定义一个int类型的变量aint a = 3;//++a;a++; // 无论是变量前++还是变量后++,结果都是4System.out.println(a);}} 2、复合使用 和其他变量放在一起使用或者和输出语句放在一起使用,前++和后++就产生了不同。变量前++ :变量先自增1,然后再运算。变量后++ :变量先运算,然后再自增1。public class ArithmeticTest4 {public static void main(String[] args) {// 其他变量放在一起使用int x = 3;//int y = ++x; // y的值是4,x的值是4,int y = x++; // y的值是3,x的值是4 System.out.println(x);System.out.println(y);System.out.println("=========="); // 和输出语句一起int z = 5;//System.out.println(++z);// 输出结果是6,z的值也是6System.out.println(z++);// 输出结果是5,z的值是6System.out.println(z); } } 7.2 赋值运算符7.2.1 基本语法符号:= 当“=”两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理。支持连续赋值。扩展赋值运算符: +=、 -=、*=、 /=、%= 赋值运算符 符号解释+= 将符号左边的值和右边的值进行相加操作,最后将结果赋值给左边的变量-= 将符号左边的值和右边的值进行相减操作,最后将结果赋值给左边的变量*= 将符号左边的值和右边的值进行相乘操作,最后将结果赋值给左边的变量/= 将符号左边的值和右边的值进行相除操作,最后将结果赋值给左边的变量%= 将符号左边的值和右边的值进行取余操作,最后将结果赋值给左边的变量public class SetValueTest1 {public static void main(String[] args) {int i1 = 10;long l1 = i1; //自动类型转换 byte bb1 = (byte)i1; //强制类型转换 int i2 = i1; //连续赋值的测试//以前的写法int a1 = 10;int b1 = 10; //连续赋值的写法int a2,b2;a2 = b2 = 10; int a3 = 10,b3 = 20; //举例说明+= -= *= /= %= int m1 = 10;m1 += 5; //类似于 m1 = m1 + 5的操作,但不等同于。System.out.println(m1);//15 //练习1:开发中,如何实现一个变量+2的操作呢?// += 的操作不会改变变量本身的数据类型。其他拓展的运算符也如此。//写法1:推荐short s1 = 10;s1 += 2; //编译通过,因为在得到int类型的结果后,JVM自动完成一步强制类型转换,将int类型强转成shortSystem.out.println(s1);//12//写法2:short s2 = 10;//s2 = s2 + 2;//编译报错,因为将int类型的结果赋值给short类型的变量s时,可能损失精度s2 = (short)(s2 + 2);System.out.println(s2); //练习2:开发中,如何实现一个变量+1的操作呢?//写法1:推荐int num1 = 10;num1++;System.out.println(num1); //写法2:int num2 = 10;num2 += 1;System.out.println(num2); //写法3:int num3 = 10;num3 = num3 + 1;System.out.println(num3); }} 7.3 比较(关系)运算符 比较运算符的结果都是boolean型,也就是要么是true,要么是false。 > < >= <= :只适用于基本数据类型(除boolean类型之外) == != :适用于基本数据类型和引用数据类型 比较运算符“==”不能误写成“=” 举例: class CompareTest {public static void main(String[] args) {int i1 = 10;int i2 = 20; System.out.println(i1 == i2);//falseSystem.out.println(i1 != i2);//trueSystem.out.println(i1 >= i2);//false int m = 10;int n = 20;System.out.println(m == n);//falseSystem.out.println(m = n);//20 boolean b1 = false;boolean b2 = true;System.out.println(b1 == b2);//falseSystem.out.println(b1 = b2);//true}} 7.4 逻辑运算符7.4.1 基本语法 逻辑运算符,操作的都是boolean类型的变量或常量,而且运算得结果也是boolean类型的值。 运算符说明: & 和 &&:表示"且"关系,当符号左右两边布尔值都是true时,结果才能为true。否则,为false。| 和 || :表示"或"关系,当符号两边布尔值有一边为true时,结果为true。当两边都为false时,结果为false! :表示"非"关系,当变量布尔值为true时,结果为false。当变量布尔值为false时,结果为true。^ :当符号左右两边布尔值不同时,结果为true。当两边布尔值相同时,结果为false。理解:异或,追求的是“异”!逻辑运算符用于连接布尔型表达式,在Java中不可以写成 3 < x < 6,应该写成x > 3 & x < 6 。 区分“&”和“&&”: 相同点:如果符号左边是true,则二者都执行符号右边的操作 不同点:& : 如果符号左边是false,则继续执行符号右边的操作 && :如果符号左边是false,则不再继续执行符号右边的操作 建议:开发中,推荐使用 &&区分“|”和“||”: 相同点:如果符号左边是false,则二者都执行符号右边的操作 不同点:| : 如果符号左边是true,则继续执行符号右边的操作 || :如果符号左边是true,则不再继续执行符号右边的操作 建议:开发中,推荐使用 || 代码举例: public class LoginTest {public static void main(String[] args) {int a = 3;int b = 4;int c = 5; // & 与,且;有false则falseSystem.out.println((a > b) & (a > c)); System.out.println((a > b) & (a < c)); System.out.println((a < b) & (a > c)); System.out.println((a < b) & (a < c)); System.out.println("===============");// | 或;有true则trueSystem.out.println((a > b) | (a > c)); System.out.println((a > b) | (a < c)); System.out.println((a < b) | (a > c));System.out.println((a < b) | (a < c));System.out.println("===============");// ^ 异或;相同为false,不同为trueSystem.out.println((a > b) ^ (a > c));System.out.println((a > b) ^ (a < c)); System.out.println((a < b) ^ (a > c)); System.out.println((a < b) ^ (a < c)); System.out.println("===============");// ! 非;非false则true,非true则falseSystem.out.println(!false);System.out.println(!true); //&和&&的区别 System.out.println((a > b) & (a++ > c)); System.out.println("a = " + a); System.out.println((a > b) && (a++ > c)); System.out.println("a = " + a); System.out.println((a == b) && (a++ > c)); System.out.println("a = " + a); //|和||的区别 System.out.println((a > b) | (a++ > c)); System.out.println("a = " + a); System.out.println((a > b) || (a++ > c)); System.out.println("a = " + a); System.out.println((a == b) || (a++ > c)); System.out.println("a = " + a);}} 7.5 位运算符7.5.1 基本语法 位运算符的运算过程都是基于二进制的补码运算(1)左移:<< 运算规则:在一定范围内,数据每向左移动一位,相当于原数据*2。(正数、负数都适用) 【注意】当左移的位数n超过该数据类型的总位数时,相当于左移(n-总位数)位 3<<4 类似于 3*2的4次幂 => 3*16 => 48 -3<<4 类似于 -3*2的4次幂 => -3*16 => -48 (2)右移:>> 运算规则:在一定范围内,数据每向右移动一位,相当于原数据/2。(正数、负数都适用) 【注意】如果不能整除,向下取整。 69>>4 类似于 69/2的4次 = 69/16 =4 -69>>4 类似于 -69/2的4次 = -69/16 = -5 (3)无符号右移:>>> 运算规则:往右移动后,左边空出来的位直接补0。(正数、负数都适用) 69>>>4 类似于 69/2的4次 = 69/16 =4 -69>>>4 结果:268435451 (4)按位与:& 运算规则:对应位都是1才为1,否则为0。 1 & 1 结果为1 1 & 0 结果为0 0 & 1 结果为0 0 & 0 结果为0 9 & 7 = 1 -9 & 7 = 7 (5)按位或:| 运算规则:对应位只要有1即为1,否则为0。 1 | 1 结果为1 1 | 0 结果为1 0 | 1 结果为1 0 & 0 结果为0 9 | 7 //结果: 15 -9 | 7 //结果: -9 (6)按位异或:^ 运算规则:对应位一个为1一个为0,才为1,否则为0。 1 ^ 1 结果为0 1 ^ 0 结果为1 0 ^ 1 结果为1 0 ^ 0 结果为0 9 ^ 7 //结果为14 -9 ^ 7 //结果为-16 (7)按位取反:~ 运算规则:对应位为1,则结果为0;对应位为0,则结果为1。 ~0就是1 ~1就是0 ~9 //结果:-10 ~-9 //结果:8 7.6 条件运算符7.6.1 基本语法条件运算符格式:(条件表达式)? 表达式1:表达式21说明:条件表达式是boolean类型的结果,根据boolean的值选择表达式1或表达式2 如果运算后的结果赋给新的变量,要求表达式1和表达式2为同种或兼容的类型 public static void main(String[] args) { int i = (1==2 ? 100 : 200); System.out.println(i);//200 boolean marry = false;System.out.println(marry ? "已婚" : "未婚" ); double d1 = (m1 > m2)? 1 : 2.0;System.out.println(d1); int num = 12; System.out.println(num > 0? true : "num非正数");} 7.6.2 与if-else的转换关系凡是可以使用条件运算符的地方,都可以改写为if-else结构。反之,不成立。 开发中,如果既可以使用条件运算符,又可以使用if-else,推荐使用条件运算符。因为执行效率稍高。 //if-else实现获取两个数的较大值 int i1 = 10;int i2 = 20; int max;//声明变量max,用于记录i1和i2的较大值 if(i1 > i2){ max = i1;}else{ max = i2;} System.out.println(max); 7.7 运算符优先级运算符有不同的优先级,所谓优先级就是在表达式运算中的运算符顺序。 上一行中的运算符总是优先于下一行的。 优先级 运算符说明 Java运算符1 括号 ()、[]、{}2 正负号 +、-3 单元运算符 ++、--、~、!4 乘法、除法、求余 *、/、%5 加法、减法 +、-6 移位运算符 <<、>>、>>>7 关系运算符 <、<=、>=、>、instanceof8 等价运算符 ==、!=9 按位与 &10 按位异或 ^11 按位或 `12 条件与 &&13 条件或 `14 三元运算符 ? :15 赋值运算符 =、+=、-=、*=、/=、%=16 位赋值运算符 &=、`开发建议: 不要过多的依赖运算的优先级来控制表达式的执行顺序,这样可读性太差,尽量使用()来控制表达式的执行顺序。不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它分成几步来完成。例如: (num1 + num2) * 2 > num3 && num2 > num3 ? num3 : num1 + num2;————————————————原文链接:https://blog.csdn.net/fj123789/article/details/145671844
-
在信息安全领域,数据加密是保护数据机密性、完整性和可用性的重要手段之一。AES(Advanced Encryption Standard)作为当前广泛使用的对称加密算法,以其高效、安全的特点,在各类应用系统中扮演着重要角色。Java作为一门广泛应用于企业级开发的编程语言,其强大的加密库为开发者提供了丰富的加密解密工具。然而,直接使用Java的加密API进行AES加密解密时,可能会面临代码繁琐、理解难度高等问题。此时,Hutool这一Java工具类库便显得尤为实用。Hutool是一个小而全的Java工具类库,它简化了Java开发中常见的繁琐操作,包括但不限于日期处理、文件操作、加密解密等。在加密解密方面,Hutool提供了简洁易用的API,使得AES加密解密变得轻松简单。本文将详细介绍如何在Java项目中使用Hutool进行AES加密解密。一、Hutool简介与引入1.1 Hutool简介Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式编程的简洁性。Hutool中的工具方法来自于每个用户的精雕细琢,它涵盖了Java开发底层代码中的方方面面,它既是大型项目开发中解决小问题的利器,也是小型项目中的效率担当。1.2 引入Hutool在Maven项目中,可以通过添加以下依赖来引入Hutool:<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>你的版本号</version> </dependency>请替换你的版本号为当前最新的Hutool版本号,以确保使用到最新的功能和修复。二、AES加密解密基础AES加密是一种对称加密算法,即加密和解密使用相同的密钥。AES支持三种长度的密钥:128位、192位和256位。在AES加密过程中,数据首先被分成多个固定长度的块(Block),然后每个块独立地进行加密。AES加密过程大致可以分为以下几个步骤:密钥扩展(Key Expansion):将用户提供的密钥扩展成一系列轮密钥(Round Keys)。初始轮(Initial Round):将明文块与初始轮密钥进行特定的操作(如异或、替换等)。中间轮(Intermediate Rounds):对初始轮的结果进行多次迭代加密,每次迭代使用不同的轮密钥。最终轮(Final Round):与中间轮类似,但可能包含一些额外的操作,如列混淆等。输出:最终轮的结果即为加密后的密文。解密过程则是加密过程的逆操作,使用相同的密钥和算法将密文还原为明文。三、使用Hutool进行AES加密解密3.1 加密在Hutool中,进行AES加密主要依赖于SecureUtil类和AES类。以下是一个简单的AES加密示例:import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.symmetric.AES; public class AesEncryptExample { public static void main(String[] args) { // 原始数据 String content = "Hello Hutool AES!"; // 密钥,AES要求密钥长度为128/192/256位 byte[] keyBytes = SecureUtil.generateKey(256).getEncoded(); // 创建AES加密对象,使用ECB/PKCS5Padding AES aes = SecureUtil.aes(keyBytes, "ECB/PKCS5Padding"); // 加密 byte[] encrypt = aes.encrypt(content); // 加密结果通常用于存储或传输,这里简单打印其Base64编码形式 String encryptHex = aes.encryptHex(content); System.out.println("加密结果(Hex): " + encryptHex); // 如果需要原始字节数组,则使用encrypt方法 // System.out.println(Base64.getEncoder().encodeToString(encrypt)); } }注意:在实际应用中,密钥keyBytes应安全地生成和存储,避免硬编码在代码中。3.2 解密解密过程与加密过程类似,只是将加密后的数据(密文)作为输入,通过AES解密对象还原为原始数据(明文)。// 假设encryptHex是之前加密得到的Hex字符串 String encryptHex = "..."; // 这里应该是加密后的Hex字符串 // 使用相同的密钥和算法进行解密 AES aes = SecureUtil.aes(keyBytes, "ECB/PKCS5Padding"); String decryptStr = aes.decryptStr(encryptHex); System.out.println("解密结果: " + decryptStr);四、AES加密模式与填充方式AES加密算法支持多种模式和填充方式,不同的模式和填充方式会影响加密解密的结果。在Hutool中,可以通过指定模式(如ECB、CBC等)和填充方式(如PKCS5Padding、NoPadding等)来创建AES加密解密对象。模式(Mode):决定了加密过程中密钥的使用方式。常见的模式有ECB、CBC、CFB、OFB等。填充方式(Padding):由于AES加密要求输入数据的长度必须是块大小的整数倍(AES块大小为128位),因此当输入数据长度不满足要求时,需要通过填充方式来达到要求。常见的填充方式有PKCS5Padding、PKCS7Padding、NoPadding等。在选择模式和填充方式时,需要根据具体的应用场景和安全需求来决定。例如,ECB模式虽然实现简单,但安全性较低,不适合用于需要高安全性的场景;而CBC模式则通过引入初始化向量(IV)来提高安全性,是较为常用的模式之一。五、安全性与性能考虑在使用AES加密解密时,安全性和性能是两个重要的考虑因素。安全性:确保密钥的安全生成、存储和传输是保障加密安全性的关键。此外,选择合适的加密模式和填充方式也是提高安全性的重要手段。性能:AES加密解密虽然高效,但在处理大量数据时仍可能对性能产生影响。因此,在性能敏感的应用中,需要合理设计加密解密策略,如采用异步处理、批量加密解密等方式来提高性能。六、总结Hutool作为一款实用的Java工具类库,为开发者提供了简洁易用的AES加密解密API。通过本文的介绍,读者可以了解到如何在Java项目中使用Hutool进行AES加密解密,包括加密解密的基本步骤、AES加密模式与填充方式的选择以及安全性和性能的考虑。希望本文能对读者在Java加密解密方面的学习和实践有所帮助。————————————————原文链接:https://blog.csdn.net/My_wife_QBL/article/details/141920322
-
1. 包装类在JAVA中,共有8种基本类型,分别是byte,short,long,int,double,float,char,boolean.但由于JAVA是一门纯面向对象的语言,而且8种基本并非继承于Object类,为了在泛型代码中可以⽀持基本类型,于是JAVA提供了包装类。 1.1 基本数据类型和对应的包装类 除了 Integer 和 Character, 其余基本类型的包装类都是⾸字⺟⼤写。 1.2 装箱和拆箱装箱:基本数据类型转换成包装类int i = 10;Integer ij = new Integer(i);//装箱 拆箱:包装类转换成基本数据类型int i = 10;Integer ij = new Integer(i);int j = ii.intValue();//拆箱 注意:现在都使用自动拆箱和自动装箱!!! 1.3 自动装箱和自动拆箱为了减少开发者的负担,java 提供了⾃动机制。 自动装箱 int i = 10; Integer j = i;//自动装箱12自动拆箱Integer j = 10;int a = j;//自动拆箱提问:下述代码分别输出什么,为什么? public class Test { public static void main(String[] args) { Integer a = 127; Integer b = 127; Integer c = 128; Integer d = 128; Integer e = -129; Integer f = -129; System.out.println(a == b);//true System.out.println(c == d);//false System.out.println(e == f);//false }}注意:a和b是应用类型,==比较的是身份,比较值要重写equals方法进行比较。 答:[-128,127]这个范围数字比较是会出现true,其他数字比较则会出现false。原因是Integer中常用的数字被放到了常量池里,常用数字的范围是[-128,127]. 2. 泛型我们以前学过的数组,只能存放指定类型的元素,但是因为所有类的父类都是Object类,所以数组类型是否可以创建成Object呢? class MyArray { private Object[] array = new Object[10]; public Object getPos(int pos) { return this.array[pos]; } public void setVal(int pos,Object val) { this.array[pos] = val; }}public class Test { public static void main(String[] args) { MyArray myArray = new MyArray(); myArray.setVal(0,10); myArray.setVal(1,"hello");//字符串也可以存放 String ret = myArray.getPos(1);//编译报错 System.out.println(ret); }}//1号下标本⾝就是字符串,但是确编译报错。必须进⾏强制类型转换 虽然在这种情况下,当前数组任何数据都可以存放,但是,更多情况下,我们还是希望 它只能够持有⼀种数据类型。⽽不是同时持有这么多类型。所以,泛型的主要目的:就是指定当前的容器,要持有什么类型的对象,让编译器去做检查。 此时,就需要把类型,作为参数传递。需要什么类型,就传⼊什么类型。 2.1 泛型的语法基础写法: class 泛型类名称<类型形参列表> {// 这⾥可以使⽤类型参数} 其他写法: class 泛型类名称<类型形参列表> extends 继承类/* 这⾥可以使⽤类型参数 */ {// 这⾥可以使⽤类型参数} 上述代码进⾏改写如下: class MyArray<T> { public Object[] array = new Object[10]; public T getPos(int pos) { return (T)this.array[pos]; } public void setVal(int pos,T val) { this.array[pos] = val; }}public class TestDemo { public static void main(String[] args) { MyArray<Integer> myArray = new MyArray<>();//1 myArray.setVal(0,10); myArray.setVal(1,12); int ret = myArray.getPos(1);//2 System.out.println(ret); myArray.setVal(2,"小朱小朱");//3 编译报错!!! }} 1. 注释1处,类型后加⼊ 指定当前类型2. 注释2处,不需要进行强制类型转换3. 注释3处,代码编译报错,此时因为在注释1处指定类当前的类型,此时编译器会在存放元素的时候帮助我们进行类型检查。 代码解释: 类名后的 < T >代表占位符,表⽰当前类是⼀个泛型类【规范】类型形参⼀般使⽤⼀个⼤写字⺟表示,常⽤的名称有:• E 表示Element• K 表示 Key• V 表示 Value• N 表示 Number• T 表示 Type• S, U, V 等等 - 第⼆、第三、第四个类型2.2 泛型类的使用泛型类<类型实参> 变量名; // 定义⼀个泛型类引⽤new 泛型类<类型实参>(构造⽅法实参); // 实例化⼀个泛型类对象 MyArray<Integer> list = new MyArray<Integer>(); 注意:泛型只能接受类,所有的基本数据类型必须使用包装类! 2.3 裸类型(Raw Type)裸类型是⼀个泛型类但没有带着类型实参,例如 MyArrayList 就是⼀个裸类型 MyArray list = new MyArray();2.4 擦除机制在编译时,Java 编译器会将泛型类型信息从代码中移除,这个过程就叫做类型擦除。擦除后,泛型类型会被替换为其边界类型(通常是 Object)或者指定的类型。同时也会在必要的地方插⼊类型转换以保持类型安全。 擦除前: class MyArray<T> { public Object[] array = new Object[10]; public T getPos(int pos) { return (T)this.array[pos]; } public void setVal(int pos,T val) { this.array[pos] = val; }} 擦除后: class MyArray { public Object[] array = new Object[10]; public Object getPos(int pos) { return this.array[pos]; } public void setVal(int pos, Object val) { this.array[pos] = val; }}2.5 泛型的上界在定义泛型类时,有时需要对传⼊的类型变量做⼀定的约束,可以通过类型边界来约束。 语法: class 泛型类名称<类型形参 extends 类型边界> {...} public class MyArray<E extends Number> {...}//只接受 Number 的⼦类型作为 E 的类型实参2.6 泛型方法语法: ⽅法限定符 <类型形参列表> 返回值类型 ⽅法名称(形参列表){...} 示例: public class Util {//静态的泛型⽅法 需要在static后⽤<>声明泛型类型参数public static <E> void swap(E[] array, int i, int j) {E t = array[i];array[i] = array[j];array[j] = t;}}2.7 通配符?用于在泛型的使⽤,即为通配符 请观察下述代码: class Message<T> { private T message ; public T getMessage() { return message; } public void setMessage(T message) { this.message = message; }}public class Test { public static void main(String[] args) { Message<String> message = new Message<>() ; message.setMessage("欢迎来到小朱的CSDN"); fun(message); } public static void fun(Message<String> temp){ System.out.println(temp.getMessage()); }} 以上程序会带来新的问题,如果现在泛型的类型设置的不是String,⽽是Integer. public class TestDemo { public static void main(String[] args) { Message<Integer> message = new Message() ; message.setMessage(99); fun(message); // 出现错误,只能接收String } public static void fun(Message<String> temp){ System.out.println(temp.getMessage()); }} 我们需要的解决方案:可以接收所有的泛型类型,但是又不能够让用户随意修改。这种情况就需要使⽤通配符"?"来处理 public class TestDemo { public static void main(String[] args) { Message<Integer> message = new Message() ; message.setMessage(55); fun(message); } // 此时使⽤通配符"?"描述的是它可以接收任意类型 public static void fun(Message<?> temp){ System.out.println(temp.getMessage()); }} 在"?"的基础上又产生了两个子通配符: 通配符上界: <? extends 上界><? extends Number>//可以传⼊的实参类型是Number或者Number的⼦类 通配符下界————————————————原文链接:https://blog.csdn.net/2401_82690001/article/details/145964769
-
一、IDEA 的下载IDEA(IntelliJ IDEA)是一款非常强大且深受开发者喜爱的 Java 集成开发环境(IDE),它提供了丰富的功能和便捷的开发体验,下面为大家详细介绍其下载步骤:访问官方网站:打开浏览器,输入 IntelliJ IDEA 的官方网址:https://www.jetbrains.com/idea/ 在官网首页,你可以看到关于 IDEA 的各种介绍和版本信息。选择版本:IDEA 分为社区版(Community Edition)和旗舰版(Ultimate Edition)。社区版是免费的,适合初学者和个人开发者,它提供了基本的 Java 开发功能;旗舰版则是付费版本,功能更为强大,支持更多的框架和技术,如 Web 开发、数据库管理等。根据自己的需求和预算,选择合适的版本。一般来说,对于学习 Java-servlet 开发,社区版就已经足够了。下载安装包:点击对应版本的“Download”按钮,根据你的操作系统(Windows、Mac 或 Linux)选择相应的安装包进行下载。等待下载完成:下载完成后,找到下载的安装包文件(通常是一个.exe 文件对于 Windows 系统,.dmg 文件对于 Mac 系统),双击运行安装程序,按照安装向导的提示逐步完成安装。二、Maven 的下载Maven 是一个项目管理和构建工具,它可以帮助我们管理项目的依赖关系、构建项目、生成文档等。使用 Maven 可以大大简化项目的管理和开发过程。以下是 Maven 的下载步骤:访问 Maven 官方网站:在浏览器中输入 Maven 的官方网址:https://maven.apache.org/ 。进入官网后,你可以了解到 Maven 的相关信息和文档。下载 Maven 安装包:在官网的首页,找到“Download”链接,点击进入下载页面。在下载页面中,你会看到不同版本的 Maven 安装包。建议选择最新的稳定版本进行下载。根据你的操作系统,选择对应的二进制压缩包(如.zip 文件对于 Windows 系统,.tar.gz 文件对于 Linux 和 Mac 系统)。 点击下载对应的版本我的gitee直接下载链接https://gitee.com/srte-7719/tool/tree/master/Maven三、Tomcat 的下载Tomcat 是 Apache 软件基金会开发的一个开源的 Web 应用服务器,它是运行 Java Web 应用程序的重要平台。下面是 Tomcat 的下载步骤:访问 Tomcat 官方网站:在浏览器中输入 Tomcat 的官方网址:https://tomcat.apache.org/ 。进入官网后,你可以看到关于 Tomcat 的介绍和下载链接。选择版本:在官网的首页,找到“Tomcat 版本”部分,选择你想要下载的 Tomcat 版本。目前较常用的版本有 Tomcat 8 和 Tomcat 9,建议选择最新的稳定版本。下载安装包:点击对应版本的“Download”按钮,进入下载页面。在下载页面中,你会看到不同类型的安装包。对于 Windows 系统,推荐下载“Windows Service Installer”(.msi 文件),它可以方便地将 Tomcat 安装为 Windows 服务;对于 Linux 和 Mac 系统,推荐下载“Core”部分的“tar.gz”格式的压缩包。安装(对于 Windows 系统的.msi 文件):下载完成后,双击运行.msi 文件,按照安装向导的提示进行安装。在安装过程中,你可以选择安装路径、配置端口号(默认是 8080)等参数。解压(对于 Linux 和 Mac 系统的.tar.gz 文件):将下载的.tar.gz 文件解压到你希望安装 Tomcat 的目录。例如,在 Linux 系统中,可以解压到“/usr/local/tomcat”目录。————————————————原文链接:https://blog.csdn.net/2402_83322742/article/details/145930212
-
1.synchronized修饰方法 synchronized public void increase(){ // synchronized修饰实例方法 count++; } public void increase1(){ synchronized (this){ // 用this作为锁对象 count++; } } synchronized public static void increase2(){ count++; } public void increase3(){ synchronized (Demo1.class){ // 通过反射拿到这个类对象 count++; } }一个.java文件编译 => .class(字节码文件) => 运行时.class文件被加载到JVM里面(就是说:JVM加载到内存中的数据结构就是类对象)解释:当JVM加载一个.class文件时,它会在内存中创建一个对应的数据结构,这个数据结构通常被称为“类对象”或“类结构”。这个类对象包含了类中定义的所有信息类对象包括:类的属性,名字,类型,权限,类方法,继承哪个类,实现了哪个接口…………2:synchronized是一个可重入锁那可重入锁是什么意思呢?一个线程针对一个对象连续加锁两次不会出现死锁(我们等会会细说死锁)。满足这个要求的就是可重入锁。我们拿一个代码进行举例: synchronized (locker){ synchronized (locker){ count++; } }按理说这个代码就卡住了:解释: 我先给第一次给locker加锁,按理说下面第二次这个加锁操作肯定阻塞等待第一次加锁释放锁,我这个synchrinized才能给locker加锁。但是第二次不加锁第一次的加的锁就没办法解锁。这完全就死了(这种情况就死锁了)但是!!!synchronized是可重入锁:就是可以连续给一个对象进行两次加锁对象头里会有一个计数器(这个线程给这个对象加锁一个,计数器就+1)解释对象头和计数器,线程给一个对象肯定会保存这个线程的信息对象在加锁时会保存这个线程的信息,这些信息保存在对象头(隐藏里的隐藏信息)的Mark Word中。每个对象都有一个对象头(Object Header),它包含了对象的一些元数据(譬如锁状态)。并且一个线程连续给一个对象加锁还会有一个计数器,这个计数器是也存在于对象头里面的Mark Word中。这一机制确保了同一个线程可以多次获得同一个对象的锁。解锁时:并且一个对象被加三把锁时加入计数器的值 = 3 ,被解锁的时候也不是真的解锁而是解一把锁计数器-1,直到减到0才是真正的解锁3:死锁3.1.死锁的引入刚才我们讲述了synchronized是一个可重入锁,我们给一个对象连续加锁并不会导致死锁(线程卡死了)。那什么情况下会出现死锁呢?接下来给大家看一个代码 public static void main(String[] args) { Object locker1 = new Object(); Object locker2 = new Object(); Thread t1 = new Thread(() ->{ synchronized (locker1){ try { Thread.sleep(1000); //这个休眠1s真的很重要,开启t1线程立马就拿到了 locker1这把锁,如果不休眠的话 //很容易一下子这个线程就把两个锁都拿到了 } catch (InterruptedException e) { throw new RuntimeException(e); } // 嵌套的锁 synchronized (locker2){ System.out.println("t1加锁成功"); } } }); Thread t2 = new Thread(() -> { synchronized(locker2){ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } // 嵌套的锁 synchronized(locker1){ System.out.println("t2 加锁成功"); } } }); t1.start(); t2.start(); }结果:什么都没有打印我们进入jconsole看一下两个线程的状态画个图解释一下所以说:所以两个线程都没有成功获得第二把锁 这个属于嵌套关系,线程A拿到locker1这把锁,又想要locker2这把锁(就可能出现死锁)但是如果是并列关系(线程A先释放前面的锁,再获取下一把锁)(就不会死锁)嵌套如何变成并列?改变代码结构! 这样就解决了,但是有时候代码结构不是那么好改变的。刚才说到死锁形成环。。。就会出现一个经典的问题——哲学家就餐问题3.2.哲学家就餐问题先搞一张图表示这个问题 这就是导致了死锁问题(怎么解决呢)说到怎么解决,就要知道形成死锁的几个必要条件,我们破坏其中一个就OK了有这四个条件(其中前两个条件是synchronized的属性(我们改变不了))1.互斥使用(锁的基本特性):一个线程拥有了锁A,另一个线程想要获取就只能阻塞等待2.不可抢占(锁的基本特性):和条件一差不多,另一个线程只能等那个线程释放锁A,不能抢占过来3.保持请求(代码结构):一个线程想要获得多把锁(嵌套,想要锁B但是又不想释放自己的锁B)(其实并形成环,你想要几个锁都没问题)4.循环等待(代码结构):(条件三导致的条件四),等待的关系形成环了条件三:其实有时候的需求就是需要进行获取多把锁,这个结构不好改变条件四:我们约定好了加锁的顺序,就可以避免循环等待(针对锁进行编号,先加小锁再加大锁)所以说哲学家就餐问题就可以这么解决!!!我们可以规定每个哲学家只能先拿起数字小的筷子,哲学家B先拿起筷子,然后最后哲学家A面前只剩下了一个筷子5,但是他现在不能拿了(这个数字比较大)所以哲学家E就能够吃面条了,然后就通了。 上述就是synchronized—死锁问题的全部内容了,死锁的出现,会让我们的程序陷入一个死循环的问题,但是我们只要知道死锁的成因,至少就知道了如何解决这个问题啦~~~预知后事如何,请听下回分解~~~能看到这里相信您一定对小编的文章有了一定的认可。有什么问题欢迎各位大佬指出欢迎各位大佬评论区留言修正~~————————————————原文链接:https://blog.csdn.net/2302_80639556/article/details/145079283
-
概述在 Spring 框架中,BeanPostProcessor 和 BeanFactoryPostProcessor 是两个重要的接口,用于在 Spring 容器管理 Bean 的生命周期过程中提供自定义的扩展点。它们允许开发者在 Spring 容器实例化和初始化 bean 的过程中进行额外的处理。 两者的定义和作用BeanPostProcessor:Spring 的 BeanPostProcessor 名称后置处理器,这是一个拓展机制,用于在 Spring 容器实例化、配置完成之后,在初始化前和初始化后,对 Bean 进行再加工的自定义处理。BeanFactoryPostProcessor:Spring 的 BeanFactoryPostProcessor 接口定义了一个方法,允许在 Spring 容器标准初始化之后、实例化任何 Bean 之前,对容器的内部 BeanFactory 进行修改。两者的区别与联系先说区别执行时机不同:BeanPostProcessor 在每个 Bean 实例化和初始化前后的过程中执行;BeanFactoryPostProcessor 在 Spring 容器的启动过程中执行,其执行时机是在容器标准初始化之后,所有的 BeanDefinition 已经加载到容器中,但还没有实例化任何 Bean 之前。作用对象不同:BeanPostProcessor 作用于每一个 Bean 实例,允许对 Bean 实例进行修改,例如修改属性、生成代理对象等。BeanFactoryPostProcessor 作用于 BeanFactory 本身,允许修改 Bean 定义,影响容器中的 Bean 配置,例如修改属性值、动态注册 Bean 定义等。再说联系Spring 容器的扩展机制:这两个接口都属于 Spring 容器的扩展机制,用于增强容器的功能和灵活性,都提供了在 Spring 容器初始化过程中进行自定义处理的扩展点,允许开发者在框架执行核心逻辑之前或之后插入自己的逻辑。需要在 Spring 配置中进行注册:无论是通过 XML 配置文件还是通过注解方式,这两个接口的实现类都需要在 Spring 配置中进行注册,以便 Spring 容器能够识别和调用它们。使用场景BeanPostProcessor动态代理:在 Bean 初始化后创建代理对象。属性检查或修改:在 Bean 初始化前检查或修改属性。BeanFactoryPostProcessor属性占位符配置:修改 Bean 定义中的占位符值。条件性 Bean 注册:根据条件动态注册或修改 Bean 定义。BeanPostProcessor 详解接口定义Spring 容器会为每个 Bean 创建实例后,分别调用 BeanPostProcessor 接口的两个方法: public interface BeanPostProcessor { @Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; }} 方法详解postProcessBeforeInitialization在 Bean 初始化之前调用,开发者可以通过该方法对 Bean 进行自定义处理,包括修改 Bean 的属性值、添加一些特殊处理等。此方法的第一个参数即本次创建的 Bean 对象,第二个参数即本次创建 Bean 的名字。需要注意处理完成后需要将 Bean 作为返回值返回,归还给 Spring 管理。 postProcessAfterInitialization在 Bean 初始化之后调用,开发者可以通过该方法对 Bean 进行自定义处理,比如动态代理、AOP 增强等。此方法的第一个参数即本次创建的 Bean 对象,第二个参数即本次创建 Bean 的名字。需要注意处理完成后需要将 Bean 作为返回值返回,归还给 Spring 管理。 注意:在实战中很少对 Spring 中的对象进行初始化的操作,所以区分 Before 和 After 的意义不是很大,我们只需要实现一种即可,推荐实现 After 方法,另外一个 Before 方法就按默认即可。 注意:BeanPostProcessor 后置处理器会对 Spring 创建的所有对象进行处理。 示例代码在某些情况下,我们可能需要在应用程序启动时扫描特定的注解,并在 Bean 初始化时进行处理。例如,我们希望在 Spring Boot应用中扫描所有带有特定注解的类,并在它们被初始化时打印日志和执行方法。 @Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic @interface XUE {} @XUE@Componentpublic class XueWei { public void hello() { System.out.println("Hello XW!"); }} @Componentpublic class MyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class<?> targetClass = bean.getClass(); if (targetClass.isAnnotationPresent(XUE.class)) { // 获取到了自定义注解标记的类 System.out.println("扫描到了标记 @XUE 注解的类:" + targetClass.getName()); Method[] methods = bean.getClass().getDeclaredMethods(); for (Method method : methods) { // 执行指定方法 if (!"hello".equals(method.getName())) { continue; } try { // 通过反射执行该类的所有的方法 method.invoke(bean); } catch (Exception e) { System.out.println("执行报错:" + e); } } } return bean; }} 基于 XML 的方式注解: <!-- 注册后置处理器 --><bean id="processor" class="world.xuewei.processor.MyProcessor"/> 优点与缺点先说优点灵活性:BeanPostProcessor 提供了一种灵活的机制,允许开发人员在 Spring 容器实例化、配置和初始化 Bean 的过程中介入,可以对 Bean 进行自定义的操作和处理。可扩展性:开发人员可以根据实际需求编写自定义的 BeanPostProcessor 实现,实现各种功能,如自动装配、依赖注入、AOP、事务管理等。AOP 实现基础:Spring AOP 的实现正是基于 BeanPostProcessor 机制。通过 BeanPostProcessor,Spring 可以在 Bean 初始化的过程中动态地生成代理对象,实现 AOP 的横切逻辑。再说缺点性能影响:使用 BeanPostProcessor 可能会对应用程序的性能产生一定的影响,特别是在大规模应用中,由于需要对所有的 Bean 进行处理,可能会增加应用程序的启动时间和内存消耗。复杂性:如果不正确地使用 BeanPostProcessor,可能会导致应用程序的复杂性增加,增加代码的维护成本和理解难度。潜在的问题:由于 BeanPostProcessor 可以对所有的 Bean 进行操作,因此可能会引入一些潜在的问题,如循环依赖、死锁等。注意事项避免循环依赖:使用 BeanPostProcessor 时,要注意避免循环依赖的问题,避免出现 Bean 依赖循环导致的应用程序启动失败。谨慎处理 Bean 初始化逻辑:在编写 BeanPostProcessor 实现时,要谨慎处理 Bean 初始化逻辑,确保不会影响到 Bean 的正常初始化过程。注意性能影响:如果应用程序启动时间较长或者内存消耗较高,可以考虑减少 BeanPostProcessor 的数量,或者优化 BeanPostProcessor 的实现,以减少性能影响。优先级设置:可以通过实现 Ordered 接口或者在 @Order 注解中设置优先级来控制多个 BeanPostProcessor 的执行顺序,确保处理顺序的正确性。测试与调试:在使用 BeanPostProcessor 时,建议进行充分的测试和调试,确保 BeanPostProcessor 的实现逻辑正确,不会引入潜在的问题。BeanFactoryPostProcessor 详解接口定义通过实现 BeanFactoryPostProcessor,可以在 Bean 实例化之前对 Bean 的定义进行修改,适用于对 Bean 配置的元数据进行全局修改,例如修改属性值,替换属性占位符等。 public interface BeanFactoryPostProcessor { void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;} 方法详解postProcessBeanFactory在容器实例化任何 Bean 之前执行,可以修改应用程序上下文的内部 Bean 定义,调整 Bean 的配置元数据。 适用场景@Componentpublic class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 场景一:修改已存在的 BeanDefinition try { // 获取指定 Bean 的定义 BeanDefinition beanDefinition = beanFactory.getBeanDefinition("student"); // 修改 BeanDefinition 属性信息 MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); propertyValues.addPropertyValue("name", "薛伟"); // 修改作用域 beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); // 快速初始化 Bean Object student = beanFactory.getBean("student"); System.out.println(student); // ... } catch (Exception e) { System.err.println("修改 Student 的 BeanDefinition 属性失败!" + e); } // 场景二:动态注册 BeanDefinition(Spring 与 MyBatis 整合就是使用的这种) DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory; GenericBeanDefinition definition = new GenericBeanDefinition(); definition.setBeanClass(Teacher.class); definition.getPropertyValues().addPropertyValue("name", "XUEW"); factory.registerBeanDefinition("teacher", definition); // 场景三:加载外部配置文件 try { Properties props = PropertiesLoaderUtils.loadAllProperties("db.properties"); for (String key : props.stringPropertyNames()) { System.setProperty(key, props.getProperty(key)); } } catch (IOException e) { throw new RuntimeException("Failed to load properties file", e); } // 场景四:条件性修改 Bean 定义 String os = System.getProperty("os.name"); if ("Mac OS X".equals(os)) { BeanDefinition osInfo = beanFactory.getBeanDefinition("osInfo"); MutablePropertyValues infoPropertyValues = osInfo.getPropertyValues(); infoPropertyValues.addPropertyValue("name", os); infoPropertyValues.addPropertyValue("desc", "苹果系统"); } // 场景五:快速初始化 Bean System.out.println(beanFactory.getBean("student")); System.out.println(beanFactory.getBean("teacher")); System.out.println(beanFactory.getBean("osInfo")); // 设置当前环境 System.setProperty("environment.active", "prod"); }} 示例场景假设我们有一个应用程序需要连接不同环境下的 Redis 实例,根据运行环境(如开发、测试、生产)动态修改 Redis 的连接信息。 创建一个名为 redis.properties 的配置文件,包含不同环境下的 Redis 连接信息。test.redis.host=test.xuewei.worldtest.redis.port=6379test.redis.password=123456prod.redis.host=prod-redis-serverprod.redis.port=16379prod.redis.password=P@ssW0RD 创建一个 Bean 类 RedisConfig,包含 Redis 连接属性。@Data@NoArgsConstructor@AllArgsConstructor@Builderpublic class RedisConfig { private String host; private int port; private String password; } 实现一个自定义的 BeanFactoryPostProcessor,根据运行时的环境修改 Bean 定义。@Componentpublic class RedisEnvironmentBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware { private Environment environment; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { String env = System.getProperty("environment.active"); BeanDefinition beanDefinition = beanFactory.getBeanDefinition("redisConfig"); MutablePropertyValues propertyValues = beanDefinition.getPropertyValues(); propertyValues.add("host", environment.getProperty(env + ".redis.host")); propertyValues.add("port", environment.getProperty(env + ".redis.port", Integer.class)); propertyValues.add("password", environment.getProperty(env + ".redis.password")); System.out.println(beanFactory.getBean("redisConfig")); }} 使用 @Configuration 和 @PropertySource 注解配置 Spring 上下文,并启用属性占位符解析。@Configuration@ComponentScan(basePackages = {"world.xuewei.dao", "world.xuewei.service", "world.xuewei.processor"})@EnableAspectJAutoProxy(proxyTargetClass = true)@PropertySource({"classpath:/student.properties", "classpath:/redis.properties"})public class AppConfig { @Bean public RedisConfig redisConfig() { return new RedisConfig(); }} 优点与缺点先说优点修改 Bean 定义:BeanFactoryPostProcessor 可以在 Bean 实例化之前修改 Bean 的定义属性,这提供了一种灵活的方式来调整 Bean 的配置。动态配置:它可以用于根据运行环境动态配置 Bean 属性,如使用外部配置文件的属性占位符进行配置。这有助于实现应用程序的配置灵活性和可维护性。增强功能:可以用来增加自定义逻辑,例如自动注册额外的 Bean 定义,动态修改现有的 Bean 配置,或者实现其他自定义处理逻辑。属性占位符解析:通过实现特定的 BeanFactoryPostProcessor(如 PropertyPlaceholderConfigurer),可以将外部配置文件中的属性值注入到 Spring Bean 中,这使得应用程序的配置管理更加灵活和集中化。再说缺点复杂性增加:使用 BeanFactoryPostProcessor 可能增加应用程序配置的复杂性,尤其是在处理大量的自定义逻辑时,可能会使配置难以理解和维护。调试困难:由于 BeanFactoryPostProcessor 在 Spring 容器初始化的早期阶段执行,调试可能会变得复杂。问题可能在 Bean 实例化之前发生,因此追踪和解决这些问题可能比较困难。依赖顺序问题:使用多个 BeanFactoryPostProcessor 时,需要注意它们的执行顺序,否则可能会导致不可预期的行为。正确管理多个 BeanFactoryPostProcessor 的执行顺序可能会增加开发复杂性。性能影响:由于 BeanFactoryPostProcessor 在 Spring 容器启动时执行,包含复杂逻辑的处理器可能会增加应用程序启动时间。如果处理逻辑比较重,会影响应用程序的启动性能。注意事项确保执行顺序:多个 BeanFactoryPostProcessor 实例可能会存在相互依赖关系。因此,了解并控制它们的执行顺序非常重要。可以实现 Ordered 接口或 PriorityOrdered 接口来指定执行顺序。官方文档中明确指出 BeanFactoryPostProcessor 不能使用 @Order 注解。 避免复杂逻辑:由于 BeanFactoryPostProcessor 在 Spring 容器启动期间执行,避免在其实现中引入复杂或耗时的逻辑。复杂逻辑可能会延长应用程序启动时间,影响性能。 确保线程安全:BeanFactoryPostProcessor 可能会在多线程环境中被调用,因此确保实现的线程安全非常重要。避免使用非线程安全的数据结构或方法。 处理依赖关系:避免在 BeanFactoryPostProcessor 中直接引用其他 Bean,因为这些 Bean 可能还没有被实例化。如果必须引用其他 Bean,可以通过 BeanFactory 进行延迟获取。 @Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { MyBean myBean = beanFactory.getBean(MyBean.class); // Use myBean} 避免循环依赖:由于 BeanFactoryPostProcessor 可能会修改 Bean 定义,从而影响其他 BeanFactoryPostProcessor 的行为,确保这些修改不会引起循环依赖。 使用正确的应用场景:BeanFactoryPostProcessor 适用于需要在 Bean 实例化之前修改 Bean 定义的场景。对于在 Bean 实例化之后需要执行的逻辑,考虑使用 BeanPostProcessor。 小心静态资源的使用:避免在 BeanFactoryPostProcessor 中使用静态资源或全局变量,除非这些资源是线程安全的并且适合在 Spring 应用程序启动期间使用。 记录日志和异常处理:在 postProcessBeanFactory 方法中添加适当的日志记录和异常处理,以便在启动时发生问题时能快速定位和解决问题。 @Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { try { // Your processing logic } catch (Exception e) { // Log and handle the exception throw new BeansException("Error processing bean factory", e); }} 配置管理:如果 BeanFactoryPostProcessor 依赖外部配置,确保配置文件加载顺序正确,并且配置文件中的值能够正确解析。 避免对 Bean 的直接操作:尽量避免在 BeanFactoryPostProcessor 中直接操作 Bean 实例。更多的操作应当在 Bean 定义层面进行,而非实例层面。———————————————— 原文链接:https://blog.csdn.net/weixin_53287520/article/details/139484810
上滑加载中
推荐直播
-
华为云码道 × 仓颉编程:工程化AI编码探索2026/05/27 周三 19:00-21:00
刘俊杰-华为云仓颉语言专家/李炎-华为云码道技术专家/王智鹏-OpenCangjie开源社区发起人
本场直播围绕华为云仓颉语言与华为云码道的深度结合,展示华为云智能编程从零基础到高效落地的完整生态能力。以华为云码道为引擎,仓颉语言为载体,带给大家日常提效、趣味创新到极速量产的开发体验。
回顾中 -
一个AI团队帮你写代码:华为云码道Agent Space实战2026/06/25 周四 19:00-21:00
张翰文-华为云码道工程师/郭英旭-青软创新科技集团股份有限公司 软件架构师
本场直播聚焦华为云码道Agent Space两大模式:研发办公、代码开发,亲身体验从需求到代码的AI自动化能力。实操演示基于华为 CodeArts CLI,依托 OpenSpec 规格体系从零搭建业务项目。
回顾中
热门标签