-
一、栈1.1. 栈的概念 栈是⼀种特殊的线性表,其只允许在固定的⼀端进⾏插⼊和删除元素操作。进⾏数据插⼊和删除操作 的⼀端称为栈顶,另⼀端称为栈底。栈中的数据元素遵守后进先出的原则。 入栈:栈的插⼊操作叫做进栈/压栈/⼊栈,⼊数据在栈顶。 出栈:栈的删除操作叫做出栈。出数据在栈顶。 1.2. 栈的使用import java.util.Stack; public class Main { public static void main(String[] args) { Stack<Integer> stack = new Stack<>(); }} 我们点进去看一下Stack的源码:public class Stack<E> extends Vector<E> { public Stack() { } public E push(E item) { addElement(item); return item; } 下面是一些Stack的方法,我们来实现一下。 方法 功能Stack() 创建一个空的栈E push(E e) 将e入栈并返回eE pop() 将栈顶元素出栈并返回E peek() 获取栈顶元素int size() 获取栈中有效元素的个数boolean empty() 检查栈是否为空import java.util.Stack; public class Main { public static void main(String[] args) { Stack<Integer> stack = new Stack<>(); System.out.println(stack.empty());//检查栈是否为空 stack.push(5);//入栈 stack.push(4); stack.push(3); stack.push(2); stack.push(1); System.out.println(stack.size());//获取栈的元素个数 System.out.println(stack); System.out.println(stack.empty()); System.out.println(stack.pop());//栈顶元素出栈 System.out.println(stack.peek());//获取栈顶元素 }}1.3. 栈的模拟实现 Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是 Vector是线程安全的,我们可以直接用数组来实现栈。import java.util.Arrays; public class MyStack { private int[] elem; private int usedSize; private static final int DEFAULT = 10; public MyStack(){ elem = new int[DEFAULT]; } public void push(int val){//模拟入栈操作 if(isFull()){//如果满了就扩容 grow(); } elem[usedSize] = val; usedSize++; } private void grow(){ elem = Arrays.copyOf(elem,elem.length); } public boolean isFull(){//判断数组是否满了 return usedSize == elem.length; } public int pop() { if(isEmpty()){ throw new StackIsEmpty("栈为空"); } usedSize--;//相当于把数据置为null return elem[usedSize-1]; } public boolean isEmpty(){ return usedSize == 0; } public int peek() { if(isEmpty()){ throw new StackIsEmpty("栈为空"); } return elem[usedSize--]; } public int size(){ return usedSize; }}public class StackIsEmpty extends RuntimeException{ public StackIsEmpty() { super(); } public StackIsEmpty(String message) { super(message); }}public class Main { public static void main(String[] args) { MyStack stack = new MyStack(); stack.push(1); stack.push(2); stack.push(3); stack.push(4); stack.push(5); System.out.println(stack.size()); int num1 = stack.pop(); System.out.println(num1); int num2 = stack.peek(); System.out.println(num2); }}二、栈的经典面试题2.1. 逆波兰表达式 逆波兰表达式,又称为后缀表达式,特点是运算符位于操作数之后。例如((2+1)*3)、(4+(13/5))是中缀表达式,转化为逆波兰表达式则为2 1 + 3 *、4 13 5 / +。 本题方法参数给出的是字符串数组,我们要先将字符转化为数字。先用引用去遍历数组,如果是数字,就放入栈中;如果引用指向的是运算符,则将栈顶的两个元素出栈进行运算。如此循环往复,直至遍历完整个字符串数组。import java.util.Stack; public class Solution { public int evalRPN(String[] tokens){ Stack<Integer> stack = new Stack<>(); int len = tokens.length; for (int i = 0; i < len; i++) {//遍历字符串数组 String str = tokens[i]; if(!isOperator(str)){ Integer num = Integer.valueOf(str);//将字符转化为数字 stack.push(num); }else{//如果是运算符,则栈顶两个元素出栈 Integer num1 = stack.pop(); Integer num2 = stack.pop(); switch (str){ case "+": stack.push(num2+num1); break; case"-": stack.push(num2-num1); break; case"*": stack.push(num2*num1); break; case"/": stack.push(num2/num1); break; } } } return stack.pop(); } private boolean isOperator(String val){//先判断字符串是数字还是运算符 if(val.equals("+") || val.equals("-") || val.equals("*") || val.equals("/")){ return true; } return false; }}2.2. 有效的括号 题目中要求字符串只有大、中、小括号,返回true的条件为:当我们遍历完字符串之后并且栈为空。如果说,当我们左右括号不匹配时,比如"([))",就返回false;当我们遍历完字符串之后,如果栈不为空,也返回false,比如"(()";当我们还没有遍历完字符串,栈已经为空,也会返回false,比如"({}))"。import java.util.Stack; public class Solution { public boolean isValid(String s){ Stack<Character> stack = new Stack<>(); int len = s.length(); for (int i = 0; i < len; i++) { char ch1 = s.charAt(i); if(ch1 == '{' || ch1 == '[' || ch1 == '('){ stack.push(ch1);//将左括号入栈 }else{ if(stack.isEmpty()){ return false; } char ch2 = stack.peek();//获取入栈的左括号,与右括号进行匹配 if((ch2=='{'&&ch1=='}') || (ch2=='['&&ch1==']') || (ch2=='('&&ch1==')')){ stack.pop(); }else { return false; } } } if(!stack.isEmpty()){ return false; } return true; }}2.3. 最小栈 如果我们抛开这道题,获取栈中的最小元素,我们就可以去遍历这个栈来找出我们的最小元素。但这道题限制我们需要在让时间复杂度为常数,所以说,一个栈是不能解决问题的,还需要在引入一个栈stack。 对于push方法,普通栈当中,所有数据都要放入,最小栈要对我们的普通栈第一次push进行维护。如果最小栈不为空,那么需要比较刚存放的数据与最小栈栈顶的数据进行比较,以对里面的最小值进行更新。这样顺便解决了getMin的实现,直接从最小栈栈顶获取。 对于pop方法,如果弹出的数据不是最小栈的栈顶数据,则只需要弹出普通栈的栈顶数据就行,否则则要弹出最小栈的栈顶数据。top相当于peek方法,只获取普通栈的栈顶元素。 这4个方法基本实现完成,但还有一个问题。如上图所示,如果我们再放入一个-1进入普通栈,那么最小栈需不需要再放入-1呢?答案是需要。因为按照我们的pop方法,把-1弹出,minstack也要弹出。完整代码:import java.util.Stack; public class MinStack { Stack<Integer> stack = new Stack<>(); Stack<Integer> minStack = new Stack<>(); public MinStack() { stack = new Stack<>(); minStack = new Stack<>(); } public void push(int val) { stack.push(val); if(minStack.empty()){ minStack.push(val); }else{ int peekMinVal = minStack.peek(); if(val <= peekMinVal){ minStack.push(val); } } } public void pop() { int val = stack.pop(); if(val == minStack.peek()){ minStack.pop(); } } public int top() { return stack.peek(); } public int getMin() { return minStack.peek(); }———————————————— 原文链接:https://blog.csdn.net/2401_85198927/article/details/145169124
-
引言在当今数字化时代,互联网已然成为人们生活不可或缺的一部分,而网页作为互联网的主要载体,其用户体验的优劣直接关乎着信息的有效传递与用户的留存。JavaScript,这门在前端开发领域占据核心地位的编程语言,犹如一位神奇的魔法师,为静态的网页注入灵动的生命力,使之蜕变成为交互性强、功能丰富的精彩世界。四、事件处理4.1 事件类型在 JavaScript 的前端开发领域,事件处理犹如一座桥梁,紧密连接着用户与网页之间的交互。它能够精准捕捉用户在页面上的各类操作,诸如鼠标的轻轻点击、键盘的敲击输入、表单的提交确认等,并迅速触发相应的 JavaScript 代码来执行特定功能,为用户带来流畅且自然的交互体验。鼠标事件堪称交互中的 “主力军”,涵盖了诸多常见类型。click 事件,作为最为常用的一种,当用户在某个元素上执行单击鼠标左键的操作时,便会如同触动了机关一般被触发,就像在网页上点击一个按钮提交表单,或是打开一个链接跳转页面。与之紧密相关的 dblclick 事件,则要求用户在极短时间内连续双击鼠标左键,常用于实现一些快速操作指令,比如在图片编辑场景下,双击图片快速进入编辑模式。mouseover 与 mouseenter 事件均在鼠标指针移至元素上方时触发,细微差别在于,mouseover 在鼠标经过元素及其子元素时都会触发,而 mouseenter 仅当鼠标初次进入元素自身范围时才触发,二者适用于不同的交互细节需求,如导航菜单的展开,mouseover 能让子菜单在鼠标滑过主菜单及子项时都灵活响应,mouseenter 则可确保仅在精准指向主菜单时才触发展开动作,避免误触。mouseout 与 mouseleave 事件则相反,对应鼠标离开元素的操作,其触发规则与上述类似,常用于收起菜单、隐藏提示信息等场景。mousedown 与 mouseup 事件分别对应鼠标按钮按下与松开的瞬间,这两个事件常与 mousemove 配合,用于实现诸如拖拽元素、绘制图形等复杂交互,像在一些图形设计软件的网页版中,用户按下鼠标并移动来绘制线条,松开鼠标完成绘制。键盘事件同样不可或缺,keydown 事件在用户按下键盘上任意键的瞬间被触发,无论是字母、数字、符号还是功能键,它都能敏锐捕捉,通过监听此事件,开发者可实时获取用户的按键输入,常用于文本输入实时校验、快捷键响应等场景。例如在一些在线文档编辑页面,输入文字时实时检查拼写错误,或是按下 Ctrl + S 组合键触发保存操作。keyup 事件紧随 keydown 之后,在按键松开时触发,与 keydown 配合可精准判断用户完整的按键动作,确保交互逻辑的准确性。需注意的是,曾经的 keypress 事件在按下字符键时触发,但已逐渐被 keydown 取代,因其在处理功能键等方面存在局限。表单事件聚焦于表单元素的交互处理。submit 事件在用户点击表单的提交按钮,或是在表单内按下回车键(前提是表单设置允许)时触发,此时通常会进行表单数据的校验与提交操作,如登录表单验证用户名和密码是否符合格式要求、是否非空等,若校验通过则向服务器发送数据。change 事件则针对表单元素状态的改变,当单选框、复选框被选中或取消选中,下拉列表选择了不同选项,文本框或 textarea 元素内容改变且失去焦点时,都会触发该事件,常用于实时更新相关联的显示内容或执行额外校验,比如电商购物选择商品规格后,实时更新商品总价;在文本框输入完信息,失去焦点时检查格式是否正确。input 事件与 change 类似,不过它更加 “实时”,只要表单元素的值发生变化,便会立即触发,对于实时反馈用户输入极为有用,如搜索框实时显示输入的关键词联想结果。不妨以一个简单实例来深入理解事件驱动机制。在网页上有一个按钮,当用户点击它时,弹出一个提示框显示 “按钮被点击了”:<button id="myButton">点击我</button><script> document.getElementById('myButton').addEventListener('click', function() { alert('按钮被点击了'); });</script>在此例中,按钮是事件源,click 是事件类型,而函数 function() { alert('按钮被点击了'); } 便是事件处理程序。当用户执行点击操作这一 “导火索” 时,便迅速激活事件处理程序,进而弹出提示框,完成一次流畅的交互。4.2 事件监听器在 JavaScript 前端开发中,事件监听器是实现高效、灵活事件处理的关键所在。它宛如一位忠诚且机智的 “守护者”,时刻监听着特定 DOM 元素上的各种事件,一旦捕捉到目标事件的发生,便会立即触发与之绑定的相应函数,执行预设的交互逻辑。addEventListener 方法无疑是其中的 “核心利器”,它以一种极为灵活且强大的方式,将事件与处理函数紧密关联起来。其语法结构如下:target.addEventListener(type, listener, useCapture);其中,target 代表目标 DOM 元素,通过诸如 document.getElementById、querySelector 等方法精准获取;type 即为要监听的事件类型,如前文提及的 click、keydown、submit 等;listener 则是对应的事件处理函数,它可以是具名函数,也可以是匿名函数,这个函数承载着当事件触发时需要执行的具体代码逻辑;useCapture 是一个可选参数,用于指定事件冒泡或捕获阶段,默认为 false,即处于冒泡阶段,后续会详细讲解冒泡与捕获机制。以一个为按钮添加点击监听器的实例来深入剖析:<button id="actionButton">执行操作</button><script> const button = document.getElementById('actionButton'); button.addEventListener('click', function() { console.log('按钮被点击,即将执行重要操作...'); // 此处可添加具体业务逻辑代码,如发送AJAX请求、更新页面数据等 });</script>在上述代码中,首先通过 document.getElementById 精准定位到 id 为 'actionButton' 的按钮元素,将其赋值给 button 变量。接着,使用 addEventListener 方法为该按钮监听 'click' 事件,当用户点击按钮时,匿名函数内的代码便会立即执行,控制台输出相应提示信息,并且可依需求在此处拓展更为复杂的业务逻辑,如向服务器提交表单数据、动态更新页面 UI 等。与传统的内联事件处理属性相比,addEventListener 具有显著优势。传统的内联方式,如在 HTML 标签内直接使用 onclick="function ()",虽然简单直接,但却将 JavaScript 代码与 HTML 结构紧密耦合在一起,使得代码的维护性与可扩展性大打折扣。一旦业务逻辑复杂起来,需要修改或新增功能,在 HTML 标签内嵌入的大量代码将变得混乱不堪,难以管理。而 addEventListener 将事件绑定逻辑统一放置在 JavaScript 脚本中,实现了 HTML 结构与 JavaScript 行为的分离,遵循了良好的代码解耦原则,使得代码结构更加清晰、易于维护与拓展。在实际应用场景中,比如一个网页表单包含多个按钮,分别用于提交表单、重置表单、保存草稿等不同操作,便可利用 addEventListener 为每个按钮绑定各自专属的点击处理函数:<form id="myForm"> <input type="text" placeholder="请输入内容"> <button id="submitButton">提交</button> <button id="resetButton">重置</button> <button id="saveButton">保存草稿</button></form><script> const submitBtn = document.getElementById('submitButton'); const resetBtn = document.getElementById('resetButton'); const saveBtn = document.getElementById('saveButton'); submitBtn.addEventListener('click', function() { // 执行表单提交逻辑,如校验数据、发送请求 console.log('表单提交中...'); }); resetBtn.addEventListener('click', function() { // 重置表单数据,恢复初始状态 console.log('表单已重置'); }); saveBtn.addEventListener('click', function() { // 保存草稿逻辑,如将数据暂存本地 console.log('草稿已保存'); });</script>如此一来,各个按钮各司其职,通过独立的事件监听器实现了多样化的交互响应,极大提升了用户体验与代码的灵活性。五、实战案例:打造简易待办事项列表5.1 HTML 结构搭建在着手构建简易待办事项列表时,精心搭建 HTML 结构是基础且关键的第一步。它犹如搭建房屋的骨架,为后续功能的添砖加瓦提供稳固支撑。以下是一段基础的 HTML 代码示例:<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>简易待办事项列表</title> <link rel="stylesheet" href="styles.css"></head> <body> <h1>待办事项列表</h1> <div id="todo-app"> <input type="text" id="new-todo" placeholder="添加新任务..."> <button id="add-todo">添加</button> <ul id="todo-list"> </ul> </div> <script src="script.js"></script></body> </html>在这段代码中,<h1> 标签醒目地展示了应用的标题,让用户一眼便能知晓此页面的用途。<div id="todo-app"> 作为核心容器,将整个待办事项的操作区域进行了整合,使之在页面布局上更为规整。其中,<input type="text" id="new-todo"> 是用户输入待办任务的入口,placeholder 属性友好地提示用户应在此处输入内容;<button id="add-todo"> 则是触发添加任务动作的按钮,简洁直观。而 <ul id="todo-list"> 如同一个空白的任务收纳盒,后续通过 JavaScript 动态生成的任务列表项(<li> 元素)都将被有序地放置其中,为用户呈现清晰的任务清单。合理且简洁的 HTML 结构设计,不仅提升了代码的可读性,更为后续 JavaScript 功能的实现铺就了顺畅之路。5.2 JavaScript 功能实现有了 HTML 结构作为基石,接下来借助 JavaScript 赋予待办事项列表鲜活的交互能力。以下是实现添加任务、删除任务以及标记任务完成功能的 JavaScript 代码示例,并附有详细注释:// 等待页面DOM加载完成后再执行后续代码,确保DOM元素已存在document.addEventListener('DOMContentLoaded', function () { const todoInput = document.getElementById('new-todo'); const addButton = document.getElementById('add-todo'); const todoList = document.getElementById('todo-list'); // 为添加按钮添加点击事件监听器 addButton.addEventListener('click', function () { const taskText = todoInput.value.trim(); if (taskText!== '') { // 创建新的列表项元素 const listItem = document.createElement('li'); listItem.textContent = taskText; // 创建删除按钮 const deleteButton = document.createElement('button'); deleteButton.textContent = '删除'; // 为删除按钮添加点击事件监听器,点击时移除对应的列表项 deleteButton.addEventListener('click', function () { todoList.removeChild(listItem); }); // 将删除按钮添加到列表项中 listItem.appendChild(deleteButton); // 将新的列表项添加到待办事项列表 todoList.appendChild(listItem); // 清空输入框,以便用户输入下一个任务 todoInput.value = ''; } }); // 为待办事项列表添加点击事件监听器,用于标记任务完成 todoList.addEventListener('click', function (e) { if (e.target.tagName === 'LI') { e.target.classList.toggle('completed'); } });});在上述代码中,首先使用 document.addEventListener('DOMContentLoaded', function () {...}) 确保整个 HTML 页面的 DOM 结构加载完毕后,才开始执行后续的 JavaScript 代码。这一步骤至关重要,因为若过早执行,可能会因 DOM 元素未完全加载而导致无法获取到相应元素,引发错误。接着,通过 document.getElementById 精准获取到输入框、添加按钮以及待办事项列表的 DOM 元素引用,并分别存储在 todoInput、addButton 和 todoList 变量中,方便后续操作。当用户在输入框输入任务文本并点击添加按钮时,addButton.addEventListener('click', function () {...}) 中的代码被触发。首先,获取输入框中的文本并去除首尾空格,若文本不为空,则开启创建新任务列表项的流程。使用 document.createElement('li') 生成一个新的 <li> 元素,并将输入的任务文本赋值给它的 textContent 属性,使其显示在页面上。同时,创建一个用于删除任务的按钮,同样为其添加点击事件监听器,当点击删除按钮时,执行 todoList.removeChild(listItem),直接从 DOM 树中移除对应的列表项,实现任务删除功能。最后,将新创建的列表项添加到待办事项列表中,并清空输入框,等待用户输入下一个任务。为了实现标记任务完成的功能,利用 todoList.addEventListener('click', function (e) {...}) 为整个待办事项列表添加点击事件监听器。当用户点击列表中的某个任务项(<li> 元素)时,通过判断 e.target.tagName === 'LI',确认点击的是任务项本身,随后使用 e.target.classList.toggle('completed'),动态切换任务项的 completed 类名。在 CSS 样式表中,可预先定义 .completed 类的样式,如添加删除线、改变字体颜色等,以此直观地呈现任务的完成状态,为用户提供清晰的视觉反馈,让待办事项管理更加便捷高效。六、进阶拓展:异步编程与 Ajax6.1 异步编程概念在 JavaScript 的编程世界里,同步与异步犹如两条截然不同的执行路径,深刻影响着程序的运行逻辑与用户体验。同步编程,恰似一位按部就班的 “执行者”,每一行代码都必须严格遵循顺序依次执行,犹如工厂流水线上的一道道工序,前一个任务未完成,后续任务只能默默等待。以读取本地文件为例,若使用同步方式,程序会在发出读取指令后,如同被定格一般,死死 “卡住”,直至文件完整读取并返回结果,才肯继续执行下一行代码。这种 “死等” 模式,在处理耗时较短的任务时,或许尚可接受;但一旦遭遇如大规模数据读取、复杂网络请求等耗时漫长的操作,问题便会接踵而至。整个程序仿佛陷入泥沼,界面冻结,用户的任何操作都得不到即时响应,极大地损害了用户体验。而异步编程,则像是一位高效的 “多面手”,当遇到诸如网络请求、文件读取这类耗时操作时,它不会傻傻等待,而是迅速开启新的任务分支,将后续代码的执行权交予主线程,让程序得以继续流畅运行。以网页加载图片为例,当浏览器发起图片加载请求后,并不会停滞不前,而是立即着手处理其他页面元素的渲染、脚本的执行等任务。待图片数据从服务器慢悠悠地传输回来,再由专门的回调函数或异步处理机制,将图片巧妙地安置到对应的位置。如此一来,用户便能在图片加载的间隙,正常进行页面滚动、点击链接等操作,页面始终保持着鲜活的响应能力,极大提升了交互的流畅性。再看一个从服务器获取数据来更新页面内容的场景。在同步模式下,页面会在数据请求发出后陷入僵局,直到数据完整抵达,才一次性更新页面,这期间用户面对的是毫无变化的 “白板”,极易产生焦虑与不耐烦。而采用异步编程,数据请求悄然在后台运作,主线程继续渲染页面骨架、设置基础样式,待数据到手,再通过 DOM 操作逐步、动态地填充内容,用户看到的是一个逐步鲜活起来的页面,交互体验天差地别。异步编程的核心优势,就在于巧妙地利用等待时间,让程序的各个部分并行推进,避免因个别耗时操作拖垮整个系统的响应速度,为用户带来丝滑流畅的浏览与操作体验。6.2 Ajax 原理与使用Ajax(Asynchronous JavaScript and XML),作为前端开发领域的一项关键技术,犹如一座隐形的桥梁,无缝连接着前端页面与后端服务器,实现了数据的异步交互与页面的局部更新,为用户带来流畅、高效的浏览体验。其核心原理依托于 XMLHttpRequest 对象(在现代浏览器中,也常使用更为简洁的 Fetch API),这一对象恰似一位 “幕后信使”,能够在不刷新整个页面的前提下,悄然向服务器发送 HTTP 请求,并机智地接收、处理服务器返回的数据。以一个常见的网页场景为例,当用户在搜索框输入关键词,期望实时获取搜索建议时,Ajax 便开始大展身手。使用 XMLHttpRequest 对象发起请求,代码大致如下:const xhr = new XMLHttpRequest();xhr.open('GET', 'https://example.com/api/search?q=' + encodeURIComponent(keyword), true);xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { const response = JSON.parse(xhr.responseText); // 处理搜索建议数据,如更新下拉列表展示 }};xhr.send();这里,首先创建了 XMLHttpRequest 实例,通过 open 方法精心配置请求的类型(GET)、目标 URL(包含搜索关键词)以及异步模式(true)。接着,为 onreadystatechange 事件绑定回调函数,宛如设置了一个敏锐的 “瞭望哨”,时刻紧盯请求状态的变化。当 readyState 达到 4(意味着请求已完成,数据接收完毕)且状态码为 200(表示请求成功)时,便迅速对返回的 JSON 数据进行解析,并依据数据内容更新页面的搜索建议区域,整个过程页面纹丝不动,用户却能实时获取反馈。Fetch API 则以一种更加现代化、简洁的语法实现类似功能:fetch('https://example.com/api/search?q=' + encodeURIComponent(keyword)) .then(response => response.json()) .then(data => { // 处理搜索建议数据 }) .catch(error => { console.error('搜索请求出错:', error); });Fetch API 采用链式调用的 Promise 风格,通过 then 方法依次处理请求成功后的响应解析、数据处理步骤,若途中出现错误,catch 方法便能精准捕获并处理,让异步数据交互的代码逻辑更加清晰、易读。不妨再看一个完整的示例,从服务器获取待办事项列表数据并实时更新页面展示:<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Ajax待办事项示例</title> <style> #todo-list { list-style-type: none; padding: 0; } #todo-list li { border: 1px solid #ccc; margin: 5px; padding: 5px; } </style></head> <body> <h1>待办事项列表</h1> <ul id="todo-list"></ul> <script> document.addEventListener('DOMContentLoaded', function () { const todoList = document.getElementById('todo-list'); // 使用Fetch API获取待办事项数据 fetch('https://example.com/api/todos') .then(response => response.json()) .then(todos => { todos.forEach(todo => { const listItem = document.createElement('li'); listItem.textContent = todo.task; todoList.appendChild(listItem); }); }) .catch(error => { console.error('获取待办事项出错:', error); }); }); </script></body> </html>在上述代码中,页面加载完成后,通过 Fetch API 向指定服务器接口发送请求,成功获取数据后,循环遍历待办事项数组,动态创建 DOM 元素并添加到页面列表中,瞬间为用户呈现出最新的待办任务清单,全程无刷新,交互体验流畅自然。Ajax 技术的应用,让网页告别了频繁整页刷新的笨拙,实现了数据与页面展示的精妙同步,极大提升了用户体验与应用的响应效率。七、前沿框架:Vue.js 入门窥探7.1 Vue.js 简介在当今蓬勃发展的前端开发领域,Vue.js 宛如一颗璀璨夺目的新星,以其卓越的特性迅速赢得了广大开发者的青睐,成为构建现代用户界面的得力工具。Vue.js 最为突出的优势之一便是其精妙绝伦的响应式数据绑定机制。传统的 JavaScript 开发模式下,当数据发生变化时,开发者需手动编写冗长繁杂的代码来精准定位并更新对应的 DOM 元素,这一过程极易出错且效率低下,犹如在错综复杂的迷宫中艰难寻路。而 Vue.js 通过其内部强大的响应式系统,能够自动 “感知” 数据的细微变化,宛如一位时刻警觉的守护者,一旦数据有所异动,便立即高效且智能地更新与之关联的 DOM 内容,确保视图与数据始终保持高度一致,实现无缝同步。组件化开发则是 Vue.js 的另一大 “杀手锏”。它倡导将复杂的用户界面拆解为一个个独立、可复用的小型组件,恰似将一座宏伟的大厦拆分为众多标准化的积木模块。每个组件都拥有自己独立的 HTML 模板、JavaScript 逻辑以及 CSS 样式,它们既能在不同场景下被重复调用,又能依据需求灵活组合,极大地提升了开发效率与代码的可维护性。例如,在构建一个大型电商网站时,头部导航栏、商品列表、购物车等功能模块均可封装为独立组件,开发团队可并行推进各组件的开发,后续若需优化某个组件,也只需聚焦于该组件内部代码,避免牵一发而动全身,让项目开发与维护变得井井有条。对比原生 JavaScript 开发,Vue.js 的高效性体现得淋漓尽致。以构建一个具有动态数据展示与交互功能的页面为例,原生 JavaScript 需要耗费大量精力处理 DOM 操作、事件绑定以及数据更新的复杂逻辑,代码往往冗长且晦涩难懂,如同杂乱无章的线缆交织在一起;而 Vue.js 凭借简洁优雅的模板语法、高效的数据绑定以及组件化架构,能够以更少的代码量、更清晰的逻辑结构实现同样甚至更为强大的功能,宛如一位技艺精湛的魔法师,用简洁的咒语变出绚丽的魔法,让开发者从繁琐的底层操作中解脱出来,将更多精力投入到业务逻辑与用户体验的优化上,快速打造出高性能、交互性强的优质网页应用。7.2 基础使用示例下面通过一个简单的计数器示例,来初步领略 Vue.js 的魅力与便捷。首先,引入 Vue.js 库。可以通过在 HTML 页面的<head>标签内使用<script>标签引入 CDN 链接,如下:<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>接着,在 HTML 的<body>标签内创建一个 DOM 元素作为 Vue 实例的挂载点,例如:<div id="app"></div>然后,编写 JavaScript 代码创建 Vue 实例并进行数据绑定:var app = new Vue({ el: '#app', data: { count: 0 }});这里,el属性指定了 Vue 实例挂载的 DOM 元素选择器,data对象则包含了应用所需的数据,此处仅有一个count属性,初始值为 0。在 HTML 中,使用 Vue.js 的模板语法将数据绑定到 DOM 元素上:<div id="app"> <p>当前计数:{ { count }}</p> <button @click="count++">点击增加</button></div>在上述代码中,双大括号{ { count }}便是 Vue.js 的文本插值语法,它能够实时将count的数据渲染到 DOM 中,让用户直观看到当前计数。而@click="count++"则是 Vue.js 的事件绑定语法,它监听按钮的点击事件,每次点击时,count的值便会自动加 1,由于 Vue.js 的响应式特性,与之绑定的 DOM 内容也会瞬间更新,完美展现数据双向绑定的效果。不妨设想一下,如果使用原生 JavaScript 来实现相同功能,需要手动获取 DOM 元素、监听按钮点击事件、更新数据并操作 DOM 来修改显示文本,代码复杂度大幅提升,且易出现诸如事件绑定错误、DOM 更新不及时等问题。而 Vue.js 通过简洁的语法糖,将复杂的交互逻辑封装得优雅而高效,让开发者能轻松构建动态交互界面,开启便捷开发之旅。八、总结与展望至此,我们已一同穿越了 JavaScript 前端开发的核心地带,领略了其从基础语法、DOM 操作、事件处理,到实战应用、异步编程以及前沿框架 Vue.js 入门的独特魅力。在这个过程中,我们明晰了变量与数据类型的精妙差异,熟练掌握了条件循环语句与函数的灵活运用,学会了运用 DOM 操作精准掌控网页元素,巧用事件处理搭建起用户与网页交互的坚实桥梁,通过实战打造出实用的待办事项列表,深入理解异步编程提升页面性能,并初探 Vue.js 感受现代框架的高效便捷。然而,JavaScript 的世界广袤无垠,始终处于蓬勃发展之中。新的特性、框架、工具如繁星般不断涌现。作为前端开发者,持续学习是我们前行的不二法则。需时刻关注 ECMAScript 的最新标准,探索如 React、Angular 等其他前沿框架的独特优势,深入钻研 WebAssembly、PWA 等新兴技术,将其巧妙融入项目,创造更为精彩卓越的用户交互体验。愿大家在 JavaScript 前端开发的征程中,不断探索、砥砺奋进,书写属于自己的精彩篇章,让互联网世界因我们的代码而绽放更加绚烂的光彩。———————————————— 原文链接:https://blog.csdn.net/weixin_73295475/article/details/145325390
-
引言在多线程编程中,线程间的数据共享与隔离是一个非常重要的话题。Java 提供了多种机制来处理多线程环境下的数据共享问题,其中 ThreadLocal 是一个非常有用的工具。ThreadLocal 允许我们为每个线程创建一个独立的变量副本,从而避免线程间的数据竞争和同步问题。本文将深入探讨 ThreadLocal 的工作原理,并通过代码示例展示如何在 Java 多线程环境中使用 ThreadLocal 进行上下文管理。一、ThreadLocal 的基本概念1.1 什么是 ThreadLocal?ThreadLocal 是 Java 提供的一个线程级别的变量存储类。它为每个使用该变量的线程提供了一个独立的变量副本,每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。ThreadLocal 通常用于在多线程环境中保存线程的上下文信息,如用户会话、数据库连接等。1.2 ThreadLocal 的使用场景线程上下文管理:在多线程环境中,ThreadLocal 可以用于保存线程的上下文信息,如用户会话、事务 ID 等。避免参数传递:在某些情况下,ThreadLocal 可以避免在方法调用链中传递参数,简化代码。线程安全的对象管理:ThreadLocal 可以用于管理线程安全的对象,如 SimpleDateFormat 等。二、ThreadLocal 的工作原理2.1 ThreadLocal 的内部结构ThreadLocal 的核心思想是为每个线程维护一个独立的变量副本。为了实现这一点,ThreadLocal 内部使用了一个名为 ThreadLocalMap 的静态内部类。ThreadLocalMap 是一个定制化的哈希表,用于存储线程的变量副本。每个 Thread 对象内部都有一个 ThreadLocalMap 的实例,用于存储该线程的所有 ThreadLocal 变量。当调用 ThreadLocal 的 get() 或 set() 方法时,ThreadLocal 会首先获取当前线程的 ThreadLocalMap,然后在该 ThreadLocalMap 中进行操作。2.2 ThreadLocal 的核心方法set(T value):将当前线程的 ThreadLocal 变量副本设置为指定的值。get():返回当前线程的 ThreadLocal 变量副本。remove():移除当前线程的 ThreadLocal 变量副本。2.3 ThreadLocalMap 的实现ThreadLocalMap 是一个定制化的哈希表,它的键是 ThreadLocal 对象,值是对应的变量副本。ThreadLocalMap 使用开放地址法来解决哈希冲突,即当发生冲突时,它会寻找下一个空闲的槽位来存储数据。ThreadLocalMap 的键是弱引用(WeakReference),这意味着当 ThreadLocal 对象不再被引用时,它会被垃圾回收器回收,从而避免内存泄漏。三、ThreadLocal 的使用示例3.1 基本使用下面是一个简单的示例,展示了如何使用 ThreadLocal 来保存线程的上下文信息。public class ThreadLocalExample { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Runnable task = () -> { // 设置线程的上下文信息 threadLocal.set(Thread.currentThread().getName() + " - context"); // 获取线程的上下文信息 System.out.println(threadLocal.get()); // 清除线程的上下文信息 threadLocal.remove(); }; // 创建多个线程并启动 Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); }}在这个示例中,我们创建了一个 ThreadLocal 变量 threadLocal,并在每个线程中设置和获取该变量的值。由于 ThreadLocal 为每个线程提供了独立的变量副本,因此每个线程都可以安全地访问和修改自己的副本,而不会影响其他线程。3.2 使用 ThreadLocal 管理线程安全的对象ThreadLocal 还可以用于管理线程安全的对象,如 SimpleDateFormat。由于 SimpleDateFormat 不是线程安全的,因此在多线程环境中使用时需要进行同步。通过 ThreadLocal,我们可以为每个线程创建一个独立的 SimpleDateFormat 实例,从而避免同步开销。public class ThreadLocalDateFormat { private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); public static String formatDate(Date date) { return dateFormatThreadLocal.get().format(date); } public static void main(String[] args) { Runnable task = () -> { String formattedDate = formatDate(new Date()); System.out.println(Thread.currentThread().getName() + " - " + formattedDate); }; // 创建多个线程并启动 Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); }}在这个示例中,我们使用 ThreadLocal 为每个线程创建了一个独立的 SimpleDateFormat 实例。这样,每个线程都可以安全地使用自己的 SimpleDateFormat 实例,而无需担心线程安全问题。四、ThreadLocal 的内存泄漏问题4.1 内存泄漏的原因虽然 ThreadLocal 提供了线程级别的变量隔离,但如果使用不当,可能会导致内存泄漏问题。ThreadLocalMap 中的键是弱引用,这意味着当 ThreadLocal 对象不再被引用时,它会被垃圾回收器回收。然而,ThreadLocalMap 中的值仍然是强引用,因此如果 ThreadLocal 对象被回收,但 ThreadLocalMap 中的值没有被清除,就会导致内存泄漏。4.2 如何避免内存泄漏为了避免内存泄漏,我们应该在使用完 ThreadLocal 变量后,调用 remove() 方法将其从 ThreadLocalMap 中移除。这样可以确保 ThreadLocalMap 中的值不会一直保留,从而避免内存泄漏。public class ThreadLocalMemoryLeakExample { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { Runnable task = () -> { try { // 设置线程的上下文信息 threadLocal.set(Thread.currentThread().getName() + " - context"); // 模拟业务逻辑 System.out.println(threadLocal.get()); } finally { // 清除线程的上下文信息 threadLocal.remove(); } }; // 创建多个线程并启动 Thread thread1 = new Thread(task); Thread thread2 = new Thread(task); thread1.start(); thread2.start(); }}在这个示例中,我们在 finally 块中调用了 threadLocal.remove() 方法,以确保在使用完 ThreadLocal 变量后将其清除,从而避免内存泄漏。五、总结ThreadLocal 是 Java 多线程编程中一个非常有用的工具,它允许我们为每个线程创建一个独立的变量副本,从而避免线程间的数据竞争和同步问题。通过 ThreadLocal,我们可以轻松地管理线程的上下文信息,简化代码,并提高程序的性能。然而,ThreadLocal 也存在内存泄漏的风险,因此在使用时需要注意及时清理不再需要的变量副本。通过合理地使用 ThreadLocal,我们可以在多线程环境中更好地管理线程的上下文信息,提高程序的稳定性和可维护性。希望本文能够帮助你更好地理解 ThreadLocal 的工作原理,并在实际开发中灵活运用它来解决多线程环境下的数据共享与隔离问题————————————————原文链接:https://blog.csdn.net/weixin_44976692/article/details/145410897
-
枚举是什么枚举(enum):是一种特殊的类,用于定义一组常量,将其组织起来。枚举使得代码更具有可读性和可维护性,特别是在处理固定集合的值时,如:星期、月份、状态码等在 Java 中,使用关键字 enum 来定义枚举类:public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;}其中,定义的枚举项就是该类的实例,且必须在第一行,最后一个枚举项后的分号; 可以省略,但是若枚举类有其他内容,则分号不能省略(最好不要省略) 当类初始化时,这些枚举项就会被实例化枚举类使用 enum 定义后,默认继承 java.lang.Enum 类,也就是说,我们自己写的枚举类,就算没有显示的继承 Enum,但是其默认继承了这个类此外,枚举在 Java 中不能被继承,自定义的枚举类隐式继承自 java.lang.Enum 类,且不能再继承其他类,这样的设计确保了枚举类的简单性和一致性。如果枚举可以继承其他类,将会导致复杂的继承关系,并且影响Java的类型系统常用方法方法 描述values() 以数组的形式返回枚举类型的所有成员ordinal()获取枚举成员的索引位置valueOf() 将普通字符串转换为枚举实例compareTo(E o)比较两个枚举成员在定义时的顺序 我们通过一个示例,来学习和使用这些方法:public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; public static void main(String[] args) { // 获取所有枚举成员 Day[] days = Day.values(); // 遍历 for (int i = 0; i < days.length; i++) { // 获取枚举成员以及索引位置 System.out.println(days[i] + " " + days[i].ordinal()); } // 将普通字符串转换为枚举实例 System.out.println(Day.valueOf("THURSDAY")); // 获取枚举实例 SUNDAY 和 SATURDAY Day sunday = Day.SUNDAY; Day saturday = Day.SATURDAY; // 比较定义时的顺序 System.out.println(sunday.compareTo(saturday)); }}运行结果: 在使用 valueOf() 方法进行转换时,传递的名称必须与枚举常量的名字完全匹配(包括大小写),若不匹配,就会抛出 IllegalArgumentException 异常: 当我们查看 java.lang.Enum 时: 可以看到,valueOf() 方法包含了两个类型的参数:Class<T> enumClass 和 String name其中Class<T> enumType 是一个 Class 对象,表示要查找的枚举类型String name: 是一个字符串,表示要查找的枚举常量的名称。名称必须与枚举常量的名字完全匹配(包括大小写) 但是在使用时,我们只传递了一个参数 name,也能够进行转换,这是为什么呢?这是因为,在 Java 中,valueOf 方法实际上是自动生成的,属于每个枚举类型的特性。虽然它的原始定义需要两个参数(类类型和名称),但是每个枚举类型都会自动提供一个与自身类型相关联的 valueOf 方法,只需传递一个字符串参数因此,当我们调用 Day.valueOf("THURSDAY") 时,Java会自动处理这个调用,实际调用的是包含类名的 valueOf 方法,而不是原始的静态方法定义再观察 java.lang.Enum: 我们会发现,其中并不存在 values() 这个方法,而当我们点击 values() 方法时,则会跳转到本类上那么,values() 方法是从哪来的呢?values() 方法是枚举类自动提供的一个静态方法,允许我们获取一个包含所有枚举常量的数组,这个方法是由Java编译器自动生成的,在编译时每个枚举类型都会自动生成一个 values() 方法,因此不需要我们显式定义它我们将枚举类进行反编译:(1)打开 cmd,切换到 Day.java 文件所在目录(2)编译 .java 文件(javac Day.java)(3)将 .class 文件进行反编译(javap -c Day.class > day.txt)打开 day.txt,可以看到: 编译器自动为我们生成了 values 和 valueOf 方法构造方法当我们创建构造方法时: 不能使用 public 来修饰构造方法,为什么呢?这是因为,在 Java 中,枚举类的构造方法都是私有的(不加任何修饰符时,默认是 private),无法在枚举类外部调用,这也就防止了在枚举类外部创建新的枚举常量在定义枚举常量时,构造方法会被隐式调用:public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; Day() { System.out.println("构造方法"); } public static void main(String[] args) { System.out.println("------------------"); }}运行结果: 当我们定义带有参数的构造方法时,在创建枚举项时,也要为其提供对应参数:public enum Day { SUNDAY("周天", 7), MONDAY("周一", 1), TUESDAY("周二", 2), WEDNESDAY("周三", 3), THURSDAY("周四", 4), FRIDAY("周五", 5), SATURDAY("周六", 6); private String name; private int key; Day(String name, int key) { this.name = name; this.key = key; }}枚举的优缺点优点:(1)枚举常量确保了只能使用定义的常量,避免了使用整型常量时可能带来的错误(2)使用枚举可以使代码更易读,表达清晰(3)枚举定义了一组固定的常量,适合表示有限的状态或选项,便于管理和维护(4)枚举可以拥有字段、方法和构造方法,能够封装与常量相关的行为和属性(5)Java的枚举类自带一些方法,如 values()、valueOf() 等(6)可以用于 switch 语句缺点:(1)枚举的集合是固定的,无法在运行时添加或删除常量。如果需要动态的集合,枚举可能不适用(2)枚举不能继承,无法扩展(3)每个枚举常量都是一个对象,可能会增加内存使用,特别是当枚举常量数量较多时枚举和反射在 Java 反射-CSDN博客 中,我们学习了反射,通过反射,我们可以拿到类的私有构造方法,从而创建实例对象那么,枚举是否可以通过反射,拿到实例对象呢?public enum Day { SUNDAY("周天", 7), MONDAY("周一", 1), TUESDAY("周二", 2), WEDNESDAY("周三", 3), THURSDAY("周四", 4), FRIDAY("周五", 5), SATURDAY("周六", 6); private String name; private int key; Day(String name, int key) { this.name = name; this.key = key; }}public class Test { public static void main(String[] args) { Class<?> classDay = null; try { classDay = Class.forName("Day"); Constructor<?> constructor = classDay.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); Day day = (Day) constructor.newInstance("sunday", 0); System.out.println(day.ordinal()); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }}运行结果: 此时程序抛出了 NoSuchMethodException 异常,也就是没有对应的构造方法但是提供的枚举的构造方法就是带有两个参数,分别为 String 和 int: 那么,问题出在哪里呢?自定义的枚举类默认继承自 java.lang.Enum因此,自定义的枚举类继承了父类除构造方法外的所有东西,且子类需要帮助父类进行构造,但我们实现的类中,并没有帮助父类进行构造因此,我们需要在枚举类中帮助父类进行构造,而父类中的构造方法为: 那么,如何实现呢?通过 super 方法吗?但是,当我们在构造方法中调用 super 时: 枚举构造方法中不能使用 super 由于枚举比较特殊,在构造方法中,除了我们自定义了两个参数,它还默认添加了父类的两个参数也就是说,构造函数中一共有四个参数:String int String int其中,前两个参数是父类参数,后两个参数是子类参数public class Test { public static void main(String[] args) { Class<?> classDay = null; try { classDay = Class.forName("enumDemo.Day"); Constructor<?> constructor = classDay.getDeclaredConstructor(String.class, int.class, String.class, int.class); constructor.setAccessible(true); Day day = (Day) constructor.newInstance("父类参数", 0, "子类参数", 0); System.out.println(day.ordinal()); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } }}再次运行: 此时抛出了 IllegalArgumentException 异常,不能通过反射创建枚举对象枚举保证了每个枚举常量只有一个实例,这种唯一性在枚举类型被定义时就已经确定,不允许外部创建新的实例,枚举类型的设计使得它们的实例在类加载时被唯一地定义,从而避免了通过反射创建新的枚举实例的可能性,确保了枚举的强类型安全性和唯一性实现单例模式在 单例模式:饿汉模式、懒汉模式_单例模式懒汉和饿汉-CSDN博客 中我们实现了单例模式,单例模式能够确保一个类只有一个实例但普通类可以通过反射机制打破,因此,我们可以使用枚举来实现单例模式public enum Singleton { INSTANCE; public Singleton getInstance() { return INSTANCE; }———————————————— 原文链接:https://blog.csdn.net/2301_76161469/article/details/143241972
-
1. 顺序结构顺序结构就是程序从上到下逐行地执行。表达式语句都是顺序执行的。并且上一行对某个变量的修改对下一行会产生影响。 public class StatementTest{public static void main(String[] args){int x = 1;int y = 2;System.out.println("x = " + x); System.out.println("y = " + y); //对x、y的值进行修改 x++; y = 2 * x + y; x = x * 10; System.out.println("x = " + x); System.out.println("y = " + y); }} Java中定义变量时采用合法的前向引用。如: public static void main(String[] args) {int num1 = 12;int num2 = num1 + 2;} 错误形式: public static void main(String[] args) {int num2 = num1 + 2;int num1 = 12;} 2. 分支语句2.1 if-else条件判断结构2.1.1 基本语法结构1:单分支条件判断:if 格式: if(条件表达式){ 语句块;} 说明:条件表达式必须是布尔表达式(关系表达式或逻辑表达式)或 布尔变量。 执行流程: 首先判断条件表达式看其结果是true还是false如果是true就执行语句块如果是false就不执行语句块 结构2:双分支条件判断:if…else 格式: if(条件表达式) { 语句块1;}else { 语句块2;} 执行流程: 首先判断条件表达式看其结果是true还是false如果是true就执行语句块1如果是false就执行语句块2 结构3:多分支条件判断:if…else if…else 格式: if (条件表达式1) { 语句块1;} else if (条件表达式2) { 语句块2;}...}else if (条件表达式n) { 语句块n;} else { 语句块n+1;} 说明:一旦条件表达式为true,则进入执行相应的语句块。执行完对应的语句块之后,就跳出当前结构。 执行流程: 首先判断关系表达式1看其结果是true还是false如果是true就执行语句块1,然后结束当前多分支如果是false就继续判断关系表达式2看其结果是true还是false如果是true就执行语句块2,然后结束当前多分支如果是false就继续判断关系表达式…看其结果是true还是false … n. 如果没有任何关系表达式为true,就执行语句块n+1,然后结束当前多分支。 当条件表达式之间是“互斥”关系时(即彼此没有交集),条件判断语句及执行语句间顺序无所谓。 当条件表达式之间是“包含”关系时,“小上大下 / 子上父下”,否则范围小的条件表达式将不可能被执行。 2.1.3 if…else嵌套在 if 的语句块中,或者是在else语句块中,又包含了另外一个条件判断(可以是单分支、双分支、多分支),就构成了嵌套结构。 执行的特点:(1)如果是嵌套在if语句块中的,只有当外部的if条件满足,才会去判断内部的条件(2)如果是嵌套在else语句块中的,只有当外部的if条件不满足,进入else后,才会去判断内部的条件 **案例4:**由键盘输入三个整数分别存入变量num1、num2、num3,对它们进行排序(使用 if-else if-else),并且从小到大输出。 class IfElseTest4 {public static void main(String[] args) { //声明num1,num2,num3三个变量并赋值int num1 = 23,num2 = 32,num3 = 12; if(num1 >= num2){ if(num3 >= num1)System.out.println(num2 + "-" + num1 + "-" + num3);else if(num3 <= num2)System.out.println(num3 + "-" + num2 + "-" + num1);elseSystem.out.println(num2 + "-" + num3 + "-" + num1);}else{ //num1 < num2 if(num3 >= num2){System.out.println(num1 + "-" + num2 + "-" + num3);}else if(num3 <= num1){System.out.println(num3 + "-" + num1 + "-" + num2);}else{System.out.println(num1 + "-" + num3 + "-" + num2);}}}} 2.1.4 其它说明语句块只有一条执行语句时,一对{}可以省略,但建议保留当if-else结构是“多选一”时,最后的else是可选的,根据需要可以省略2.2 switch-case选择结构2.2.1 基本语法语法格式: switch(表达式){ case 常量值1: 语句块1; //break; case 常量值2: 语句块2; //break; // ... [default: 语句块n+1; break; ]} 执行过程: 第1步:根据switch中表达式的值,依次匹配各个case。如果表达式的值等于某个case中的常量值,则执行对应case中的执行语句。 第2步:执行完此case的执行语句以后, 情况1:如果遇到break,则执行break并跳出当前的switch-case结构 情况2:如果没有遇到break,则会继续执行当前case之后的其它case中的执行语句。—>case穿透 … 直到遇到break关键字或执行完所有的case及default的执行语句,跳出当前的switch-case结构 使用注意点: switch(表达式)中表达式的值必须是下述几种类型之一:byte,short,char,int,枚举 (jdk 5.0),String (jdk 7.0); case子句中的值必须是常量,不能是变量名或不确定的表达式值或范围; 同一个switch语句,所有case子句中的常量值互不相同; break语句用来在执行完一个case分支后使程序跳出switch语句块; 如果没有break,程序会顺序执行到switch结尾; default子句是可选的。同时,位置也是灵活的。当没有匹配的case时,执行default语句。 2.2.3 利用case的穿透性在switch语句中,如果case的后面不写break,将出现穿透现象,也就是一旦匹配成功,不会在判断下一个case的值,直接向后运行,直到遇到break或者整个switch语句结束,执行终止。 案例:编写程序:从键盘上输入2023年的“month”和“day”,要求通过程序输出输入的日期为2023年的第几天。 import java.util.Scanner; class SwitchCaseTest4 {public static void main(String[] args) { Scanner scan = new Scanner(System.in); System.out.println("请输入2023年的month:");int month = scan.nextInt(); System.out.println("请输入2023年的day:");int day = scan.nextInt(); //这里就不针对month和day进行合法性的判断了,以后可以使用正则表达式进行校验。 int sumDays = 0;//记录总天数 //写法1 :不推荐(存在冗余的数据)/*switch(month){case 1:sumDays = day;break;case 2:sumDays = 31 + day;break;case 3:sumDays = 31 + 28 + day;break;//.... case 12://sumDays = 31 + 28 + ... + 30 + day;break;}*/ //写法2:推荐switch(month){case 12:sumDays += 30;//这个30是代表11月份的满月天数case 11:sumDays += 31;//这个31是代表10月份的满月天数case 10:sumDays += 30;//这个30是代表9月份的满月天数case 9:sumDays += 31;//这个31是代表8月份的满月天数case 8:sumDays += 31;//这个31是代表7月份的满月天数case 7:sumDays += 30;//这个30是代表6月份的满月天数case 6:sumDays += 31;//这个31是代表5月份的满月天数case 5:sumDays += 30;//这个30是代表4月份的满月天数case 4:sumDays += 31;//这个31是代表3月份的满月天数case 3:sumDays += 28;//这个28是代表2月份的满月天数case 2:sumDays += 31;//这个31是代表1月份的满月天数case 1:sumDays += day;//这个day是代表当月的第几天} System.out.println(month + "月" + day + "日是2023年的第" + sumDays + "天"); //关闭资源scan.close();}} 2.2.4 if-else语句与switch-case语句比较结论:凡是使用switch-case的结构都可以转换为if-else结构。反之,不成立。 开发经验:如果既可以使用switch-case,又可以使用if-else,建议使用switch-case。因为效率稍高。 细节对比: if-else语句优势if语句的条件是一个布尔类型值,if条件表达式为true则进入分支,可以用于范围的判断,也可以用于等值的判断,使用范围更广。switch语句的条件是一个常量值(byte,short,int,char,枚举,String),只能判断某个变量或表达式的结果是否等于某个常量值,使用场景较狭窄。switch语句优势当条件是判断某个变量或表达式是否等于某个固定的常量值时,使用if和switch都可以,习惯上使用switch更多。因为效率稍高。当条件是区间范围的判断时,只能使用if语句。使用switch可以利用穿透性,同时执行多个分支,而if…else没有穿透性。3. 循环语句理解:循环语句具有在某些条件满足的情况下,反复执行特定代码的功能。 循环结构分类: for 循环while 循环do-while 循环循环结构四要素: 初始化部分循环条件部分循环体部分迭代部分3.1 for循环3.1.1 基本语法语法格式: for (①初始化部分; ②循环条件部分; ④迭代部分){ ③循环体部分;} **执行过程:**①-②-③-④-②-③-④-②-③-④-…-② 说明: for(;;)中的两个;不能多也不能少①初始化部分可以声明多个变量,但必须是同一个类型,用逗号分隔②循环条件部分为boolean类型表达式,当值为false时,退出循环④可以有多个变量更新,用逗号分隔说明: 1、我们可以在循环中使用break。一旦执行break,就跳出当前循环结构。 2、小结:如何结束一个循环结构? 结束情况1:循环结构中的循环条件部分返回false 结束情况2:循环结构中执行了break。 3、如果一个循环结构不能结束,那就是一个死循环!我们开发中要避免出现死循环。 3.2 while循环3.2.1 基本语法语法格式: ①初始化部分while(②循环条件部分){ ③循环体部分; ④迭代部分;} **执行过程:**①-②-③-④-②-③-④-②-③-④-…-② 说明: while(循环条件)中循环条件必须是boolean类型。注意不要忘记声明④迭代部分。否则,循环将不能结束,变成死循环。for循环和while循环可以相互转换。二者没有性能上的差别。实际开发中,根据具体结构的情况,选择哪个格式更合适、美观。for循环与while循环的区别:初始化条件部分的作用域不同。3.3 do-while循环3.3.1 基本语法语法格式: ①初始化部分;do{③循环体部分④迭代部分}while(②循环条件部分); **执行过程:**①-③-④-②-③-④-②-③-④-…-② 图示: 说明: 结尾while(循环条件)中循环条件必须是boolean类型do{}while();最后有一个分号do-while结构的循环体语句是至少会执行一次,这个和for和while是不一样的循环的三个结构for、while、do-while三者是可以相互转换的。3.4 对比三种循环结构三种循环结构都具有四个要素:循环变量的初始化条件循环条件循环体语句块循环变量的修改的迭代表达式从循环次数角度分析do-while循环至少执行一次循环体语句。for和while循环先判断循环条件语句是否成立,然后决定是否执行循环体。如何选择遍历有明显的循环次数(范围)的需求,选择for循环遍历没有明显的循环次数(范围)的需求,选择while循环如果循环体语句块至少执行一次,可以考虑使用do-while循环本质上:三种循环之间完全可以互相转换,都能实现循环的功能3.5 "无限"循环 3.5.1 基本语法语法格式: 最简单"无限"循环格式:while(true) , for(;;)适用场景: 开发中,有时并不确定需要循环多少次,需要根据循环体内部某些条件,来控制循环的结束(使用break)。如果此循环结构不能终止,则构成了死循环!开发中要避免出现死循环。3.6 嵌套循环(或多重循环)3.6.1 使用说明所谓嵌套循环,是指一个循环结构A的循环体是另一个循环结构B。比如,for循环里面还有一个for循环,就是嵌套循环。其中,for ,while ,do-while均可以作为外层循环或内层循环。外层循环:循环结构A内层循环:循环结构B实质上,嵌套循环就是把内层循环当成外层循环的循环体。只有当内层循环的循环条件为false时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的外层循环。设外层循环次数为m次,内层为n次,则内层循环体实际上需要执行m*n次。**技巧:**从二维图形的角度看,外层循环控制行数,内层循环控制列数。**开发经验:**实际开发中,我们最多见到的嵌套循环是两层。一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内。否则,可读性会很差。例如:两个for嵌套循环格式 for(初始化语句①; 循环条件语句②; 迭代语句⑦) { for(初始化语句③; 循环条件语句④; 迭代语句⑥) { 循环体语句⑤; }} //执行过程:① - ② - ③ - ④ - ⑤ - ⑥ - ④ - ⑤ - ⑥ - ... - ④ - ⑦ - ② - ③ - ④ - ⑤ - ⑥ - ④.. **执行特点:**外层循环执行一次,内层循环执行一轮。 4. 关键字break和continue的使用4.1 break和continue的说明适用范围 在循环结构中使用的作用 相同点 break switch-case循环结构 一旦执行,就结束(或跳出)当前循环结构 此关键字的后面,不能声明语句 continue 循环结构 一旦执行,就结束(或跳出)当次循环结构 此关键字的后面,不能声明语句 此外,很多语言都有goto语句,goto语句可以随意将控制转移到程序中的任意一条语句上,然后执行它,但使程序容易出错。Java中的break和continue是不同于goto的。 4.2 应用举例class BreakContinueTest1 {public static void main(String[] args) { for(int i = 1;i <= 10;i++){ if(i % 4 == 0){//break;//123continue;//123567910//如下的语句不可能被执行,编译不通过//System.out.println("今晚迪丽热巴要约我吃饭");} System.out.print(i);} System.out.println("####"); //嵌套循环中的使用for(int i = 1;i <= 4;i++){ for(int j = 1;j <= 10;j++){if(j % 4 == 0){//break; //结束的是包裹break关键字的最近的一层循环!continue;//结束的是包裹break关键字的最近的一层循环的当次!}System.out.print(j);}System.out.println();} }} 4.3 带标签的使用break语句用于终止某个语句块的执行{ …… break;……} break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块 label1: { …… label2: { ……label3: { …… break label2; ……} }} continue语句出现在多层嵌套的循环语句体中时,也可以通过标签指明要跳过的是哪一层循环。 标号语句必须紧接在循环的头部。标号语句不能用在非循环语句的前面。 举例: class BreakContinueTest2 {public static void main(String[] args) {l:for(int i = 1;i <= 4;i++){ for(int j = 1;j <= 10;j++){if(j % 4 == 0){//break l;continue l;}System.out.print(j);}System.out.println();}}} 5. Scanner:键盘输入功能的实现如何从键盘获取不同类型(基本数据类型、String类型)的变量:使用Scanner类。 键盘输入代码的四个步骤: 导包:import java.util.Scanner;创建Scanner类型的对象:Scanner scan = new Scanner(System.in);调用Scanner类的相关方法(next() / nextXxx()),来获取指定类型的变量释放资源:scan.close();注意:需要根据相应的方法,来输入指定类型的值。如果输入的数据类型与要求的类型不匹配时,会报异常 导致程序终止。 5.1 各种类型的数据输入**案例:**小明注册某交友网站,要求录入个人相关信息。如下: 请输入你的网名、你的年龄、你的体重、你是否单身、你的性别等情况。 //① 导包import java.util.Scanner; public class ScannerTest1 { public static void main(String[] args) { //② 创建Scanner的对象 //Scanner是一个引用数据类型,它的全名称是java.util.Scanner //scanner就是一个引用数据类型的变量了,赋给它的值是一个对象(对象的概念我们后面学习,暂时先这么叫) //new Scanner(System.in)是一个new表达式,该表达式的结果是一个对象 //引用数据类型 变量 = 对象; //这个等式的意思可以理解为用一个引用数据类型的变量代表一个对象,所以这个变量的名称又称为对象名 //我们也把scanner变量叫做scanner对象 Scanner scanner = new Scanner(System.in);//System.in默认代表键盘输入 //③根据提示,调用Scanner的方法,获取不同类型的变量 System.out.println("欢迎光临你好我好交友网站!"); System.out.print("请输入你的网名:"); String name = scanner.next(); System.out.print("请输入你的年龄:"); int age = scanner.nextInt(); System.out.print("请输入你的体重:"); double weight = scanner.nextDouble(); System.out.print("你是否单身(true/false):"); boolean isSingle = scanner.nextBoolean(); System.out.print("请输入你的性别:"); char gender = scanner.next().charAt(0);//先按照字符串接收,然后再取字符串的第一个字符(下标为0) System.out.println("你的基本情况如下:"); System.out.println("网名:" + name + "\n年龄:" + age + "\n体重:" + weight + "\n单身:" + isSingle + "\n性别:" + gender); //④ 关闭资源 scanner.close(); }} 6. 如何获取一个随机数如何产生一个指定范围的随机整数? 1、Math类的random()的调用,会返回一个[0,1)范围的一个double型值 2、Math.random() * 100 —> [0,100)(int)(Math.random() * 100) —> [0,99](int)(Math.random() * 100) + 5 ----> [5,104] 3、如何获取[a,b]范围内的随机整数呢?(int)(Math.random() * (b - a + 1)) + a 4、举例 System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}}class MathRandomTest {public static void main(String[] args) {double value = Math.random(); System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}} System.out.println(value); //[1,6]int number = (int)(Math.random() * 6) + 1; //System.out.println(number);}}———————————————— 原文链接:https://blog.csdn.net/fj123789/article/details/145672376
-
1. 前言在当今的软件开发中,数据的导入与导出是常见的需求,尤其是在企业级应用中,Excel文件作为数据交互的一种重要形式被广泛使用。传统的Excel导入导出功能虽然基本满足需求,但在处理大数据量或需要动态配置时,往往显得效率低下且灵活性不足。本文将围绕在Java中基于实体类的高效Excel数据导入导出展开,介绍如何利用FastExcel这一库实现高性能和灵活的Excel处理。同时,结合动态下拉框和多选下拉框的设置,使得数据的导入导出不仅高效,还能具备较强的可定制性和交互性。通过本篇文章,你将能够掌握在Java中使用实体类驱动Excel导入导出的技术,并学会如何在系统中动态生成下拉框和多选框的配置。2. Fastexcel介绍FastExcel 是一个高性能的 Java 库,旨在提供高效的 Excel 文件操作,尤其是在处理大数据量时,其性能远超常见的POI库。相比其他 Excel 处理库,FastExcel 采用了更加优化的内存管理和流式处理方式,使得它在内存占用和速度上具有显著优势。尤其在导入导出大量数据时,FastExcel 可以有效避免内存溢出和性能瓶颈,保证程序的稳定运行。FastExcel 的特点:高性能: 快速的读取和写入速度,特别适用于大数据量的 Excel 文件。低内存占用: 采用流式读取和写入的方式,极大地减少了内存的使用。简洁易用: 相较于其他复杂的Excel操作库,FastExcel提供了简洁的API接口,易于上手。Excel文件格式支持: 支持 .xlsx 格式的文件,且兼容大部分常见的 Excel 文件操作需求。动态功能扩展: 可以灵活地与 Java 实体类进行绑定,支持动态生成表头、表格内容和格式设置。为什么选择FastExcel?在实际开发中,Excel文件的导入导出经常用于大规模的数据交换,尤其是在财务、报表等领域。对于这些场景,传统的 Excel 处理库(如 Apache POI)可能在面对大数据量时会出现性能瓶颈,尤其是在需要频繁进行读写操作的情况下。而FastExcel通过采用流式读取和写入的方式,有效解决了这一问题,并且通过内存管理优化,使得应用能够在处理大量数据时仍保持高效运行。由于FastExcel的这些优势,它成为了许多Java开发者在实现Excel数据导入导出时的首选工具,尤其是对于需要处理海量数据或需要提高导入导出性能的应用场景。3. 技术实现模拟需求:本案例模拟导出学生的相关信息,包括学号、姓名、性别、父母职业类型和家庭住址所属区域等内容。具体来说,学生信息包含以下字段:学号:唯一标识学生的编号,作为数据的主键。姓名:学生的姓名,文本类型字段。性别:此字段通过下拉框进行选择,支持男、女等选项,方便用户快速选择。父母职业类型:此字段也是一个下拉框,列出了多种常见的职业类型,便于系统自动识别父母的职业分类。家庭住址所属区域:该字段设置为多选下拉框,支持学生家庭地址涉及多个区域的情况。例如,假设某些富裕学生的家庭可能在不同城市或区域拥有多个房产,因此可以选择多个区域。此设置充分考虑了复杂的地址情况,提高了数据录入的灵活性。本文将实现excel文件下拉框基于java程序动态设置,并支持多选下拉框。动态下拉框的实现步骤如下:1. 创建支持宏的xlsm模板文件2. 编写vba代码3. 基于java代码动态写入下拉框数据4. 导出动态设置下拉框的excel模板 3.1. 表结构及实体类说明 1. PostgreSQL数据库表结构(SQL)我这里创建了一个比较简单的数据字典表存储下拉框数据,完整版数据字典请移步:基于AOP的数据字典实现:实现前端下拉框的可配置更新_数据字典下拉框怎么写-CSDN博客CREATE TABLE students ( student_id VARCHAR(20) PRIMARY KEY, -- 学号 name VARCHAR(100) NOT NULL, -- 姓名 gender VARCHAR(10), -- 性别 parent_occupation VARCHAR(100), -- 父母职业类型 home_area TEXT[] -- 家庭住址所属区域 (使用数组类型来存储多个区域)); -- 创建一个独立的字典表,用于存储性别、父母职业类型等下拉框选项CREATE TABLE gender_options ( id SERIAL PRIMARY KEY, gender VARCHAR(10) NOT NULL); CREATE TABLE parent_occupation_options ( id SERIAL PRIMARY KEY, occupation VARCHAR(100) NOT NULL); -- 插入默认的字典数据INSERT INTO gender_options (gender) VALUES ('男'), ('女'); INSERT INTO parent_occupation_options (occupation) VALUES ('教师'), ('医生'), ('工程师'), ('律师'), ('其他');在student表中插入10条数据:INSERT INTO students (student_id, name, gender, parent_occupation, home_area)VALUES('S10001', '张三', '男', '教师', '{"北京", "上海"}'),('S10002', '李四', '女', '医生', '{"广州", "深圳"}'),('S10003', '王五', '男', '工程师', '{"北京"}'),('S10004', '赵六', '女', '律师', '{"杭州", "南京"}'),('S10005', '孙七', '男', '商人', '{"上海", "广州"}'),('S10006', '周八', '女', '公务员', '{"北京", "武汉"}'),('S10007', '吴九', '男', '教师', '{"成都"}'),('S10008', '郑十', '女', '护士', '{"重庆", "成都"}'),('S10009', '冯十一', '男', '程序员', '{"深圳", "上海"}'),('S10010', '陈十二', '女', '医生', '{"北京", "上海", "广州"}');2. 实体类@Data@EqualsAndHashCode(callSuper = false)@Accessors(chain = true)@ApiModel(value="Students对象", description="")@TableName(value = "students",autoResultMap = true)public class Students implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "student_id", type = IdType.ASSIGN_ID) @ExcelProperty(value = "学号",index = 0) private String studentId; @ExcelProperty(value = "姓名",index = 1) private String name; @ExcelProperty(value = "性别",index = 2) @DropDownSetField(source = {"男", "女"}) private String gender; @ExcelProperty(value = "父母职业", index = 3) @DropDownSetField(dynamicSource = ParentOccupationOptions.class) private String parentOccupation; @ExcelProperty(value = "所属区域",index = 4,converter = SimpleStringToListConverter.class) @DropDownSetField(source = {"东城区", "西城区", "海淀区", "朝阳区", "丰台区", "石景山区", "门头沟区", "房山区", "通州区", "顺义区", "昌平区", "大兴区", "怀柔区", "平谷区", "密云区", "延庆区"}) @TableField(typeHandler = StringArrayTypeHandler.class) private List<String> homeArea; }3.2. 适配PostgresSQL中text[]类型handler编写@ConditionalOnClass({BaseTypeHandler.class})@MappedTypes({List.class})public class StringArrayTypeHandler extends BaseTypeHandler<List<String>> { @Override public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException { Connection conn = ps.getConnection(); Array array = conn.createArrayOf("text", parameter.toArray(new String[0])); ps.setArray(i, array); } @Override public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException { Array array = rs.getArray(columnName); return array == null ? null : Arrays.asList((String[]) array.getArray()); } @Override public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException { Array array = rs.getArray(columnIndex); return array == null ? null : Arrays.asList((String[]) array.getArray()); } @Override public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { Array array = cs.getArray(columnIndex); return array == null ? null : Arrays.asList((String[]) array.getArray()); }}这个类是一个 MyBatis 的类型处理器(TypeHandler),主要功能是:数据转换:实现 PostgreSQL 数据库中的数组类型与 Java 中的 List<String> 类型之间的双向转换Java -> DB:将 List<String> 转换为 PostgreSQL 的 text[] 数组类型DB -> Java:将 PostgreSQL 的 text[] 数组类型转换为 List<String>应用场景:适用于需要在单个字段中存储多个值的情况,如学生所属区域(可以属于多个区域)的存储和读取技术特点:继承自 BaseTypeHandler<List<String>>使用 @MappedTypes 注解指定处理 List 类型使用 @ConditionalOnClass 实现条件化配置3.3. 注解编写@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface DropDownSetField { String[] source() default {}; String value() default ""; Class<?>[] dynamicSource() default {};}3.4. 动态设置下拉框核心工具类编写@Component@Slf4jpublic class ExchangeSheetUtils { @Autowired private IDropDownDataService dropDownDataService; private static final int MAX_EXCEL_ROWS = 65536; private static final String HIDDEN_SHEET_NAME = "字典sheet"; private final char[] alphabet = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; // 使用ThreadLocal来确保线程安全 private final ThreadLocal<List<String>> dropDownArrays = ThreadLocal.withInitial(ArrayList::new); private final ThreadLocal<Map<Integer, List<String>>> dropDownMap = ThreadLocal.withInitial(HashMap::new); // 在方法结束时清理ThreadLocal public void clearThreadLocals() { dropDownArrays.get().clear(); dropDownMap.get().clear(); } /** * 根据实体类解析字段,并获取动态或固定的下拉数据。 */ public void getEntityField(Class<?> clazz) { try { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { processDropDownField(field); } } catch (Exception e) { log.error("处理实体类字段失败", e); clearThreadLocals(); throw new RuntimeException("处理实体类字段失败", e); } } /** * 处理下拉框字段 */ private void processDropDownField(Field field) { ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); if (excelProperty == null || excelProperty.value().length == 0) { log.warn("字段 {} 缺少 ExcelProperty 注解或 value 为空", field.getName()); return; } String columnName = excelProperty.value()[0]; dropDownArrays.get().add(columnName); DropDownSetField dropDownSetField = field.getAnnotation(DropDownSetField.class); if (dropDownSetField != null) { List<String> dropDownOptions = new ArrayList<>(); if (dropDownSetField.dynamicSource().length > 0) { dropDownOptions = getDropDownDataFromDynamicSource(dropDownSetField.dynamicSource()); } else if (dropDownSetField.source().length > 0) { dropDownOptions = Arrays.asList(dropDownSetField.source()); } if (!dropDownOptions.isEmpty()) { int columnIndex = dropDownArrays.get().size() - 1; dropDownMap.get().put(columnIndex, dropDownOptions); } } } /** * 从动态数据源获取下拉数据 */ private List<String> getDropDownDataFromDynamicSource(Class<?>[] dynamicSourceClasses) { List<String> dropDownOptions = new ArrayList<>(); for (Class<?> dynamicSourceClass : dynamicSourceClasses) { try { // 调用动态数据源的接口获取下拉数据(例如通过远程接口) List<String> data = dropDownDataService.fetchDynamicDropDownData(dynamicSourceClass); dropDownOptions.addAll(data); } catch (Exception e) { log.error("获取动态下拉框数据失败,错误信息: {}", e.getMessage()); } } return dropDownOptions; } /** * 创建并更新隐藏Sheet页,添加下拉框 */ public void updateHiddenSheet(Sheet curSheet, Workbook templateWorkbook) { if (dropDownMap.get().isEmpty()) { return; // 如果没有下拉框数据则不进行处理 } DataValidationHelper helper = curSheet.getDataValidationHelper(); String hiddenSheetName = HIDDEN_SHEET_NAME; Sheet hiddenSheet = templateWorkbook.createSheet(hiddenSheetName); hideOtherSheets(templateWorkbook); clearOldNamedRanges(templateWorkbook); // 填充隐藏Sheet的数据 Set<Map.Entry<Integer, List<String>>> entrySet = dropDownMap.get().entrySet(); for (Map.Entry<Integer, List<String>> entry : entrySet) { createDropDownList(helper, hiddenSheet, entry); } } /** * 隐藏所有除第一个外的Sheet */ private void hideOtherSheets(Workbook templateWorkbook) { int totalSheets = templateWorkbook.getNumberOfSheets(); for (int i = 1; i < totalSheets; i++) { templateWorkbook.setSheetHidden(i, true); } } /** * 清除之前的命名范围 */ private void clearOldNamedRanges(Workbook templateWorkbook) { for (int i = 0; i < 26; i++) { Name workbookName = templateWorkbook.getName("dict" + i); if (workbookName != null) { templateWorkbook.removeName(workbookName); // 使用 Name 对象删除 } } } /** * 创建并配置下拉框 */ private void createDropDownList(DataValidationHelper helper, Sheet hiddenSheet, Map.Entry<Integer, List<String>> entry) { Integer column = entry.getKey(); List<String> values = entry.getValue(); // 填充数据到隐藏sheet int rowLen = values.size(); for (int i = 0; i < rowLen; i++) { Row row = hiddenSheet.getRow(i); if (row == null) { row = hiddenSheet.createRow(i); } Cell cell = row.createCell(column); cell.setCellValue(values.get(i)); } String excelColumn = getExcelColumn(column); String refersTo = HIDDEN_SHEET_NAME + "!$" + excelColumn + "$1:$" + excelColumn + "$" + rowLen; // 创建命名范围 Name name = hiddenSheet.getWorkbook().createName(); name.setNameName("dict" + column); name.setRefersToFormula(refersTo); // 获取第一个sheet(主sheet) Sheet mainSheet = hiddenSheet.getWorkbook().getSheetAt(0); // 创建数据验证 DataValidationConstraint constraint = helper.createFormulaListConstraint("dict" + column); CellRangeAddressList addressList = new CellRangeAddressList(1, MAX_EXCEL_ROWS, column, column); DataValidation validation = helper.createValidation(constraint, addressList); // 设置验证属性 validation.setSuppressDropDownArrow(true); validation.setShowErrorBox(true); validation.setErrorStyle(DataValidation.ErrorStyle.STOP); validation.createErrorBox("提示", "此值与单元格定义格式不一致!"); // 将验证添加到主sheet mainSheet.addValidationData(validation); } /** * 将数字列转化为字母列 */ private String getExcelColumn(int num) { int len = alphabet.length; int first = num / len; int second = num % len; if (num < len) { return String.valueOf(alphabet[num]); } else { return String.valueOf(alphabet[first - 1]) + alphabet[second - 1]; } } /** * 设置数据Sheet页的初始化 */ public void setDataSheet(Sheet sheet, Workbook templateWorkbook) { Row row = sheet.createRow(0); List<String> arrays = dropDownArrays.get(); for (int i = 0; i < arrays.size(); i++) { row.createCell(i).setCellValue(arrays.get(i)); } } }ExchangeSheetUtils 核心方法说明1. getEntityFieldpublic void getEntityField(Class<?> clazz)功能:解析实体类的字段注解,收集下拉框配置信息处理流程:获取类的所有字段通过 processDropDownField 处理每个字段的注解将下拉框数据存入 ThreadLocal2. processDropDownFieldprivate void processDropDownField(Field field)功能:处理单个字段的下拉框配置处理流程:读取 @ExcelProperty 注解获取列名读取 @DropDownSetField 注解获取下拉选项将数据保存到 dropDownArrays 和 dropDownMap3. updateHiddenSheetpublic void updateHiddenSheet(Sheet curSheet, Workbook templateWorkbook)功能:创建和配置隐藏的数据字典sheet处理流程:创建隐藏sheet清理旧的命名范围通过 createDropDownList 设置下拉框4. createDropDownListprivate void createDropDownList(DataValidationHelper helper, Sheet hiddenSheet, Map.Entry<Integer, List<String>> entry)功能:创建Excel下拉框处理流程:在隐藏sheet中填充下拉选项创建命名范围(Named Range)设置数据验证规则配置下拉框和错误提示这些方法通过 ThreadLocal 实现线程安全,通过 POI 提供的 API 实现 Excel 的各种操作,最终生成一个带有下拉框的 Excel 模板文件。 3.5. 制作多选下拉框exel模板1. 新建.xlsx文件 2. 点击顶部【文件】后点击【选项】 3. 在弹出的弹窗中,点击【信任中心】选项页中的【信任中心设置】按钮 4. 开启宏 5. 另存为.xlsm文件 6. 编写VBA代码选中Sheet,右键弹出菜单,选择【查看代码】,将下面代码粘进去 Sub Worksheet_Change(ByVal Target As Range) ' 让数据有效性选择可以多选,且不可重复 Dim rngDV As Range Dim oldVal As String Dim newVal As String ' 如果修改的范围超过1个单元格,则退出 If Target.Count > 1 Then GoTo exitHandler On Error Resume Next Set rngDV = Cells.SpecialCells(xlCellTypeAllValidation) On Error GoTo exitHandler If rngDV Is Nothing Then GoTo exitHandler If Intersect(Target, rngDV) Is Nothing Then ' 如果目标单元格不在数据验证区域,什么都不做 Else Application.EnableEvents = False newVal = Target.Value ' 假设字段映射如下: ' 第3列是 "gender" (性别) ' 第4列是 "parentOccupation" (父母职业类型) ' 第5列是 "homeArea" (家庭住址所属区域) ' 如果修改的是 "gender" 或 "parentOccupation",则是单选,直接替换 ' 如果修改的是 "homeArea",则是多选,去重并追加 If Target.Column = 3 Or Target.Column = 4 Then ' 对性别(gender)和父母职业类型(parentOccupation)做单选处理 Application.Undo oldVal = Target.Value Target.Value = newVal ' 如果原值与新值不同,直接替换 If oldVal <> newVal Then Target.Value = newVal End If ElseIf Target.Column = 5 Then ' 对家庭住址所属区域(homeArea)做多选处理 Application.Undo oldVal = Target.Value Target.Value = newVal If oldVal = "" Then ' 如果原值为空,直接返回 Else If newVal = "" Then ' 如果新值为空,什么都不做 Else ' 去除重复项 If InStr(1, oldVal, newVal) <> 0 Then ' 如果新值在旧值中已存在 If InStr(1, oldVal, newVal) + Len(newVal) - 1 = Len(oldVal) Then ' 如果是最后一个选项重复,则删除 Target.Value = Left(oldVal, Len(oldVal) - Len(newVal) - 1) Else ' 否则删除逗号后面的重复值 Target.Value = Replace(oldVal, newVal & ",", "") End If Else ' 如果是新选项,则追加 Target.Value = oldVal & "," & newVal End If End If End If End If End If exitHandler: Application.EnableEvents = TrueEnd Sub 这是一个 Excel 工作表的 Worksheet_Change 事件处理程序,主要实现了单元格数据验证的自定义处理逻辑:功能目标:实现单选和多选下拉框的不同处理逻辑具体实现:第3列(性别)和第4列(父母职业)实现单选功能,新值直接替换旧值第5列(所属区域)实现多选功能:允许多个选项,用逗号分隔自动去重(避免重复选择)支持取消选择(点击已选项可移除)使用 Application.Undo 和 Application.EnableEvents 确保操作的原子性和避免事件循环该代码通过 VBA 扩展了 Excel 默认的下拉框功能,使其支持更复杂的业务需求,特别是实现了多选下拉框的去重和动态更新功能。将上述步骤保存,支持动态下拉框的模板文件(.xlsm)就制作完成了。3.6. 导出模板方法 public void exportTemplate(HttpServletResponse response) { Workbook templateWorkbook = null; FileInputStream fileInputStream = null; try { // 设置响应头 response.setContentType("application/vnd.ms-excel.sheet.macroEnabled.12"); response.setCharacterEncoding("utf-8"); String name = "学生数据模板"; response.setHeader("Content-Disposition", "attachment; filename=" + java.net.URLEncoder.encode(name, "UTF-8") + ".xlsm"); // 读取模板文件 File file = new File("D:/学生数据模板.xlsm"); fileInputStream = new FileInputStream(file); templateWorkbook = WorkbookFactory.create(fileInputStream); // 获取数据 - 这三个方法的调用顺序不能变 exchangeSheetUtils.getEntityField(Students.class); Sheet outputSheet = templateWorkbook.getSheetAt(0); templateWorkbook.setSheetName(0, name); exchangeSheetUtils.updateHiddenSheet(outputSheet, templateWorkbook); exchangeSheetUtils.setDataSheet(outputSheet, templateWorkbook); // 输出文件 templateWorkbook.write(response.getOutputStream()); } catch (Exception e) { log.error("导出学生模板失败:"+e.getMessage(), e); throw new RuntimeException("导出模板失败", e); } finally { // 清理 ThreadLocal 资源 exchangeSheetUtils.clearThreadLocals(); // 关闭其他资源 if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) { log.error("关闭文件流失败", e); } } if (templateWorkbook != null) { try { templateWorkbook.close(); } catch (IOException e) { log.error("关闭工作簿失败", e); } } } }这是一个用于导出 Excel 模板文件的方法,其核心功能是:读取预设的 Excel 模板文件(D:/student.xlsm),通过 ExchangeSheetUtils 工具类解析 Students 实体类的注解信息(@ExcelProperty 和 @DropDownSetField),设置下拉框和数据验证,最后将处理好的模板文件(包含表头、下拉框配置和 VBA 代码)以 .xlsm 格式输出到 HTTP 响应流中。整个过程包含了完整的资源管理(使用 try-finally 确保资源正确关闭)和线程安全处理(通过 clearThreadLocals 清理 ThreadLocal 资源)。关键步骤:设置响应头(.xlsm 格式)读取模板文件处理下拉框配置输出文件清理资源3.7 导入数据方法 @Override @Transactional(rollbackFor = Exception.class) public String importData(MultipartFile file) throws IOException { try { final List<Students> studentsList = new ArrayList<>(); // 使用Map方式读取数据 EasyExcel.read(file.getInputStream()) .sheet(0) .headRowNumber(1) // 将表头行设置为1,因为第0行是表头 .registerReadListener(new AnalysisEventListener<Map<Integer, String>>() { @Override public void invoke(Map<Integer, String> data, AnalysisContext context) { log.info("读取到一行数据: {}", JSON.toJSONString(data)); // 手动转换为Students对象,使用正确的key Students student = new Students(); student.setStudentId(data.get(0)); // 学号 student.setName(data.get(1)); // 姓名 student.setGender(data.get(2)); // 性别 student.setParentOccupation(data.get(3)); // 家长职业 // 使用与SimpleStringToListConverter相同的逻辑处理homeArea String areaStr = data.get(4); if (areaStr != null && !areaStr.trim().isEmpty()) { student.setHomeArea(Arrays.asList(areaStr.split(","))); } studentsList.add(student); } @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("所有数据解析完成!共读取到 {} 条数据", studentsList.size()); } }) .doRead(); if (CollectionUtils.isEmpty(studentsList)) { return "Excel中没有数据"; } // 保存数据 this.saveBatch(studentsList); return "导入成功,共导入 " + studentsList.size() + " 条数据"; } catch (Exception e) { log.error("导入失败:", e); throw e; } }这个 importData 函数的主要功能是导入Excel文件中的学生数据。具体流程如下:使用 @Transactional 注解确保数据导入的事务性,如果出现异常会自动回滚创建一个 studentsList 列表用于存储解析后的数据使用 EasyExcel 读取上传的 Excel 文件:读取第一个 sheet(sheet(0))设置表头行号为1(headRowNumber(1))使用 Map 方式读取数据,其中 key 是列索引(0-4),value 是单元格内容在 invoke 方法中处理每一行数据:将 Map 数据手动转换为 Students 对象特别处理 homeArea 字段,将字符串用逗号分割转换为 List最后批量保存数据到数据库(saveBatch)如果过程中出现异常,会记录错误日志并抛出异常触发事务回滚4. 源码地址源码里面有导出模板,导入数据和导出数据三个接口,实现了功能闭环,完整代码:xfc-fdw-cloud: 公共解决方案5. 结语本文介绍了如何通过 FastExcel 实现高效的 Excel 数据导入导出,基于实体类动态设置excel下拉框(支持多选),解决了实际开发中的常见需求。如有疑问,欢迎在评论区留言,我看到都会回复。————————————————原文链接:https://blog.csdn.net/c18213590220/article/details/145184445
-
一、引言在当今数字化时代,Java 后端开发技术占据着举足轻重的地位。从大型企业级应用到小型创业项目,Java 凭借其卓越的性能、强大的生态系统和跨平台特性,成为后端开发的首选语言之一。作为一名在 Java 后端领域摸爬滚打许久的开发者,我深知学习过程中的酸甜苦辣,也积累了不少实用的知识与经验。在这篇博客中,我将毫无保留地与大家分享我的学习总结,涵盖基础语法、常用框架、数据库交互以及项目实战中的宝贵实操经验,同时附上大量实用代码示例,希望能助力各位在 Java 后端开发的道路上少走弯路。二、Java 基础语法夯实数据类型与变量Java 是强类型语言,数据类型分为基本数据类型(如 int、double、char、boolean 等)和引用数据类型(如类、接口、数组)。在定义变量时,务必明确其数据类型,例如:int num = 10;double pi = 3.14;基本数据类型有其默认值,例如 int 默认值为 0,引用数据类型默认值为 null。在变量作用域方面,要注意块级作用域的影响,避免变量重复定义或在不该使用的地方使用。控制结构条件判断语句 if - else 是最常用的结构之一,例如:int score = 85;if (score >= 90) { System.out.println("优秀");} else if (score >= 80 && score < 90) { System.out.println("良好");} else { System.out.println("再接再厉");}循环结构 for、while 和 do - while 各有适用场景。for 循环常用于已知循环次数的情况,如遍历数组:int[] arr = {1, 2, 3, 4, 5};for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]);}while 循环则适用于条件判断在前,只要条件满足就持续执行的场景,注意要避免死循环,确保循环条件能在某个时刻变为 false。数组与集合数组是固定长度的数据结构,定义时需指定长度:int[] numbers = new int[5];numbers[0] = 1;// 遍历数组for (int num : numbers) { System.out.println(num);}集合框架(如 ArrayList、LinkedList、HashSet、HashMap 等)提供了更灵活的存储方式。ArrayList 适合频繁的随机访问,示例代码:ArrayList<String> list = new ArrayList<>();list.add("apple");list.add("banana");// 访问元素System.out.println(list.get(0));HashSet 用于去重,HashMap 用于键值对存储,合理选择集合类型能优化代码性能。三、常用 Java 后端框架探秘Spring 框架Spring 是 Java 后端开发的基石,其核心特性 IoC(控制反转)和 AOP(面向切面编程)极大地简化了开发流程。通过依赖注入,对象的创建和管理交给 Spring 容器,例如:@Servicepublic class UserService { @Autowired private UserRepository userRepository; // 业务逻辑}在配置文件(如 application.properties 或 application.yml)中,可以设置数据库连接、端口等信息,Spring Boot 更是凭借其自动配置功能,让项目快速启动。Spring MVC用于构建 Web 应用,它将请求处理、模型、视图进行分离。定义一个简单的 Controller:@RestController@RequestMapping("/api")public class UserController { @Autowired private UserService userService; @GetMapping("/users") public List<User> getUsers() { return userService.getAllUsers(); }}这里通过注解指定请求路径和方法类型,方便快捷地处理前端发来的 HTTP 请求,返回相应的数据。MyBatis专注于数据库访问层,它通过 XML 配置文件或注解的方式将 SQL 语句与 Java 方法绑定。在 XML 配置中:<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//XML" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.dao.UserDao"> <select id="getUserById" resultType="com.example.entity.User"> SELECT * FROM users WHERE id = #{id} </select></mapper>对应的 Java 接口:@Mapperpublic interface UserDao { User getUserById(int id);}这种方式使得 SQL 编写更加灵活,同时与 Java 代码解耦,方便维护。四、数据库交互实战JDBC(Java Database Connectivity)基础JDBC 是 Java 连接数据库的底层 API,首先需要加载驱动:Class.forName("com.mysql.cj.jdbc.Driver");然后建立连接:Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "password");执行 SQL 语句,例如查询:Statement statement = connection.createStatement();ResultSet resultSet = statement.executeQuery("SELECT * FROM users");while (resultSet.next()) { System.out.println(resultSet.getString("name"));}最后记得关闭连接、语句和结果集,释放资源。使用连接池优化数据库连接频繁创建和关闭数据库连接开销较大,连接池(如 HikariCP、Druid 等)能有效解决这个问题。以 HikariCP 为例,配置连接池:HikariConfig hikariConfig = new HikariConfig();hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");hikariConfig.setUsername("root");hikariConfig.setPassword("password");HikariDataSource dataSource = new HikariDataSource(hikariConfig);从连接池中获取连接:Connection connection = dataSource.getConnection();连接池会自动管理连接的复用、空闲连接回收等,提升性能。数据库事务处理在涉及多个数据库操作且需保证原子性时,事务至关重要。使用 Spring 的事务管理,在 Service 层方法上添加注解:@Transactionalpublic void transferMoney(int fromUserId, int toUserId, double amount) { // 从一个用户账户扣除金额 userRepository.decreaseBalance(fromUserId, amount); // 向另一个用户账户增加金额 userRepository.increaseBalance(toUserId, amount);}若其中一个操作失败,整个事务将回滚,确保数据的一致性。五、项目实操经验大放送项目架构设计在启动一个项目前,合理的架构设计能避免后期的混乱。通常采用分层架构,如表现层(Controller)、业务逻辑层(Service)、数据访问层(Dao)、实体层(Entity)和工具层(Utils)。各层职责明确,相互协作,降低耦合度。考虑扩展性,例如设计接口时预留扩展字段,以便后续功能迭代时能轻松兼容新需求。日志管理引入日志框架(如 Log4j、Logback 等),在关键代码段添加日志记录:private static final Logger logger = LoggerFactory.getLogger(MyClass.class);public void doSomething() { logger.info("开始执行 doSomething 方法"); // 业务逻辑 logger.info("doSomething 方法执行完毕");}通过配置日志级别,可以灵活控制日志输出的详细程度,方便在开发、测试和生产环境中排查问题。异常处理统一的异常处理机制能提升系统的稳定性。在 Spring 项目中,可以定义全局异常处理器:@ControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException(Exception e) { logger.error("系统出现异常:", e); return new ResponseEntity<>("系统繁忙,请稍后再试", HttpStatus.INTERNAL_SERVER_ERROR); }}针对不同类型的异常(如业务异常、数据库异常等),可以提供个性化的错误提示,提升用户体验。六、总结与展望通过对 Java 后端开发技术从基础语法到框架应用,再到数据库交互和项目实战的深入学习,我们逐步构建起坚实的知识体系。然而,技术的发展日新月异,Java 生态也在不断演进。未来,我们需要持续关注新的框架版本、性能优化技巧以及云计算、微服务等新兴领域与 Java 后端的融合。希望这篇学习总结能成为各位读者在 Java 后端之旅中的得力伙伴,让我们一起在代码的世界里不断探索,创造出更强大、更高效的后端应用。————————————————原文链接:https://blog.csdn.net/2301_80350265/article/details/145096011
-
一、泛型概述1. 什么是泛型?为什么要使用泛型?泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错 那参数化类型是什么?以方法的定义为例,在方法定义时,将方法签名中的形参的数据类型也设置为参数(也可称之为类型参数),在调用该方法时再从外部传入一个具体的数据类型和变量。 泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。 //没有泛型的时候,集合如何存储数据//结论://如果我们没有给集合指定类型,默认认为所有的数据类型都是Object类型//此时可以往集合添加任意的数据类型。//带来一个坏处:我们在获取数据的时候,无法使用他的特有行为。 //此时推出了泛型,可以在添加数据的时候就把类型进行统一。//而且我们在获取数据的时候,也省的强转了,非常的方便。2. 泛型使用场景在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。 public class demo1 { public static void main(String[] args) { ArrayList<String> list=new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); for(String s:list){ System.out.println(s); } }}上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码: public static void main(String[] args) { ArrayList list = new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("ccc"); list.add(666); for (int i = 0; i < list.size(); i++) { System.out.println((String)list.get(i)); } } 上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。 使用泛型代码如下: public static void main(String[] args) { ArrayList<String> list = new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("ccc"); //list.add(666);// 在编译阶段,编译器会报错 for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } }< String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)。3.总结泛型的出现就是为了统一集合当中数据类型的 二、泛型类泛型类的定义 尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。 泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下: T :代表一般的任何类。 E :代表 Element 元素的意思,或者 Exception 异常的意思。 K :代表 Key 的意思。 V :代表 Value 的意思,通常与 K 一起配合使用。 S :代表 Subtype 的意思,文章后面部分会讲解示意。 自己实现集合 代码如下: package fangxing; import java.util.Arrays; public class MyArrayList<E> { Object[] obj = new Object[10]; int size = 0; /* E: 表示不确定的类型,该类型在类名后面已经定义过了 e: 形参的名字,变量名 */ public boolean add(E e) { obj[size++] = e; return true; //当添加成功以后,集合还是会把这些数据当做Object类型处理 } public E get(int index) { return (E) obj[index]; //获取的时候集合在把他强转<E>类型 } @Override public String toString() { return Arrays.toString(obj); }}package fangxing; import javax.xml.stream.events.StartDocument; public class demo3 { public static void main(String[] args) { MyArrayList<String> list = new MyArrayList<>(); list.add("aaa"); list.add("bbb"); list.add("ccc"); System.out.println(list); }}三、泛型方法 格式 package fangxing; import java.util.ArrayList;import java.util.Arrays;import java.util.List; public class ListUtil { private ListUtil() { } /* 参数一:集合 参数二: 最后要添加的元素 */ public static <E> void addAll(ArrayList<E> list, E e1, E e2) { list.add(e1); list.add(e2); } } package fangxing; import java.util.ArrayList; public class demo4 { public static void main(String[] args) { ArrayList<String>list=new ArrayList<>(); ListUtil.addAll(list,"zhangsan","lisi"); System.out.println(list);//[zhangsan, lisi] }} 添加很多元素 public static <E> void addAll(ArrayList<E> list, E ...e1) { for (E e : e1) { list.add(e); }四、泛型接口 方法1:实现类给出具体类型 举例: public class MyArrayList2 implements List<String> public static void main(String[] args) { MyArrayList2 list2=new MyArrayList2(); }方法2: 实现类延续泛型,创建对象再确定 public class MyArrayList3 <E> implements List<E> MyArrayList3<String> list = new MyArrayList3<>();五、类型擦除1. 什么是类型擦除 泛型的本质是将数据类型参数化,它通过擦除的方式来实现,即编译器会在编译期间擦除代码中的所有泛型语法并相应的做出一些类型转换动作。 换而言之,泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。也就是说,成功编译过后的 class 文件中不包含任何泛型信息,泛型信息不会进入到运行时阶段。 其实Java中的泛型本质是伪泛型 当把集合定义为string类型的时候,当数据添加在集合当中的时候,仅仅在门口检查了一下数据是否符合String类型, 如果是String类型,就添加成功,当添加成功以后,集合还是会把这些数据当做Object类型处理,当往外获取的时候,集合在把他强转String类型 当代码编译到class文件的时候,泛型就消失,叫泛型的擦除 看一个例子,假如我们给 ArrayList 集合传入两种不同的数据类型,并比较它们的类信息。 public class GenericType { public static void main(String[] args) { ArrayList<String> arrayString = new ArrayList<String>(); ArrayList<Integer> arrayInteger = new ArrayList<Integer>(); System.out.println(arrayString.getClass() == arrayInteger.getClass());// true } }在这个例子中,我们定义了两个 ArrayList 集合,不过一个是 ArrayList< String>,只能存储字符串。一个是 ArrayList< Integer>,只能存储整型对象。我们通过 arrayString 对象和 arrayInteger 对象的 getClass() 方法获取它们的类信息并比较,发现结果为true。 明明我们在 <> 中传入了两种不同的数据类型,那为什么它们的类信息还是相同呢? 这是因为,在编译期间,所有的泛型信息都会被擦除, ArrayList< Integer > 和 ArrayList< String >类型,在编译后都会变成ArrayList< Objec t>类型。 那么是不是所有的类型参数被擦除后都以 Object 类进行替换呢? 答案是否定的,大部分情况下,类型参数 T 被擦除后都会以 Object 类进行替换;而有一种情况则不是,那就是使用到了 extends 和 super 语法的有界类型参数(即泛型通配符,后面我们会详细解释 2. 类型擦除的原理假如我们定义了一个 ArrayList< Integer > 泛型集合,若向该集合中插入 String 类型的对象,不需要运行程序,编译器就会直接报错。这里可能有小伙伴就产生了疑问: 不是说泛型信息在编译的时候就会被擦除掉吗?那既然泛型信息被擦除了,如何保证我们在集合中只添加指定的数据类型的对象呢? 换而言之,我们虽然定义了 ArrayList< Integer > 泛型集合,但其泛型信息最终被擦除后就变成了 ArrayList< Object > 集合,那为什么不允许向其中插入 String 对象呢? Java 是如何解决这个问题的? 其实在创建一个泛型类的对象时, Java 编译器是先检查代码中传入 < T > 的数据类型,并记录下来,然后再对代码进行编译,编译的同时进行类型擦除;如果需要对被擦除了泛型信息的对象进行操作,编译器会自动将对象进行类型转换。 可以把泛型的类型安全检查机制和类型擦除想象成演唱会的验票机制:以 ArrayList< Integer> 泛型集合为例。 当我们在创建一个 ArrayList< Integer > 泛型集合的时候,ArrayList 可以看作是演唱会场馆,而< T >就是场馆的验票系统,Integer 是验票系统设置的门票类型;当验票系统设置好为< Integer >后,只有持有 Integer 门票的人才可以通过验票系统,进入演唱会场馆(集合)中;若是未持有 Integer 门票的人想进场,则验票系统会发出警告(编译器报错)。在通过验票系统时,门票会被收掉(类型擦除),但场馆后台(JVM)会记录下观众信息(泛型信息)。进场后的观众变成了没有门票的普通人(原始数据类型)。但是,在需要查看观众的信息时(操作对象),场馆后台可以找到记录的观众信息(编译器会自动将对象进行类型转换)。 举例如下: public class GenericType { public static void main(String[] args) { ArrayList<Integer> arrayInteger = new ArrayList<Integer>();// 设置验票系统 arrayInteger.add(111);// 观众进场,验票系统验票,门票会被收走(类型擦除) Integer n = arrayInteger.get(0);// 获取观众信息,编译器会进行强制类型转换 System.out.println(n); } }擦除 ArrayList< Integer > 的泛型信息后,get() 方法的返回值将返回 Object 类型,但编译器会自动插入 Integer 的强制类型转换。也就是说,编译器把 get() 方法调用翻译为两条字节码指令: 对原始方法 get() 的调用,返回的是 Object 类型;将返回的 Object 类型强制转换为 Integer 类型; 代码如下: Integer n = arrayInteger.get(0);// 这条代码底层如下: //(1)get() 方法的返回值返回的是 Object 类型Object object = arrayInteger.get(0);//(2)编译器自动插入 Integer 的强制类型转换Integer n = (Integer) object;3. 类型擦除小结 1.泛型信息(包括泛型类、接口、方法)只在代码编译阶段存在,在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类,若有 extends 或者 super 则另外分析); 2.在泛型信息被擦除后,若还需要使用到对象相关的泛型信息,编译器底层会自动进行类型转换(从原始类型转换为未擦除前的数据类型)。 六、泛型通配符1. 泛型的继承泛型不具备继承性,但是数据具备继承性 此时,泛型里面写的什么类型,那么就传递什么类型的数据 泛型不具备继承性举例 package fangxing; import java.util.ArrayList; public class demo5 { public static void main(String[] args) { /* 泛型不具备继承性,但是数据具备继承性 */ ArrayList<Ye> list1=new ArrayList<>(); ArrayList<Fu> list2=new ArrayList<>(); ArrayList<Zi> list3=new ArrayList<>(); //调用method方法 method(list1); //method(list2);//编译错误//method(list3);//编译错误 } /* 此时,泛型里面写的什么类型,那么就传递什么类型的数据 */ public static void method(ArrayList<Ye> list){ }}class Ye{}class Fu extends Ye{}class Zi extends Fu{}数据具备继承性 //数据具备继承性 list1.add(new Ye());//添加爷爷的对象等 list1.add(new Fu()); list1.add(new Zi());定义一个方法,形参是一个集合,但是集合中的数据类型不确定。应用场景:* 1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。* 2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符* 泛型的通配符:* 关键点:可以限定类型的范围。/* * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。 * 弊端: * 利用泛型方法有一个小弊端,此时他可以接受任意的数据类型 * Ye Fu Zi Student * * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi * * 此时我们就可以使用泛型的通配符: * ?也表示不确定的类型 * 他可以进行类型的限定 * ? extends E: 表示可以传递E或者E所有的子类类型 * ? super E:表示可以传递E或者E所有的父类类型 *举例 package fangxing; import java.util.ArrayList; /* * 需求: * 定义一个方法,形参是一个集合,但是集合中的数据类型不确定。 * * *//* * 此时,泛型里面写的是什么类型,那么只能传递什么类型的数据。 * 弊端: * 利用泛型方法有一个小弊端,此时他可以接受任意的数据类型 * Ye Fu Zi Student * * 希望:本方法虽然不确定类型,但是以后我希望只能传递Ye Fu Zi * * 此时我们就可以使用泛型的通配符: * ?也表示不确定的类型 * 他可以进行类型的限定 * ? extends E: 表示可以传递E或者E所有的子类类型 * ? super E:表示可以传递E或者E所有的父类类型 * * 应用场景: * 1.如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。 * 2.如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以泛型的通配符 * 泛型的通配符: * 关键点:可以限定类型的范围。 * * */public class demo6 { public static void main(String[] args) { //创建集合的对象 ArrayList<Ye> list1 = new ArrayList<>(); ArrayList<Fu> list2 = new ArrayList<>(); ArrayList<Zi> list3 = new ArrayList<>(); ArrayList<Student2> list4 = new ArrayList<>(); method(list1); method(list2); //method(list3); //method(list4); } public static void method(ArrayList<? super Fu> list) { }}class Ye {}class Fu extends Ye {}class Zi extends Fu {}class Student2{}2.练习/* 需求: 定义一个继承结构: 动物 | | 猫 狗 | | | | 波斯猫 狸花猫 泰迪 哈士奇 属性:名字,年龄 行为:吃东西 波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干 狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼 泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭 哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家 测试类中定义一个方法用于饲养动物 public static void keepPet(ArrayList<???> list){ //遍历集合,调用动物的eat方法 } 要求1:该方法能养所有品种的猫,但是不能养狗 要求2:该方法能养所有品种的狗,但是不能养猫 要求3:该方法能养所有的动物,但是不能传递其他类型 */测试类 package lx; import java.util.ArrayList; public class demo1 { /* 需求: 定义一个继承结构: 动物 | | 猫 狗 | | | | 波斯猫 狸花猫 泰迪 哈士奇 属性:名字,年龄 行为:吃东西 波斯猫方法体打印:一只叫做XXX的,X岁的波斯猫,正在吃小饼干 狸花猫方法体打印:一只叫做XXX的,X岁的狸花猫,正在吃鱼 泰迪方法体打印:一只叫做XXX的,X岁的泰迪,正在吃骨头,边吃边蹭 哈士奇方法体打印:一只叫做XXX的,X岁的哈士奇,正在吃骨头,边吃边拆家 测试类中定义一个方法用于饲养动物 public static void keepPet(ArrayList<???> list){ //遍历集合,调用动物的eat方法 } 要求1:该方法能养所有品种的猫,但是不能养狗 要求2:该方法能养所有品种的狗,但是不能养猫 要求3:该方法能养所有的动物,但是不能传递其他类型 */ public static void main(String[] args) { HuskyDog h = new HuskyDog("哈士奇", 1); LihuaCat l = new LihuaCat("狸花猫", 2); PersianCat p = new PersianCat("波斯猫", 3); TeddyDog t = new TeddyDog("泰迪", 4); ArrayList<LihuaCat> list1 = new ArrayList<>(); ArrayList<PersianCat> list2 = new ArrayList<>(); // 向列表中添加一些猫的实例 list1.add(l); list2.add(p); //调用方法 keepPet1(list1); keepPet1(list2); System.out.println("-------------------------------------------"); ArrayList<HuskyDog> list3 = new ArrayList<>(); ArrayList<TeddyDog> list4 = new ArrayList<>(); // 向列表中添加一些狗的实例 list3.add(h); list4.add(t); //调用方法 keepPet2(list3); keepPet2(list4); System.out.println("-------------------------------------------"); list1.add(l); list2.add(p); list3.add(h); list4.add(t); keepPet3(list1); keepPet3(list2); keepPet3(list3); keepPet3(list4); } /* 此时我们就可以使用泛型的通配符: ?也表示不确定的类型 他可以进行类型的限定 ? extends E: 表示可以传递E或者E所有的子类类型 ? super E:表示可以传递E或者E所有的父类类型 */ // 要求1:该方法能养所有品种的猫,但是不能养狗 public static void keepPet1(ArrayList<? extends Cat> list) { //遍历集合,调用动物的eat方法 for (Cat cat : list) { cat.eat(); } } // 要求2:该方法能养所有品种的狗,但是不能养猫 public static void keepPet2(ArrayList<? extends Dog> list) { //遍历集合,调用动物的eat方法 for (Dog dog : list) { dog.eat(); } } // 要求3:该方法能养所有的动物,但是不能传递其他类型 public static void keepPet3(ArrayList<? extends Animal> list) { //遍历集合,调用动物的eat方法 for (Animal animal : list) { animal.eat(); } }}Animal类 package lx; public abstract class Animal { private String name; private int age; public Animal() { } public Animal(String name, int age) { this.name = name; this.age = age; } /** * 获取 * @return name */ public String getName() { return name; } /** * 设置 * @param name */ public void setName(String name) { this.name = name; } /** * 获取 * @return age */ public int getAge() { return age; } /** * 设置 * @param age */ public void setAge(int age) { this.age = age; } public String toString() { return "Animal{name = " + name + ", age = " + age + "}"; } public abstract void eat();}cat类型 package lx; public abstract class Cat extends Animal{ public Cat() { } public Cat(String name, int age) { super(name, age); }}Dog类 package lx; public abstract class Dog extends Animal{ public Dog() { } public Dog(String name, int age) { super(name, age); }}哈士奇类 package lx; public class HuskyDog extends Dog{ @Override public void eat() { System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的哈士奇,正在吃骨头,边吃边拆家"); } public HuskyDog() { } public HuskyDog(String name, int age) { super(name, age); }} 狸花猫类 package lx; public class LihuaCat extends Cat { @Override public void eat() { System.out.println("一只叫做" + getName() + "的," + getAge() + "岁的狸花猫,正在吃鱼"); } public LihuaCat() { } public LihuaCat(String name, int age) { super(name, age); }}波斯猫类 package lx; public class PersianCat extends Cat{ @Override public void eat() { System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁的波斯猫,正在吃小饼干"); } public PersianCat() { } public PersianCat(String name, int age) { super(name, age); }}泰迪猫类 package lx; public class TeddyDog extends Dog{ @Override public void eat() { System.out.println("一只叫做"+getName()+"的,"+getAge()+"岁泰迪,正在吃骨头,边吃边蹭"); } public TeddyDog() { } public TeddyDog(String name, int age) { super(name, age); }}———————————————— 原文链接:https://blog.csdn.net/weixin_65752158/article/details/140321808
-
前言本文基于 Java 和 Spring Boot 3,从 0 到 1 完成一个企业级后端项目的开发。依次整合 MySQL 和 Redis,实现基础的增删改查(CRUD)接口,并通过 Spring Security 完成登录认证与接口权限控制,最终构建完整的企业级安全管理框架。 作为开源项目youlai-boot 的入门篇,本文旨在帮助前端开发者或后端初学者快速上手 Java 后端开发。通过一步步实践,掌握项目的核心逻辑与实现细节,不仅能放心使用,还能轻松扩展和二次开发 环境准备本章节介绍安装 Java 开发所需的环境,包括 JDK、Maven 和 IntelliJ IDEA(简称 IDEA),这些工具是 Java 开发的核心环境。 安装 JDKJDK(Java Development Kit) 是 Java 开发工具包,包含编译器、运行时环境等,支持 Java 应用程序的开发与运行。 下载 JDK访问以下链接,下载最新版本的 JDK 安装包:https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe 安装 JDK下载完成后,双击安装包,根据引导完成安装。示例安装路径:D:\Java\jdk-17.0.3.1 配置环境变量打开 系统属性 -> 高级系统设置 -> 环境变量新建系统变量 JAVA_HOME,值为 D:\Java\jdk-17.0.3.1 在 Path 环境变量中,添加 %JAVA_HOME%\bin 验证安装在命令行中执行以下命令,查看 Java 版本: java -version 输出类似如下内容表示安装成功: 安装 MavenMaven 是一个流行的 Java 构建和依赖管理工具,类似于前端的 npm,用于管理项目的构建流程及第三方依赖库。 下载 Maven访问以下链接,下载最新的 bin.zip 文件:https://maven.apache.org/download.cgi 将 bin.zip 解压到本地目录,示例解压路径:D:\Soft\apache-maven-3.9.5 配置阿里云镜像编辑配置文件 D:\Soft\apache-maven-3.9.5\conf\settings.xml,在 <mirrors> 节点中添加以下配置: <mirrors> <mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> </mirror></mirrors> 配置环境变量新建系统变量 M2_HOME,值为 D:\Soft\apache-maven-3.9.5 在 Path 环境变量中,添加 %M2_HOME%\bin 验证安装在命令行中执行以下命令,查看 Maven 版本: mvn -v1输出类似如下内容表示安装成功: 安装 IDEAIntelliJ IDEA 是一款功能强大的 Java 集成开发环境(IDE),由 JetBrains 开发,广泛用于 Java 项目的开发、调试和运行。 下载 IDEA访问以下链接,下载适合您系统的安装包:https://www.jetbrains.com/idea/download 安装 IDEA下载完成后,双击安装包,按引导完成安装即可。 具体的配置在创建项目之前说明。 安装 MySQL安装 MySQL 服务Windows 安装 MySQL 8:https://youlai.blog.csdn.net/article/details/133272887Linux 安装 MySQL 8:https://youlai.blog.csdn.net/article/details/130398179安装 MySQL 可视化工具推荐使用 Navicat,这是一款功能强大的数据库管理工具,但需要付费。如果你因未付费而遇到使用限制,可以选择 DBeaver 作为替代方案。 Navicat 官方下载地址:https://www.navicat.com.cn/download/navicat-premiumDBeaver 官方下载地址:https://dbeaver.io/下载并安装 Navicat 后,你将获得 14 天的免费试用期。安装完成后,连接到 MySQL 服务,即可对数据库和表进行可视化操作,体验非常流畅。 Navicat 界面效果: 安装 Redis安装 Redis 服务Windows 安装 Redis:https://youlai.blog.csdn.net/article/details/133410293Linux 安装 Redis:https://youlai.blog.csdn.net/article/details/130439335安装 Redis 可视化工具推荐使用开源的 AnotherRedisDesktopManager,这是一款功能强大且免费的 Redis 可视化工具。 AnotherRedisDesktopManager 下载地址:https://gitee.com/qishibo/AnotherRedisDesktopManager/releases 安装步骤: 下载安装程序(.exe 文件)。按照安装向导逐步操作,完成安装。使用步骤: 打开软件,点击“新建连接”。输入 Redis 服务器的连接信息(如主机地址、端口、密码等)。连接成功后,即可对 Redis 数据进行可视化操作。AnotherRedisDesktopManager 界面效果: 连接成功示例: 项目搭建新建项目打开 IDEA,选择 Projects → New Project。 项目名称:输入 youlai-boot(可根据实际需求调整)。项目类型:选择 Maven。JDK 版本:选择 JDK 17(确保已安装 JDK 17)。项目信息:Group:填写 com.youlai(可根据实际需求调整)。Artifact:填写 youlai-boot(可根据实际需求调整)。Package name:填写 com.youlai.boot(可根据实际需求调整)。 点击 Next,在左侧的依赖列表中勾选项目所需的依赖。 完成项目初始化后,项目结构如下 配置开发环境配置 JDK通过 File → Project Structure(快捷键 Ctrl + Alt + Shift + S)打开项目结构配置面板,确保 Project 和 Modules 使用的 SDK 版本为前面安装的 JDK 17。 配置 Maven通过 File → Settings(快捷键 Ctrl + Alt + S)打开设置面板,切换到 Maven 选项,并将 Maven 设置为前面安装到本地的版本。 验证配置在 IDEA 的 Terminal 中输入以下命令 mvn -v,验证 Maven 是否正确使用了 JDK 17: 快速开始创建第一个接口在 src/main/java 目录下的 com.youlai.boot 包中,新建一个名为 controller 的包。在 controller 包下创建一个名为 TestController 的 Java 类。在 TestController 类中添加一个简单的 hello-world 接口。 以下是 TestController 类的代码: /** * 测试接口 * * @author youlai */@RestControllerpublic class TestController { @GetMapping("/hello-world") public String test() { return "hello world"; } } 启动测试在项目的右上角,点击 🐞 (手动绿色)图标以调试运行项目。 控制台显示 Tomcat 已在端口 8080 (http) ,表示应用成功启动 打开浏览器,访问 http://localhost:8080/hello-world,页面将显示 hello world,表示接口正常运行。 连接数据库为实现应用与 MySQL 的连接与操作,整合 MyBatis-Plus 可简化数据库操作,减少重复的 CRUD 代码,提升开发效率,实现高效、简洁、可维护的持久层开发。 创建数据库使用 MySQL 可视化工具 (Navicat) 执行下面脚本完成数据库的创建名为 youlai-boot 的数据库,其中包含测试的用户表 -- ---------------------------- -- 1. 创建数据库 -- ---------------------------- CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; -- ---------------------------- -- 2. 创建表 && 数据初始化 -- ---------------------------- use youlai_boot; -- ---------------------------- -- Table structure for sys_user -- ---------------------------- DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(64) NULL DEFAULT NULL COMMENT '用户名', `nickname` varchar(64) NULL DEFAULT NULL COMMENT '昵称', `gender` tinyint(1) NULL DEFAULT 1 COMMENT '性别(1-男 2-女 0-保密)', `password` varchar(100) NULL DEFAULT NULL COMMENT '密码', `dept_id` int NULL DEFAULT NULL COMMENT '部门ID', `avatar` varchar(255) NULL DEFAULT '' COMMENT '用户头像', `mobile` varchar(20) NULL DEFAULT NULL COMMENT '联系方式', `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态(1-正常 0-禁用)', `email` varchar(128) NULL DEFAULT NULL COMMENT '用户邮箱', `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID', `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID', `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Records of sys_user -- ---------------------------- INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, NULL, 0); INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668887', 1, '', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (3, 'websocket', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); 添加依赖项目 pom.xml 添加 MySQL 驱动和 Mybatis-Plus 依赖: <!-- MySQL 8 驱动 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>9.1.0</version><scope>runtime</scope></dependency> <!-- Druid 数据库连接池 --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.2.24</version></dependency> <!-- MyBatis Plus Starter--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version></dependency> 配置数据源将 src/main/resources/application.properties 文件修改为 src/main/resources/application.yml,因为我们更倾向于使用 yml 格式。然后,在 yml 文件中添加以下内容: server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/youlai_boot?useSSL=false&serverTimezone=Asia/Shanghai&&characterEncoding=utf8 username: root password: 123456 mybatis-plus: configuration: # 驼峰命名映射 map-underscore-to-camel-case: true # 打印 sql 日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto # 主键策略 logic-delete-field: is_deleted # 全局逻辑删除字段(可选) 增删改查接口安装 MybatisX 插件在 IDEA 中依次点击 File → Settings(快捷键 Ctrl + Alt + S),打开设置面板,切换到 Plugins 选项卡,搜索 MybatisX 并安装插件。 自动代码生成在 IDEA 右侧导航栏点击 Database,打开数据库配置面板,选择新增数据源。 输入数据库的 主机地址、用户名 和 密码,测试连接成功后点击 OK 保存。 配置完数据源后,展开数据库中的表,右击 sys_user 表,选择 MybatisX-Generator 打开代码生成面板。 设置代码生成的目标路径,并选择 Mybatis-Plus 3 + Lombok 代码风格。 点击 Finish 生成,自动生成相关代码。 MybatisX 生成的代码存在以下问题: SysUserMapper.java 文件未标注 @Mapper 注解,导致无法被 Spring Boot 识别为 Mybatis 的 Mapper 接口。如果已配置 @MapperScan,可以省略此注解,但最简单的方法是直接在 SysUserMapper.java 文件中添加 @Mapper 注解。注意避免导入错误的包。 添加增删改查接口在 controller 包下创建 UserController.java,编写用户管理接口: /** * 用户控制层 * * @author youlai * @since 2024/12/04 */@RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController { private final SysUserService userService; /** * 获取用户列表 */ @GetMapping public List<SysUser> listUsers() { return userService.list(); } /** * 获取用户详情 */ @GetMapping("/{id}") public SysUser getUserById(@PathVariable Long id) { return userService.getById(id); } /** * 新增用户 */ @PostMapping public String createUser(@RequestBody SysUser user) { userService.save(user); return "用户创建成功"; } /** * 更新用户信息 */ @PutMapping("/{id}") public String updateUser(@PathVariable Long id, @RequestBody SysUser user) { userService.updateById(user); return "用户更新成功"; } /** * 删除用户 */ @DeleteMapping("/{id}") public String deleteUser(@PathVariable Long id) { userService.removeById(id); return "用户删除成功"; } } 接口测试重新启动应用,在浏览器中访问 http://localhost:8080/users,查看用户数据。 其他增删改接口可以通过后续整合接口文档进行测试。 集成 Knife4j 接口文档Knife4j 是基于 Swagger2 和 OpenAPI3 的增强解决方案,旨在提供更友好的界面和更多功能扩展,帮助开发者更便捷地调试和测试 API。以下是通过参考 Knife4j 官方文档 Spring Boot 3 整合 Knife4j 实现集成的过程。 添加依赖在 pom.xml 文件中引入 Knife4j 的依赖: <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <version>4.5.0</version></dependency> 配置接口文档在 application.yml 文件中进行配置。注意,packages-to-scan 需要配置为项目的包路径,以确保接口能够被正确扫描,其他配置保持默认即可。 # springdoc-openapi 项目配置springdoc: swagger-ui: path: /swagger-ui.html tags-sorter: alpha operations-sorter: alpha api-docs: path: /v3/api-docs group-configs: - group: 'default' paths-to-match: '/**' packages-to-scan: com.youlai.boot.controller # 需要修改成自己项目的接口包路径# knife4j的增强配置,不需要增强可以不配knife4j: enable: true # 是否为生产环境,true 表示生产环境,接口文档将被禁用 production: false setting: language: zh_cn # 设置文档语言为中文 添加接口文档配置,在 com.youlai.boot.config 添加 OpenApiConfig 接口文档配置 package com.youlai.boot.config; import io.swagger.v3.oas.models.Components;import io.swagger.v3.oas.models.OpenAPI;import io.swagger.v3.oas.models.info.Info;import io.swagger.v3.oas.models.security.SecurityRequirement;import io.swagger.v3.oas.models.security.SecurityScheme;import lombok.RequiredArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springdoc.core.customizers.GlobalOpenApiCustomizer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.env.Environment;import org.springframework.http.HttpHeaders; /** * OpenAPI 接口文档配置 * * @author youlai */@Configuration@RequiredArgsConstructor@Slf4jpublic class OpenApiConfig { private final Environment environment; /** * 接口信息 */ @Bean public OpenAPI openApi() { String appVersion = environment.getProperty("project.version", "1.0.0"); return new OpenAPI() .info(new Info() .title("系统接口文档") .version(appVersion) ) // 配置全局鉴权参数-Authorize .components(new Components() .addSecuritySchemes(HttpHeaders.AUTHORIZATION, new SecurityScheme() .name(HttpHeaders.AUTHORIZATION) .type(SecurityScheme.Type.APIKEY) .in(SecurityScheme.In.HEADER) .scheme("Bearer") .bearerFormat("JWT") ) ); } /** * 全局自定义扩展 * <p> * 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。 * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。 */ @Bean public GlobalOpenApiCustomizer globalOpenApiCustomizer() { return openApi -> { // 全局添加鉴权参数 if (openApi.getPaths() != null) { openApi.getPaths().forEach((s, pathItem) -> { // 登录接口/验证码不需要添加鉴权参数 if ("/api/v1/auth/login".equals(s)) { return; } // 接口添加鉴权参数 pathItem.readOperations() .forEach(operation -> operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) ); }); } }; } } 完善接口文档完善接口描述 在已有的 REST 接口中,使用 OpenAPI 规范注解来描述接口的详细信息,以便通过 Knife4j 生成更加清晰的接口文档。以下是如何为用户的增删改查接口添加文档描述注解的示例: @Tag(name = "用户接口")@RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController { private final SysUserService userService; @Operation(summary = "获取用户列表") @GetMapping public List<SysUser> listUsers() { return userService.list(); } @Operation(summary = "获取用户详情") @GetMapping("/{id}") public SysUser getUserById( @Parameter(description = "用户ID") @PathVariable Long id ) { return userService.getById(id); } @Operation(summary = "新增用户") @PostMapping public String createUser(@RequestBody SysUser user) { userService.save(user); return "新增用户成功"; } @Operation(summary = "修改用户") @PutMapping("/{id}") public String updateUser( @Parameter(description = "用户ID") @PathVariable Long id, @RequestBody SysUser user ) { userService.updateById(user); return "修改用户成功"; } @Operation(summary = "删除用户") @DeleteMapping("/{id}") public String deleteUser( @Parameter(description = "用户ID") @PathVariable Long id ) { userService.removeById(id); return "用户删除成功"; } } 完善实体类描述 在 SysUser 实体类中为每个字段添加 @Schema 注解,用于在接口文档中显示字段的详细说明及示例值: @Schema(description = "用户对象")@TableName(value = "sys_user")@Datapublic class SysUser implements Serializable { @Schema(description = "用户ID", example = "1") @TableId(type = IdType.AUTO) private Integer id; @Schema(description = "用户名", example = "admin") private String username; @Schema(description = "昵称", example = "管理员") private String nickname; @Schema(description = "性别(1-男,2-女,0-保密)", example = "1") private Integer gender; @Schema(description = "用户头像URL", example = "https://example.com/avatar.png") private String avatar; @Schema(description = "联系方式", example = "13800000000") private String mobile; @Schema(description = "用户邮箱", example = "admin@example.com") private String email; // ... } 使用接口文档完成以上步骤后,重新启动应用并访问生成的接口文档。 Swagger UI 文档地址:http://localhost:8080/swagger-ui/index.htmlKnife4j 文档地址:http://localhost:8080/doc.html通过左侧的接口列表查看增删改查接口,并点击具体接口查看详细参数说明及示例值: 接着,可以通过接口文档新增用户,接口返回成功后,可以看到数据库表中新增了一条用户数据: 集成 Redis 缓存Redis 是当前广泛使用的高性能缓存中间件,能够显著提升系统性能,减轻数据库压力,几乎成为现代应用的标配。 添加依赖在 pom.xml 文件中添加 Spring Boot Redis 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency> 配置 Redis 连接在 application.yml 文件中配置 Redis 连接信息: spring: data: redis: database: 0 # Redis 数据库索引 host: localhost # Redis 主机地址 port: 6379 # Redis 端口 # 如果Redis 服务未设置密码,需要将password删掉或注释,而不是设置为空字符串 password: 123456 timeout: 10s 自定义序列化Spring Boot 默认使用 JdkSerializationRedisSerializer 进行序列化。我们可以通过自定义 RedisTemplate,将其修改为更易读的 String 和 JSON 序列化方式: /** * Redis 自动装配配置 * * @author youlai * @since 2024/12/5 */@Configurationpublic class RedisConfig { /** * 自定义 RedisTemplate * <p> * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer * * @param redisConnectionFactory {@link RedisConnectionFactory} * @return {@link RedisTemplate} */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(RedisSerializer.json()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashValueSerializer(RedisSerializer.json()); redisTemplate.afterPropertiesSet(); return redisTemplate; } } 单元测试在 src/test/java 目录的 com.youlai.boot 包下创建 RedisTests 单元测试类,用于验证数据的存储与读取。 @SpringBootTest@Slf4jclass RedisTests { @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private SysUserService userService; @Test void testSetAndGet() { Long userId = 1L; // 1. 从数据库中获取用户信息 SysUser user = userService.getById(userId); log.info("从数据库中获取用户信息: {}", user); // 2. 将用户信息缓存到 Redis redisTemplate.opsForValue().set("user:" + userId, user); // 3. 从 Redis 中获取缓存的用户信息 SysUser cachedUser = (SysUser) redisTemplate.opsForValue().get("user:" + userId); log.info("从 Redis 中获取用户信息: {}", cachedUser); }} 点击测试类方法左侧的图标运行单元测试。 运行后,稍等片刻,若控制台成功打印从 Redis 获取的用户信息,则表示 Spring Boot 已成功集成 Redis。 完善 Web 框架统一响应处理为什么需要统一响应? 默认接口返回的数据结构仅包含业务数据,缺少状态码和提示信息,无法清晰表达操作结果。通过统一封装响应结构,可以提升接口的规范性,便于前后端协同开发和快速定位问题。 下图展示了不规范与规范响应数据的对比:左侧是默认返回的非标准数据,右侧是统一封装后的规范数据。 定义统一业务状态码 在 com.youlai.boot.common.result 包下创建 ResultCode 枚举,错误码规范参考 阿里开发手册-错误码设计。 package com.youlai.boot.common.result; import java.io.Serializable;import lombok.Getter; /** * 统一业务状态码枚举 * * @author youlai */@Getterpublic enum ResultCode implements Serializable { SUCCESS("00000", "操作成功"), TOKEN_INVALID("A0230", "Token 无效或已过期"), ACCESS_UNAUTHORIZED("A0301", "访问未授权"), SYSTEM_ERROR("B0001", "系统错误"); private final String code; private final String message; ResultCode(String code, String message) { this.code = code; this.message = message; }} 创建统一响应结构 定义 Result 类,封装响应码、消息和数据。 package com.youlai.boot.common.result; import lombok.Data;import java.io.Serializable; /** * 统一响应结构 * * @author youlai **/@Datapublic class Result<T> implements Serializable { // 响应码 private String code; // 响应数据 private T data; // 响应信息 private String msg; /** * 成功响应 */ public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(ResultCode.SUCCESS.getCode()); result.setMsg(ResultCode.SUCCESS.getMsg()); result.setData(data); return result; } /** * 失败响应 */ public static <T> Result<T> failed(ResultCode resultCode) { Result<T> result = new Result<>(); result.setCode(resultCode.getCode()); result.setMsg(resultCode.getMsg()); return result; } /** * 失败响应(系统默认错误) */ public static <T> Result<T> failed() { Result<T> result = new Result<>(); result.setCode(ResultCode.SYSTEM_ERROR.getCode()); result.setMsg(ResultCode.SYSTEM_ERROR.getMsg()); return result; } }封装接口返回结果 调整接口代码,返回统一的响应格式。 @Operation(summary = "获取用户详情")@GetMapping("/{id}")public Result<SysUser> getUserById( @Parameter(description = "用户ID") @PathVariable Long id) { SysUser user = userService.getById(id); return Result.success(user);} 效果预览 接口返回结构变为标准格式: 通过以上步骤,接口响应数据已完成统一封装,具备良好的规范性和可维护性,有助于前后端协同开发与错误定位。 全局异常处理为什么需要全局异常处理 如果没有统一的异常处理机制,抛出的业务异常和系统异常会以非标准格式返回,给前端的数据处理和问题排查带来困难。为了规范接口响应数据格式,需要引入全局异常处理。 以下接口模拟了一个业务逻辑中的异常: @Operation(summary = "获取用户详情")@GetMapping("/{id}")public Result<SysUser> getUserById( @Parameter(description = "用户ID") @PathVariable Long id) { // 模拟异常 int i = 1 / 0; SysUser user = userService.getById(id); return Result.success(user);} 当发生异常时,默认返回的数据格式如下所示: 这类非标准的响应格式既不直观,也不利于前后端协作。 全局异常处理器 在 com.youlai.boot.common.exception 包下创建全局异常处理器,用于捕获和处理系统异常。 package com.youlai.boot.common.exception; import com.youlai.boot.common.result.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理器 * * @author youlai */@RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler { /** * 处理系统异常 * <p> * 兜底异常处理,处理未被捕获的异常 */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public <T> Result<T> handleNullPointerException(Exception e) { log.error(e.getMessage(), e); return Result.failed("系统异常:" + e.getMessage()); } } 验证全局异常处理 再次访问用户接口 localhost:8080/users/1 ,可以看到响应已经包含状态码和提示信息,数据格式变得更加规范: 自定义业务异常 在实际开发中,可能需要对特定的业务异常进行处理。通过自定义异常类 BusinessException,可以实现更灵活的异常处理机制。 package com.youlai.boot.common.exception; import com.youlai.boot.common.result.ResultCode;import lombok.Getter; /** * 自定义业务异常 * * @author youlai */@Getterpublic class BusinessException extends RuntimeException { public ResultCode resultCode; public BusinessException(ResultCode errorCode) { super(errorCode.getMsg()); this.resultCode = errorCode; } public BusinessException(String message) { super(message); } } 在全局异常处理器中添加业务异常处理逻辑 @RestControllerAdvice@Slf4jpublic class GlobalExceptionHandler { /** * 处理自定义业务异常 */ @ExceptionHandler(BusinessException.class) public <T> Result<T> handleBusinessException(BusinessException e) { log.error(e.getMessage(), e); if(e.getResultCode()!=null){ return Result.failed(e.getResultCode()); } return Result.failed(e.getMessage()); } } 模拟业务异常 @Operation(summary = "获取用户详情") @GetMapping("/{id}") public Result<SysUser> getUserById( @Parameter(description = "用户ID") @PathVariable Long id ) { SysUser user = userService.getById(-1); // 模拟异常 if (user == null) { throw new BusinessException("用户不存在"); } return Result.success(user); } 请求不存在的用户时,响应如下: 通过全局异常处理的引入和自定义业务异常的定义,接口的响应数据得以标准化,提升了前后端协作的效率和系统的可维护性。 日志输出配置日志作为企业级应用项目中的重要一环,不仅是调试问题的关键手段,更是用户问题排查和争议解决的强有力支持工具 配置 logback-spring.xml 日志文件 在 src/main/resources 目录下,新增 logback-spring.xml 配置文件。基于 Spring Boot “约定优于配置” 的设计理念,项目默认会自动加载并使用该配置文件。 <?xml version="1.0" encoding="UTF-8"?><!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 --><configuration> <!-- SpringBoot默认logback的配置 --> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty scope="context" name="APP_NAME" source="spring.application.name"/> <property name="LOG_HOME" value="/logs/${APP_NAME}"/> <!-- 1. 输出到控制台--> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <!-- <withJansi>true</withJansi>--> <!--此日志appender是为开发使用,只配置最低级别,控制台输出的日志级别是大于或等于此级别的日志信息--> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> <encoder> <Pattern>${CONSOLE_LOG_PATTERN}</Pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 2. 输出到文件 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 当前记录的日志文档完整路径 --> <file>${LOG_HOME}/log.log</file> <encoder> <!--日志文档输出格式--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} -%5level ---[%15.15thread] %-40.40logger{39} : %msg%n%n</pattern> <charset>UTF-8</charset> </encoder> <!-- 日志记录器的滚动策略,按大小和时间记录 --> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!-- 滚动后的日志文件命名模式 --> <fileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 单个日志文件的最大大小 --> <maxFileSize>10MB</maxFileSize> <!-- 最大保留30天的日志 --> <maxHistory>30</maxHistory> <!-- 总日志文件大小不超过3GB --> <totalSizeCap>1GB</totalSizeCap> </rollingPolicy> <!-- 临界值过滤器,输出大于INFO级别日志 --> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>INFO</level> </filter> </appender> <!-- 根日志记录器配置 --> <root level="INFO"> <!-- 引用上面定义的两个appender,日志将同时输出到控制台和文件 --> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root></configuration> 查看日志输出效果 添加配置文件后,启动项目并触发相关日志行为,控制台和日志文件会同时输出日志信息: 集成 Spring SecuritySpring Security 是一个强大的安全框架,可用于身份认证和权限管理。 添加依赖在 pom.xml 添加 Spring Security 依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency> 获取用户认证信息从数据库获取用户信息(用户名、密码、角色),用于和前端输入的用户名密码做判读,如果认证成功,将角色权限信息绑定到用户会话,简单概括就是提供给认证授权的用户信息。 定义用户认证信息类 UserDetails 创建 com.youlai.boot.security.model 包,新建 SysUserDetails 用户认证信息对象,继承 Spring Security 的 UserDetails 接口 /** * Spring Security 用户认证信息对象 * <p> * 封装了用户的基本信息和权限信息,供 Spring Security 进行用户认证与授权。 * 实现了 {@link UserDetails} 接口,提供用户的核心信息。 * * @author youlai */@Data@NoArgsConstructorpublic class SysUserDetails implements UserDetails { /** * 用户ID */ private Integer userId; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 账号是否启用(true:启用,false:禁用) */ private Boolean enabled; /** * 用户角色权限集合 */ private Collection<SimpleGrantedAuthority> authorities; /** * 根据用户认证信息初始化用户详情对象 */ public SysUserDetails(SysUser user) { this.userId = user.getId(); this.username = user.getUsername(); this.password = user.getPassword(); this.enabled = ObjectUtil.equal(user.getStatus(), 1); // 初始化角色权限集合 this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) ? user.getRoles().stream() // 角色名加上前缀 "ROLE_",用于区分角色 (ROLE_ADMIN) 和权限 (sys:user:add) .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) .collect(Collectors.toSet()) : Collections.emptySet(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isEnabled() { return this.enabled; }} 获取用户认证信息服务类 创建 com.youlai.boot.security.service 包,新建 SysUserDetailsService 用户认证信息加载服务类,继承 Spring Security 的 UserDetailsService 接口 /** * 用户认证信息加载服务类 * <p> * 在用户登录时,Spring Security 会自动调用该类的 {@link #loadUserByUsername(String)} 方法, * 获取封装后的用户信息对象 {@link SysUserDetails},用于后续的身份验证和权限管理。 * * @author youlai */@Service@RequiredArgsConstructorpublic class SysUserDetailsService implements UserDetailsService { private final SysUserService userService; /** * 根据用户名加载用户的认证信息 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户基本信息 SysUser user = userService.getOne(new LambdaQueryWrapper<SysUser>() .eq(SysUser::getUsername, username) ); if (user == null) { throw new UsernameNotFoundException(username); } // 模拟设置角色,实际应从数据库获取用户角色信息 Set<String> roles = Set.of("ADMIN"); user.setRoles(roles); // 模拟设置权限,实际应从数据库获取用户权限信息 Set<String> perms = Set.of("sys:user:query"); user.setPerms(perms); // 将数据库中查询到的用户信息封装成 Spring Security 需要的 UserDetails 对象 return new SysUserDetails(user); }} 认证鉴权异常处理在 com.youlai.boot.common.util 添加响应工具类 ResponseUtils @Slf4jpublic class ResponseUtils { /** * 异常消息返回(适用过滤器中处理异常响应) * * @param response HttpServletResponse * @param resultCode 响应结果码 */ public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { // 根据不同的结果码设置HTTP状态 int status = switch (resultCode) { case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); default -> HttpStatus.BAD_REQUEST.value(); }; response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); try (PrintWriter writer = response.getWriter()) { String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); writer.print(jsonResponse); writer.flush(); // 确保将响应内容写入到输出流 } catch (IOException e) { log.error("响应异常处理失败", e); } } } 功能 AuthenticationEntryPoint AccessDeniedHandler对应异常 AuthenticationException AccessDeniedException适用场景 用户未认证(无凭证或凭证无效) 用户已认证但无权限返回 HTTP 状态码 401 Unauthorized 403 Forbidden常见使用位置 用于处理身份认证失败的全局入口逻辑 用于处理权限不足时的逻辑用户未认证处理器 /** * 未认证处理器 * * @author youlai */@Slf4jpublic class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { if (authException instanceof BadCredentialsException) { // 用户名或密码错误 ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR); } else { // token 无效或者 token 过期 ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID); } } } 无权限访问处理器 /** * 无权限访问处理器 * * @author youlai */public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED); } } 注意事项 在全局异常处理器中,认证异常(AuthenticationException)和授权异常(AccessDeniedException)不应被捕获,否则这些异常将无法交给 Spring Security 的异常处理机制进行处理。因此,当捕获到这类异常时,应该将其重新抛出,交给 Spring Security 来处理其特定的逻辑。 public class GlobalExceptionHandler { /** * 处理系统异常 * <p> * 兜底异常处理,处理未被捕获的异常 */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public <T> Result<T> handleNullPointerException(Exception e) throws Exception { // 如果是 Spring Security 的认证异常或授权异常,直接抛出,交由 Spring Security 的异常处理器处理 if (e instanceof AccessDeniedException || e instanceof AuthenticationException) { throw e; } log.error(e.getMessage(), e); return Result.failed("系统异常,请联系管理员"); } } 认证授权配置在 com.youlai.boot.config 包下新建 SecurityConfig 用来 Spring Security 安全配置 /** * Spring Security 安全配置 * * @author youlai */@Configuration@EnableWebSecurity // 启用 Spring Security 的 Web 安全功能,允许配置安全过滤链@EnableMethodSecurity // 启用方法级别的安全控制(如 @PreAuthorize 等)public class SecurityConfig { /** * 忽略认证的 URI 地址 */ private final String[] IGNORE_URIS = {"/api/v1/auth/login"}; /** * 配置安全过滤链,用于定义哪些请求需要认证或授权 */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // 配置认证与授权规则 http .authorizeHttpRequests(requestMatcherRegistry -> requestMatcherRegistry .requestMatchers(IGNORE_URIS).permitAll() // 登录接口无需认证 .anyRequest().authenticated() // 其他请求必须认证 ) // 使用无状态认证,禁用 Session 管理(前后端分离 + JWT) .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) // 禁用 CSRF 防护(前后端分离通过 Token 验证,不需要 CSRF) .csrf(AbstractHttpConfigurer::disable) // 禁用默认的表单登录功能 .formLogin(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证(统一使用 JWT 认证) .httpBasic(AbstractHttpConfigurer::disable) // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中 .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable) ) // 异常处理 .exceptionHandling(configurer -> { configurer .authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未认证处理器 .accessDeniedHandler(new MyAccessDeniedHandler()); // 无权限访问处理器 }); ; return http.build(); } /** * 配置密码加密器 * * @return 密码加密器 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 用于配置不需要认证的 URI 地址 */ @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> { web.ignoring().requestMatchers( "/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html", "/webjars/**", "/doc.html" ); }; } /** *认证管理器 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); }} Token 工具类在 com.youlai.boot.security.manager 包下新建 JwtTokenManager ,用于生成和解析 token /** * JWT Token 管理类 * * @author youlai */@Servicepublic class JwtTokenManager { /** * JWT 密钥,用于签名和解签名 */ private final String secretKey = " SecretKey012345678901234567890123456789012345678901234567890123456789"; /** * 访问令牌有效期(单位:秒), 默认 1 小时 */ private final Integer accessTokenTimeToLive = 3600; /** * 生成 JWT 访问令牌 - 用于登录认证成功后生成 JWT Token * * @param authentication 用户认证信息 * @return JWT 访问令牌 */ public String generateToken(Authentication authentication) { SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); Map<String, Object> payload = new HashMap<>(); // 将用户 ID 放入 JWT 载荷中, 如有其他扩展字段也可以放入 payload.put("userId", userDetails.getUserId()); // 将用户的角色和权限信息放入 JWT 载荷中,例如:["ROLE_ADMIN", "sys:user:query"] Set<String> authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()); payload.put("authorities", authorities); Date now = new Date(); payload.put(JWTPayload.ISSUED_AT, now); // 设置过期时间 -1 表示永不过期 if (accessTokenTimeToLive != -1) { Date expiresAt = DateUtil.offsetSecond(now, accessTokenTimeToLive); payload.put(JWTPayload.EXPIRES_AT, expiresAt); } payload.put(JWTPayload.SUBJECT, authentication.getName()); payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID()); return JWTUtil.createToken(payload, secretKey.getBytes()); } /** * 解析 JWT Token 获取 Authentication 对象 - 用于接口请求时解析 JWT Token 获取用户信息 * * @param token JWT Token * @return Authentication 对象 */ public Authentication parseToken(String token) { JWT jwt = JWTUtil.parseToken(token); JSONObject payloads = jwt.getPayloads(); SysUserDetails userDetails = new SysUserDetails(); userDetails.setUserId(payloads.getInt("userId")); // 用户ID userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 // 角色集合 Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray("authorities") .stream() .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority))) .collect(Collectors.toSet()); return new UsernamePasswordAuthenticationToken(userDetails, "", authorities); } /** * 验证 JWT Token 是否有效 * * @param token JWT Token 不携带 Bearer 前缀 * @return 是否有效 */ public boolean validateToken(String token) { JWT jwt = JWTUtil.parseToken(token); // 检查 Token 是否有效(验签 + 是否过期) return jwt.setKey(secretKey.getBytes()).validate(0); } } 登录认证接口在 com.youlai.boot.controller 包下新建 AuthController /** * 认证控制器 * * @author youlai */@Tag(name = "01.认证中心")@RestController@RequestMapping("/api/v1/auth")@RequiredArgsConstructorpublic class AuthController { // 认证管理器 - 用于执行认证 private final AuthenticationManager authenticationManager; // JWT 令牌服务类 - 用于生成 JWT 令牌 private final JwtTokenManager jwtTokenManager; @Operation(summary = "登录") @PostMapping("/login") public Result<String> login( @Parameter(description = "用户名", example = "admin") @RequestParam String username, @Parameter(description = "密码", example = "123456") @RequestParam String password ) { // 1. 创建用于密码认证的令牌(未认证) UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username.trim(), password); // 2. 执行认证(认证中) Authentication authentication = authenticationManager.authenticate(authenticationToken); // 3. 认证成功后生成 JWT 令牌(已认证) String accessToken = jwtTokenManager.generateToken(authentication); return Result.success(accessToken); }} 访问本地接口文档 http://localhost:8080/doc.html 选择登录接口进行调试发送请求,输入用户名和密码,如果登录成功返回访问令牌 token 访问 https://jwt.io/ 解析返回的 token ,主要分为三部分 Header(头部) 、Payload(负载) 和 Signature(签名) ,其中负载除了固定字段之外,还出现自定义扩展的字段 userId。 访问鉴权我们拿获取用户列表举例,首先需要验证我们在上一步登录拿到的访问令牌 token 是否有效(验签、是否过期等),然后需要校验该用户是否有访问接口的权限,本节就围绕以上问题展开。 验证解析 Token 过滤器 新建 com.youlai.boot.security.filter 添加 JwtValidationFilter 过滤器 用于验证和解析token /** * JWT Token 验证和解析过滤器 * <p> * 负责从请求头中获取 JWT Token,验证其有效性并将用户信息设置到 Spring Security 上下文中。 * 如果 Token 无效或解析失败,直接返回错误响应。 * </p> * * @author youlai */public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String BEARER_PREFIX = "Bearer "; private final JwtTokenManager jwtTokenManager; public JwtAuthenticationFilter(JwtTokenManager jwtTokenManager) { this.jwtTokenManager = jwtTokenManager; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader(HttpHeaders.AUTHORIZATION); try { if (StrUtil.isNotBlank(token) && token.startsWith(BEARER_PREFIX)) { // 去除 Bearer 前缀 token = token.substring(BEARER_PREFIX.length()); // 校验 JWT Token ,包括验签和是否过期 boolean isValidate = jwtTokenService.validateToken(token); if (!isValidate) { writeErrMsg(response, ResultCode.TOKEN_INVALID); return; } // 将 Token 解析为 Authentication 对象,并设置到 Spring Security 上下文中 Authentication authentication = jwtTokenManager.parseToken(token); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { SecurityContextHolder.clearContext(); writeErrMsg(response, ResultCode.TOKEN_INVALID); return; } // 无 Token 或 Token 验证通过时,继续执行过滤链。 // 如果请求不在白名单内(例如登录接口、静态资源等), // 后续的 AuthorizationFilter 会根据配置的权限规则和安全策略进行权限校验。 // 例如: // - 匹配到 permitAll() 的规则会直接放行。 // - 需要认证的请求会校验 SecurityContext 中是否存在有效的 Authentication。 // 若无有效 Authentication 或权限不足,则返回 403 Forbidden。 filterChain.doFilter(request, response); } /** * 异常消息返回 * * @param response HttpServletResponse * @param resultCode 响应结果码 */ public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { int status = switch (resultCode) { case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); default -> HttpStatus.BAD_REQUEST.value(); }; response.setStatus(status); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); try (PrintWriter writer = response.getWriter()) { String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); writer.print(jsonResponse); writer.flush(); } catch (IOException e) { // 日志记录:捕获响应写入失败异常 // LOGGER.error("Error writing response", e); } }} 添加 JWT 验证和解析过滤器 在 SecurityConfig 过滤器链添加 JWT token校验和解析成 Authentication 对象的过滤器。 /** * Spring Security 安全配置 * * @author youlai */@RequiredArgsConstructorpublic class SecurityConfig { // JWT Token 服务 , 用于 Token 的生成、解析、验证等操作 private final JwtTokenManager jwtTokenManager; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http // ... // JWT 验证和解析过滤器 .addFilterBefore(new JwtAuthenticationFilter(jwtTokenManager), UsernamePasswordAuthenticationFilter.class) .build(); } // ...} 获取用户列表接口 @RestController@RequestMapping("/users")@RequiredArgsConstructorpublic class UserController { private final SysUserService userService; @Operation(summary = "获取用户列表") @GetMapping @PreAuthorize("hasAuthority('sys:user:query')") public List<SysUser> listUsers() { return userService.list(); } }访问一个主要测试访问凭据令牌是否认证以及对应的用户是否有访问该接口所需的权限,上面获取用户信息列表的接口未配置在security的白名单中,也就是需要认证,且被 @PreAuthorize(“hasAuthority(‘sys:user:query’)”) 标记说明用户需要有 sys:user:query的权限,也就是所谓的鉴权。 正常访问 不携带 token 访问 携带错误/过期的 token 有访问权限 用户拥有的权限 sys:user:query public class UserController { @Operation(summary = "获取用户列表") @GetMapping @PreAuthorize("hasAuthority('sys:user:query')") // 需要 sys:user:query 权限 public List<SysUser> listUsers() { return userService.list(); } } 无访问权限 用户没有拥有的权限 sys:user:info———————————————— 原文链接:https://blog.csdn.net/u013737132/article/details/145177011
-
理解CookieHTTP协议自身是“无状态”协议,但是在实际开发中,我们很多时候是需要知道请求之间的关联关系的。 上述图中的 "令牌" 通常就存储在 Cookie 字段中.此时在服务器这边就需要记录"令牌"信息, 以及令牌对应的⽤⼾信息, 这个就是 Session 机制所做的⼯作.理解Session我们先来了解⼀下什么是会话.会话: 对话的意思 在计算机领域, 会话是⼀个客⼾与服务器之间的不中断的请求响应. 对客⼾的每个请求,服务器能够识别出请求来⾃于同⼀个客⼾. 当⼀个未知的客⼾向Web应⽤程序发送第⼀个请求时就开始了⼀个会话.当客⼾明确结束会话或服务器在⼀个时限内没有接受到客⼾的任何请求时,会话就结束了.服务器同⼀时刻收到的请求是很多的. 服务器需要清楚的区分每个请求是从属于哪个⽤⼾, 也就是属于哪个会话, 就需要在服务器这边记录每个会话以及与⽤⼾的信息的对应关系.Session是服务器为了保存⽤⼾信息⽽创建的⼀个特殊的对象. Session的本质就是⼀个 "哈希表", 存储了⼀些键值对结构. Key 就是SessionID, Value 就是⽤⼾信息(⽤⼾信息可以根据需求灵活设计). SessionId是由服务器⽣成的⼀个 "唯⼀性字符串", 从 Session 机制的⻆度来看, 这个唯⼀性字符串称为 "SessionId". 但是站在整个登录流程中看待, 也可以把这个唯⼀性字符串称为 "token".上述例⼦中的令牌ID, 就可以看做是SessionId, 只不过令牌除了ID之外, 还会带⼀些其他信息, ⽐如时间, 签名等. 1. 当⽤⼾登陆的时候, 服务器在 Session 中新增⼀个新记录, 并把 sessionId返回给客⼾端. (通过HTTP 响应中的 Set-Cookie 字段返回).2. 客⼾端后续再给服务器发送请求的时候, 需要在请求中带上 sessionId. (通过 HTTP 请求中的Cookie 字段带上).3. 服务器收到请求之后, 根据请求中的 sessionId在 Session 信息中获取到对应的⽤⼾信息, 再进⾏后续操作.找不到则重新创建Session, 并把SessionID返回. Session 默认是保存在内存中的. 如果重启服务器则 Session 数据就会丢失.Cookie和Session的区别• Cookie 是客⼾端保存⽤⼾信息的⼀种机制. Session 是服务器端保存⽤⼾信息的⼀种机制.• Cookie 和 Session之间主要是通过 SessionId 关联起来的, SessionId 是 Cookie 和 Session 之间的桥梁• Cookie 和 Session 经常会在⼀起配合使⽤. 但是不是必须配合.• 完全可以⽤ Cookie 来保存⼀些数据在客⼾端. 这些数据不⼀定是⽤⼾⾝份信息, 也不⼀定是SessionId• Session 中的sessionId 也不需要⾮得通过 Cookie/Set-Cookie 传递, ⽐如通过URL传递.1. 存储位置Cookie:存储在客户端(浏览器)上。当服务器响应一个HTTP请求时,它可以在响应头中包含一个Set-Cookie字段,浏览器会保存这个Cookie,并在后续的请求中通过Cookie请求头将Cookie发送回服务器。Session:存储在服务器端。服务器为每个用户会话创建一个唯一的标识符(通常是Session ID),这个标识符被发送到客户端(通常是通过Cookie,但也可以通过URL重写等方式),客户端在后续的请求中携带这个标识符,服务器通过这个标识符来识别用户会话。2. 安全性Cookie:由于存储在客户端,因此相对容易受到攻击,如跨站脚本攻击(XSS)可以读取或修改Cookie。但是,可以通过设置HttpOnly和Secure标志来增加安全性,HttpOnly标志可以防止JavaScript访问Cookie,Secure标志则要求Cookie仅通过HTTPS发送。Session:存储在服务器端,因此相对更安全。但是,如果Session ID被泄露(例如,通过URL重写并泄露在日志中),则可能面临会话劫持的风险。3. 容量限制Cookie:由于存储在客户端,其大小受到浏览器和服务器设置的限制。大多数浏览器对每个Cookie的大小和每个域名下的Cookie总数都有限制。Session:存储在服务器端,因此其大小限制主要取决于服务器的内存和配置,通常远大于Cookie的限制。4. 生命周期Cookie:可以设置过期时间(Expires/Max-Age),也可以不设置(会话Cookie,浏览器关闭时失效)。Session:通常依赖于服务器端的配置和Session的存储方式(如内存、数据库等)。如果服务器配置了Session的超时时间,则Session在达到超时时间后会被销毁。5. 使用场景Cookie:适用于存储少量数据,如用户偏好设置、登录状态等。由于存储在客户端,可以跨多个页面和请求持久化数据。Session:适用于存储大量数据,如用户信息、购物车内容等。由于存储在服务器端,可以更安全地管理用户会话。获取Cookie首先先设置Cookie 再获取: @RequestMapping("/getC") public String getCookie(HttpServletRequest request){ //获取参数// String name = request.getParameter("name"); Cookie[] cookies = request.getCookies(); if (cookies!=null){ Arrays.stream(cookies).forEach(ck -> System.out.println(ck.getName()+":"+ck.getValue())); } return "获取Cookie"; }更为简洁的代码:@RequestMapping("/getC2")public String getCookie2(@CookieValue("name") String name){ return "从Cookie中获取值, name:"+name;}获取SessionSession是服务器端的机制, 我们需要先存储, 才能再获取.Session 也是基于HttpServletRequest 来存储和获取的.设置Session @RequestMapping("/setSess") public String setSess(HttpServletRequest request){ //从cookie中获取到了sessionID, 根据sessionID获取Session对象, 如果没有获取到, 会创建一个session对象 HttpSession session = request.getSession(); session.setAttribute("name", "zhangsan"); return "设置session成功"; }再获取: @RequestMapping("/getSess") public String getSess(HttpServletRequest request){ //从cookie中获取到了sessionID, 根据session获取Session对象 HttpSession session = request.getSession(); String name = (String)session.getAttribute("name"); return "从session中获取name:"+name; } 获取Session更为简洁的代码: @RequestMapping("/getSess2") public String getSess2(HttpSession session){ String name = (String)session.getAttribute("name"); return "从session中获取name:"+name; } @RequestMapping("/getSess3") public String getSess3(@SessionAttribute("name") String name){ return "从session中获取name:"+name; }获取Header获取User-Agent @RequestMapping("/getHeader") public String getHeader(HttpServletRequest request){ String userAgent = request.getHeader("User-Agent"); return "从header中获取信息, userAgent:"+userAgent; }更为简洁的代码: @RequestMapping("/getHeader2") public String getHeader2(@RequestHeader("User-Agent") String userAgent){ return "从header中获取信息, userAgent:"+userAgent; ———————————————— 原文链接:https://blog.csdn.net/wmh_1234567/article/details/141640649
-
前言相信大家都有去过图书馆吧,那么在借阅图书和归还的时候,都有一个系统来记录并操作这些过程,所以今天就带着大家利用之前的所学知识来简单实现一下图书管理系统的基本逻辑构造一. 图书管理系统的核心图书管理系统的核心包括三个部分:书籍的信息(书本属性)、操作书籍的人(管理员和读者)、 对书籍的操作(借阅、归还)所以在这里我们分别创建3个包 book、operation、user 来实现各个部分的操作: 二. 图书管理系统基本框架2.1 book包我们在book这个包中创建2个类:Book、Booklist,Book用来描述书籍的基本信息,Booklist充当书架,里面用来记录书架中书籍的信息。 2.1.1 Book(书籍类)首先在Book中我们要创建变量来记录书籍的基本信息: private String name; //书名 private String author; //作者 private int price; //价格 private String type; //书籍类型 private boolean isborrow; //是否被借出并且这里的书籍基本信息,我们是用private访问修饰限定符修饰变量的,其他类想要直接访问是访问不了的,所以这里需要我们创建方法间接访问。public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public String getType() { return type; } public void setType(String type) { this.type = type; } public boolean isIsborrow() { return isborrow; } public void setIsborrow(boolean isborrow) { this.isborrow = isborrow; }接下来我们来写Book类的构造方法:那么当我们每次新增一本书的时候 ,该书籍默认就是未借出的状态,默认初始值就是false,所以不需要在构造方法中进行初始化public Book(String name, String author, int price, String type) { //初始化书的属性 this.name = name; this.author = author; this.price = price; this.type = type; }这时候书籍的信息属性都能够被记录了,那么我们想看看这些书籍的信息该怎么办呢?没错打印出来,此时我们需要对 ToString 方法进行重写: 2.1.2 Booklist (书架类)能够记录书籍信息的Book类创建完成后,我们来创建一个书架类(Booklist),书架类的作用就是能够存放每一本书,并且记录每一本书的借阅状态。 private Book [] books;//表示书架可以存放10本书 private int usedbooks;//表示books数组中存放了几本书那么我们依然需要通过间接访问才能访问书架中的基本信息,需要自己提供方法间接访问: public int getUsedbooks() { return usedbooks;//返回有效书本数量 } public void setUsedbooks(int usedbooks) { this.usedbooks = usedbooks;//更改有效书本数量 } public Book getbook(int pos){ return books[pos]; //返回书架中 第pos本书 } public void setBooks(int pos,Book book){ this.books[pos]=book; //更改书架中的书 }那么我们还是写一下Booklist类的构造方法,默认书架中已经存放了书籍:public BookList() { this.books = new Book [10]; //默认创建存放10本书大小的数组 this.books[0]=new Book("三国演义","罗贯中",20,"四大名著"); this.books[1]=new Book("西游记","吴承恩",22,"四大名著"); this.books[2]=new Book("红楼梦","曹雪芹",24,"四大名著"); this.books[3]=new Book("水浒传","施耐庵",26,"四大名著"); this.usedbooks=4;//初始化有效书本数量为4本 } 那么在book这个包的操作就告一段落,后续随着思路的深入再进行补充~2.2 user包在user包中我们新建三个类来描述管理员和用户,那么管理员和用户也是拥有相同属性的,而第三个类User,我们就定义成抽象类,让管理员类与用户类去继承,这样就可以省略一些重复的代码啦2.2.1 User类那么管理员与用户共同拥有的特征就是都有名字,那么我们就在User类中定义一个名字属性的变量,并且通过构造方法初始化 管理员 或 用户 的名称:public abstract class User { public String name; //名字 public User(String name) { this.name = name; //初始化名称 }}2.2.2 Administrator(管理员类)因为我们的管理员类继承了User类,所以需要在构造方法中调用父类的构造方法:public class Administrator extends User{ public Administrator(String name) { super(name);//通过父类初始化管理员名称 }}2.2.3 Visitor(用户类)同理用户类也继承了User类,所以也需要构造方法调用父类的构造方法:public class Visitor extends User{ public Visitor(String name) { super(name); //通过父类初始化用户名称 }}那么书籍的属性和操作者的基本框架我们已经实现了,接下来该实现一下图书管理系统的功能了~2.3 图书管理系统操作菜单管理员菜单1.查找图书2.新增图书3.删除图书4.显示图书0.退出系统用户菜单1.查找图书2.借阅图书3.归还图书0.退出系统接下来,我们就针对这些功能创建一个包,实现这些功能。2.4 operation(操作包)在这个包中我们去实现上述描述的相关操作,接着我们创建一个接口来实现多态,从而降低代码的复杂度operate接口:public interface Operate { //服务接口 void work(BookList books); //功能的实现我们都要利用到书架,所以这里参数给定一个书架对象}接下来我们创建实现各个功能的类: Addbook类:(新增书籍)public class Addbook implements Operate{ @Override public void work(BookList books) { System.out.println("增加书籍..."); }}Borrowbook类:(借阅书籍)public class Borrowbook implements Operate { @Override public void work(BookList books) { System.out.println("借阅书籍...."); }} Deletebook类:(删除书籍)public class Deletebook implements Operate{ @Override public void work(BookList books) { System.out.println("删除书籍..."); }}Exitbook类:(退出系统)public class Exitbook implements Operate{ @Override public void work(BookList books) { System.out.println("退出系统....."); }}Findbook类:(查阅书籍)public class Findbook implements Operate{ @Override public void work(BookList books) { System.out.println("查找书籍...."); ]}Returnbook类:(归还书籍)public class Retuenbook implements Operate{ @Override public void work(BookList books) { System.out.println("归还书籍....");Showbook类:(显示书籍)public class Showbook implements Operate { @Override public void work(BookList books) { System.out.println("显示书籍...."); }}接下来就要慢慢的去将每个类的功能具体的实现,并且将每个类联系起来运作图书管理系统。既然知道了管理员和用户的菜单后,我们就分别给管理员和用户添加一下菜单:在Administrator类添加管理员菜单:public void menu() { System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统"); System.out.println("<<**************管理员操作菜单******************>>"); System.out.println("1. 查找图书 "); System.out.println("2. 新增图书 "); System.out.println("3. 删除图书 "); System.out.println("4. 显示图书"); System.out.println("0. 退出系统 "); System.out.println("<<******************************************>>");}在Visitor类中添加用户菜单:public void menu(){ System.out.println("欢迎 "+this.name+" 读者来到图书管理系统"); System.out.println("<<***************游客读者菜单*************>>"); System.out.println("1. 查找图书 "); System.out.println("2. 借阅图书 "); System.out.println("3. 归还图书 "); System.out.println("0. 退出系统 "); System.out.println("<<**************************************>>");} 那么我们现在创建一个Main类,在类中写上我们的main方法实现一下基本逻辑:public class Main { public static User login(){ //登录系统过程 Scanner scanner=new Scanner(System.in); System.out.println("请输入你的名字:"); String name = scanner.nextLine(); System.out.println("请输入你要登入的帐号:1.管理员登录 -----> 2.游客登录 ----->"); int choice=scanner.nextInt(); if(choice==1){ return new Administrator(name); //如果选择了1,就实例化一个管理员对象,并返回 }else{ return new Visitor(name); //如果选择了2,就实例化一个用户对象,并返回 } //此时的返回值我们不能确定返回的是管理员对象还是用户对象,所以这里用向上转型返回User类型的对象 } public static void main(String[] args) { User user=login(); //进入登录系统 user.menu(); //打印菜单 }} 但是此时login方法的返回对象我们使用User类型的对象进行接收的,此时我们是不能使用user.menu()的,那么该怎么办呢?我们之前将User抽象出来作为管理员类和用户类的父类,那么此时我们在User类中写一个menu()方法就可以实现多态,打印菜单啦~public abstract class User { public String name; //用户名称 public User(String name) { this.name = name; //初始化用户名称 } public abstract int menu(); //抽象方法实现多态,打印菜单}那么此时只需要将子类做出相应的改动,改变成重写的形式就可以了: 这就实现了打印菜单的功能,但是光打印选择不了功能可不行呀,所以我们要在菜单中加上一个选择功能: @Override public int menu() { System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统"); System.out.println("<<**************管理员操作菜单******************>>"); System.out.println("1. 查找图书 "); System.out.println("2. 新增图书 "); System.out.println("3. 删除图书 "); System.out.println("4. 显示图书"); System.out.println("0. 退出系统 "); System.out.println("<<******************************************>>"); //选择功能 Scanner scanner=new Scanner(System.in); System.out.println("请选择您需要的服务:-->>"); int choice=scanner.nextInt(); return choice; }通过输入标号来选择我们想要的服务,那么我们应该返回这个标号,所以menu()方法的返回值也要改成 int 类型,Visitor类的menu()方法也是同理。那么能够选择服务后,我们应该根据对象(管理员或用户)选择该调用哪一个菜单方法:Administrator(管理员类):public Administrator(String name) { super(name);//通过父类初始化管理员名称 this.operate=new Operate[]{ //创建 管理员账户提供的服务 new Exitbook(), new Findbook(), new Addbook(), new Deletebook(), new Showbook() }; }Visitor(用户类):public class Visitor extends User{ public Visitor(String name) { super(name); //通过父类初始化用户名称 this.operate=new Operate[]{ //创建 游客账户提供的服务 new Exitbook(), new Findbook(), new Borrowbook(), new Retuenbook() }; } 那么当我们分别在它们的构造方法中创建一个Operate类型的数组后,里面存放Operate接口实现的方法,那么在new一个管理员对象或者用户对象时,系统就会为这个数组分配内存: 那么我们需要在User类中加上这么一段代码利用动态绑定调用管理员菜单中的方法还是调用用户菜单中的方法: public Operate []operate; //创建服务数组,子类通过数组下标调用 各个服务 public void Dooperate(int choice,BookList books){ this.operate[choice].work(books); //调用 游客/管理员 所选择的服务 }this.operate[ choice ]就是我们new的那个对象中构造方法中下标为 choice 的那个类,而后面的 .work(books)就是调用的对应类的work方法。那么现在我们在main方法中打印菜单的时候就需要有个变量来接收选择的服务啦,再通过new对应的对象调用上面写的 Dooperate方法调用所选择的服务。public static void main(String[] args) { BookList bookList=new BookList(); //实例化书架 User user=login(); //进入登录系统 while(true) { int chioce = user.menu(); //接收选择服务的选项 user.Dooperate(chioce, bookList); //调用 游客/管理员 提供的服务 } }而为了实现用户输入0时才退出系统,所以这里的while循环我们设置为死循环。此时基本框架就搭建完成啦~我们来看看测试效果:那么我们来画图分析一下运行的过程:1. 通过主函数调用login方法: 2. 通过login方法返回的对象调用该对象类中的menu()方法 3. 最后通过调用user.Dooperate方法一步一步实现所选择的服务 三. 实现服务 3.1 显示所有书籍那么在实现显示书籍功能之前,我们应该在Booklist类中写一个方法让Showbook类中的work方法能够通过数组下标打印书籍的信息: public Book getbook(int pos){ return this.books[pos]; //返回书架中 第pos本书 }接下来我们就可以实现显示书籍这个服务啦~在Showbook类中实现: public class Showbook implements Operate { @Override public void work(BookList books) { System.out.println("显示书籍...."); int count=books.getUsedbooks(); for(int i=0;i<count;i++) { //通过循环遍历数组;调用books.getbook(i)方法获取指定的书籍对象 System.out.println(books.getbook(i).toString()); } System.out.println(); }}3.2 查找书籍查找书籍的过程与显示所有书籍相似,也是通过循环遍历数组,利用equal()方法进行对比,实现服务。在Findbook类中实现:public class Findbook implements Operate{ @Override public void work(BookList books) { System.out.println("查找书籍...."); Scanner scanner=new Scanner(System.in); System.out.print("请输入要查找书籍的名字:-->"); String name=scanner.nextLine(); int count=books.getUsedbooks(); for(int i=0;i<count;i++) { Book book=books.getbook(i); if(book.getName().equals(name)){ System.out.println("找到了,书本内容如下:"); System.out.println(book); System.out.println(); return; } } System.out.println("很抱歉,没有查找到你要查找的书籍"); System.out.println("后续会尽快联系管理员添加~"); System.out.println(); }} 3.3 退出系统在Exitbook类中实现:public class Exitbook implements Operate{ @Override public void work(BookList books) { System.out.println("退出系统....."); int count=books.getUsedbooks(); for(int i=0;i<count;i++) { books.setBooks(i,null); // } books.setUsedbooks(0); //将有效书籍数量 制为空 System.exit(0); }}书架中存放的书籍都是对象,那么在退出系统之前我们应该将书籍对象都进行回收,避免发生内存泄漏,并且将书架中的书本数量变成0,最后在结尾加上 System.exit(0); 就可以结束程序,退出系统了。3.4 增加书籍在增加书籍之前,我们应该先判断这个书籍是否已经在书架中存放,如果书架中没有那么就添加到书架中在Addbook类中实现:public class Addbook implements Operate{ @Override public void work(BookList books) { System.out.println("增加书籍..."); int count=books.getUsedbooks(); Scanner scanner=new Scanner(System.in); System.out.print("请输入新添加的书名:--->"); String name=scanner.nextLine(); System.out.print("请输入新添加书籍的作者:--->"); String author=scanner.nextLine(); System.out.print("请输入书籍的价格:--->"); int price=scanner.nextInt(); scanner.nextLine(); System.out.print("请输入书籍的类型:--->"); String type=scanner.nextLine(); for(int i=0;i<count;i++) { if(books.getbook(i).getName().equals(name)){ System.out.println("该书籍已经存在了,不需要添加-----"); System.out.println(); return ; } } Book book=new Book(name,author,price,type); books.setBooks(count,book); System.out.println("添加成功---"); books.setUsedbooks(count+1); System.out.println(); }}那么在Booklist类中我们就要提供一个方法用来新增书籍: public void setBooks(int pos,Book book){ this.books[pos]=book; //更改书架中的书 }3.5 删除书籍在删除书籍中我们有两个点需要注意:当我们删除了某本书籍后,需要将该书籍后面的书籍往前移动,并且将移动完最后的那个空间进行回收,并且更改书架中书籍的数量在Deletebook类中实现:public class Deletebook implements Operate{ @Override public void work(BookList books) { System.out.println("删除书籍..."); System.out.print("请输入你要删除的书籍:--->"); Scanner scanner=new Scanner(System.in); String name =scanner.nextLine(); int count=books.getUsedbooks(); int index=-1; boolean flag=false; for (int i = 0; i <count ; i++) { if(books.getbook(i).getName().equals(name)){ index=i; flag=true; break; } } if(flag==false) { System.out.println("没有找到你要删除的书籍-----"); System.out.println(); return ; } if (books.getbook(index).isIsborrow()==true){ System.out.println("该书本已经被借出,暂时不能进行操作-----"); System.out.println(); return; }else{ for(int j=index;j<count-1;j++) { books.setBooks(j,books.getbook(j+1)); } System.out.println("删除成功------->"); books.setUsedbooks(count-1); books.setBooks(count-1,null); } }}那么在删除书籍的时候为了实现将后面的书籍往前面移动,我们需要在Booklist类中调用setBooks方法来实现这个过程: public void setBooks(int pos,Book book){ this.books[pos]=book; //更改书架中的书 }3.6 借阅书籍借阅书籍的时候我们应该判断该书籍是否在书架上存在,如果存在再判断该书籍是否被借出,如果没有被借出,那么就可以进行借阅在Borrowbook类中实现:ublic class Borrowbook implements Operate { @Override public void work(BookList books) { System.out.println("借阅书籍...."); System.out.print("请输入你要借阅的图书:-->"); Scanner scanner = new Scanner(System.in); String name = scanner.nextLine(); int count = books.getUsedbooks(); for (int i = 0; i < count; i++) { if (books.getbook(i).getName().equals(name)) { if (books.getbook(i).isIsborrow() == false) { books.getbook(i).setIsborrow(true); System.out.println("借阅成功---"); System.out.println(books.getbook(i)); System.out.println(); return; }else{ System.out.println("这本书已经被借出,请等待读者归还后再进行借阅---"); System.out.println(); return ; } } } System.out.println("查找不到你要借阅的书籍,后续会联系管理员尽快上架!!!"); System.out.println(); }}这个时候我们需要在Book类中提供一个方法,返回当前书籍是否被借出的状态,并且还要提供一个方法改变书籍是否被借出的状态,在Book类中实现: public boolean isIsborrow() { return isborrow; } public void setIsborrow(boolean isborrow) { this.isborrow = isborrow; } 3.7 归还书籍在归还之前我们需要判断一下这本书是否在书架上存在过,如果存在再进行判断这本书是否有被借出,如果有被借出则可以归还在Returnbook类中实现: public class Retuenbook implements Operate{ @Override public void work(BookList books) { System.out.println("归还书籍...."); System.out.print("请输入你要归还的图书:-->"); Scanner scanner = new Scanner(System.in); String name = scanner.nextLine(); int count = books.getUsedbooks(); for (int i = 0; i < count; i++) { if (books.getbook(i).getName().equals(name)) { if (books.getbook(i).isIsborrow() == true) { books.getbook(i).setIsborrow(false); System.out.println("归还成功---欢迎再次借阅"); System.out.println(books.getbook(i)); System.out.println(); return; }else{ System.out.println("这本书没有被借出,不需要归还---"); System.out.println(); return ; } } } System.out.println("查找不到你要归还的书籍,你可能是在其他图书馆借阅的书籍---"); System.out.println(); }}如果将书籍归还后我们需要将书籍是否借出的状态进行改变,在Book类中实现:public boolean isIsborrow() { return isborrow; } public void setIsborrow(boolean isborrow) { this.isborrow = isborrow; }四. 总代码Main类:import book.BookList;import user.Administrator;import user.User;import user.VipPerson;import user.Visitor;import java.util.Scanner; public class Main { public static User login(){ //登录系统过程 Scanner scanner=new Scanner(System.in); System.out.println("请输入你的名字:"); String name = scanner.nextLine(); System.out.println("请输入你要登入的帐号:1.管理员登录 -----> 2.游客登录 ----->"); int choice=scanner.nextInt(); if(choice==1){ return new Administrator(name); //如果选择了1,就创建一个管理员对象,并返回 }else if(choice==2){ return new Visitor(name); //如果选择了2,就创建一个游客对象,并返回 }else{ return new VipPerson(name); } //此时的返回值我们不能确定返回的是管理员对象还是用户对象,所以这里用向上转型返回User类型的对象 } public static void main(String[] args) { BookList bookList=new BookList(); //实例化书架 User user=login(); //进入登录系统 while(true) { int chioce = user.menu(); //接收选择服务的选项 user.Dooperate(chioce, bookList); //调用 游客/管理员 提供的服务 } }}User类:package user;import book.BookList;import operation.Operate; public abstract class User { public String name; //用户名称 public Operate []operate; //创建服务数组,子类通过数组下标调用 各个服务 public User(String name) { this.name = name; //初始化用户名称 } public abstract int menu(); //服务菜单 public void Dooperate(int choice,BookList books){ this.operate[choice].work(books); //调用 游客/管理员 所选择的服务 }} Administrator类:package user;import operation.*;import java.util.Scanner;public class Administrator extends User{ public Administrator(String name) { super(name);//通过父类初始化管理员名称 this.operate=new Operate[]{ //创建 管理员账户提供的服务 new Exitbook(), new Findbook(), new Addbook(), new Deletebook(), new Showbook() }; } @Override public int menu() { System.out.println("欢迎 "+this.name+" 管理员来到图书管理系统"); System.out.println("<<**************管理员操作菜单******************>>"); System.out.println("1. 查找图书 "); System.out.println("2. 新增图书 "); System.out.println("3. 删除图书 "); System.out.println("4. 显示图书"); System.out.println("0. 退出系统 "); System.out.println("<<******************************************>>"); //选择功能 Scanner scanner=new Scanner(System.in); System.out.println("请选择您需要的服务:-->>"); int choice=scanner.nextInt(); return choice; }}Visitor类:package user;import operation.*;import java.util.Scanner;public class Visitor extends User{ public Visitor(String name) { super(name); //通过父类初始化用户名称 this.operate=new Operate[]{ //创建 游客账户提供的服务 new Exitbook(), new Findbook(), new Borrowbook(), new Retuenbook() }; } public int menu(){ System.out.println("欢迎 "+this.name+" 读者来到图书管理系统"); System.out.println("<<***************游客读者菜单*************>>"); System.out.println("1. 查找图书 "); System.out.println("2. 借阅图书 "); System.out.println("3. 归还图书 "); System.out.println("0. 退出系统 "); System.out.println("<<**************************************>>"); Scanner scanner=new Scanner(System.in); System.out.println("请选择您需要的服务:-->>"); int choice=scanner.nextInt(); return choice; }}Book类:package book;public class Book { //书的属性 private String name; //书名 private String author; //作者 private int price; //价格 private String type; //书籍类型 private boolean isborrow; //是否被借出 public Book(String name, String author, int price, String type) { //初始化书的属性 this.name = name; this.author = author; this.price = price; this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public String getType() { return type; } public void setType(String type) { this.type = type; } public boolean isIsborrow() { return isborrow; } public void setIsborrow(boolean isborrow) { this.isborrow = isborrow; } @Override public String toString() { return "Book{" + "书名='" + name + '\'' + ", 作者='" + author + '\'' + ", 价格=" + price + ", 类型='" + type + '\'' + (isborrow==true?" ,已经借出":" ,未被借出") + '}'; }}Booklist类:package book;public class BookList { private Book [] books;//表示书架可以存放10本书 private int usedbooks;//表示books数组中存放了几本书 public BookList() { this.books = new Book [10]; //默认创建存放10本书大小的数组 this.books[0]=new Book("三国演义","罗贯中",20,"四大名著"); this.books[1]=new Book("西游记","吴承恩",22,"四大名著"); this.books[2]=new Book("红楼梦","曹雪芹",24,"四大名著"); this.books[3]=new Book("水浒传","施耐庵",26,"四大名著"); this.usedbooks=4;//初始化有效书本数量为4本 } public int getUsedbooks() { return usedbooks;//返回有效书本数量 } public void setUsedbooks(int usedbooks) { this.usedbooks = usedbooks;//更改有效书本数量 } public Book getbook(int pos){ return this.books[pos]; //返回书架中 第pos本书 } public void setBooks(int pos,Book book){ this.books[pos]=book; //更改书架中的书 }} 那么operation这个包中各个类的代码都在 实现服务 那里写啦,这里就不在重新上传了~结语以上就是利用我们过去的所学知识实现的图书管理系统的基本逻辑构造,在此感谢大家的观看!!———————————————— 原文链接:https://blog.csdn.net/2402_86304740/article/details/143254119
-
引言在当今数字化浪潮中,深度学习作为人工智能领域的核心驱动力,正以前所未有的速度改变着我们的生活和工作方式。从图像识别到自然语言处理,从医疗诊断到金融预测,深度学习的应用场景无处不在,展现出巨大的潜力和价值。Java作为一门广泛应用于企业级开发的编程语言,以其稳定性、可移植性和丰富的类库资源,在软件开发领域占据着重要地位。然而,传统的Java开发在面对深度学习复杂的模型构建和大规模数据处理时,往往显得力不从心。DL4J(Deeplearning4j)的出现,为Java开发者打开了一扇通往深度学习世界的大门。DL4J是一个专为Java和Scala设计的深度学习框架,它将深度学习的强大功能与Java的企业级特性完美结合。通过DL4J,Java开发者无需深入掌握复杂的底层数学原理和编程语言,就能够利用Java的生态优势,快速搭建和训练深度学习模型。在过去的一年里,我深入研究和实践了Java DL4J深度学习,积累了丰富的经验和见解。在本文,我们将对这一年在Java DL4J深度学习领域的技术探索进行全面总结,让为我们一起来回顾DL4J的整个概况吧!一、Java DL4J深度学习概述1.1 DL4J框架简介DL4J是基于 Java 和 Scala的分布式深度学习库,它构建在ND4J(一个用于 Java 和 Scala 的数值运算库)之上,提供了丰富的神经网络模型和工具,支持多种深度学习任务,如卷积神经网络(CNN)用于图像识别、循环神经网络(RNN)及其变体(如LSTM、GRU)用于处理序列数据等。DL4J的设计目标是让Java开发者能够像使用传统Java库一样轻松地进行深度学习开发,同时保持高性能和可扩展性。1.2 与其他深度学习框架的比较与Python的TensorFlow和PyTorch等热门深度学习框架相比,DL4J具有独特的优势。首先,在语言层面,Java的静态类型系统和强大的企业级生态使其更适合构建大规模、高可靠性的深度学习应用,尤其在对稳定性和安全性要求较高的企业级场景中。其次,DL4J提供了与Java生态系统的无缝集成,方便与其他Java技术栈(如Spring框架、Hadoop等)结合使用,实现端到端的解决方案。然而,Python的深度学习框架由于其简洁的语法和庞大的社区支持,在快速原型开发和研究领域更为流行。DL4J(Deeplearning4j)则在生产环境部署和企业级应用开发方面展现出明显的优势。1.3 DL4J 的优势1.3.1 与 Java 生态系统的无缝集成由于 DL4J 是用 Java 编写的,它可以与现有的 Java 项目轻松集成,利用 Java 丰富的类库和工具,提高开发效率。1.3.2 分布式计算支持DL4J 支持分布式训练,能够充分利用集群计算资源,加速模型训练过程,适用于大规模数据集的深度学习任务。1.3.3 高度可定制DL4J 提供了丰富的 API,开发者可以根据具体需求灵活定制神经网络结构、优化算法和训练参数,实现个性化的深度学习模型。二、开发环境搭建2.1 安装Java JDK首先,确保系统安装了合适版本的Java JDK(Java Development Kit)。DL4J支持Java 8及以上版本。可以从Oracle官方网站或OpenJDK官网下载并安装相应的JDK。安装完成后,配置系统环境变量JAVA_HOME,指向JDK的安装目录,并将%JAVA_HOME%\bin添加到系统的PATH环境变量中,以便在命令行中能够正确识别java和javac命令。2.2 配置Maven项目Maven是Java项目中常用的构建工具,用于管理项目的依赖和构建过程。创建一个新的Maven项目,可以使用Maven的命令行工具或集成开发环境(IDE)如Eclipse、IntelliJ IDEA等。在项目的pom.xml文件中,需要引入DL4J相关的依赖。<!-- 引入DL4J(deeplearning4j)核心依赖 --><dependency> <groupId>org.deeplearning4j</groupId> <artifactId>deeplearning4j-core</artifactId> <version>1.0.0-beta7</version></dependency><!-- 引入ND4J后端依赖,这里以CPU后端为例。Nd4j 是 DL4J 的底层数值运算库,为 DL4J 提供了高效的矩阵运算支持。 --><dependency> <groupId>org.nd4j</groupId> <artifactId>nd4j-native-platform</artifactId> <version>1.0.0-beta7</version></dependency><!-- 引入数据加载和预处理相关依赖 --><dependency> <groupId>org.deeplearning4j</groupId> <artifactId>deeplearning4j-ui</artifactId> <version>1.0.0-beta7</version></dependency><dependency> <groupId>org.deeplearning4j</groupId> <artifactId>deeplearning4j-datavec</artifactId> <version>1.0.0-beta7</version></dependency>上述代码中:deeplearning4j-core是DL4J的核心库,包含了深度学习模型构建、训练和评估的基本功能。nd4j-native-platform是ND4J的本地平台实现,提供了数值计算的底层支持。这里选择了CPU版本,如果需要使用GPU加速,可以引入相应的GPU版本依赖。deeplearning4j-ui提供了可视化工具,方便监控模型训练过程。deeplearning4j-datavec用于数据加载、预处理和转换,是构建深度学习模型的重要环节。2.3 选择合适的IDE选择一个功能强大的IDE对于开发效率至关重要。IntelliJ IDEA以其丰富的Java开发功能和对Maven项目的良好支持,成为许多Java开发者的首选。在IntelliJ IDEA中,导入创建好的Maven项目,IDE会自动下载并解析pom.xml中定义的依赖。同时,IDE提供了代码自动完成、调试等功能,方便开发者编写和测试DL4J应用程序。三、深度学习基础概念3.1 神经网络神经网络是深度学习的核心概念之一,它模仿人类神经系统的结构和工作方式。一个简单的神经网络由输入层、隐藏层和输出层组成。输入层接收外部数据,隐藏层对数据进行特征提取和转换,输出层根据隐藏层的处理结果产生最终的预测或分类结果。例如,在一个手写数字识别的神经网络中,输入层接收图像的像素值,隐藏层通过一系列的神经元计算提取图像中的特征,如线条、轮廓等,输出层则根据这些特征判断图像中的数字是 0 到 9 中的哪一个。3.2 神经元与激活函数神经元是神经网络的基本计算单元,它接收多个输入信号,并通过加权求和的方式将这些输入信号组合起来,再经过激活函数的处理得到输出。激活函数的作用是为神经网络引入非线性因素,使得神经网络能够学习到复杂的非线性关系。常见的激活函数有 sigmoid 函数、ReLU(Rectified Linear Unit)函数等。sigmoid 函数将输入值映射到 0 到 1 之间,其公式为:σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}}σ(x)= 1+e −xReLU 函数则更为简单,当输入大于 0 时,输出等于输入;当输入小于等于 0 时,输出为 0,其公式为:f ( x ) = max ( 0 , x ) f(x) = \max(0, x)f(x)=max(0,x)3.3 反向传播算法反向传播算法是神经网络训练的核心算法,它用于计算损失函数关于网络参数(权重和偏置)的梯度,以便通过梯度下降等优化算法更新参数,使得损失函数最小化。反向传播算法的基本思想是从输出层开始,根据损失函数计算输出层的误差,然后将误差反向传播到隐藏层,依次计算每个隐藏层的误差,最后根据误差计算梯度并更新参数。四、核心概念与模型构建4.1 神经网络基础神经网络是深度学习的核心概念,它由大量的神经元组成,通过模拟人类大脑的神经元结构和工作方式来处理数据。在DL4J中,神经网络的基本构建块是Layer(层)。常见的层类型包括:输入层(Input Layer):负责接收输入数据,数据以张量(Tensor)的形式传入。例如,对于图像识别任务,输入层可以接收一个三维张量,分别表示图像的高度、宽度和通道数(如RGB图像通道数为3)。// 定义输入层InputLayer inputLayer = new InputLayer.Builder() .nIn(inputSize) .build();上述代码中,inputSize表示输入数据的维度,通过InputLayer.Builder来配置输入层的参数并构建输入层对象。全连接层(Fully Connected Layer):也称为密集层(Dense Layer),层中的每个神经元都与前一层的所有神经元相连。它通过权重矩阵和偏置项对输入数据进行线性变换,然后通过激活函数引入非线性。// 定义全连接层DenseLayer denseLayer = new DenseLayer.Builder() .nIn(inputSize) .nOut(outputSize) .activation("relu") .build();这里,nIn表示输入维度,nOut表示输出维度,activation指定激活函数,如relu(修正线性单元)。激活函数(Activation Function):用于引入非线性,使神经网络能够学习复杂的模式。常见的激活函数有Sigmoid、Tanh、ReLU等。不同的激活函数具有不同的特性和适用场景。例如,ReLU函数在处理大规模数据时具有计算效率高、不易出现梯度消失等优点。4.2 卷积神经网络(CNN)CNN是专门为处理具有网格结构数据(如图像、音频)而设计的神经网络。它通过卷积层、池化层和全连接层的组合来自动提取数据的特征。卷积层(Convolutional Layer):使用卷积核(Filter)对输入数据进行卷积操作,提取局部特征。卷积核在输入数据上滑动,每次滑动计算卷积核与局部数据的点积,得到卷积结果。// 定义卷积层ConvolutionLayer convolutionLayer = new ConvolutionLayer.Builder() .kernelSize(3, 3) .stride(1, 1) .nIn(inputChannels) .nOut(outputChannels) .activation("relu") .build();其中,kernelSize指定卷积核的大小,stride表示卷积核滑动的步长,nIn是输入通道数,nOut是输出通道数。池化层(Pooling Layer):用于对卷积层的输出进行下采样,减少数据维度,同时保留主要特征。常见的池化方法有最大池化(Max Pooling)和平均池化(Average Pooling)。// 定义最大池化层SubsamplingLayer poolingLayer = new SubsamplingLayer.Builder(SubsamplingLayer.PoolingType.MAX) .kernelSize(2, 2) .stride(2, 2) .build();这里选择了最大池化,kernelSize和stride的含义与卷积层类似。4.3 循环神经网络(RNN)及其变体RNN适用于处理序列数据,如时间序列、文本等。它通过引入反馈机制,能够记住过去的信息并用于当前的决策。然而,传统RNN存在梯度消失和梯度爆炸的问题,限制了其在长序列数据处理中的应用。为了解决这些问题,出现了一些RNN的变体,如长短期记忆网络(LSTM)和门控循环单元(GRU)。LSTM:LSTM通过引入记忆单元(Cell)和多个门控机制(输入门、遗忘门、输出门)来有效地控制信息的流动,从而能够处理长序列数据。// 定义LSTM层LSTM.Builder lstmBuilder = new LSTM.Builder() .nIn(inputSize) .nOut(outputSize) .build();GRU:GRU是LSTM的简化版本,它将输入门和遗忘门合并为一个更新门,减少了模型的参数数量,同时在性能上与LSTM相当。// 定义GRU层GRU.Builder gruBuilder = new GRU.Builder() .nIn(inputSize) .nOut(outputSize) .build();五、数据处理与加载5.1 数据预处理在将数据输入到深度学习模型之前,需要进行预处理,以提高模型的训练效果和效率。常见的数据预处理步骤包括:数据归一化(Normalization):将数据的特征值缩放到一定范围内,如[0, 1]或[-1, 1]。这有助于加速模型的收敛和提高泛化能力。在DL4J中,可以使用DataNormalization接口及其实现类进行数据归一化。// 使用MinMaxScaler进行数据归一化MinMaxScaler scaler = new MinMaxScaler(0, 1);scaler.fit(data);INDArray normalizedData = scaler.transform(data);这里,data是输入的数据集,MinMaxScaler将数据缩放到[0, 1]区间。数据标准化(Standardization):将数据的特征值转换为均值为0,标准差为1的分布。这可以通过计算数据的均值和标准差,并对每个特征值进行相应的变换来实现。// 使用StandardScaler进行数据标准化StandardScaler scaler = new StandardScaler();scaler.fit(data);INDArray standardizedData = scaler.transform(data);5.2 数据加载DL4J提供了DataVec库来加载和处理各种格式的数据。对于常见的数据集格式,如CSV、图像文件等,都有相应的加载器。加载CSV数据:可以使用CSVRecordReader来读取CSV文件中的数据。// 创建CSVRecordReaderCSVRecordReader recordReader = new CSVRecordReader();recordReader.initialize(new FileSplit(new File("data.csv")));// 创建DataSetIteratorDataSetIterator iterator = new CSVDataSetIterator(recordReader, batchSize, labelIndex, numClasses);这里,batchSize表示每次加载的数据批次大小,labelIndex是标签所在的列索引,numClasses是分类问题中的类别数。加载图像数据:对于图像数据,可以使用ImageLoader和ImageRecordReader来加载和预处理图像。// 创建ImageRecordReaderImageRecordReader recordReader = new ImageRecordReader(height, width, channels, new LabelsSource() { @Override public List<String> getLabels() { return Arrays.asList("class1", "class2", "class3"); }});recordReader.initialize(new FileSplit(new File("images")));// 创建DataSetIteratorDataSetIterator iterator = new ImageDataSetIterator(recordReader, batchSize, 1, numClasses);其中,height、width和channels分别表示图像的高度、宽度和通道数。六、模型训练与优化6.1 定义损失函数损失函数(Loss Function)用于衡量模型预测结果与真实标签之间的差异,是模型训练的目标函数。常见的损失函数有:均方误差(Mean Squared Error,MSE):适用于回归问题,计算预测值与真实值之间误差的平方的平均值。// 使用均方误差损失函数LossFunction lossFunction = LossFunction.MSE;12交叉熵损失(Cross Entropy Loss):常用于分类问题,衡量两个概率分布之间的差异。在多分类问题中,通常使用Softmax交叉熵损失。// 使用Softmax交叉熵损失函数LossFunction lossFunction = LossFunction.NEGATIVELOGLIKELIHOOD;6.2 选择优化器优化器用于调整模型的参数,以最小化损失函数。DL4J提供了多种优化器,如随机梯度下降(SGD)、Adagrad、Adadelta、Adam等。随机梯度下降(SGD):最基本的优化器,每次迭代使用一个小批量的数据计算梯度并更新参数。// 使用随机梯度下降优化器Optimizer optimizer = new SGD.Builder() .learningRate(0.01) .build();这里,learningRate是学习率,控制每次参数更新的步长。Adam优化器:结合了Adagrad和Adadelta的优点,自适应调整学习率,在许多情况下表现良好。// 使用Adam优化器Optimizer optimizer = new Adam.Builder() .learningRate(0.001) .build();6.3 模型训练在定义好模型结构、损失函数和优化器后,就可以进行模型训练了。训练过程通常包括多个epoch(轮次),在每个epoch中,模型对训练数据进行多次迭代,不断调整参数以降低损失。// 创建MultiLayerNetwork模型MultiLayerNetwork model = new MultiLayerNetwork(new NeuralNetConfiguration.Builder() .list() .layer(0, inputLayer) .layer(1, denseLayer) .layer(2, outputLayer) .build());model.init();// 定义训练配置TrainingConfig trainingConfig = new TrainingConfig.Builder() .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT) .lossFunction(lossFunction) .optimizer(optimizer) .build();// 创建Trainer对象进行训练Trainer trainer = model.trainer(trainingConfig);for (int epoch = 0; epoch < numEpochs; epoch++) { trainer.fit(trainingData);}上述代码中,MultiLayerNetwork是DL4J中用于构建多层神经网络的类,TrainingConfig配置了训练的相关参数,Trainer负责执行训练过程。七、模型评估与调优7.1 模型评估指标在训练完成后,需要对模型的性能进行评估。常见的评估指标有:准确率(Accuracy):分类问题中,预测正确的样本数占总样本数的比例。// 计算准确率Evaluation evaluation = new Evaluation(numClasses);INDArray output = model.output(testData.getFeatures());evaluation.eval(testData.getLabels(), output);System.out.println(evaluation.stats());这里,Evaluation类用于计算各种评估指标,testData是测试数据集。召回率(Recall):在分类问题中,召回率衡量模型正确预测出的正例占所有正例的比例。F1值(F1-Score):F1值是准确率和召回率的调和平均数,综合反映了模型的性能。7.2 超参数调优除了上述方法,还有一些高级的超参数调优技巧。例如,学习率调度(Learning Rate Scheduling)是一种动态调整学习率的策略。在训练初期,较大的学习率有助于模型快速收敛到一个较好的解空间;而在训练后期,较小的学习率可以防止模型在最优解附近振荡,从而提高模型的精度。在 DL4J 中,可以使用 LearningRatePolicy 来实现不同的学习率调度策略。例如,StepDecay 策略会在指定的步数后按一定比例降低学习率:// 每 1000 步将学习率降低为原来的 0.1 倍LearningRatePolicy learningRatePolicy = new StepDecay(1000, 0.1);MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() .learningRate(0.01) .learningRatePolicy(learningRatePolicy) // 其他配置参数 .build();随机搜索和网格搜索虽然有效,但在高维超参数空间中效率较低。而模拟退火(Simulated Annealing)算法则提供了一种在超参数空间中更智能的搜索方式。它基于物理退火过程的思想,在搜索过程中以一定概率接受较差的解,从而避免陷入局部最优。虽然在 DL4J 中没有直接的内置实现,但可以通过自定义搜索算法来结合 DL4J 使用。7.3 模型监控与早期停止为了实时监控模型的训练过程,DL4J 提供了丰富的回调函数(Callback)机制。例如,IterationListener 接口可以用于在每次迭代结束时执行特定的操作,如记录损失值和准确率:public class MyIterationListener implements IterationListener { @Override public void iterationDone(IterationEvent iterationEvent) { int iteration = iterationEvent.getIteration(); double loss = iterationEvent.getNet().calculateScore(); System.out.println("Iteration " + iteration + ": Loss = " + loss); }}// 在训练时添加监听器MultiLayerNetwork network = new MultiLayerNetwork(conf);network.init();network.setListeners(new MyIterationListener());network.fit(trainingData);早期停止机制可以通过 EpochListener 来实现。我们可以记录验证集上的性能,并在性能不再提升时停止训练:public class EarlyStoppingListener implements EpochListener { private int noImprovementCount = 0; private int patience = 10; private double bestValidationScore = Double.MAX_VALUE; @Override public void onEpochEnd(EpochEvent epochEvent) { double validationScore = epochEvent.getNet().calculateScore(validationData); if (validationScore < bestValidationScore) { bestValidationScore = validationScore; noImprovementCount = 0; } else { noImprovementCount++; if (noImprovementCount >= patience) { System.out.println("Early stopping triggered."); epochEvent.getNet().setListeners(new ArrayList<>()); // 停止训练 } } }}// 添加早期停止监听器network.setListeners(new EarlyStoppingListener());network.fit(trainingData);八、模型部署与集成8.1 模型部署到生产环境将训练好的 DL4J 模型部署到生产环境,首先要考虑模型的序列化和反序列化。DL4J 支持将 MultiLayerNetwork 模型保存为二进制文件,以便在不同环境中加载使用。// 保存模型MultiLayerNetwork model = // 训练好的模型try (OutputStream os = new FileOutputStream("model.zip")) { ModelSerializer.writeModel(model, os, true);} catch (IOException e) { e.printStackTrace();}在生产环境中加载模型进行预测:// 加载模型MultiLayerNetwork loadedModel;try (InputStream is = new FileInputStream("model.zip")) { loadedModel = ModelSerializer.restoreMultiLayerNetwork(is);} catch (IOException e) { e.printStackTrace(); return;}// 进行预测INDArray input = Nd4j.create(new double[]{/* 输入数据 */});INDArray output = loadedModel.output(input);对于生产环境中的实时预测服务,我们可以使用 Java 的 Servlet 或更现代化的框架如 Spring Boot 来构建 RESTful API。以下是一个简单的Spring Boot 示例,用于接收输入数据并返回模型预测结果:import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RestController;import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;import org.nd4j.linalg.api.ndarray.INDArray;import org.nd4j.linalg.factory.Nd4j;@SpringBootApplication@RestControllerpublic class ModelDeploymentApplication { private static MultiLayerNetwork loadedModel; static { try (InputStream is = new FileInputStream("model.zip")) { loadedModel = ModelSerializer.restoreMultiLayerNetwork(is); } catch (IOException e) { e.printStackTrace(); } } @PostMapping("/predict") public double[] predict(@RequestBody double[] inputData) { INDArray input = Nd4j.create(inputData); INDArray output = loadedModel.output(input); return output.toDoubleVector(); } public static void main(String[] args) { SpringApplication.run(ModelDeploymentApplication.class, args); }}8.2 与其他系统的集成在实际项目中,深度学习模型通常需要与其他系统进行集成。例如,与企业的数据库系统集成,以获取训练数据或存储预测结果。假设我们使用 MySQL 数据库,使用 JDBC 来读取数据用于模型训练:import java.sql.Connection;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.Statement;public class DatabaseReader { public static INDArray readDataFromDatabase() { try { Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/your_database", "username", "password"); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT * FROM your_table"); int rows = 0; while (resultSet.next()) { rows++; } resultSet.beforeFirst(); int cols = resultSet.getMetaData().getColumnCount(); INDArray data = Nd4j.create(rows, cols); int rowIndex = 0; while (resultSet.next()) { for (int colIndex = 1; colIndex <= cols; colIndex++) { data.putScalar(new int[]{rowIndex, colIndex - 1}, resultSet.getDouble(colIndex)); } rowIndex++; } connection.close(); return data; } catch (Exception e) { e.printStackTrace(); return null; } }}将预测结果存储回数据库:import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;public class DatabaseWriter { public static void writePredictionsToDatabase(double[] predictions) { try { Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/your_database", "username", "password"); String sql = "INSERT INTO prediction_results (prediction) VALUES (?)"; PreparedStatement preparedStatement = connection.prepareStatement(sql); for (double prediction : predictions) { preparedStatement.setDouble(1, prediction); preparedStatement.executeUpdate(); } connection.close(); } catch (Exception e) { e.printStackTrace(); } }}九、年度总结与展望在过去一年对 Java DL4J 深度学习的实践探索中,我们经历了从理论学习到实际项目落地的完整过程。从最初搭建简单的神经网络模型,到通过不断优化和调优构建复杂且高效的深度学习架构,每一步都积累了宝贵的经验。在技术实现方面,我们熟练掌握了 DL4J 的核心 API,能够根据不同的业务需求灵活构建、训练和评估模型。通过模型评估与调优策略,我们显著提升了模型的性能和泛化能力,使其在面对各种实际数据时都能表现出色。然而,实践过程并非一帆风顺。在处理大规模数据时,内存管理和计算资源的优化成为了关键挑战。通过采用分布式计算框架和数据预处理技术,我们有效地缓解了这些问题,但仍需不断探索更高效的解决方案。深度学习领域的快速发展为我们提供了广阔的创新空间。我们计划进一步探索 DL4J 在新兴领域的应用,如强化学习与深度学习的结合,以实现更智能的决策系统。同时,随着硬件技术的持续进步,我们将致力于优化模型在新型硬件设备上的运行效率,充分发挥 GPU、TPU 等加速设备的潜力。此外,模型的可解释性和安全性也将成为重要的研究方向。在实际应用中,尤其是在医疗、金融等关键领域,理解模型的决策过程以及确保数据和模型的安全性至关重要。我们将积极探索相关技术,如特征重要性分析、对抗攻击防御等,以提升模型的可信度和可靠性。通过持续学习和实践,我们坚信能够在 Java DL4J 深度学习领域不断取得新的突破,为解决实际问题提供更强大、更可靠的技术支持,为推动行业发展贡献自己的力量。————————————————原文链接:https://blog.csdn.net/lilinhai548/article/details/145224986
-
引言:亲爱的 Java 和 大数据爱好者们,大家好!在 Java 大数据技术的探索之路上,我们已经搭建起了一套完整且稳固的技术体系。从《Java 大视界 – 基于 Java 的大数据分布式索引技术探秘(50)》中,我们了解到分布式索引技术借助分布式哈希表等精妙设计,实现了海量数据的高效存储与毫秒级检索,为大数据应用筑牢了根基。而在《Java 大视界 – Java 与大数据流式机器学习:理论与实战(51)》里,我们踏入了实时数据处理的前沿,领略到 Java 与流式机器学习融合在金融风险实时监测、工业物联网设备故障预警等场景中的卓越效能。如今,随着数字化进程的迅猛推进,数据已然成为关键资产,数据安全的重要性愈发凸显。Java 大数据安全多方计算技术应运而生,它宛如一把钥匙,开启了跨机构、跨领域安全数据合作的大门,在严守数据隐私的同时深度挖掘数据价值,为大数据时代注入新的活力与机遇。 正文:一、安全多方计算技术基础1.1 安全多方计算的概念与原理安全多方计算(Secure Multi-Party Computation,MPC)作为现代密码学领域的重要成果,其核心在于允许多个参与方在不暴露各自私有数据的情况下,协同完成既定计算任务。这一过程依赖于一系列精妙的密码学协议。以混淆电路(Garbled Circuit)协议为例,它通过对电路进行加密混淆,使得参与方在不知晓其他方输入的情况下完成计算。假设参与方 A 持有数据 x ,参与方 B 持有数据 y ,双方希望计算函数 f(x, y) ,却不想让对方知晓自己的数据。通过混淆电路协议,A 将数据 x 加密后发送给 B,B 在不知 x 具体值的情况下,结合自身数据 y 进行计算,最终得出 f(x, y) 的结果,且 A 和 B 均无法获取对方原始数据。如图 1 所示,清晰展示了混淆电路协议的工作流程:AB使用加密算法(如AES)准备数据x并加密发送加密后的数据x准备数据y结合接收到的加密数据x与自身数据y进行计算(基于混淆电路协议,使用特定计算规则)返回计算结果f(x,y)返回错误信息alt[计算成功][计算失败]AB不经意传输(Oblivious Transfer)协议则保证接收方只能获取特定信息,而发送方无法得知接收方获取的具体内容。在医疗数据查询场景中,医院 A 拥有大量患者病历数据,患者 B 希望查询自己的病历,却不想让医院 A 知道查询的是哪一份。通过不经意传输协议,患者 B 能在不暴露查询目标的情况下获取病历信息,医院 A 也无法知晓患者 B 的查询行为。具体步骤如下表所示:步骤 描述1 发送方准备多个数据项,并利用非对称加密算法对每个数据项进行加密处理,生成密文数据集合。2 接收方生成随机选择信息,例如生成一个随机数作为索引,用于指定要获取的数据项。3 发送方根据接收方的选择信息,以一种特殊方式将加密数据发送给接收方,接收方只能利用自己的私钥解密出自己选择的数据项,而发送方无法得知接收方的选择。1.2 与传统数据处理方式的区别传统数据处理模式常采用集中式架构,将所有数据汇聚到一个中心节点进行处理。这种方式虽便于管理和计算,但存在巨大安全隐患。一旦中心节点遭受攻击,数据泄露风险极高,还可能面临数据合规性难题,如违反《通用数据保护条例》(GDPR)等法规。例如,某知名社交平台曾因数据中心被攻击,导致数亿用户个人信息泄露,引发了严重的信任危机,用户对该平台的信任度大幅下降,平台也面临着巨额的罚款和业务整改。安全多方计算采用分布式计算架构,数据分散存储在各参与方本地,仅在计算时通过加密协议以密文形式交互。整个传输和计算过程如同被层层加密的黑盒,外界难以窥探其中奥秘,极大地增强了数据隐私保护能力。为更直观呈现二者差异,以下通过详细对比表格说明:对比项 传统数据处理方式 安全多方计算数据存储位置 集中于中心节点,易成为攻击目标 分散在各参与方本地,降低整体风险,即使部分节点数据泄露,也不会影响全局数据处理方式 集中计算,依赖中心节点性能,一旦中心节点出现故障,计算任务将中断 分布式计算,各节点协同工作,利用并行处理能力,提高计算效率和容错性数据安全性 中心节点安全漏洞可能导致大规模数据泄露,数据在传输和存储过程中面临较高风险 密文传输与计算,隐私保护机制严密,采用多种加密算法和协议确保数据安全应用场景 适用于数据敏感度低、追求处理效率的简单场景,如一般性的数据分析和报表生成 主要应用于对数据隐私和合规要求极高的复杂场景,如医疗、金融、政务等领域的数据共享与分析二、Java 在安全多方计算中的技术实现2.1 基于 Java 的密码学库应用Java 凭借丰富且强大的密码学库,为安全多方计算提供了坚实技术支撑。Java Cryptography Architecture(JCA)和 Java Cryptography Extension(JCE)是其中的核心组件,涵盖加密、解密、数字签名、密钥管理等全方位密码学功能。以 AES(Advanced Encryption Standard)算法为例,它是一种对称加密算法,广泛应用于数据加密场景。以下是使用 AES 算法结合 GCM(Galois/Counter Mode)模式进行数据加密和解密的 Java 代码示例,GCM 模式不仅提供数据保密性,还具备完整性验证功能:import javax.crypto.Cipher;import javax.crypto.KeyGenerator;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.charset.StandardCharsets;import java.security.SecureRandom;public class AESExample { public static void main(String[] args) throws Exception { // 生成256位AES密钥,密钥长度越长,安全性越高 KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); keyGenerator.init(256); SecretKey secretKey = keyGenerator.generateKey(); // 创建加密器,使用AES/GCM/NoPadding模式 Cipher encryptCipher = Cipher.getInstance("AES/GCM/NoPadding"); // 生成12字节的初始化向量(IV),用于加密过程的随机化 byte[] iv = new byte[12]; SecureRandom secureRandom = new SecureRandom(); secureRandom.nextBytes(iv); // 创建GCM参数规范,设置认证标签长度为128位 GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv); encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); // 待加密的敏感数据 String originalData = "sensitive data"; // 执行加密操作 byte[] encryptedData = encryptCipher.doFinal(originalData.getBytes(StandardCharsets.UTF_8)); // 创建解密器,使用相同的模式和参数 Cipher decryptCipher = Cipher.getInstance("AES/GCM/NoPadding"); decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); // 执行解密操作 byte[] decryptedData = decryptCipher.doFinal(encryptedData); // 将解密后的字节数组转换为字符串 String decryptedString = new String(decryptedData, StandardCharsets.UTF_8); System.out.println("Original Data: " + originalData); System.out.println("Decrypted Data: " + decryptedString); }}为帮助读者更好理解代码,对每一步操作详细解释如下:生成密钥:使用KeyGenerator生成 256 位的 AES 密钥,密钥长度决定加密强度,越长越安全。128 位密钥在面对强大的暴力破解攻击时,可能在较短时间内被破解,而 256 位密钥的破解难度呈指数级增长,大大提高了数据的安全性。根据密码学研究,256 位 AES 密钥在目前的计算能力下,破解时间可能长达数百年甚至更久。创建加密器:选择AES/GCM/NoPadding模式创建加密器,同时生成 12 字节的初始化向量(IV),用于加密过程的随机化,防止相同明文加密后结果相同。IV 就像加密过程中的 “随机种子”,即使相同的明文,在不同 IV 下加密结果也不同,有效增强了加密的安全性。在实际应用中,IV 的随机性和唯一性至关重要,否则可能会被攻击者利用来破解加密数据。执行加密:将待加密的数据转换为字节数组,调用加密器的doFinal方法进行加密,得到加密后的数据。这一步是加密的核心操作,doFinal方法会根据之前设置的密钥、模式和参数,对数据进行加密处理。在加密过程中,数据会被分成多个块进行处理,每个块都会经过复杂的加密运算,最终生成密文。创建解密器:使用相同的模式和参数创建解密器,确保能够正确解密。解密器的配置必须与加密器一致,才能准确还原原始数据。如果模式或参数不一致,解密将无法成功,可能会得到错误的结果或无法解密。执行解密:将加密后的数据传入解密器,得到解密后的字节数组,再转换为字符串。经过解密器处理后,加密数据被还原为原始数据,以字符串形式呈现,方便查看和使用。在解密过程中,解密器会按照加密时的逆过程对密文进行处理,将其还原为原始的明文数据。2.2 分布式计算框架与安全多方计算的融合在大数据时代,数据规模和计算复杂度呈指数级增长,分布式计算框架成为处理海量数据的关键工具。Apache Spark 作为业界领先的分布式计算框架,与安全多方计算的融合为大规模数据的安全处理开辟了新路径。下面通过一个更详细的代码示例,展示如何在 Spark 中初步实现安全多方计算的逻辑。假设我们有两个参与方的数据,分别存储在不同的 RDD 中,我们希望在不暴露原始数据的情况下计算它们的交集(简化的安全多方计算场景)。这里使用 Paillier 同态加密算法(基于 Java 实现的简单版本)来对数据进行加密处理,以保证数据安全传输和计算。首先,引入相关依赖(假设使用 Maven 管理项目):<dependencies> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.68</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.12</artifactId> <version>3.3.1</version> </dependency></dependencies>然后是 Java 代码实现:import org.apache.spark.SparkConf;import org.apache.spark.api.java.JavaRDD;import org.apache.spark.api.java.JavaSparkContext;import org.bouncycastle.crypto.AsymmetricCipherKeyPair;import org.bouncycastle.crypto.generators.PaillierKeyGenerator;import org.bouncycastle.crypto.params.PaillierKeyGenerationParameters;import org.bouncycastle.crypto.params.PaillierPrivateKeyParameters;import org.bouncycastle.crypto.params.PaillierPublicKeyParameters;import org.bouncycastle.math.ec.WNafUtil;import java.math.BigInteger;import java.security.SecureRandom;import java.util.Arrays;import java.util.List;public class SparkMPCExample { public static void main(String[] args) { SparkConf conf = new SparkConf().setAppName("SparkMPCExample").setMaster("local[*]"); JavaSparkContext sc = new JavaSparkContext(conf); // 生成Paillier密钥对 AsymmetricCipherKeyPair keyPair = generatePaillierKeyPair(); PaillierPublicKeyParameters publicKey = (PaillierPublicKeyParameters) keyPair.getPublic(); PaillierPrivateKeyParameters privateKey = (PaillierPrivateKeyParameters) keyPair.getPrivate(); // 模拟参与方1的数据 List<Integer> data1 = Arrays.asList(1, 2, 3, 4, 5); JavaRDD<BigInteger> encryptedData1 = sc.parallelize(data1) .map(num -> encrypt(num, publicKey)); // 模拟参与方2的数据 List<Integer> data2 = Arrays.asList(3, 4, 5, 6, 7); JavaRDD<BigInteger> encryptedData2 = sc.parallelize(data2) .map(num -> encrypt(num, publicKey)); // 模拟安全多方计算求交集(这里只是简单模拟,实际会更复杂) JavaRDD<BigInteger> intersection = encryptedData1.intersection(encryptedData2); // 解密结果(假设只有一方有私钥可以解密) intersection.map(encrypted -> decrypt(encrypted, privateKey)) .collect() .forEach(System.out::println); sc.stop(); } private static AsymmetricCipherKeyPair generatePaillierKeyPair() { PaillierKeyGenerator keyGen = new PaillierKeyGenerator(); keyGen.init(new PaillierKeyGenerationParameters(new SecureRandom(), 1024)); return keyGen.generateKeyPair(); } private static BigInteger encrypt(int num, PaillierPublicKeyParameters publicKey) { BigInteger plaintext = BigInteger.valueOf(num); return publicKey.encrypt(plaintext); } private static int decrypt(BigInteger encrypted, PaillierPrivateKeyParameters privateKey) { BigInteger decrypted = privateKey.decrypt(encrypted); return decrypted.intValue(); }}代码解释如下:初始化 Spark:创建SparkConf和JavaSparkContext,配置应用名称和运行模式。SparkConf用于设置 Spark 应用的各种参数,如应用名称、运行模式等;JavaSparkContext是与 Spark 集群交互的入口,负责创建和管理 RDD 等分布式数据集。在实际应用中,还可以根据集群的资源情况和计算任务的需求,对 SparkConf 进行更多的配置,如设置内存分配、线程数等。生成密钥对:使用PaillierKeyGenerator生成 Paillier 密钥对,包括公钥和私钥。密钥对是安全多方计算的基础,公钥用于加密数据,私钥用于解密数据,确保数据的安全性和隐私性。在生成密钥对时,需要选择合适的密钥长度和随机数生成器,以保证密钥的安全性。数据加密:将参与方 1 和参与方 2 的数据分别转换为JavaRDD,并使用公钥对数据进行加密。JavaRDD是 Spark 中分布式弹性数据集,通过map操作将数据转换为加密后的形式,实现数据在分布式环境下的安全传输。在实际应用中,可能会遇到数据格式不统一、数据量过大等问题,需要进行相应的数据预处理和优化。计算交集:对加密后的数据调用intersection方法,模拟安全多方计算求交集。这一步是在加密数据上进行计算,确保原始数据不被泄露。在实际场景中,计算交集可能会涉及到更复杂的逻辑和算法,需要根据具体需求进行优化。解密结果:使用私钥对交集结果进行解密,并打印输出。只有拥有私钥的一方才能解密出计算结果,保证了结果的安全性和隐私性。在实际应用中,需要妥善保管私钥,防止私钥泄露导致数据安全问题。通过将安全多方计算协议无缝集成到 Spark 的分布式计算流程中,各节点能够在保护数据隐私的前提下协同工作。例如,在电商行业的联合营销分析中,多个电商平台拥有各自的用户浏览、购买、评价等数据。利用 Spark 的分布式计算能力和安全多方计算技术,这些平台可以联合分析用户行为数据,挖掘潜在的市场需求和用户偏好,同时确保各自平台的用户隐私数据不被泄露。具体实现时,可将安全多方计算的加密、解密和计算逻辑封装成 Spark 的自定义算子(Operator),融入 Spark 的 RDD(Resilient Distributed Datasets)或 DataFrame 处理流程中,实现高效、安全的分布式计算。如图 2 所示,展示了安全多方计算与 Spark 融合的架构图:三、安全多方计算的高级应用场景3.1 医疗数据共享与联合研究医疗领域积累了海量的患者数据,这些数据蕴含着巨大的医学价值,但由于患者隐私保护和严格的法规限制,数据共享与联合研究面临重重困难。安全多方计算技术为这一困境提供了破局之道。多家医疗机构可以借助安全多方计算,在不泄露患者个人隐私的前提下,联合开展疾病研究、药物研发等工作。例如,针对罕见病的研究,不同地区的医院可以联合分析患者的基因数据、临床症状、治疗记录等信息,通过安全多方计算挖掘疾病的潜在致病基因和有效的治疗方案。据权威研究表明,采用安全多方计算进行医疗数据联合研究后,疾病研究的效率提高了 30%,新药研发周期平均缩短了 20%,为攻克疑难病症带来了新的希望。在一项针对罕见病的跨国联合研究项目中,来自 5 个国家的 10 家顶级医院参与其中。通过安全多方计算技术,这些医院在不泄露患者隐私的情况下,共享了超过 5000 份患者病例数据。经过联合分析,研究团队成功发现了一种与该罕见病相关的新基因靶点,基于此开发的新型治疗药物已进入临床试验阶段,有望为全球数千名患者带来治愈的可能。从社会人文角度来看,安全多方计算促进医疗数据共享,让更多患者受益于先进医疗研究成果,减少医疗资源分配不均带来的影响,体现了技术对人文关怀的促进作用。例如,偏远地区的患者可以通过安全多方计算参与到国际前沿的医疗研究中,获得更精准的诊断和治疗方案。某偏远地区的医院与国际知名医疗机构合作,利用安全多方计算技术共享患者数据,成功为一位罕见病患者制定了个性化治疗方案,使患者病情得到有效控制,生活质量显著提高。这种技术的应用不仅改善了患者的健康状况,还增强了患者对医疗系统的信任,体现了科技发展对社会公平和人文关怀的积极影响。3.2 金融风控联合建模在金融领域,风险评估和风控建模是保障金融稳定的核心任务,而这需要大量多维度的数据支持。不同金融机构如银行、保险公司、消费金融公司等持有各自客户的信用数据、消费行为数据、资产数据等,但出于数据安全和商业竞争的考虑,难以直接共享数据。安全多方计算技术使得金融机构能够在不泄露客户敏感信息的情况下,联合进行风险评估模型的训练和优化。通过安全多方计算,各机构可以共同分析客户数据,建立更精准的风险评估模型,有效降低金融风险。在实际应用中,某大型金融集团采用安全多方计算进行金融风控联合建模后,风险评估的准确率提高了 15%,不良贷款率降低了 10%,显著提升了金融风险管理水平。该金融集团旗下拥有银行、证券、保险等多个子公司,以往各子公司独立进行风险评估,数据孤立且模型不够精准。引入安全多方计算技术后,各子公司在保护客户隐私的前提下,共享部分数据进行联合建模。例如,银行提供客户的信贷记录,保险公司提供客户的理赔数据,证券子公司提供客户的投资行为数据。通过整合这些多维度数据,新的风险评估模型能够更全面地评估客户风险,为金融决策提供更可靠的依据。随着金融科技的发展,安全多方计算与区块链、人工智能等前沿技术的融合趋势逐渐显现。区块链可用于确保参与方身份验证和数据不可篡改,人工智能则能助力更精准的风险预测,这种跨领域融合为金融行业带来创新变革,提升金融服务的普惠性和安全性。例如,在普惠金融领域,通过安全多方计算和区块链技术,金融机构可以更准确地评估小微企业的信用风险,为其提供更合理的贷款额度和利率,促进小微企业的发展。某地区多家银行和小额贷款公司合作开展普惠金融项目,它们利用安全多方计算技术,整合各自掌握的小微企业的交易流水、纳税记录、社保缴纳等数据,同时借助区块链确保数据来源可靠且不可篡改。经过联合分析,原本被传统金融机构拒之门外的许多小微企业获得了合理的贷款,贷款额度平均提升了 20%,利率降低了 15%,有力地推动了当地小微企业的发展,促进了就业和经济增长。此外,人工智能算法能够对安全多方计算产生的海量金融数据进行深度挖掘,发现潜在的风险模式和市场趋势。例如,利用机器学习算法对客户的消费行为、还款记录等数据进行分析,提前预测客户的违约风险,为金融机构及时采取风险防范措施提供支持。四、安全多方计算面临的挑战与解决方案4.1 性能效率问题安全多方计算由于涉及复杂的密码学计算和频繁的网络通信,计算和通信开销较大,导致性能效率成为制约其广泛应用的瓶颈。为提升性能,可从以下几个方面着手:优化密码学算法:采用更高效的同态加密算法变体,如基于格的同态加密算法,在保证安全性的前提下,大幅减少加密和解密的计算量。基于格的同态加密算法利用格的数学性质,能够在较短的计算时间内完成加密和解密操作,相比传统同态加密算法效率更高。例如,在处理大规模数据时,传统同态加密算法可能需要数小时才能完成加密,而基于格的同态加密算法可以将时间缩短至几十分钟,大大提高了计算效率。研究表明,在处理 10GB 的数据集时,基于格的同态加密算法的加密时间仅为传统算法的 1/5,且在安全性上能够抵御量子计算攻击。分布式计算架构优化:设计更合理的分布式集群架构,利用多节点的并行计算能力,将复杂计算任务分解为多个子任务并行执行,提高整体计算效率。例如,采用主从架构结合分布式缓存技术,减少数据传输次数,提高计算速度。在一个包含 100 个节点的分布式集群中,通过优化架构和使用分布式缓存,数据传输时间减少了 50%,计算任务的完成时间缩短了 30%。通过合理分配计算任务和优化数据存储方式,使各节点能够充分发挥其计算能力,避免出现节点负载不均衡的情况。网络通信优化:采用高速网络通信协议和优化的数据传输策略,减少数据传输延迟和带宽消耗,如使用 UDP 协议进行部分数据传输,并结合数据压缩技术降低传输数据量。在数据传输前,对数据进行压缩处理,可有效减少传输时间和带宽占用。实验表明,对 1GB 的数据进行压缩后再传输,传输时间可缩短 70%,带宽占用降低 80%。选择合适的压缩算法,如 Snappy、Gzip 等,根据数据特点和网络环境进行优化配置,以实现最佳的传输效果。4.2 安全漏洞与攻击防范安全多方计算系统面临着诸多安全威胁,如中间人攻击、恶意参与者攻击、数据泄露攻击等。为有效防范这些攻击,需采取以下措施:强化密码学协议安全性:持续研究和改进密码学协议,修复潜在的安全漏洞,增强协议的抗攻击能力,如对混淆电路协议进行优化,防止电路结构被破解。通过引入随机化机制和多重加密技术,提高混淆电路协议的安全性。例如,在混淆电路协议中加入随机噪声,使得攻击者难以通过分析电路结构获取原始数据。研究人员不断提出新的密码学协议和改进方案,如基于不经意传输扩展的混淆电路协议,进一步提高了协议的安全性和效率。多方认证机制:引入严格的多方认证机制,确保参与计算的各方身份真实可靠,防止恶意节点混入。可采用基于数字证书的认证方式,结合区块链技术实现身份信息的不可篡改和可追溯。区块链的分布式账本特性,能将各方身份信息以加密形式存储在多个节点,确保数据的真实性和完整性。一旦身份信息被篡改,区块链的共识机制会立即检测到异常,保障计算环境的安全可靠。例如,在一个多方参与的医疗数据共享项目中,通过基于区块链的数字证书认证,成功阻止了一次恶意节点试图冒充医疗机构参与计算的攻击。利用区块链的智能合约技术,实现自动化的身份验证和权限管理,提高认证的效率和安全性。审计跟踪技术:建立完善的审计跟踪系统,记录计算过程中的关键操作和数据流向,便于及时发现和追溯潜在的安全问题。系统可以记录每次数据加密、传输、计算以及解密的时间、参与方、操作类型等关键信息。通过对审计日志的实时分析,能够快速检测到异常行为。例如,当监测到某个参与方在短时间内发起大量不合理的计算请求,或者数据传输量远超正常范围时,系统可自动触发警报,安全团队能迅速介入,对该参与方进行进一步审查,查明异常原因,采取相应的防范措施,如暂时中断该参与方的计算任务,对其身份和操作进行详细核实。采用大数据分析技术对审计日志进行深度挖掘,发现潜在的安全威胁和异常模式,提前预警并防范安全风险。五、技术发展的多维洞察5.1 前沿技术趋势下的安全多方计算在科技飞速发展的当下,量子计算技术的崛起给传统密码学带来了前所未有的挑战,安全多方计算自然也无法置身事外。基于量子 - resistant 密码学的安全多方计算协议,正成为学术界和产业界共同关注的焦点。以格密码为例,它基于复杂的格理论构建,独特的数学结构赋予其卓越的抗量子攻击特性。在未来量子计算普及的时代,格密码有望成为安全多方计算的中流砥柱,确保数据在计算和传输过程中的安全性。目前,许多科研团队正在研究基于格密码的安全多方计算协议的优化与应用拓展,力求在保障安全性的同时,提升计算效率和实用性。例如,某知名科研机构的研究团队成功将基于格密码的安全多方计算协议应用于金融数据的跨境传输与计算,在保证数据安全的同时,实现了比传统协议快 2 倍的计算速度。该研究成果为金融机构在全球化业务中保护数据安全提供了新的解决方案,降低了计算成本和时间成本。与此同时,联邦学习与安全多方计算的融合也在不断深化。联邦学习允许多个参与方在不直接共享原始数据的情况下协同训练模型,而安全多方计算则为这种协作提供了坚实的数据隐私保护屏障。在医疗领域,多家医院可以利用联邦学习与安全多方计算的结合,共同训练疾病诊断模型,在保护患者隐私的同时,提升模型的准确性和泛化能力。通过整合不同医院的病例数据进行联合建模,模型能够学习到更广泛的疾病特征,从而提高诊断的准确性,为患者提供更可靠的医疗服务。据某医学研究报告显示,采用联邦学习与安全多方计算结合的方式训练的疾病诊断模型,准确率比单一医院训练的模型提高了 12%。在实际应用中,通过联邦学习与安全多方计算的融合,能够整合不同地区、不同医院的医疗数据,解决数据孤岛问题,提高医疗资源的利用效率。5.2 跨领域融合驱动的创新变革安全多方计算与区块链的融合,为数据处理带来了全新的信任机制。区块链的去中心化特性确保了计算过程不受单一节点控制,不可篡改的账本则保证了数据和计算结果的真实性与可追溯性。在政务数据共享中,各部门可以通过这种融合技术,安全地共享和协同处理数据,提高政务服务的效率和透明度。不同政府部门之间共享公民的社保、税务、医疗等数据时,利用安全多方计算保护公民隐私,同时借助区块链确保数据的准确性和完整性,避免数据被恶意篡改,提升政府部门间的协作效率,为公民提供更便捷的一站式政务服务。例如,某城市通过采用安全多方计算与区块链融合技术,实现了社保、医保、民政等部门的数据共享,办理社保相关业务的时间从原来的平均 7 个工作日缩短至 3 个工作日。该城市的市民在办理社保转移、医保报销等业务时,不再需要在多个部门之间来回奔波提交材料,只需在一个平台上即可完成所有业务办理,大大提高了政务服务的便捷性和满意度。安全多方计算与人工智能的融合同样展现出巨大潜力。人工智能算法能够对安全多方计算产生的海量数据进行深度挖掘,提取其中的潜在价值。例如,在智能安防领域,通过安全多方计算共享监控数据,利用人工智能进行图像识别和行为分析,既能保障数据隐私,又能提高安防系统的智能化水平。将不同区域的监控数据在安全多方计算的框架下进行整合分析,人工智能算法可以实时监测异常行为,如人员聚集、异常闯入等,及时发出警报,有效提升公共安全保障能力。在某大型活动安保项目中,采用安全多方计算与人工智能融合技术,成功预警并处理了多起潜在的安全事件,保障了活动的顺利进行。通过人工智能算法对监控数据进行实时分析,能够快速准确地识别出异常行为,为安保人员提供及时的决策支持,提高安保工作的效率和效果。5.3 技术演进对社会人文的深远影响安全多方计算技术的发展,深刻地改变着社会的隐私观念和数据治理模式。在个人层面,它为人们参与线上服务提供了更可靠的隐私保护。当个人在进行在线医疗咨询、金融交易等活动时,涉及敏感信息的数据通过安全多方计算进行处理,大大降低了隐私泄露的风险,增强了人们对数字社会的信任。用户在进行线上医疗问诊时,个人的病历、症状等敏感信息在加密状态下传输和处理,医生能够获取必要的诊断信息,却无法获取患者的其他隐私细节,保障了患者的隐私安全,让患者更放心地使用在线医疗服务。据某市场调研机构的调查显示,在采用安全多方计算技术处理数据的在线医疗平台上,用户满意度提升了 25%。这表明安全多方计算技术不仅保护了用户的隐私,还提升了用户体验,促进了在线医疗服务的发展。从全球视角来看,安全多方计算促进了数据的合法合规流通,打破了数据孤岛。不同国家和地区的科研机构可以借助这一技术共享科研数据,加速科研进展,推动人类社会在医学、环保、能源等多个领域的共同进步。在医学研究中,跨国合作研究罕见病时,各国科研机构可以通过安全多方计算共享患者数据和研究成果,共同探索疾病的治疗方法,缩短研发周期,为全球患者带来福音。在商业领域,它也为企业间的合作创新提供了可能,促进了全球商业生态的繁荣与发展 ,体现了技术对社会公平和创新活力的积极推动作用。例如,某国际科研合作项目通过安全多方计算技术,整合了来自 5 个国家的科研数据,成功研发出一种新型药物,为全球患者带来了新的治疗选择。该项目的成功不仅展示了安全多方计算技术在科研合作中的重要作用,还促进了不同国家科研机构之间的交流与合作,推动了全球医学研究的发展。结束语:亲爱的 Java 和 大数据爱好者们,通过对 Java 大数据安全多方计算的深度剖析,我们全面掌握了其核心技术、Java 实现方法、丰富的应用场景、面临的挑战及解决方案,以及技术发展的多维洞察。安全多方计算作为大数据时代数据安全的坚固盾牌,有力地推动了跨领域数据合作与创新发展。然而,随着技术的不断演进和应用场景的日益复杂,我们仍需持续探索和创新,不断优化技术性能,强化安全防护体系。————————————————原文链接:https://blog.csdn.net/atgfg/article/details/145233339
-
大家好,2025年开年的第一篇合集,本次带来的是Python,Java,MySql,Golang,JSON,等等希望可以帮到大家。1.Python判断for循环最后一次的方法【转】https://bbs.huaweicloud.com/forum/thread-0248173698858425071-1-1.html2.使用Python实现高效的端口扫描器【转】https://bbs.huaweicloud.com/forum/thread-0248173699028101072-1-1.html3.使用Python实现操作mongodb详解【转】https://bbs.huaweicloud.com/forum/thread-02109173699263711070-1-1.html4.一文详解Python中数据清洗与处理的常用方法【转】https://bbs.huaweicloud.com/forum/thread-02109173699342905071-1-1.html5.Go中sync.Once源码的深度讲解【转】https://bbs.huaweicloud.com/forum/thread-0271173699402065058-1-1.html6.从源码解析golang Timer定时器体系【转】https://bbs.huaweicloud.com/forum/thread-0251173701525255062-1-1.html7.golang1.23版本之前 Timer Reset方法无法正确使用【转】https://bbs.huaweicloud.com/forum/thread-02127173701584637057-1-1.html8.Python文件读写实用方法小结【转】https://bbs.huaweicloud.com/forum/thread-02104173701685566070-1-1.html9.mysql外键创建不成功/失效如何处理【转】https://bbs.huaweicloud.com/forum/thread-02109173701958630072-1-1.html10.Redis的Zset类型及相关命令详细讲解【转】https://bbs.huaweicloud.com/forum/thread-02109173702031434073-1-1.html11.大数据小内存排序问题如何巧妙解决【转】https://bbs.huaweicloud.com/forum/thread-02127173702077058058-1-1.html12.Redis多种内存淘汰策略及配置技巧分享【转】https://bbs.huaweicloud.com/forum/thread-0272173702166312062-1-1.html13.MySQL通过binlog实现恢复数据【转】https://bbs.huaweicloud.com/forum/thread-02109173702268081074-1-1.html14.MySQL如何将一个表的字段更新到另一个表中【转】https://bbs.huaweicloud.com/forum/thread-0272173702328248063-1-1.html15.JSON字符串转成java的Map对象详细步骤【转】https://bbs.huaweicloud.com/forum/thread-02109173702572327075-1-1.html
-
大数据小内存排序问题,很经典,很常见,类似的还有比如 “如何对上百万考试的成绩进行排序” 等等。三种方法:数据库排序(对数据库设备要求较高)分治法(常见思路)位图法(Bitmap)方法概要数据库排序(对数据库设备要求较高)操作:将数据全部导入数据库,建立索引,数据库对数据进行排序,提取出数据。特点:操作简单, 运算速度较慢,对数据库设备要求较高。分治法(常见思路)操作:操作与归并排序的思想类似,都是分治。将数据进行分块,然后对每个数据块进行内部的排序(假如是对int形数据升序)。和归并排序类似,每个数据块取第一个数据(当前块的最小数据),然后比较取出的数据,取其最小加入结果集。重复2操作,直到取完所有数据,此时排序完毕。特点:位图法(Bitmap)操作:基本思想就是利用一位(bit)代表一个数字,例如第 3 位上为 1,则说明 3 这个数字出现过,若为0,则说明 3 这个数字没有出现过。很简单~ java.util 封装了 BitSet 这样一个类,是位图法的典型实现。特点:可读性差(不是一般的差 🤔)位图存储的元素个数虽然比一般做法多,但是存储的元素大小受限于存储空间的大小。要想定义存储空间大小就需要实现知道存储的元素到底有多少对于有符号类型的数据,需要用 2 位来表示,比如 第 0 位和第 1 位表示 0 这个数据,第 2 位和第 3 位表示 1 这个数据......,这会让位图能存储的元素个数,元素值大小上限减半只知道元素是否出现,无法知道出现的具体次数
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签