-
清华大学出版社《Java程序员,上班那点事儿》作者:钟声——第10章《高手有多高菜鸟有多菜》部分节选。 大家也许都用过FTP上传下载工具,比如“LeapFTP”这个工具是一个很方便的FTP服务器上传下载工具,如图所示。这个工具很方便,输入用户名密码以后,就可以看到FTP服务器端的文件列表,便于进行上传与下载操作。 你是否试过自己用Java编写一个FTP的文件上传与下载应用程序? Java也可以开发出这样的程序来,并不复杂,我们先看看,利用Java的FTP类工具包制作FTP应用程序的开发是怎么做的,请看如下程序: import sun.net.*; import sun.net.ftp.*; public class FTP { public static void main(String[] args) { String server="192.168.0.12"; //输入的FTP服务器的IP地址 String user="useway"; //登录FTP服务器的用户名 String password=" !@#$%abce"; //登录FTP服务器的用户名的口令 String path="/home/useway"; //FTP服务器上的路径 try { FtpClient ftpClient=new FtpClient(); //创建FtpClient对象 ftpClient.openServer(server); //连接FTP服务器 ftpClient.login(user, password); //登录FTP服务器 if (path.length()!=0) ftpClient.cd(path); TelnetInputStream is=ftpClient.list(); int c; while ((c=is.read())!=-1) { System.out.print((char)c); } is.close(); ftpClient.closeServer();//退出FTP服务器 } catch(Exception ex){ } } } 如果你感兴趣的话,可以自己写一个这个程序,当本程序运行以后,我们看到如图所示的情况,列出了服务器端程序的目录内容。 这个程序是一个简单的得到FTP服务器端文件列表的程序,但不要误会,这个程序可称不上“网络应用层协议”程序的开发! 这个程序仅仅是利用“sun.net.*;”和“sun.net.ftp.*;”中的相关类进行的对FTP端的操作的,我们根本没有利用Java的Socket的在网络层面向FTP服务器端发送任何请求,而是通过Java提供的工具包,向服务器端发送的链接请求。 利用Java的FTP包来链接FTP服务器的好处在于我们不需要关心网络层面发送数据的具体细节,而只要调用相应的方法就行了。利用Java的FTP包来链接FTP服务器的缺点是使开发者不知道应用层协议收发的来龙去脉,搞不清楚其中原理,对底层数据的把握程度非常弱。 讲到这里有程序员会问:“那么FTP在网络层面和PC与服务器间是如何交互的呢?”,好,就给大家列出FTP协议交互过程。 请看下面的一段FTP协议交互的例子: FTP服务器: 220 (vsFTPd 2.0.1) FTP客户端: USER useway FTP服务器: 331 Please specify the password. FTP客户端: PASS !@#$%abce FTP服务器: 230 Login successful. FTP客户端: CWD /home/useway FTP服务器: 250 Directory successfully changed. FTP客户端: EPSV ALL FTP服务器: 200 EPSV ALL ok. FTP客户端: EPSV FTP服务器: 229 Entering Extended Passive Mode (|||62501|) FTP客户端: LIST FTP服务器: 150 Here comes the directory listing. FTP服务器: 226 Directory send OK. FTP客户端: QUIT FTP服务器: 221 Goodbye. 以上这段文字其实就是FTP服务器和FTP客户端之间相互交互的过程,它们之间传递信息的协议是TCP协议,互相发送的内容就是上面这段文字所写的内容。 我们下面逐步的去解释每一句话的含义: FTP服务器: 220 (vsFTPd 2.0.1) |说明:链接成功 FTP客户端: USER useway |说明:输入用户名 FTP服务器: 331 Please specify the password. |说明:请输入密码 FTP客户端: PASS !@#$%abce |说明:输入密码 FTP服务器: 230 Login successful. |说明:登录成功 FTP客户端: CWD /home/useway |说明:切换目录 FTP服务器: 250 Directory successfully changed. |说明:目录切换成功 FTP客户端: EPSV ALL |说明:为EPSV被动链接方式 FTP服务器: 200 EPSV ALL ok. |说明:OK FTP客户端: EPSV |说明:链接 FTP服务器: 229 Entering Extended Passive Mode (|||62501|) |说明:被动链接端口为62501 FTP客户端: LIST |说明:执行LIST显示文件列表 FTP服务器: 150 Here comes the directory listing. |说明:列表从62501端口被发送 FTP服务器: 226 Directory send OK. |说明:发送完成 FTP客户端: QUIT |说明:退出FTP FTP服务器: 221 Goodbye. |说明:再见 有了以上文字的内容,我们不需要任何工具也可以得到FTP文件列表了,不信你跟着我一起做一遍。 第一步:首先打开CMD进入DOS命令行模式,键入: telnet 192.168.0.1 21[回车] 说明:Telnet 到Ftp服务器的21端口。 执行该命令 大家发现什么问题了吗? 提示的内容正好就是,我们上面一段文字的第一句:220 (vsFTPd 2.0.1),这说明FTP服务器已经接受了我们的链接,已经可以进行下一步操作了。 第二步:将后面的一系列发送内容逐个键入: USER useway[回车] PASS !@#$%abce[回车] CWD /home/useway[回车] EPSV ALL[回车] EPSV[回车] 好,这回FTP服务器给出了一系列的回应,在最后给出了一个新的端口号"58143"。 第三步:再打开一个新的CMD窗口,键入: telnet 192.168.0.1 58143[ 回车 ] 注意,这次Telnet请求链接服务器的端口号是“58143”,是FTP服务器给我们的一个链接端口。链接后,窗口为空白没有任何提示,如图所示。 第四步:回到第一个CMD窗口,键入: LIST[ 回车 ] 第五步:这时候第二CMD窗口就接收到了文件列表: 第二个窗口接收到了文件列表如图所示。 第六步:退出操作 QUIT[ 回车 ] 执行完成后,失去与主机的链接。 大家看到了吧,FTP协议就是这样的一个交互过程,利用系统自带的Telnet工具也可以完成FTP的这些基本命令的操作。如果,你想用Java的Socket完成以上操作就只需要一步一步的按照上述内容发送字符串给FTP服务器端就行了。 我们下面也给出例子代码: import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; public class FTPClient{ public static void main(String[] args) throws Exception{ Socket socket = new Socket("192.168.0.1",21); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); //接收初始链接信息 byte[] buffer = new byte[100]; int length = is.read(buffer); String s = new String(buffer, 0, length); System.out.println(s); //发送用户名 String str = "USER useway\n"; os.write(str.getBytes()); //得到返回值 length = is.read(buffer); s = new String(buffer, 0, length); System.out.println(s); //发送密码 str = "PASS !@#$%abcd\n"; os.write(str.getBytes()); //得到返回值 length = is.read(buffer); s = new String(buffer, 0, length); System.out.println(s); //发送切换文件夹指令 str = "CWD /home/useway\n"; os.write(str.getBytes()); //得到返回值 length = is.read(buffer); s = new String(buffer, 0, length); System.out.println(s); //设置模式 str = "EPSV ALL\n"; os.write(str.getBytes()); //得到返回值 length = is.read(buffer); s = new String(buffer, 0, length); System.out.println(s); //得到被动监听信息 str = "EPSV\n"; os.write(str.getBytes()); //得到返回值 length = is.read(buffer); s = new String(buffer, 0, length); System.out.println(s); //取得FTP被动监听的端口号 String portlist=s.substring(s.indexOf("(|||")+4,s.indexOf("|)")); System.out.println(portlist); //实例化ShowList线程类,链接FTP被动监听端口号 ShowList sl=new ShowList(); sl.port=Integer.parseInt(portlist); sl.start(); //执行LIST命令 str = "LIST\n"; os.write(str.getBytes()); //得到返回值 length = is.read(buffer); s = new String(buffer, 0, length); System.out.println(s); //关闭链接 is.close(); os.close(); socket.close(); } } //得到被动链接信息类,这个类是多线程的 class ShowList extends Thread{ public int port=0; public void run(){ try{ Socket socket = new Socket("192.168.0.1",this.port); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); byte[] buffer = new byte[10000]; int length = is.read(buffer); String s = new String(buffer, 0, length); System.out.println(s); //关闭链接 is.close(); os.close(); socket.close(); } catch(Exception ex){ } } } 该程序运行后得到的运行结果如图所示,基本上和上面的运行效果相同吧,底层又如何,无非是将那些封装好的方法解开来运行,只要了解到了它们运行的规则,我们自己可以开发出一样的程序来 ———————————————— 原文链接:https://blog.csdn.net/weixin_33854644/article/details/85128720
-
引言 在Java开发中,文件传输协议(FTP)和安全文件传输协议(SFTP)是处理文件传输的两种常见方式。FTP是标准的网络文件传输协议,而SFTP则在FTP基础上增加了安全层(SSH),提供了更加安全的文件传输方式。本文将详细介绍如何在Java中实现与FTP和SFTP服务器的连接,并深入讲解各种函数的用法,以及如何进行二次封装,以提升代码的可复用性和可维护性。 文章目录 引言 一、Java连接FTP的实现 1. 配置与依赖 2. 基本连接与文件操作 3. 常用函数详解 4. 二次封装的工具类 二、Java连接SFTP的实现 1. 配置与依赖 2. 基本连接与文件操作 3.常用函数详解 4. 二次封装的工具类 FTP工具类的级联创建目录封装 SFTP工具类的级联创建目录封装 使用注意事项 三、Java连接FTP与SFTP的对比 1. 安全性 2. 性能 3. 支持与兼容性 四、总结与个人看法 一、Java连接FTP的实现 FTP协议(File Transfer Protocol)是一种用于在网络中传输文件的标准协议。Java提供了丰富的库来实现FTP连接,其中最常用的是Apache Commons Net库。该库提供了一个FTPClient类,简化了与FTP服务器的交互。 1. 配置与依赖 在使用FTPClient类之前,首先需要在项目中引入Apache Commons Net库的依赖。在Maven项目中,可以通过在pom.xml文件中添加以下依赖项来实现: <dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.8.0</version> </dependency> 2. 基本连接与文件操作 使用FTPClient连接到FTP服务器的基本流程如下: 创建FTPClient对象。 使用connect方法连接到FTP服务器。 使用login方法进行身份验证。 使用相应的方法执行文件操作,如上传、下载、删除文件等。 使用logout方法注销。 最后关闭连接。 以下是一个简单的示例,展示了如何连接到FTP服务器并上传文件: import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class FTPExample { public static void main(String[] args) { FTPClient ftpClient = new FTPClient(); try { // 连接到FTP服务器 ftpClient.connect("ftp.example.com", 21); // 登录到FTP服务器 ftpClient.login("username", "password"); // 设置文件传输类型为二进制文件 ftpClient.setFileType(FTP.BINARY_FILE_TYPE); // 上传文件 try (InputStream inputStream = new FileInputStream("local/file/path")) { boolean done = ftpClient.storeFile("/remote/file/path", inputStream); if (done) { System.out.println("The file is uploaded successfully."); } } // 注销并关闭连接 ftpClient.logout(); } catch (IOException ex) { ex.printStackTrace(); } finally { try { if (ftpClient.isConnected()) { ftpClient.disconnect(); } } catch (IOException ex) { ex.printStackTrace(); } } } } 在这个示例中,ftpClient.connect用于连接FTP服务器,ftpClient.login进行身份验证,ftpClient.storeFile方法用于上传文件。需要注意的是,FTP传输的默认模式是ASCII,因此在上传二进制文件时,需要调用setFileType(FTP.BINARY_FILE_TYPE)来设置传输模式。 3. 常用函数详解 FTPClient类提供了丰富的功能,以下是一些常用的FTP操作函数: 连接与登录 connect(String hostname, int port):连接到指定的FTP服务器和端口。 login(String username, String password):使用用户名和密码登录FTP服务器。 logout():注销当前用户。 文件操作 storeFile(String remote, InputStream local):将本地文件上传到服务器。 retrieveFile(String remote, OutputStream local):从服务器下载文件到本地。 deleteFile(String pathname):删除服务器上的文件。 listFiles(String pathname):列出指定目录下的文件和子目录。 目录操作 makeDirectory(String pathname):在服务器上创建一个新目录。 removeDirectory(String pathname):删除服务器上的目录。 changeWorkingDirectory(String pathname):改变当前工作目录。 printWorkingDirectory():获取当前工作目录的路径。 连接管理 disconnect():断开与FTP服务器的连接。 isConnected():检查是否已连接到服务器。 4. 二次封装的工具类 为了提高代码的可复用性,可以将上述FTP操作封装到一个工具类中。这不仅简化了使用,还可以集中处理异常和日志记录等通用逻辑。 import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class FTPUtil { private FTPClient ftpClient; public FTPUtil(String host, int port, String username, String password) throws IOException { ftpClient = new FTPClient(); ftpClient.connect(host, port); ftpClient.login(username, password); ftpClient.setFileType(FTP.BINARY_FILE_TYPE); } public boolean uploadFile(String remoteFilePath, InputStream inputStream) throws IOException { try { return ftpClient.storeFile(remoteFilePath, inputStream); } finally { inputStream.close(); } } public boolean downloadFile(String remoteFilePath, OutputStream outputStream) throws IOException { try { return ftpClient.retrieveFile(remoteFilePath, outputStream); } finally { outputStream.close(); } } public boolean deleteFile(String filePath) throws IOException { return ftpClient.deleteFile(filePath); } public void logoutAndDisconnect() throws IOException { if (ftpClient.isConnected()) { ftpClient.logout(); ftpClient.disconnect(); } } } 这个FTPUtil类提供了基本的上传、下载、删除文件功能,并封装了连接和注销逻辑,简化了FTP操作的使用。 二、Java连接SFTP的实现 SFTP(SSH File Transfer Protocol)是基于SSH协议的文件传输协议,提供了更加安全的文件传输方式。Java中实现SFTP连接通常使用JSch库,这个库由JCraft开发,用于SSH2的Java实现。 1. 配置与依赖 在使用JSch库之前,同样需要在项目中引入依赖。在Maven项目中,添加以下依赖项: <dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.55</version> </dependency> 2. 基本连接与文件操作 使用JSch连接到SFTP服务器的基本流程如下: 创建JSch对象。 使用getSession方法创建会话并进行身份验证。 使用connect方法连接到SFTP服务器。 使用ChannelSftp对象执行文件操作。 关闭连接。 以下是一个简单的示例,展示了如何连接到SFTP服务器并上传文件: import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.Session; import java.io.FileInputStream; import java.io.InputStream; import java.util.Properties; public class SFTPExample { public static void main(String[] args) { String SFTPHOST = "sftp.example.com"; int SFTPPORT = 22; String SFTPUSER = "username"; String SFTPPASS = "password"; String SFTPWORKINGDIR = "/remote/path/"; Session session = null; ChannelSftp channelSftp = null; try { JSch jsch = new JSch(); session = jsch.getSession(SFTPUSER, SFTPHOST, SFTPPORT); session.setPassword(SFTPPASS); // 配置StrictHostKeyChecking属性 Properties config = new Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); session.connect(); channelSftp = (ChannelSftp) session.openChannel("sftp"); channelSftp.connect(); // 上传文件 try (InputStream inputStream = new FileInputStream("local/file/path")) { channelSftp.put(inputStream, SFTPWORKINGDIR + "fileName"); } } catch (Exception ex) { ex.printStackTrace(); } finally { if (channelSftp != null) { channelSftp.disconnect(); } if (session != null) { session.disconnect(); } } } } 在这个示例中,JSch类用于管理SSH连接和会话,ChannelSftp类用于执行具体的SFTP操作。需要注意的是,在实际开发中,StrictHostKeyChecking属性建议设置为yes以增强安全性。 3.常用函数详解 ChannelSftp类提供了丰富的功能,用于实现SFTP操作。以下是一些常用的SFTP操作函数: 连接与会话管理 connect():连接到SFTP服务器。 disconnect():断开与SFTP服务器的连接。 getSession():获取当前连接的会话(Session)。 文件操作 put(InputStream src, String dst):将本地文件上传到SFTP服务器的目标路径。 get(String src, OutputStream dst):从SFTP服务器下载文件到本地。 rm(String path):删除服务器上的文件。 ls(String path):列出指定目录下的文件和子目录。 目录操作 cd(String path):切换到指定目录。 mkdir(String path):在SFTP服务器上创建新目录。 rmdir(String path):删除SFTP服务器上的目录。 权限与属性管理 chmod(int permissions, String path):修改文件或目录的权限。 chown(int uid, String path):修改文件或目录的所有者。 chgrp(int gid, String path):修改文件或目录的所属组。 stat(String path):获取文件或目录的属性。 4. 二次封装的工具类 为了更好地管理SFTP连接和操作,实现更强大的功能,如级联创建目录和其他操作,我们可以将常用功能封装到一个工具类中,提高代码的可读性和可维护性的同时,确保可以顺利处理复杂的文件系统操作。比如级联创建目录,在级联创建目录时,我们需要逐级检查每个目录是否存在,如果不存在则创建它。这在FTP和SFTP操作中都非常常见。 FTP工具类的级联创建目录封装 import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; public class FTPUtil { private FTPClient ftpClient; public FTPUtil(String host, int port, String username, String password) throws IOException { ftpClient = new FTPClient(); ftpClient.connect(host, port); ftpClient.login(username, password); ftpClient.setFileType(FTP.BINARY_FILE_TYPE); } // 上传文件 public boolean uploadFile(String remoteFilePath, InputStream inputStream) throws IOException { try { return ftpClient.storeFile(remoteFilePath, inputStream); } finally { inputStream.close(); } } // 下载文件 public boolean downloadFile(String remoteFilePath, OutputStream outputStream) throws IOException { try { return ftpClient.retrieveFile(remoteFilePath, outputStream); } finally { outputStream.close(); } } // 删除文件 public boolean deleteFile(String filePath) throws IOException { return ftpClient.deleteFile(filePath); } // 级联创建目录 public boolean createDirectories(String remoteDirPath) throws IOException { String[] directories = remoteDirPath.split("/"); String currentDir = ""; boolean dirExists = false; for (String dir : directories) { if (dir.isEmpty()) continue; currentDir += "/" + dir; dirExists = ftpClient.changeWorkingDirectory(currentDir); if (!dirExists) { if (!ftpClient.makeDirectory(currentDir)) { throw new IOException("Failed to create directory: " + currentDir); } ftpClient.changeWorkingDirectory(currentDir); } } return true; } // 删除目录(仅删除空目录) public boolean deleteDirectory(String directoryPath) throws IOException { return ftpClient.removeDirectory(directoryPath); } // 递归删除目录(包括子文件和子目录) public boolean deleteDirectoryRecursively(String remoteDirPath) throws IOException { FTPFile[] files = ftpClient.listFiles(remoteDirPath); for (FTPFile file : files) { String filePath = remoteDirPath + "/" + file.getName(); if (file.isDirectory()) { deleteDirectoryRecursively(filePath); } else { deleteFile(filePath); } } return deleteDirectory(remoteDirPath); } // 退出并断开连接 public void logoutAndDisconnect() throws IOException { if (ftpClient.isConnected()) { ftpClient.logout(); ftpClient.disconnect(); } } } SFTP工具类的级联创建目录封装 import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.Session; import java.io.InputStream; import java.io.OutputStream; import java.util.Properties; import java.util.Vector; public class SFTPUtil { private ChannelSftp channelSftp; private Session session; public SFTPUtil(String host, int port, String username, String password) throws Exception { JSch jsch = new JSch(); session = jsch.getSession(username, host, port); session.setPassword(password); Properties config = new Properties(); config.put("StrictHostKeyChecking", "no"); session.setConfig(config); session.connect(); channelSftp = (ChannelSftp) session.openChannel("sftp"); channelSftp.connect(); } // 上传文件 public void uploadFile(String remoteDir, String remoteFileName, InputStream inputStream) throws Exception { channelSftp.cd(remoteDir); channelSftp.put(inputStream, remoteFileName); } // 下载文件 public void downloadFile(String remoteFilePath, OutputStream outputStream) throws Exception { channelSftp.get(remoteFilePath, outputStream); } // 删除文件 public void deleteFile(String remoteFilePath) throws Exception { channelSftp.rm(remoteFilePath); } // 级联创建目录 public void createDirectories(String remoteDirPath) throws Exception { String[] directories = remoteDirPath.split("/"); String currentDir = ""; for (String dir : directories) { if (dir.isEmpty()) continue; currentDir += "/" + dir; try { channelSftp.cd(currentDir); } catch (Exception e) { channelSftp.mkdir(currentDir); channelSftp.cd(currentDir); } } } // 删除目录(仅删除空目录) public void deleteDirectory(String remoteDir) throws Exception { channelSftp.rmdir(remoteDir); } // 递归删除目录(包括子文件和子目录) public void deleteDirectoryRecursively(String remoteDirPath) throws Exception { Vector<ChannelSftp.LsEntry> files = channelSftp.ls(remoteDirPath); for (ChannelSftp.LsEntry entry : files) { if (!entry.getFilename().equals(".") && !entry.getFilename().equals("..")) { String filePath = remoteDirPath + "/" + entry.getFilename(); if (entry.getAttrs().isDir()) { deleteDirectoryRecursively(filePath); } else { deleteFile(filePath); } } } deleteDirectory(remoteDirPath); } // 退出并断开连接 public void logoutAndDisconnect() { if (channelSftp != null) { channelSftp.disconnect(); } if (session != null) { session.disconnect(); } } } 使用注意事项 级联目录创建:在处理复杂路径时,必须确保各级目录按顺序被创建。如果操作失败,应当立即停止并报告错误。 递归删除:在删除目录时,递归删除可以确保目录及其所有内容都被彻底删除。然而,必须注意,这是一项危险操作,可能会导致数据丢失,因此在执行之前应该进行仔细确认。 异常处理:在目录操作过程中,由于路径错误、权限不足或网络问题可能导致异常,应确保这些异常被捕获并妥善处理,以避免影响系统的稳定性。 三、Java连接FTP与SFTP的对比 FTP和SFTP在实际项目中各有应用场景。FTP由于其简单性和广泛支持,适用于一些对安全性要求不高的场景;而SFTP由于具备更高的安全性和数据加密特性,更适合用于涉及敏感数据的场景。 1. 安全性 SFTP在安全性方面具有明显的优势。它通过SSH协议加密数据传输,避免了数据在传输过程中的被窃听和篡改风险。相比之下,FTP采用明文传输,容易受到中间人攻击。 2. 性能 FTP通常在速度上略优于SFTP,因为它省去了加密和解密过程。不过,在现代硬件环境下,这种性能差异通常可以忽略不计。 3. 支持与兼容性 FTP在许多系统中得到了广泛的支持,几乎所有的操作系统和许多文件管理工具都支持FTP。而SFTP的支持相对较少,但在涉及安全要求的项目中,SFTP是更合适的选择。 四、总结与个人看法 通过本文,我们深入探讨了如何在Java中实现FTP和SFTP连接,并详细介绍了相关的函数用法和二次封装工具类的实现。对于两者的应用场景和差异,我们也进行了对比分析。 个人看法:在选择FTP还是SFTP时,应该根据项目的具体需求进行权衡。如果安全性是重中之重,SFTP无疑是首选。此外,随着互联网安全要求的日益提高,FTP的应用场景可能会逐渐减少。因此,在新项目中,建议优先考虑SFTP作为文件传输的解决方案。当然,在某些内网环境或临时性项目中,FTP的简单性和较低的配置成本也使其成为一个不错的选择。在处理文件传输任务时,封装常用的操作功能,不仅可以提高代码的复用性,还能减少开发过程中的重复劳动,提升项目的开发效率。对于级联操作和递归操作,虽然实现起来相对复杂,但这些功能对于许多实际场景是必不可少的,因此在开发中应当积极采用这些封装技术,确保系统的健壮性和灵活性。 希望这篇随笔能够帮助你更好地理解和掌握Java中FTP和SFTP的实现方法,并为实际项目提供一些有价值的参考。如果你在开发过程中有任何问题或需要进一步探讨,欢迎随时交流。 ———————————————— 原文链接:https://blog.csdn.net/QWERTYwqj/article/details/141160425
-
1、Java常用的设计模式 总体来说设计模式分为三大类,共23种: (1)创建型模式,共五种: Factory(工厂模式:简单工厂模式和抽象工厂模式)、Factory Method(工厂方法模式)、Singleton(单例模式)、Builder(建造者模式)、Prototype(原始模型模式)。 (2)结构型模式,共七种: Adapter(适配器模式)、Decorator(装饰器模式)、Proxy(代理模式)、Facade(外观模式)、Bridge(桥梁模式)、Composite(组合模式)、Flyweight(享元模式)。 (3)行为型模式,共十一种: Strategy(策略模式)、Template Method(模板方法模式)、Observer(观察者模式)、Iterator(迭代子模式)、Chain Of Responsibility(责任链模式)、Command(命令模式)、Memento(备忘录模式)、State(状态模式)、Visitor(访问者模式)、Mediator(中介者模式)、Interpreter(解释器模式)。 (4)其实还有两类:并发型模式和线程池模式。 2、Java遵循的设计原则 总原则:开闭原则(Open Close Principle,OCP) 开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等。 (1)单一职责原则(Single Responsibility Principle,SRP) 不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。 (2)里氏替换原则(Liskov Substitution Principle,LSP) 面向对象设计的基本原则之一。 里氏替换原则中说,任何父类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。 (3)依赖倒转原则(Dependence Inversion Principle,DIP) 这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。 (4)接口隔离原则(Interface Segregation Principle,ISP) 这个原则的意思是:每个接口中不应存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。 (5)迪米特原则(最少知道原则)(Law of Demeter,LOD或Least Knowledge Principle,LKP) 就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。 (6)合成复用原则(Composite Reuse Principle,CRP) 原则是尽量首先使用合成/聚合的方式,而不是使用继承。 ———————————————— 原文链接:https://blog.csdn.net/jsc123581/article/details/81748124
-
1.背景介绍 面向对象设计模式是一种软件设计的方法,它提供了一种抽象的方式来解决常见的软件设计问题。这种方法通过将问题分解为一组可重用的、可组合的对象来解决。这些对象可以表示实体、概念或抽象,并可以通过消息传递来交互。设计模式可以帮助程序员更快地开发高质量的软件,并提高代码的可读性、可维护性和可扩展性。 在本文中,我们将讨论23种经典的面向对象设计模式,并详细解释它们的定义、原理、优缺点和应用场景。这些设计模式可以分为三个类别:创建型模式、结构型模式和行为型模式。 2.核心概念与联系 2.1 创建型模式 创建型模式是一种用于创建对象的设计模式,它们提供了一种抽象的方式来创建对象,使得代码更加可维护和可扩展。创建型模式包括以下七种: 单例模式(Singleton) 工厂方法模式(Factory Method) 抽象工厂模式(Abstract Factory) 建造者模式(Builder) 原型模式(Prototype) 模板方法模式(Template Method) 代理模式(Proxy) 2.2 结构型模式 结构型模式是一种用于定义和组织类和对象的设计模式,它们提供了一种抽象的方式来组合类和对象,使得代码更加可维护和可扩展。结构型模式包括以下七种: 适配器模式(Adapter) 桥接模式(Bridge) 组合模式(Composite) 装饰模式(Decorator) 外观模式(Facade) 享元模式(Flyweight) 代理模式(Proxy) 2.3 行为型模式 行为型模式是一种用于定义对象之间的交互和行为的设计模式,它们提供了一种抽象的方式来描述对象之间的交互,使得代码更加可维护和可扩展。行为型模式包括以下十一种: 命令模式(Command) 策略模式(Strategy) 状态模式(State) 观察者模式(Observer) 中介模式(Mediator) 迭代子模式(Iterator) 访问者模式(Visitor) 备忘录模式(Memento) 责任链模式(Chain of Responsibility) 解释器模式(Interpreter) 3.核心算法原理和具体操作步骤以及数学模型公式详细讲解 在这个部分,我们将详细讲解每种设计模式的算法原理、具体操作步骤以及数学模型公式。由于篇幅限制,我们将只讨论其中的一部分设计模式。 3.1 单例模式 单例模式是一种创建型模式,它限制一个类只能有一个实例。这种模式通常用于管理全局资源,如数据库连接、文件输出等。 算法原理:单例模式使用一个静态变量来存储一个类的实例,并提供一个公共的静态方法来访问这个实例。当第一次访问这个方法时,它会创建一个新的实例,并将其存储在静态变量中。在后续的访问中,它会返回已存储的实例。 具体操作步骤: 在类中声明一个静态变量来存储实例。 在类中声明一个私有的构造函数,以防止外部创建新的实例。 在类中声明一个公共的静态方法,用于访问实例。 在静态方法中,检查静态变量是否已经存在实例。如果不存在,创建一个新的实例并将其存储在静态变量中。如果存在,返回已存储的实例。 数学模型公式: $$ Singleton = {S \mid \forall i,j \in S, i \neq j \Rightarrow si = sj} $$ 其中,$S$ 是单例集合,$si$ 和 $sj$ 是 $S$ 中的不同元素。 3.2 工厂方法模式 工厂方法模式是一种创建型模式,它提供了一个用于创建对象的接口,但让子类决定实例化哪个具体的类。这种模式通常用于创建不同类型的对象,而不需要知道它们的具体类。 算法原理:工厂方法模式定义了一个接口用于创建对象,并将实例化过程委托给子类。子类实现这个接口,并在其中调用具体的构造函数来创建对象。 具体操作步骤: 创建一个抽象的工厂类,包含一个用于创建对象的接口。 创建一个或多个具体的工厂类,继承抽象工厂类,并实现接口。 在具体工厂类中,实现创建对象的方法,调用具体的构造函数。 使用具体工厂类来创建对象。 数学模型公式: $$ FactoryMethod = {F \mid \forall fi \in F, fi(c) = c_i} $$ 其中,$F$ 是工厂方法集合,$fi$ 是 $F$ 中的一个方法,$c$ 是类的集合,$ci$ 是 $c$ 中的一个具体类。 4.具体代码实例和详细解释说明 在这个部分,我们将通过一个具体的代码实例来演示单例模式和工厂方法模式的使用。 4.1 单例模式 ```python class Singleton: _instance = None def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs) return cls._instance def __init__(self): self.value = 42 s1 = Singleton() s2 = Singleton() print(s1 is s2) # True ``` 在这个例子中,我们定义了一个 Singleton 类,它使用一个静态变量 _instance 来存储实例。在 __new__ 方法中,我们检查静态变量是否已经存在实例,如果不存在,创建一个新的实例并将其存储在静态变量中。如果存在,返回已存储的实例。 4.2 工厂方法模式 ```python from abc import ABC, abstractmethod class Animal(ABC): @abstractmethod def speak(self): pass class Dog(Animal): def speak(self): return "Woof!" class Cat(Animal): def speak(self): return "Meow!" class AnimalFactory: @staticmethod def createanimal(animaltype): if animaltype == "Dog": return Dog() elif animaltype == "Cat": return Cat() else: raise ValueError("Invalid animal type") dog = AnimalFactory.createanimal("Dog") cat = AnimalFactory.createanimal("Cat") print(dog.speak()) # Woof! print(cat.speak()) # Meow! ``` 在这个例子中,我们定义了一个 Animal 抽象类和两个具体的子类 Dog 和 Cat。这些类实现了一个 speak 方法,用于表示不同的动物发出的声音。我们还定义了一个 AnimalFactory 类,它包含一个静态方法 create_animal,用于根据传入的字符串创建不同类型的动物。这个方法使用了工厂方法模式,因为它提供了一个用于创建对象的接口,但让子类决定实例化哪个具体的类。 5.未来发展趋势与挑战 面向对象设计模式已经成为软件开发的一部分,它们已经广泛应用于各种类型的软件系统。未来,我们可以预见以下几个方面的发展趋势和挑战: 随着软件系统的复杂性和规模的增加,设计模式将更加重要,因为它们可以帮助我们更好地组织和管理代码。 随着编程语言和框架的发展,设计模式将更加灵活和可扩展,以适应不同的应用场景。 随着人工智能和机器学习的发展,设计模式将更加关注如何构建可扩展、可维护的机器学习模型和系统。 随着云计算和分布式系统的发展,设计模式将更加关注如何构建高性能、高可用性的分布式系统。 随着安全性和隐私问题的加剧,设计模式将更加关注如何构建安全、隐私保护的软件系统。 6.附录常见问题与解答 在这个部分,我们将回答一些常见问题: Q:设计模式是否适用于所有的软件项目? A:不适用。设计模式是一种软件设计的方法,它们可以帮助程序员更快地开发高质量的软件,并提高代码的可读性、可维护性和可扩展性。但是,在某些情况下,使用设计模式可能会导致代码过于复杂和难以理解。因此,在选择使用设计模式时,需要权衡其优缺点。 Q:设计模式是否会限制我的创造力? A:不会。设计模式是一种抽象的方式来解决常见的软件设计问题,它们可以帮助程序员更快地开发高质量的软件,并提高代码的可读性、可维护性和可扩展性。使用设计模式并不意味着限制你的创造力,而是提供了一种更高效、更结构化的方式来构建软件系统。 Q:如何选择适合的设计模式? A:选择适合的设计模式需要考虑以下几个因素: 问题的具体性:设计模式应该与问题紧密相关,不应该过度设计。 问题的复杂性:更复杂的问题可能需要多个设计模式来解决。 设计模式的可读性和可维护性:选择易于理解和维护的设计模式。 设计模式的适用性:确保选定的设计模式适用于当前的技术栈和团队能力。 Q:设计模式是否会导致代码冗余? A:可能。使用设计模式可能会导致代码冗余,因为设计模式通常包括一些重复的代码。但是,这种冗余通常是可以接受的,因为它可以提高代码的可读性、可维护性和可扩展性。在选择使用设计模式时,需要权衡其优缺点。 ———————————————— 原文链接:https://blog.csdn.net/universsky2015/article/details/137313201
-
设计原则:高内聚低耦合,开闭原则。 两大基础设计原则 在说面向对象设计的六大原则之前,我们先来说下程序设计的原则:模块内高内聚,模块间低耦合。我们在面向对象时只需把类看成模块,那么就容易理解封装等了。 说是七大原则,这里我先提出来一个:对扩展开放,对修改关闭。 为啥这么说,因为我们都知道软件是要改的。对扩展开放保证了可以增加功能,像泛型啦这些。对修改关闭保证了像前的兼容性,jdk7兼容jdk6这样。所以开闭原则围绕软件的整个生命周期。 从基础原则出发,产生六个具体的原则: 1.单一职责(一个方法或一个类只做一件事,为了模块内高内聚) 2.迪米特法则(也叫最少知道原则,为了模块间低耦合) 3.里氏替换(就是继承原则,子类可以无缝替代父类。很好的符合了开闭原则) 4.依赖倒置(类之间的依赖通过接口实现,低耦合的同时对扩展开放) 5.接口隔离(即把单个复杂接口拆分为多个独立接口,与上条共同实现面向接口编程) 6.合成复用原则(即尽量使用合成/聚合的方式,而不是使用继承。主要为了防止继承滥用而导致的类之间耦合严重。记住只有符合继承原则时才用继承) 设计模式 我觉得程序员最好的沟通方式是代码,所以每个设计模式都是一个例子。所有例子都很方便,可以复制直接运行。因为对java熟悉,所以下面设计模式例子都是用java语言来实现的。 创建型模式(IOC:控制反转,就是创建分离的集大成) 1.Singleton:单例模式(全局只要一个实例) 2.Prototype:原型模式(通过拷贝原对象创建新对象) 3.Factory Method:工厂方法模式(对象创建可控,隐藏具体类名等实现解耦) 4.Abstract Factory:抽象工厂模式(解决对象与其属性匹配的工厂模式) 5.Builder:建造者模式(封装降低耦合,生成的对象与构造顺序无关) 创建型模式的五种有各自的使用环境,单例和原型比较简单就不说了,工厂方法模式和建造者模式,都是封装和降低耦合有啥不同呢,其实工厂方法关注的是一个类有多个子类的对象创建(汽车类的各种品牌),而建造者模式关注的是属性较多的对象创建(能达到过程无关)。而抽象工厂模式关注的是对象和属性及属性与属性的匹配关系(如奥迪汽车与其发动机及空调的匹配)。 结构型模式(对象的组成以及对象之间的依赖关系) 1.Adapter:适配器模式(适配不同接口和类,一般解决历史遗留问题) 2.Decorator:装饰器模式(比继承更灵活,可用排列组合形成多种扩展类) 3.Proxy:代理模式(可以给类的每个方法增加逻辑,如身份验证) 4.Facade:外观模式(对模块或产品的封装,降低耦合) 5.Bridge:桥接模式(就是接口模式,抽象与实现分离) 6.Plyweight:享元模式(相同对象的重用) 7.Composite:组合模式(整体和部分相同时,如文件夹包含文件夹) 我们可以看到适配器模式、装饰器模式、代理模式都可以用包装对象来实现(把对象作为一个属性放在用的对象里),所以模式关注的并不是实现,而是解决的问题。模式更多体现的是类与类之间的逻辑关系,比如代理模式和装饰器模式很像。但从字面就知道,代理是访问不了实际工作对象的,这是他们的区别。 行为型模式(即方法及其调用关系) 1.Strategy:策略模式(提供功能不同实现方式,且实现可选) 2.Template Method:模板方法模式(相同流程用一个模板方法) 3.Observer:观察者模式(用订阅-发布实现的被观察者变化时回调) 4.Iterator:迭代器模式(一种内部实现无关的集合遍历模式) 5.Chain of Responsibility:责任链模式(事件处理的分层结构产生的责任链条) 6.Command:命令模式(将命令者与被命令者分离) 7.Memento:备忘录模式(需要撤销与恢复操作时使用) 8.State:状态模式 (当对象两种状态差别很大时使用) 9.Visitor:访问者模式 (当对同一对象有多种不同操作时使用) 10.Mediator:中介者模式(以中介为中心,将网状关系变成星型关系) 11.Interpreter:解释器模式(常用于纯文本的表达式执行) 写完设计模式之后感觉设计模式更多的一种逻辑关系,如果代码中有这种逻辑关系就可以用了。记得需要时候再用,不能为了设计模式而设计模式。没有什么就是好的,最主要用起来舒服吧。 ———————————————— 原文链接:https://blog.csdn.net/wanyouzhi/article/details/77248710
-
什么是设计模式? 设计模式是指在软件开发中,经过验证的,用于解决在特定环境下,重复出现的,特定问题的解决方案。面向对象设计模式通常以类或者对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。 设计模式的基本原则 1. 依赖倒置原则 高层模块不应该依赖低层模块,二者都应该依赖抽象; 抽象不应该依赖具体实现,具体实现应该依赖于抽象; 举个例子,自动驾驶系统公司是高层,汽⻋生产厂商为低层,它们不应该互相依赖,一方变动另一方也会跟着变动;而应该抽象一个自动驾驶行业标准,高层和低层都依赖它;这样以来就解耦了两方的变动;自动驾驶系统、汽⻋生产厂商都是具体实现,它们应该都依赖自动驾驶行业标准(抽象)。 2. 开放封闭原则 一个类应该对扩展开放,对修改关闭。也就是对一个类尽量做扩展而不是修改,扩展一般是用继承或者组合的方式。 3. 面向接口编程 不将变量类型声明为某个特定的具体类,而是声明为某个接口。比如某个类成员是另一个类的具体对象,那么应该改成指向这个类的指针,因为根据多态原理,这个类指针实际上成为了一个接口。 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。 减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。 4. 封装变化点 将稳定点和变化点分离,扩展修改变化点;让稳定点与变化点的实现层次分离。 5. 单一职责原则 一个类应该仅有一个引起它变化的原因。 6. 里式替换原则 子类型必须能够替换掉它的父类型;主要出现在子类覆盖父类实现,原来使用父类型的程序可 能出现错误;覆盖了父类方法却没实现父类方法的职责。也就是子类如果声明了父类存在的函数 就要把功能实现完全,父类有的子类也一定要有。 7. 接口隔离原则 不应该强迫客户依赖于他们不用的方法。 一般用于处理一个类拥有比较多的接口,而这些接口涉及到很多职责。 所以要正确使用public,protected,private这三种权限。 8. 对象组合优于类继承 继承耦合度高,组合耦合度低(继承不能换爹,组合可以换爹)。 如何找到设计模式 从重构中获得: 1. 静态转变为动态 2. 早绑定转为晚绑定 3. 继承转为组合 4. 编译时依赖转变为运行时依赖 5. 紧耦合转变为松耦合 掌握设计模式的原则比具体的设计模式更重要,只有当你对业务非常熟悉的时候,才能信手拈来的写出适合的设计模式。 常见的设计模式和使用场景 1. 模板模式 定义一个操作中的算法的⻣架 ,而将一些步骤延迟到子类中。 Template Method使得子类可以不 改变一个算法的结构即可重定义该算法的某些特定步骤。 下面来看一个例子: class IGame { public: Play() { Process1(); Process2(); Process3(); Process4(); } protected: virtual void Process1(){} virtual void Process2(){} virtual void Process3(){} virtual void Process4(){} }; class Game1:public IGame { protected: virtual void Process2(){} virtual void Process4(){} }; class Game2:public IGame { protected: virtual void Process1(){} virtual void Process3(){} }; 某个游戏IGame类有固定的流程,然后Game1和Game2分别继承自IGame,但是在具体某个步骤上各有不同,因此将Process1~4写成虚函数,由各个子类自己去实现。 这样写的前提当然是主框架流程是固定的,Play函数对外开放给用户使用,但是具体流程的函数是protected,提供子类去修改。这体现了接口隔离原则。 流程的接口在基类都抽象成了虚函数,由各个子类具体实现,这体现了依赖倒置原则。 2. 观察者模式 观察者模式在分布式架构中应用广泛,比如actor框架,skynet,redis,zoomkeeper,订阅和发布等。 观察者模式定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有 依赖于它的对象都得到通知并自动更新。 // 终端基类 class ITerminal { public: virtual void show(int data){} }; class DataCenter { public: void Attach(ITerminal* ob){} // 注册绑定 void Detach(ITerminal* ob){} // 解绑 void Notify() { for(auto it = obs.begin(); it != obs.end(); ++it) { it->show(); } } protected: virtual int GetData(){return 0;} // 获取数据 private: std::vector<ITerminal*> obs; }; class Terminal1:public ITerminal { public: void show(int data){} } class Terminal2:public ITerminal { public: void show(int data){} } 上面是一个发布和订阅的简单例子,假设有个数据中心提供数据给不同的终端展示,数据中心是一个稳定点,不同的终端设备是变化点,因此将终端封装成基类,提供一个展示的接口,不同的设备继承去实现不同的展示方式。 数据中心用一个vector或者set把注册过来的终端记录起来,需要展示的时候通过notify接口逐个通知展示。这样数据中心不必关注具体有哪些设备订阅了数据,是否订阅的操作交给了各个终端设备上。 3. 策略模式 定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。该模式使得算法可独立于使用 它的客户程序而变化。 策略模式提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法 之间进行切换; 策略模式消除了条件判断语句;就是在解耦合; 充分体现了开闭原则;单一职责; 其本质是分离算法,选择实现。 来看一个简单的例子,假设某电商网站一件商品在不同节假日的价格算法使不同的,可能会这么写: enum VacationEnum { chunjie, // 春节 wuyi; // 五一 qixi; // 七夕 guoqing; // 国庆 }; class Context { }; class Promotion { VacationEnum vac; public: double calc() { if(vac == chunjie){} else if(vac == wuyi){} else if(vac == qixi){} else if(vac == guoqing){} } }; 这样写并不符合设计模式的单一职责原则和开放封闭原则。所以可以改成这样: class Context {}; // 变化的 扩展的 class BaseVac { public: virtual calc(Context &ctx){} }; class Vacchunjie: public BaseVac { public: virtual calc(Context &ctx){} }; class Vacwuyi: public BaseVac { public: virtual calc(Context &ctx){} }; class Vacxiqi: public BaseVac { public: virtual calc(Context &ctx){} }; // 稳定的 单一职责的 class Promotion { public: Promotion(BaseVac *ss):s(ss){} double calc(Context &ctx) { s->calc(ctx); } private: BaseVac *s; }; int main() { BaseVac *v = new Vacxiqi(); Promotion *p = new Promotion(v); return 0; } 把变化点(不同节假日的不同算法)隔离出去,只保留单一的职责:获取价格。 这样做非常符合设计模式的原则,但是前提是对业务非常熟悉,因为别人来扩展维护的话,必须先了解之前已有的扩展有哪些。 4. 责任链模式 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成 一条链,并沿着这条链传递请求,直到有一个对象处理它为止。 这个模式的一些要点: a.解耦请求方和处理方,请求方不知道请求是如何被处理,处理方的组成是由相互独立的子处理构成,子处理流程通过链表的方式连接,子处理请求可以按任意顺序组合; b.责任链请求强调请求最终由一个子处理流程处理;通过了各个子处理条件判断; c.责任链扩展就是功能链,功能链强调的是,一个请求依次经由功能链中的子处理流程处理; d.充分体现了单一职责原则;将职责以及职责顺序运行进行抽象,那么职责变化可以任意扩展, 同时职责顺序也可以任意扩展; 本质是分离职责,动态组合。 责任链模式是比较过时的,在c语言里用的比较多,c++里有更好的数据结构(list)可以代替这个模式。 来看一个具体例子,公司请假审批规则是小于3天只给人事审批,大于3天且小于5天给经理审批,5天以上的给老板审批。下面是第一个版本的代码: class Context { public: std::string name; int day; }; class LeaveRequest { public: void HandleRequest(const Context &cxt) { if(cxt.day < 3) { HandleByHR(cxt); } else if(cxt.day < 5); { HandleByMgr(cxt); } else { HandleByBoss(cxt); } } private: // 接口隔离原则 void HandleByHR(const Context &cxt){} void HandleByMgr(const Context &cxt){} void HandleByBoss(const Context &cxt){} }; 怎么重构这个代码,从封装变化点的原则来看,先把HandleRequest里的各种条件拆分出来,然后新增一个类成员指针指向上一级,子类只需要判断自己是否应该处理,不能处理的就通过指针转移到上级去处理。 class Context { public: std::string name; int day; }; class IHandler { public: virtual ~ IHandler(){} void SetNext(IHandler *n){bext = n;} virtual bool HandleRequest(const Context &cxt) { if(CanHandle(cxt)) { } else if(GetNext()) { GetNext()-> HandleRequest(cxt); } else { // error } } protected: virtual bool CanHandle(const Context &cxt){} IHandler * GetNext(){return next;} private: IHandler *next; }; class HandleByMainProgram : public IHandler { protected: virtual bool HandleRequest(const Context &ctx){ // } virtual bool CanHandle() { // } }; class HandleByProjMgr : public IHandler { protected: virtual bool HandleRequest(const Context &ctx){ // } virtual bool CanHandle() { // } }; class HandleByBoss : public IHandler { public: virtual bool HandleRequest(const Context &ctx){ // } protected: virtual bool CanHandle() { // } }; int main () { IHandler * h1 = new MainProgram(); IHandler * h2 = new HandleByProjMgr(); IHandler * h3 = new HandleByBoss(); h1->SetNextHandler(h2); h2->SetNextHandler(h3); Context ctx; h1->handle(ctx); return 0; } 把不同部门的职责拆出来作为子类继承,主流程封装进基类里,上面这个版本同时使用了模板模式和责任链模式。 子类的接口都是protected的,这体现了接口隔离原则和依赖倒置原则。 责任链模式可以变形成功能链模式,比如nginx阶段处理,不同之处在于责任链每次只有一个模块会处理,功能链是按顺序执行不同的模块。 5. 装饰器模式 动态地给一个对象增加一些额外的职责。就增加功能而言,装饰器模式比生成子类更为灵活。 这个模式看起来会和责任链模式很像。 通过采用组合而非继承的手法, 装饰器模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。 避免了使用继承带来的“灵活性差”和“多子类衍生问题”。 不是解决“多子类衍生的多继承”问题,而是解决“父类在多个方向上的扩展功能”问题; 装饰器模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,实现复用装饰器的功能; 装饰器模式本质是动态组合。 举个例子,普通员工有销售奖金,累计奖金,部⻔经理除此之外还有团队奖金;后面可能会添加环比增⻓奖 金,同时可能针对不同的职位产生不同的奖金组合。那么代码可以这样写: class Context { public: bool isMgr; // User user; // double groupsale; }; class Bonus { public: double CalcBonus(Context &ctx) { double bonus = 0.0; bonus += CalcMonthBonus(ctx); bonus += CalcSumBonus(ctx); if (ctx.isMgr) { bonus += CalcGroupBonus(ctx); } return bonus; } private: double CalcMonthBonus(Context &ctx) { double bonus/* = */; return bonus; } double CalcSumBonus(Context &ctx) { double bonus/* = */; return bonus; } double CalcGroupBonus(Context &ctx) { double bonus/* = */; return bonus; } }; int main() { Context ctx; // 设置 ctx Bonus *bonus = new Bonus; bonus->CalcBonus(ctx); } 重构成装饰器模式后的代码: class Context { public: bool isMgr; // User user; // double groupsale; }; // 试着从职责出发,将职责抽象出来 class CalcBonus { public: CalcBonus(CalcBonus * c = nullptr) {} virtual double Calc(Context &ctx) { return 0.0; // 基本工资 } virtual ~CalcBonus() {} protected: CalcBonus* cc; }; class CalcMonthBonus : public CalcBonus { public: CalcMonthBonus(CalcBonus * c) : cc(c) {} virtual double Calc(Context &ctx) { double mbonus /*= 计算流程忽略*/; return mbonus + cc->Calc(ctx); } }; class CalcSumBonus : public CalcBonus { public: CalcSumBonus(CalcBonus * c) : cc(c) {} virtual double Calc(Context &ctx) { double sbonus /*= 计算流程忽略*/; return sbonus + cc->Calc(ctx); } }; class CalcGroupBonus : public CalcBonus { public: CalcGroupBonus(CalcBonus * c) : cc(c) {} virtual double Calc(Context &ctx) { double gbnonus /*= 计算流程忽略*/; return gbnonus + cc->Calc(ctx); } }; class CalcCycleBonus : public CalcBonus { public: CalcGroupBonus(CalcBonus * c) : cc(c) {} virtual double Calc(Context &ctx) { double gbnonus /*= 计算流程忽略*/; return gbnonus + cc->Calc(ctx); } }; int main() { // 1. 普通员工 Context ctx1; CalcBonus *base = new CalcBonus(); CalcBonus *cb1 = new CalcMonthBonus(base); CalcBonus *cb2 = new CalcSumBonus(cb1); cb2->Calc(ctx1); // 2. 部门经理 Context ctx2; CalcBonus *cb3 = new CalcGroupBonus(cb2); cb3->Calc(ctx2); } 在计算奖金的形式上,有点像递归,因为构造函数里设置的基类成员是给用户自己设置的,可以 灵活组合。而不是第一个版本那样用ifelse去判断是否要添加奖金 ,这样也很容易扩展和修改。 打个比方,责任链的模式看起来就像一个链表的结构,而装饰器模式看起来像一个俄罗斯套娃。 6. 单例模式 保证一个类仅有一个实例,并提供一个该实例的全局访问点。 先了解一个知识点,由C/C++编译的程序占用的内存分为以下几个部分: 1、栈区(stack): 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其 操作方式类似于数据结构中的栈。 2、堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 3、全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。 4、文字常量区 :常量字符串就是放在这里的。 程序结束后由系统释放 5、程序代码区:存放函数体的二进制代码。 下面来看看几种实现单例模式的例子。 版本一: #include <mutex> class Singleton { // 懒汉模式 lazy load public: static Singleton * GetInstance() { //std::lock_guard<std::mutex> lock(_mutex); // 这里加锁粒度太大 切换线程开销大 if (_instance == nullptr) { std::lock_guard<std::mutex> lock(_mutex); // 在这里加锁 if (_instance == nullptr) // 双重判断 { _instance = new Singleton(); atexit(Destructor); } } return _instance; } private: static void Destructor() { if (nullptr != _instance) { delete _instance; _instance = nullptr; } } Singleton(){} //构造 Singleton(const Singleton &cpy){} //拷⻉构造 Singleton& operator=(const Singleton&) {} static Singleton * _instance; static std::mutex _mutex; }; Singleton* Singleton::_instance = nullptr;//静态成员需要初始化 std::mutex Singleton::_mutex; //互斥锁初始化 这个例子有几个需要注意的点,第一个是加锁的粒度,当单例指针是空的时候再去加锁,减少锁粒度可以减少线程切换的开销。第二个是双重判断,防止多线程重入导致多次分配的问题。第三这是个懒汉模式,用到的时候才决定分配初始化。 但是这个例子有个问题。 c++的new操作里分为了三个步骤:分配内存,调用构造函数,赋值操作。在多线程环境下,cpu会进行指令重排,前面三个步骤可能执行起来是132的顺序。所以可能会出现某个线程判断单例指针不为空,但是还没调用构造函数,就立即返回,最后导致程序崩溃。所以上面这个例子还是有问题的。(如果在判断前就加锁是没问题的,代价是增加了开销) 版本二: #include <mutex> #include <atomic> class Singleton { public: static Singleton * GetInstance() { Singleton* tmp = _instance.load(std::memory_order_relaxed); //取出原子对象 std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障 if (tmp == nullptr) { std::lock_guard<std::mutex> lock(_mutex); tmp = _instance.load(std::memory_order_relaxed); if (tmp == nullptr) { tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release);//释放内存屏障 _instance.store(tmp, std::memory_order_relaxed);// 赋值给原子对象 atexit(Destructor); } } return tmp; } private: static void Destructor() { Singleton* tmp = _instance.load(std::memory_order_relaxed); if (nullptr != tmp) { delete tmp; } } Singleton(){} Singleton(const Singleton&) {} Singleton& operator=(const Singleton&) {} static std::atomic<Singleton*> _instance; static std::mutex _mutex; }; std::atomic<Singleton*> Singleton::_instance;//原子对象 静态成员需要初始化 std::mutex Singleton::_mutex; //互斥锁初始化 // g++ Singleton.cpp -o singleton -std=c++11 上面代码用c++11特性内存屏障来解决cpu指令重排的问题。 版本三: class Singleton { public: ~Singleton(){} static Singleton& GetInstance() { static Singleton instance; return instance; } private: Singleton(){} Singleton(const Singleton&) {} Singleton& operator=(const Singleton&) {} }; // g++ Singleton.cpp -o singleton -std=c++11 这个版本需要在c++11或者更高级版本的编译后才能用。 c++11 magic static 特性:如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这就解决了多线程场景带来的问题。 该版本的优点: a. 利用静态局部变量特性,延迟加载; b. 利用静态局部变量特性,系统自动回收内存,自动调用析构函数; c. 静态局部变量初始化时,没有 new 操作带来的cpu指令reorder操作; d. c++11 静态局部变量初始化时,具备线程安全; 版本四: template<typename T> class Singleton { public: static T& GetInstance() { static T instance; // 这里要初始化DesignPattern,需要调用DesignPattern 构造函数,同时会调用父类的构造函数。 return instance; } protected: virtual ~Singleton() {} Singleton() {} // protected修饰构造函数,才能让别人继承 Singleton(const Singleton&) {} Singleton& operator =(const Singleton&) {} }; class DesignPattern : public Singleton<DesignPattern> { friend class Singleton<DesignPattern>; // friend 能让 Singleton<T> 访 问到 DesignPattern构造函数 private: DesignPattern(){} DesignPattern(const DesignPattern&) {} DesignPattern& operator=(const DesignPattern&) {} } 这个版本将单例模式封装成了模板,需要注意两个地方: a.单例模板类的构造函数用protected修饰是为了它本身能被继承。 b. 单例模板里的GetInstance接口要获取类型T的构造函数,因此继承这个模板的类必须声明模板类为友元。 7. 工厂方法模式 定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化 延迟到子类。 一般这些业务会使用工厂模式:解决创建过程比较复杂,希望对外隐藏这些细节;比如连接池,线程池; 隐藏对象真实类型; 对象创建会有很多参数来决定如何创建; 创建对象有复杂的依赖关系; 线程池的初始化需要由机器的内核数来决定线程数量,但是用户并不需要关心内核数,用户只需要获取到可用的线程就行。因此可以用工厂模式。 其本质是延迟到子类来选择实现; 举个例子,实现一个导出数据的接口,让客户选择数据的导出方式: #include <string> // 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv class IExport { public: virtual bool Export(const std::string &data) = 0; virtual ~IExport(){} }; class ExportXml : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportJson : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportTxt : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; // =====1 int main() { std::string choose/* = */; if (choose == "txt") { IExport *e = new ExportTxt(); e->Export("hello world"); } else if (choose == "json") { IExport *e = new ExportJson(); e->Export("hello world"); } else if (choose == "xml") { IExport *e = new ExportXml(); e->Export("hello world"); } } 对于数据导出有多种不同格式的类,在业务代码层面上,第一个版本里需要使用者区分判断多种格式,我们希望把这种复杂的创建操作封装起来,因此创建一个工厂类,这个类绑定了导出数据的操作和指向具体数据格式的类成员,使用者只需要创建某个具体的工厂子类,调用导出的接口就完成任务了。 重构后的代码: #include <string> // 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv class IExport { public: virtual bool Export(const std::string &data) = 0; virtual ~IExport(){} }; class ExportXml : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportJson : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportTxt : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportCSV : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class IExportFactory { public: IExportFactory() { _export = nullptr; } virtual ~IExportFactory() { if (_export) { delete _export; _export = nullptr; } } bool Export(const std::string &data) { if (_export == nullptr) { _export = NewExport(); } return _export->Export(data); } protected: virtual IExport * NewExport(/* ... */) = 0; private: IExport* _export; }; class ExportXmlFactory : public IExportFactory { protected: virtual IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportXml(); // 可能之后有什么操作 return temp; } }; class ExportJsonFactory : public IExportFactory { protected: virtual IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportJson; // 可能之后有什么操作 return temp; } }; class ExportTxtFactory : public IExportFactory { protected: IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportTxt; // 可能之后有什么操作 return temp; } }; class ExportCSVFactory : public IExportFactory { protected: virtual IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportCSV; // 可能之后有什么操作 return temp; } }; int main () { IExportFactory *factory = new ExportTxtFactory(); factory->Export("hello world"); return 0; } 8. 抽象工厂模式 提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无需指定它们具体的类。 抽象工厂类是基于工厂类实现的,实际上是一回事,抽象工厂类拥有更多的更复杂的操作接口,但是对于使用者来说,仍然只需要创建时指定某个子工厂类,无需关心具体的操作内容。 举个例子,实现一个拥有导出导入数据的接口,让客户选择数据的导出导入方式: #include <string> // 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv class IExport { public: virtual bool Export(const std::string &data) = 0; virtual ~IExport(){} }; class ExportXml : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportJson : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportTxt : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class ExportCSV : public IExport { public: virtual bool Export(const std::string &data) { return true; } }; class IImport { public: virtual bool Import(const std::string &data) = 0; virtual ~IImport(){} }; class ImportXml : public IImport { public: virtual bool Import(const std::string &data) { return true; } }; class ImportJson : public IImport { public: virtual bool Import(const std::string &data) { return true; } }; class ImportTxt : public IImport { public: virtual bool Import(const std::string &data) { return true; } }; class ImportCSV : public IImport { public: virtual bool Import(const std::string &data) { // .... return true; } }; class IDataApiFactory { public: IDataApiFactory() { _export = nullptr; _import = nullptr; } virtual ~IDataApiFactory() { if (_export) { delete _export; _export = nullptr; } if (_import) { delete _import; _import = nullptr; } } bool Export(const std::string &data) { if (_export == nullptr) { _export = NewExport(); } return _export->Export(data); } bool Import(const std::string &data) { if (_import == nullptr) { _import = NewImport(); } return _import->Import(data); } protected: virtual IExport * NewExport(/* ... */) = 0; virtual IImport * NewImport(/* ... */) = 0; private: IExport *_export; IImport *_import; }; class XmlApiFactory : public IDataApiFactory { protected: virtual IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportXml; // 可能之后有什么操作 return temp; } virtual IImport * NewImport(/* ... */) { // 可能有其它操作,或者许多参数 IImport * temp = new ImportXml; // 可能之后有什么操作 return temp; } }; class JsonApiFactory : public IDataApiFactory { protected: virtual IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportJson; // 可能之后有什么操作 return temp; } virtual IImport * NewImport(/* ... */) { // 可能有其它操作,或者许多参数 IImport * temp = new ImportJson; // 可能之后有什么操作 return temp; } }; class TxtApiFactory : public IDataApiFactory { protected: virtual IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportTxt; // 可能之后有什么操作 return temp; } virtual IImport * NewImport(/* ... */) { // 可能有其它操作,或者许多参数 IImport * temp = new ImportTxt; // 可能之后有什么操作 return temp; } }; class CSVApiFactory : public IDataApiFactory { protected: virtual IExport * NewExport(/* ... */) { // 可能有其它操作,或者许多参数 IExport * temp = new ExportCSV; // 可能之后有什么操作 return temp; } virtual IImport * NewImport(/* ... */) { // 可能有其它操作,或者许多参数 IImport * temp = new ImportCSV; // 可能之后有什么操作 return temp; } }; int main () { IDataApiFactory *factory = new CSVApiFactory(); factory->Import("hello world"); factory->Export("hello world"); return 0; } 9. 适配器模式 将一个类的接口转换成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起 工作的那些类可以一起工作。 原来的接口是稳定的,新的外来的需求是变化的,那么可以通过继承原来的接口,让原来的接 口继续保持稳定,在子类通过组合的方式来扩展功能。 其本质是转换匹配,复用功能。 举个日志系统的例子,原来是通过写磁盘的方式进行存储,后来因为查询不便,需要额外添加往数据库写日志 的功能(写文件和数据库并存): #include <string> #include <vector> using namespace std; class LogSys { public: LogSys() {} void WriteLog(const vector<string> &) { // ... 日志id 时间戳 服务器id 具体日志内容 roleid } vector<string>& ReadLog() { // ... vector<string> data /* = ...*/; return data; } }; class DB; // 面向接口编程 而不是具体类 强依赖 耦合性高 mysql mongo class LogSysEx : public LogSys { public: LogSysEx(DB *db) : _db(db) {} void AddLog(const vector<string> &data) { LogSys::WriteLog(data); /* 这里调用 _db 的方法将 data 数据存储到数据库 */ } void DelLog(const int logid) { vector<string>& data = LogSys::ReadLog(); // 从 vector<string> 中删除 logid的日志 LogSys::WriteLog(data); // 调用 _db 的方法将 logid的日志删除 } void UpdateLog(const int logid, const string &udt) { vector<string>& data = LogSys::ReadLog(); // 从 vector<string> 中更新 logid的日志 udt LogSys::WriteLog(data); // 调用 _db 的方法将 logid的日志更改 } string& LocateLog(const int logid) { vector<string>& data = LogSys::ReadLog(); string log1 /* = from log file*/; string log2 /* = from db */; string temp = log1 + ";" + log2; return temp; } private: DB* _db; }; 咱就是说在对一个类进行扩展修改的时候,尽量不要动原来的代码,而是通过继承组合的方式创建新的类,在这个新的类里实现新功能和包含旧功能。 10. 代理模式 代理模式是为其他对象提供一种代理以控制对这对象的访问。 比如远程代理(隐藏一个对象存在不同的地址空间的事实),虚代理(延迟加载lazyload),保护 代理(在代理前后做额外操作,权限管理,引用计数等); 在分布式系统中,actor模型(skynet)等会常用到代理模式。 其本质是控制对象访问。 举个例子,在有些系统中,为了某些对象的纯粹性,只进行了功能相关封装(稳定点),后期添加了其他功能 需要对该对象进行额外操作(变化点),为了隔离变化点(也就是不直接在稳定点进行修改,这样 会让稳定点也变得不稳定),可以抽象一层代理层: class ISubject { public: virtual void Handle() = 0; virtual ~ISubject() {} }; // 该类在当前进程,也可能在其他进程当中 class RealSubject : public ISubject { public: virtual void Handle() { // 只完成功能相关的操作,不做其他模块的判断 } }; // 在当前进程当中 只会在某个模块中使用 class Proxy1 : public ISubject { public: Proxy1(ISubject *subject) : _subject(subject) {} virtual void Handle() { // 在访问 RealSubject 之前做一些处理 //if (不满足条件) // return; _subject->Handle(); count++; // 在访问 RealSubject 之后做一些处理 } private: ISubject* _subject; static int count; }; int Proxy1::count = 0; // 在分布式系统当中 skynet actor class Proxy2 : public ISubject { public: virtual void Handle() { // 在访问 RealSubject 之前做一些处理 // 发送到数据到远端 网络处理 同步非阻塞 ntyco c协程 //IResult * val = rpc->call("RealSubject", "Handle"); // 在访问 RealSubject 之后做一些处理 } private: /*void callback(IResult * val) { // 在访问 RealSubject 之后做一些处理 }*/ }; 上面代码中proxy类可以继承ISubject类也可以不继承,写成继承的样式是为了让程序员明白这是该类的一个代理模块,如果不是继承的,看起来会和适配器模式很像。 proxy类中的基类指针可以改成一个容器,管理多个对象,这在游戏服务器的网关里经常看到。 ———————————————— 原文链接:https://blog.csdn.net/zhoujiajie0521/article/details/122194799
-
synchronized关键字是Java中用于控制多线程访问共享资源的一种机制,它确保同一时刻只有一个线程可以执行某个方法或代码块。其底层原理主要基于JVM(Java虚拟机)中的Monitor(监视器)机制来实现。以下是synchronized关键字的详细底层原理:1. Monitor机制Monitor(监视器):是Java虚拟机中的一种内置同步机制,每个Java对象都有一个与之关联的Monitor。Monitor通过内部的一些同步原语(如lock和unlock指令)来实现线程间的互斥访问和协调。锁的获取与释放:当一个线程尝试进入被synchronized修饰的代码块或方法时,它首先需要获取对象的Monitor。如果Monitor已被其他线程持有,则当前线程将被阻塞,直到Monitor变为可用状态。当线程完成执行后,它会释放Monitor,以便其他线程可以获取并继续执行。2. 锁的升级偏向锁(Biased Locking):Java 6及以后版本中引入的一种锁优化技术。如果某个对象锁在整个运行过程中只被一个线程持有,那么JVM会将该锁升级为偏向锁,以减少锁获取的开销。偏向锁通过CAS(Compare-And-Swap)操作将线程ID设置到对象的Mark Word中,以表示该对象锁已被当前线程占有。轻量级锁(Lightweight Locking):当存在多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。轻量级锁通过CAS操作在JVM的栈帧中创建锁记录(Lock Record),并将锁对象的Mark Word指向该锁记录。如果锁记录中的Mark Word与锁对象的Mark Word一致,则当前线程获取锁成功;否则,表示存在锁竞争,可能需要升级到重量级锁。重量级锁(Heavyweight Locking):当轻量级锁无法满足需求时(如多个线程长时间竞争同一个锁),锁会升级为重量级锁。重量级锁依赖于操作系统层面的互斥量(Mutex)来实现,涉及到用户态和内核态的切换,成本较高。3. 锁的释放与等待/通知锁的释放:当线程完成synchronized代码块的执行后,它会通过unlock指令释放Monitor,并将Monitor返还给对象池,以便其他线程可以获取。等待/通知:在synchronized代码块中,线程可以通过调用对象的wait()方法进入等待状态,并释放Monitor。其他线程可以通过调用对象的notify()或notifyAll()方法唤醒等待的线程。这些操作都是通过操纵与对象关联的Monitor来实现的。4. 锁的获取流程线程尝试获取对象的Monitor。如果Monitor未被其他线程持有,则当前线程获取Monitor并继续执行。如果Monitor已被其他线程持有,则当前线程进入阻塞状态,等待Monitor变为可用。当Monitor变为可用时,当前线程重新尝试获取Monitor。5. 注意事项synchronized关键字是JVM层面的机制,其实现依赖于JVM的具体实现(如HotSpot)。锁的升级和降级是JVM自动进行的,无需开发者手动干预。在使用synchronized时,应尽量避免长时间持有锁,以减少线程阻塞和死锁的风险。综上所述,synchronized关键字的底层原理主要基于JVM中的Monitor机制,通过锁的获取、释放、升级和降级等机制来实现线程间的同步控制。
-
在Java中,锁是并发编程中用来控制多个线程对共享资源的访问的重要机制。Java的synchronized关键字和ReentrantLock等都是实现锁的方式。Java虚拟机(JVM)为了提高锁的性能,引入了偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)和重量级锁(Heavyweight Locking)三种锁状态。这三种锁状态的转换主要是为了提高锁的性能,减少线程在获取锁时的开销。1. 偏向锁(Biased Locking)偏向锁是Java 6引入的一种锁优化,它假设大多数情况下,锁不存在多线程竞争,锁总是由同一线程多次获得。当一个线程访问同步块并获取锁时,锁会进入偏向模式,此时Mark Word里会存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下锁是否还指向当前线程ID即可。如果测试成功,表示线程已经获得了锁。如果失败,则尝试使用轻量级锁。偏向锁的优势在于,当锁只被一个线程访问时,可以极大地减少锁的获取和释放的开销。2. 轻量级锁(Lightweight Locking)轻量级锁是相对于使用操作系统互斥量(Mutex)来实现的传统锁而言的。它用于解决在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当锁处于偏向锁状态,但发生线程竞争,或者锁一开始就不是偏向锁时,就会尝试使用轻量级锁。轻量级锁通过CAS(Compare-And-Swap)操作来尝试把锁对象的Mark Word更新为指向当前线程的栈帧中的Lock Record的指针。如果成功,当前线程就获得了锁;如果失败,表示有其他线程竞争锁,锁会膨胀为重量级锁。3. 重量级锁(Heavyweight Locking)重量级锁是传统意义上的锁,它依赖于底层操作系统的Mutex Lock来实现。当锁膨胀为重量级锁时,意味着线程竞争非常激烈,JVM会调用操作系统的互斥量来实现同步控制。线程阻塞和唤醒都需要操作系统的帮助,因此开销较大。总结偏向锁、轻量级锁和重量级锁是Java虚拟机为了提高锁的性能而引入的三种锁状态。它们之间的转换是JVM根据锁的竞争情况自动进行的。偏向锁适用于锁只被一个线程访问的场景;轻量级锁适用于锁被少量线程竞争的场景;重量级锁适用于锁被多个线程激烈竞争的场景。这三种锁状态的存在,使得Java的锁机制能够灵活应对不同场景下的并发需求,从而提高程序的性能。
-
Arthas是一款由Alibaba开源的Java诊断工具,它能够在不重启JVM的情况下,实时查看和修改JVM的运行参数,从而帮助开发者诊断和解决生产环境中的性能问题。以下是Arthas中JVM相关命令的详细解析:1. dashboard功能:查看当前系统的实时数据面板,包括CPU、内存、线程、GC等关键指标。参数:-i:刷新实时数据的时间间隔(ms),默认5000ms。使用示例:dashboard -i 10000:间隔10秒刷新一次大盘信息。注意:此命令是一个可视化的控制台,显示CPU、内存等使用指标以及线程的状态,是初步查看系统性能问题的常用命令。2. thread功能:查看当前JVM的线程堆栈信息。参数:id:指定线程ID,查看该线程的运行堆栈。-n <N>:指定最忙的前N个线程并打印堆栈。-b:找出当前阻塞其他线程的线程。-i <value>:指定CPU使用率统计的采样间隔,单位为毫秒,默认值为200。--all:显示所有匹配的线程。使用示例:thread:显示当前线程。thread -n 3:显示最忙的前3个线程并打印堆栈。thread --state RUNNABLE:查看运行时状态的线程。thread -b:打印当前阻塞其他线程的线程。3. jvm功能:查看当前JVM的信息,包括线程数、文件描述符数、内存信息等。使用示例:jvm:查看JVM的基本信息。数据说明:THREAD相关:包括活跃的线程数、守护线程数、曾经活着的最大线程数等。文件描述符相关:包括最大可以打开的文件描述符数、当前打开的文件描述符数等。内存信息:包括堆大小、非堆大小、代码缓存、元空间等。4. memory功能:查看JVM的内存信息,包括堆内存和非堆内存的使用情况。使用示例:memory:查看JVM的内存信息。5. vmoption功能:查看和修改JVM里诊断相关的参数。参数:查看所有option:vmoption查看指定option:vmoption <optionName>更新指定option(如果可写):vmoption <optionName> <newValue>注意:不是所有的JVM启动参数都可以在运行时通过vmoption命令进行修改。一些关键参数(如-Xms和-Xmx)在JVM启动时设置,并且在运行期间不能修改。6. heapdump功能:dump Java堆内存,类似于jmap命令的heap dump功能。参数:--live:可选参数,表示只打印有活跃引用的对象,丢弃进行垃圾回收的对象。[文件路径/文件名.hprof]:指定dump文件的保存路径和名称。使用示例:heapdump --live /root/test/study-06-01.hprof:dump当前JVM的堆内存到指定文件,只包含活跃对象。7. 其他命令getstatic:查看类的静态属性。不推荐使用,推荐使用ognl命令。ognl:执行OGNL表达式,用于更复杂的对象属性访问和操作。mbean:查看MBean的信息。MBean是一种规范的JavaBean,用于在JMX中注册和管理。sysenv:查看JVM的环境变量。sysprop:查看和修改JVM的系统属性。vmtool:从JVM里查询对象,执行forceGC等操作。总结Arthas提供的JVM相关命令覆盖了线程、内存、JVM信息、堆dump等多个方面,为Java应用的性能诊断和调优提供了强大的工具。开发者可以根据实际需要选择合适的命令来监控和分析JVM的性能问题。
-
探索高效能搜索引擎新境界:Easy-Es 项目介绍 Easy-Es,一个致力于简化Elasticsearch操作的开源框架,它提供了一种全自动智能索引托管模式,让开发者不必再为繁琐的索引管理而烦恼。与传统的SpringData-Elasticsearch相比,Easy-Es在功能丰富度、易用性和性能上都有着显著优势。它采用了Elasticsearch官方的RestHighLevelClient,确保原始性能的同时,带来了更高的灵活性。 项目技术分析 Easy-Es的独特之处在于它的全自动索引托管,这是全球开源领域的创新之举。它智能地处理索引的创建、更新以及数据迁移,整个过程无需停机,用户无感知。此外,框架还能智能推断字段类型,避免了因不当使用导致的问题。其语法设计借鉴了Mybatis-Plus,降低了学习曲线,使得即便是对Elasticsearch不甚熟悉的开发者也能快速上手。 项目及技术应用场景 无论是大型企业还是初创公司,只要有大量数据搜索的需求,Easy-Es都能大显身手。例如,在电子商务网站中,它可以实现高效的商品搜索;在新闻门户中,用于实时热点新闻的检索;在社交媒体平台,支持用户动态的快速定位。其丰富的功能涵盖了MySQL的大部分特性,并针对Elasticsearch特有的如分词、权重、高亮、地理位置和IP地址查询提供了支持。 项目特点 全自动索引托管 - 解放开发者,专注于业务逻辑。 智能字段类型推断 - 减少错误,提升效率。 屏蔽语言差异 - MySQL语法友好,易于理解。 代码量极低 - 相较于直接使用RestHighLevelClient,代码量大大减少。 零魔法值 - 字段名直接从实体获取,清晰无混淆。 低学习成本 - 类似Mybatis-Plus的语法,易于迁移。 强大的功能集 - 支持多种高级查询和Elasticsearch特性。 语法优雅 - Lambda风格链式编程,增强代码可读性。 安全可靠 - 完全的墨菲安全扫描,代码覆盖率达到95%以上。 完善文档 - 中英文双语文档,助您迅速上手。 社区支持与贡献 Easy-Es有一个活跃的技术交流群,有专门的健身教练为您解答技术问题和分享健康计划。此外,项目在GitHub和Gitee上的官方主页包含了详细的教程、示例和最新版本信息。您的星标、关注和fork都是对我们工作的极大鼓励! 立即加入Easy-Es的世界,开启高效、便捷的Elasticsearch开发之旅吧!让我们一起打造更好的开源生态,为中国的开发者社区贡献力量。 ———————————————— 原文链接:https://blog.csdn.net/gitblog_00045/article/details/138841825
-
背景 目前公司的一个老项目,查询贼慢,需要想办法提升一下速度,于是就想到了ES,现在尝试一下将ES整合到项目中来提升检索效率。 ES是基于倒排索引实现的,倒排索引中一个表相当于一个索引,表中的每条记录都是一个文档(JSON数据),系统会先对字段数据进行分词,然后给词条建立索引,并映射到文档id。在查询的时候根据输入进行分词,然后根据词条走索引查询文档id,再根据文档id查询文档并放入结果集,最后将结果集返回。 一般来说,ES算是难度较高的一个技术栈,需要中高级才能熟练驾驭,新手入门比较难,因而我选中了对新手更加友好的easy-es,其在ES的基础上做了封装,使得使用起来和MybatisPlus很像,简单上手。 开始使用easy-es之前,建议先看一下避坑指南 Elastic Search下载 ES官网 按照官方推荐下载7.x的ES,我下载了和官方demo一样的7.14.0版本。 输入7.14.0搜索该版本并下载 下载完成之后解压,并去到bin目录,双击elasticsearch.bat文件启动elasticsearch。 Springboot整合ES 打开Springboot项目(或创建一个Springboot项目),先全局搜索elastic,看看项目是否已经引入过ES,如果有,需要去掉或者更改版本为7.14.0。印象中不同版本的Springboot默认引入的一定版本的ES。 在POM文件引入依赖 <!-- 引入easy-es最新版本的依赖--> <dependency> <groupId>org.dromara.easy-es</groupId> <artifactId>easy-es-boot-starter</artifactId> <!--这里Latest Version是指最新版本的依赖,比如2.0.0,可以通过下面的图片获取--> <version>2.0.0-beta4</version> </dependency> <!-- 排除springboot中内置的es依赖,以防和easy-es中的依赖冲突--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </exclusion> <exclusion> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.14.0</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.14.0</version> </dependency> YAML文件增加ES配置(更多配置可以在官网看) easy-es: enable: true #默认为true,若为false则认为不启用本框架 address: 127.0.0.1:9200 # es的连接地址,必须含端口 若为集群,则可以用逗号隔开 例如:127.0.0.1:9200,127.0.0.2:9200 # username: elastic #若无 则可省略此行配置。因为刚下载的ES默认不用账号密码登录,所以注掉 # password: WG7WVmuNMtM4GwNYkyWH #若无 则可省略此行配置 在启动类设置ES的mapper包扫描路径(一般应该设置成带*的通配格式,方便多模块项目的扫描。)同时注意,该mapper的包和mybatisplus的包不能是同一个,不然框架区分不开。 @EsMapperScan("org.jeecg.modules.test.esmapper") @SpringBootApplication public class JeecgSystemApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(JeecgSystemApplication.class); } public static void main(String[] args) throws UnknownHostException { ConfigurableApplicationContext application = SpringApplication.run(JeecgSystemApplication.class, args); } } 创建实体类(加上@IndexName注解) package org.jeecg.modules.message.entity; import org.dromara.easyes.annotation.IndexField; import org.dromara.easyes.annotation.IndexName; import org.dromara.easyes.annotation.rely.Analyzer; import org.dromara.easyes.annotation.rely.FieldType; import org.jeecg.common.aspect.annotation.Dict; import org.jeecg.common.system.base.entity.JeecgEntity; import org.jeecgframework.poi.excel.annotation.Excel; import org.springframework.format.annotation.DateTimeFormat; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; @Data @IndexName public class SysMessage{ /** ID */ @TableId(type = IdType.ASSIGN_ID) private java.lang.String id; /**推送内容*/ private java.lang.String esContent; /**推送所需参数Json格式*/ private java.lang.String esParam; /**接收人*/ private java.lang.String esReceiver; /**推送失败原因*/ private java.lang.String esResult; /**发送次数*/ private java.lang.Integer esSendNum; /**推送状态 0未推送 1推送成功 2推送失败*/ private java.lang.String esSendStatus; /**推送时间*/ @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") private java.util.Date esSendTime; /**消息标题*/ private java.lang.String esTitle; /**推送方式:1短信 2邮件 3微信*/ private java.lang.String esType; /**备注*/ private java.lang.String remark; /** 创建人 */ private java.lang.String createBy; /** 创建时间 */ @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private java.util.Date createTime; /** 更新人 */ private java.lang.String updateBy; /** 更新时间 */ @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private java.util.Date updateTime; } 创建mapper类 package org.jeecg.modules.test.esmapper; import org.dromara.easyes.core.core.BaseEsMapper; import org.jeecg.modules.message.entity.SysMessage; public interface DocumentMapper extends BaseEsMapper<SysMessage> { } 编写测试类 @Resource private DocumentMapper documentMapper; @GetMapping("/createIndex") @ApiOperation("创建索引") public Object createIndex(String a){ Boolean index = documentMapper.createIndex(); System.out.println(index); return index; } @GetMapping("/createDoc") @ApiOperation("创建文档") public Object createDoc(String a,String b){ SysMessage sysMessage=new SysMessage(); sysMessage.setEsContent(a); sysMessage.setEsTitle(b); sysMessage.setEsReceiver("系统管理员"); sysMessage.setEsSendNum(10); Integer insert = documentMapper.insert(sysMessage); return insert; } @GetMapping("/updateDoc") @ApiOperation("updateDoc") public Object updateDoc(String a){ LambdaEsUpdateWrapper<SysMessage> wrapper=new LambdaEsUpdateWrapper<>(); wrapper.eq(SysMessage::getEsContent,a) .set(SysMessage::getEsContent,"更改后的标题"); Integer update = documentMapper.update(null, wrapper); return update; } @GetMapping("/getDoc") @ApiOperation("查询文档") public Object getDoc(String a){ List<SysMessage> list = EsWrappers.lambdaChainQuery(documentMapper).like(SysMessage::getEsContent, a).list(); return list; } 浏览器安装一个ES可视化插件。我安装的是es-client 添加连接 选中创建的连接,目前还没有索引。 测试 启动Springboot项目,调用createIndex接口,创建索引 。然后回到浏览器插件,点击刷新,可以看到创建了一个索引。 调用createDoc接口,创建一个文档记录 点击左侧的数据展示选项,右上角选中创建的索引,点击刷新,可以看到多了一条记录。 调用getDoc接口,查询记录,成功查出。 更新的语法和MybatisPlus的wrapper差不多。先用查询条件eq,in等去筛选要更新的记录,然后用set去设置新的值,然后调用update方法即可。(如下,通过“测试内容”找到记录,并将其的标题改成新的内容) 在浏览器刷新,可以看到数据更新了。 删除的语法比更新还简单,也是创建一个esupdatewrapper,用eq、in等筛选,然后调用delete方法就可以了,就不演示了。 总结 Springboot整合ES最大可能遇到的问题就是ES版本的问题,也就是依赖冲突。如果依赖冲突,在项目启动的时候会有一个ERROR日志提醒,看到了就想办法去掉原来带着的ES依赖或者更改依赖版本为7.14.0 关于ES的数据更新,就要去了解ES同步数据库相关的知识了。 想要了解easy-es的更多特性,建议去看easy-es的官网文档。 ———————————————— 原文链接:https://blog.csdn.net/weixin_43975276/article/details/134670744
-
前言 最近笔者为了捡回以前自学的ES知识,准备重新对ES的一些基础使用做个大致学习总结。然后在摸鱼逛开源社区时无意中发现了一款不错的ElasticSearch插件-Easy-ES,可称之为“ES界的MyBatis-Plus”。联想到之前每次用RestHighLevelClient写一些DSL操作时都很麻烦(复杂点的搜索代码量确实不少),加之用过MyBatisPlus,深感其对于简化开发、提高效率确实有一套,不知道这个Easy-ES能高效到什么水平,因此抱着学习的心态结合其文档一探究竟。 Easy-ES介绍 Easy-Es(简称EE)是一款基于ElasticSearch(简称Es)官方提供的RestHighLevelClient打造的ORM开发框架,在 RestHighLevelClient 的基础上,只做增强不做改变,为简化开发、提高效率而生,属于由国内开发者打造并完全开源的ElasticSearch-ORM框架! 因为它采用和Mybatis-Plus一致的语法设计,一定程度上能够显著降低ElasticSearch搜索引擎使用门槛,和额外学习成本,并大幅减少开发者工作量,帮助企业降本提效。如果有用过Mybatis-Plus(简称MP),那么基本可以零学习成本直接上手EE,EE是MP的Es平替版,在有些方面甚至比MP更简单,同时也融入了更多Es独有的功能,助力咱们快速实现各种场景的开发. 优势点 全自动索引托管: 全球开源首创的索引托管模式,开发者无需关心索引的创建更新及数据迁移等繁琐步骤,索引全生命周期皆可托管给框架,由框架自动完成,过程零停机,用户无感知,彻底解放开发者 智能字段类型推断: 根据索引类型和当前查询类型上下文综合智能判断当前查询是否需要拼接.keyword后缀,减少小白误用的可能 屏蔽语言差异: 开发者只需要会MySQL语法即可使用Es,真正做到一通百通,无需学习枯燥易忘的Es语法,Es使用相对MySQL较低频,学了长期不用也会忘,没必要浪费这时间,开发就应该专注于业务 代码量极少: 与直接使用RestHighLevelClient相比,相同的查询平均可以节省3-5倍左右的代码量 零魔法值: 字段名称直接从实体中获取,无需输入字段名称字符串这种魔法值,提高代码可读性,杜绝因字段名称修改而代码漏改带来的Bug 零额外学习成本: 开发者只要会国内最受欢迎的Mybatis-Plus语法,即可无缝迁移至EE,EE采用和前者相同的语法,消除使用者额外学习成本,直接上手,爽 降低开发者门槛: Es通常需要中高级开发者才能驾驭,但通过接入EE,即便是只了解ES基础的初学者也可以轻松驾驭ES完成绝大多数需求的开发,可以提高人员利用率,降低企业成本 主要特点 无侵入:只做增强不做改变,引入它不会对现有工程产生影响 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作 强大的 CRUD 操作:内置通用 Mapper,仅仅通过少量配置即可实现大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错段 支持主键自动生成:支持2 种主键策略,可自由配置,完美解决主键问题 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere ) 内置分页插件:基于RestHighLevelClient 物理分页,开发者无需关心具体操作,且无需额外配置插件,写分页等同于普通 List 查询,且保持和PageHelper插件同样的分页返回字段,无需担心命名影响 ES功能全覆盖:ES中支持的功能通过EE都可以轻松实现 支持ES高阶语法:支持高亮搜索,分词查询,权重查询,Geo地理位置查询,IP查询,聚合查询等高阶语法 良好的拓展性:底层仍使用RestHighLevelClient,可保持其拓展性,开发者在使用EE的同时,仍可使用RestHighLevelClient的功能 …… 与Spring Data的功能对比 由于ES本身的高复杂性和高门槛,以及相比MySQL更少的用户群体,这块领域高投入,低回报,因此像ES这类的ORM框架并不多,截止目前除了Springdata-Es几乎没有竞对,两者在使用体感上可以类比Mybait-Plus与SpringData-JPA,由于双方底层都是ES官方套件,所以对比ES官方套件本身就支持的原生查询功能毫无意义,于是笔者根据Easy-ES汇总的功能对比如下: Easy-ES SpringData-ES 语法 支持 支持 索引自动创建 支持 不支持 索引自动更新 支持 不支持 索引手动创建及更新 支持 不支持 简单CRUD 支持 支持 复杂CRUD 支持 不支持 父子查询 支持 不支持 嵌套查询 支持 不支持 排序及权重 支持 不支持 分页查询 支持全部三种模式 仅支持一种 GEO地理位置查询 支持 不支持 聚合查询 支持 不支持 字段类型及查询推断 支持 不支持 数据自动平滑迁移 支持 不支持 性能 高(优于SpringData20%) 高 代码量 极低 中低 与原生查询的语法对比 再回头看看传统的原生查询操作: // ES原生的RestHighLevel语法 List<Integer> values = Arrays.asList(2, 3); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); boolQueryBuilder.must(QueryBuilders.termQuery("business_type", 1)); boolQueryBuilder.must(QueryBuilders.boolQuery() .must(QueryBuilders.termQuery("state", 9)) .should(QueryBuilders.boolQuery().must(QueryBuilders.termQuery("state", 8)) .must(QueryBuilders.termQuery("bidding_sign", 1)))); boolQueryBuilder.should(QueryBuilders.boolQuery().must(QueryBuilders.termQuery("business_type", 2)) .must(QueryBuilders.termsQuery("state", values))); 在对比Easy-Es的简化后: // Easy-Es及Mybatis-Plus语法 wrapper.eq("business_type", 1) .and(a -> a.eq("state", 9).or(b -> b.eq("state", 8).eq("bidding_sign", 1))) .or(i -> i.eq("business_type", 2).in("state", 2, 3)); 综合看来,Easy-ES基本实碾压的姿态...可能有夸大嫌疑哈,不过文末笔者也会根据自己的一些了解做点简单总结~ 下面接着看~ 使用注意 官方文档很早就把这块放在使用手册前面了,并且明确说明了,该框架在使用时需要避坑的地方:由于开发者开发Easy-ES时底层用了ES官方的RestHighLevelClient,所以对ES版本有要求,要求ES和RestHighLevelClient JAR依赖版本必须为7.14.0,至于es客户端,实际7.X任意版本都可以很好的兼容。 值得注意的是,由于SpringData-ElasticSearch的存在,Springboot它内置了和ES及RestHighLevelClient依赖版本,这导致了不同版本的Springboot实际引入的ES及RestHighLevelClient 版本不同,而ES官方的这两个依赖在不同版本间的兼容性非常差,进一步导致很多用户无法正常使用Easy-Es。可谓非常良心,也就是说在使用时必须指定ES和RestHighLevelClient JAR依赖版本必须为7.14.0,其实笔者也试了一下,因为笔者本地ES是7.6.1的,在不排除依赖冲突的时候,其实也可以正常运行项目的,并且执行一些基础操作也是可以的(低版本的没试过)。但是会报错: 部分操作是正常的,但是总有这个异常看着也很难受,因此在后续实践中也改为了7.14.0,不知道随着ES8.0出现后,这个框架会不会优化更新,敬请期待~ 排除依赖冲突也很容易,直接照着避坑指南重新引入7.14.0依赖即可: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </exclusion> <exclusion> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.14.0</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.14.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.dromara.easy-es/easy-es-boot-starter --> <dependency> <groupId>org.dromara.easy-es</groupId> <artifactId>easy-es-boot-starter</artifactId> <version>2.0.0-beta2</version> </dependency> 后续简单配置一些基础配置后运行就正常了,客户端保持7.6.1版本也不影响使用。 基础配置介绍 这里主要是在yml文件中对ES的一些基础配置,如果并没有太多需求,只要配置个地址即可,如果ES设置了账号密码认证,相应配置上账号密码即可。 easy-es: banner: true address: 127.0.0.1:9200 # es连接地址+端口 格式必须为ip:port,如果是集群则可用逗号隔开 connect-timeout: 5000 # username: zhangsan # password: 123456 另外如果为了提高生产环境性能,也持支按需配置: easy-es: keep-alive-millis: 1000 # 心跳策略时间 单位:ms connect-timeout: 2000 # 连接超时时间 单位:ms socket-timeout: 3000 # 通信超时时间 单位:ms request-timeout: 4000 # 请求超时时间 单位:ms connection-request-timeout: 5000 # 连接请求超时时间 单位:ms max-conn-total: 20 # 最大连接数 单位:个 max-conn-per-route: 20 # 最大连接路由数 单位:个 #其他全局配置---------------------------------------------------- enable: true # 是否开启Easy-ES自动配置 默认开启,为false时则不启用该框架 schema: http # 默认为http 可缺省 banner: true # 默认为true 打印banner 若您不期望打印banner,可配置为false global-config: process-index-mode: smoothly #索引处理模式,smoothly:平滑模式,默认开启此模式, not_smoothly:非平滑模式, manual:手动模式 print-dsl: true # 开启控制台打印通过本框架生成的DSL语句,默认为开启,测试稳定后的生产环境建议关闭,以提升少量性能 distributed: false # 当前项目是否分布式项目,默认为true,在非手动托管索引模式下,若为分布式项目则会获取分布式锁,非分布式项目只需synchronized锁. reindexTimeOutHours: 72 # 重建索引超时时间 单位小时,默认72H 可根据ES中存储的数据量调整 async-process-index-blocking: true # 异步处理索引是否阻塞主线程 默认阻塞 数据量过大时调整为非阻塞异步进行 项目启动更快 active-release-index-max-retry: 4320 # 分布式环境下,平滑模式,当前客户端激活最新索引最大重试次数,若数据量过大,重建索引数据迁移时间超过4320/60=72H,可调大此参数值,此参数值决定最大重试次数,超出此次数后仍未成功,则终止重试并记录异常日志 active-release-index-fixed-delay: 60 # 分布式环境下,平滑模式,当前客户端激活最新索引最大重试次数 分布式环境下,平滑模式,当前客户端激活最新索引重试时间间隔 若您期望最终一致性的时效性更高,可调小此值,但会牺牲一些性能 db-config: map-underscore-to-camel-case: false # 是否开启下划线转驼峰 默认为false index-prefix: daily_ # 索引前缀,可用于区分环境 默认为空 用法和MP的tablePrefix一样的作用和用法 id-type: customize # id生成策略 customize为自定义,id值由用户生成,比如取MySQL中的数据id,如缺省此项配置,则id默认策略为es自动生成 field-strategy: not_empty # 字段更新策略 默认为not_null enable-track-total-hits: true # 默认开启,开启后查询所有匹配数据,若不开启,会导致无法获取数据总条数,其它功能不受影响,若查询数量突破1W条时,需要同步调整@IndexName注解中的maxResultWindow也大于1w,并重建索引后方可在后续查询中生效(不推荐,建议分页查询). refresh-policy: immediate # 数据刷新策略,默认为不刷新,若对数据时效性要求比较高,可以调整为immediate,但性能损耗高,也可以调整为折中的wait_until batch-update-threshold: 10000 # 批量更新接口的阈值 默认值为1万,突破此值需要同步调整enable-track-total-hits=true,@IndexName.maxResultWindow > 1w,并重建索引. smartAddKeywordSuffix: true # 是否智能为字段添加.keyword后缀 默认开启,开启后会根据当前字段的索引类型及当前查询类型自动推断本次查询是否需要拼接.keyword后缀 如果需要实时记录DSL的执行日志,也可以进行相应的日志信息打印配置: #开启es的DSL日志 logging: level: trace: trace 核心注解使用介绍 在Easy-ES中也有相对于MyBatisPlus那样的注解支持。比较重点的注解有如下4个: @EsMapperScan @IndexName @IndexId @IndexField @EsMapperScan 这个注解类似于Mybatis框架的mapper扫描注解。在ES项目中只要我们的mapper接口继承BaseEsMapper<>就能调用内部封装的可供直接使用的方法。 package com.yy.config.mapper; import com.yy.config.pojo.TestUser; import org.dromara.easyes.core.core.BaseEsMapper; import org.springframework.stereotype.Component; /** * @author young * Date 2023/5/25 16:01 * Description: springboot-demo08-elasticsearch */ @Component public interface TestMapper extends BaseEsMapper<TestUser> { } 但是前提是需要@EsMapperScan扫描到改包,在SpringBoot项目启动类上标明mapper接口所在包的位置@EsMapperScan("com.yy.config.mapper")即可,否则是会出错的。另外为了区别于MyBatis的扫描注解扫描mapper(dao)接口,因为两个框架彼此独立,扫描的时候没办法隔离,所以格外需要注意将MyBatis扫描的包与Easy-ES扫描的包区分开来,不能共用一个,否则也会报错! @IndexName 同MyBatisPlus中的@TableName注解一样,主要是在实体类中标识对应的索引名称,因为ES中没有表的概念,而是对应的Index。其字段功能如下表所示: 属性 类型 必须指定 默认值 描述 value String 否 "" 索引名,可简单理解为MySQL表名 shardsNum int 否 1 索引分片数 replicasNum int 否 1 索引副本数 aliasName String 否 "" 索引别名 keepGlobalPrefix boolean 否 false 是否保持使用全局的 tablePrefix 的值,与MP用法一致 child boolean 否 false 是否子文档 childClass Class 否 DefaultChildClass.class 父子文档-子文档类 maxResultWindow int 否 10000 分页返回的最大数据量,默认值为1万条,超出推荐使用searchAfter或滚动查询等方式,详见拓展功能章节. 当此值调整至大于1W后,需要重建索引并同步开启配置文件中的enable-track-total-hits=true方可生效 routing String 否 "" 路由,CRUD作用的路由 如果在实体类上不使用该注解,则ES会默认将实体类名作为索引名。如果有全局配置或者自动生成过索引名,但是也用注解指定了,则优先级排序: 注解索引>全局配置索引前缀>自动生成。另外Easy-ES也支持动态索引名称,可以调用mapper或者CRUD中的wrapper修改索引名称。 @IndexId 这个同样对应@TableId,可以指定id的生成类型并且标识索引id。在ES中如果实体类中有一个类型为String的id,在不添加该注解的条件下,会默认将该id识别为ES中的_id。如果是其他名称(比如ids),则会新建ids的索引字段。此时如果用@IndexId注解标识该ids,则不会创建新字段,而是映射为 _id。 @IndexField 用于实体类字段的注解。标识实体类中被作为ES索引字段的字段,参数功能如下: 属性 类型 必须指定 默认值 描述 value String 否 "" 字段名 exist boolean 否 true 字段是否存在 fieldType Enum 否 FieldType.NONE 字段在es索引中的类型 fieldData boolean 否 false text类型字段是否支持聚合 analyzer String 否 Analyzer.NONE 索引文档时用的分词器 searchAnalyzer String 否 Analyzer.NONE 查询分词器 strategy Enum 否 FieldStrategy.DEFAULT 字段验证策略 dateFormat String 否 "" es索引中的日期格式,如yyyy-MM-dd nestedClass Class 否 DefaultNestedClass.class 嵌套类 parentName String 否 "" 父子文档-父名称 childName String 否 "" 父子文档-子名称 joinFieldClass Class 否 JoinField.class 父子文档-父子类型关系字段类 ignoreCase boolean 否 false keyword类型字段是否忽略大小写 可根据自己不同场景的应用需求灵活配置: public class TestUser { private String stephen; // 场景一:标记es中不存在的字段 @IndexField(exist = false) private String token; // 场景二:更新时,此字段非空字符串才会被更新 @IndexField(strategy = FieldStrategy.NOT_EMPTY) private String description; // 场景三: 指定fieldData @IndexField(fieldType = FieldType.TEXT, fieldData = true) private String content; // 场景四:自定义字段名 @IndexField("my_music") private String music; // 场景五:支持日期字段在es索引中的format类型 @IndexField(fieldType = FieldType.DATE, dateFormat = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis") private String createTime; // 场景六:支持指定字段在es索引中的分词器类型 @IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART, searchAnalyzer = Analyzer.IK_MAX_WORD) private String username; // 场景七:支持指定字段在es的索引中忽略大小写,以便在term查询时不区分大小写,仅对keyword类型字段生效,es的规则,并非框架限制. @IndexField(fieldType = FieldType.KEYWORD, ignoreCase = true) private String title; } 额外需要注意的是strategy = FieldStrategy.NOT_EMPTY,它有三个可选配置: not_null: 非Null判断,字段值为非Null时,才会被更新 not_empty: 非空判断,字段值为非空字符串时才会被更新 ignore: 忽略判断,无论字段值为什么,都会被更新 并且该配置是可以在yml配置文件中全局配置的,但是也可以通过该注解个性化配置,不过此时:全局配置的优先级是小于注解配置 索引模式介绍 在ES中,索引的创建和更新不仅复杂,而且难于维护,一旦索引有变动,就必须面对索引重建带来的服务停机和数据丢失等问题... 尽管ES官方提供了索引别名机制来解决问题,但门槛依旧很高,步骤繁琐,在生产环境中由人工操作非常容易出现失误带来严重的问题. 为了解决这些痛点,Easy-Es提供了多种策略,将用户彻底从索引的维护中解放出来,并提供了多种索引处理策略,来满足不同用户的个性化需求。 其中它支持三种索引托管模式可供我们使用:自动平滑模式、自动非平滑模式和手动模式。 自动平滑模式 该模式相当于将索引的创建、更新、数据迁移等操作全部交由Easy-Es自动完成,过程零停机,连索引类型都可以自动推断,这个方式也是目前Easy-Es默认支持的方式之一。其核心处理流程如下图(来源于Easy-Es官网-索引托管模式): 需要注意的是:在自动托管模式下,系统会自动生成一条名为ee-distribute-lock的索引,该索引为框架内部使用,用户可忽略,若不幸因断电等其它因素极小概率下发生死锁,可删除该索引即可。另外,在使用时如碰到索引变更,原索引名称可能会被追加后缀s0或s1。关于s0和s1后缀,在此模式下无法避免,因为要保留原索引数据迁移,又不能同时存在两个同名索引。 自动非平滑模式 该模式下,索引额创建及更新由EE全自动异步完成,但不处理数据迁移工作,适合在开发及测试环境使用,当然如果使用logstash等其它工具来同步数据,亦可在生产环境开启此模式,在此模式下不会出现s0和s1后缀,索引会保持原名称。其核心流程如下图所示: 以上两种自动模式中,索引信息主要依托于实体类,如果用户未对该实体类进行任何配置,Easy-Es依然能够根据字段类型智能推断出该字段在ES中的存储类型。当然,仅靠框架自动推断是不够的,我们仍然建议您在使用中尽量进行详细的配置,以便框架能自动创建出生产级的索引。 ES的自动推断映射表如下: JAVA ES byte byte short short int integer long long float float double double BigDecimal keyword char keyword String keyword_text boolean boolean Date date LocalDate date LocalDateTime date List text ... ... 手动模式 在此模式下,索引的所有维护工作Easy-Es框架均不介入,由用户自行处理,Easy-Es提供了开箱即用的索引CRUD相关API,我们可以选择使用该API手动维护索引,由于API高度完善,就算自己创建也比原生简单。 在手动模式下,我们可以通过注解+mapper接口提供的createIndex方法创建索引。也可以通过api创建,每个需要被索引的字段都需要处理,比较繁琐,但灵活性最好,支持所有es能支持的所有索引创建,供0.01%场景使用(不推荐) @Test public void testCreatIndex() { LambdaEsIndexWrapper<TestUser> wrapper = new LambdaEsIndexWrapper<>(); wrapper.indexName(TestUser.class.getSimpleName().toLowerCase()); // 此处将文章标题映射为keyword类型(不支持分词),文档内容映射为text类型,可缺省 // 支持分词查询,内容分词器可指定,查询分词器也可指定,,均可缺省或只指定其中之一,不指定则为ES默认分词器(standard) wrapper.mapping(TestUser::getTitle, FieldType.KEYWORD) .mapping(TestUser::getContent, FieldType.TEXT,Analyzer.IK_MAX_WORD,Analyzer.IK_MAX_WORD); // 如果上述简单的mapping不能满足你业务需求,可自定义mapping Map<String, Object> map = new HashMap<>(); Map<String, Object> prop = new HashMap<>(); Map<String, String> field = new HashMap<>(); field.put("type", FieldType.KEYWORD.getType()); prop.put("this_is_field", field); map.put("properties", prop); wrapper.mapping(map); // 设置分片及副本信息,2个shards,1个replicas,可缺省 wrapper.settings(2,1); // 如果上述简单的settings不能满足你业务需求,可自定义settings // 设置别名信息,可缺省 String aliasName = "user"; wrapper.createAlias(aliasName); // 创建索引 boolean isOk = testUserMapper.createIndex(wrapper); } Tips:在使用手动模式时需要手动在yml文件中配置该模式: easy-es: global-config: process_index_mode: manual #索引处理模式,smoothly:平滑模式,默认开启此模式, not_smoothly:非平滑模式, manual:手动模式 测试项目准备 完成这些基础了解后可以试着使用Easy-ES的功能了。 Step1:导入依赖 在自己的项目中根据要求导入7.14.0的ES依赖(不影响7.x的客户端使用),并配置yml文件中的ES服务地址信息,上面也说过,如果只用RestHighLevelClient不配置也无所谓,但是这个Easy-Es需要手动配置。 Step2:配置yml文件中的ES地址 server: port: 8080 easy-es: banner: true address: 127.0.0.0:9200 global-config: distributed: false # connect-timeout: 5000 #开启es的DSL日志 logging: level: trace: trace spring: application: name: Easy-Es_Test 当然,配置这个好处就是不用我们手动再去配置RestHighLevelClient了,并且同样可以使用RestHighLevelClient来自定义需要的功能,保证了其扩展性。 Step3:创建ES独立的mapper接口 根据使用要求需要一个类MyBatis中的数据层接口继承Easy-Es的功能,这样就能使用其封装的功能了。但是不同之处在于省掉了service接口,相比MyBatisPlus更简洁了,功能全集中在mapper接口中。 package com.yy.config.mapper; import com.yy.config.pojo.TestUser; import org.dromara.easyes.core.core.BaseEsMapper; import org.springframework.stereotype.Component; /** * @author young * Date 2023/5/25 16:01 * Description: 继承Easy-Es功能的mapper接口 */ @Component public interface TestMapper extends BaseEsMapper<TestUser> { } Step4:扫描mapper接口 为了让mapper层功能正常使用,需要在SpringBoot项目启动类上扫描ES接口,并将其所在的包与MyBatis中的扫描包区分,不能将接口建在同一个扫描包路径下。 package com.yy; import org.dromara.easyes.starter.register.EsMapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EsMapperScan("com.yy.config.mapper") public class SpringbootDemo08ElasticsearchApplication { public static void main(String[] args) { SpringApplication.run(SpringbootDemo08ElasticsearchApplication.class, args); } } 功能使用 搭建好Easy-Es的基础使用框架后现在就可以试一下这个框架的功能咋样了。 数据CRUD 在Easy-Es中推荐了两种条件封装方式以便我们进行数据操作:创建LambdaEsQueryWrapper对象以及通过EsWrappers创建对象。第一种与MyBatisPlus中的QueryWrapper类似,而EsWrappers这个类更像一个工具类,方便创建不同的包装对象以及链式调用的对象,并在索引和数据的查询、更新中发挥包装执行条件的作用。 新增操作 @Test void testAdd(){ //添加数据,可在数据同步时使用 TestUser lebron = new TestUser().setAge(38).setName("勒布朗詹姆斯").setTeam("Los Angeles Lakers").setDescription("历史得分王"); TestUser stephen = new TestUser().setAge(35).setName("斯蒂芬库里").setTeam("Golden State Warriors").setDescription("历史三分王"); TestUser young = new TestUser().setAge(17).setName("斯蒂芬young").setTeam("yy").setDescription("yy"); ArrayList<TestUser> testUsers = new ArrayList<>(); testUsers.add(lebron); testUsers.add(stephen); testUsers.add(young); // 批量插入多条记录 System.out.println(testMapper.insertBatch(testUsers)); } 这里测试的是一个批量插入操作,向ES中插入数据,用过MyBatisPlus的应该很熟悉,方法名称都是一致的。 2023-05-31 21:33:31.468 INFO 18292 --- [ main] easy-es : Elasticsearch jar version:7.14.0 2023-05-31 21:33:31.665 INFO 18292 --- [ main] easy-es : Elasticsearch client version:7.6.1 2023-05-31 21:33:31.665 WARN 18292 --- [ main] easy-es : Elasticsearch clientVersion:7.6.1 not equals jarVersion:7.14.0, It does not affect your use, but we still recommend keeping it consistent! 2023-05-31 21:33:32.535 INFO 18292 --- [ main] gbootDemo08ElasticsearchApplicationTests : Started SpringbootDemo08ElasticsearchApplicationTests in 4.123 seconds (JVM running for 5.533) 2023-05-31 21:33:32.604 INFO 18292 --- [ main] easy-es : ===> Smoothly process index mode activated 2023-05-31 21:33:32.711 INFO 18292 --- [onPool-worker-1] easy-es : ===> Index not exists, automatically creating index by easy-es... 2023-05-31 21:33:33.155 INFO 18292 --- [onPool-worker-1] easy-es : ===> Congratulations auto process index by Easy-Es is done ! 3 在没有索引时,由于我们使用的是自动平滑模式生成索引,因此会在日志中生成相应提示以及插入操作成功返回的数据数3。 查看Es-head后即可看见相应的索引创建成功以及里面的数据成功插入: 修改操作 @Test void testUpdate(){ TestUser newDate = new TestUser().setDescription("明年会退役吗?"); Integer update1 = EsWrappers.lambdaChainUpdate(testMapper).eq(TestUser::getName, "勒布朗詹姆斯").update(newDate); System.out.println(update1>0?"修改成功!":"修改失败!"); } 这里笔者为了方便也是使用了链式调用一步完成的,修改name为“勒布朗詹姆斯”的数据: 2023-05-31 21:38:00.460 INFO 5616 --- [ main] easy-es : Elasticsearch jar version:7.14.0 2023-05-31 21:38:00.672 INFO 5616 --- [ main] easy-es : Elasticsearch client version:7.6.1 2023-05-31 21:38:00.672 WARN 5616 --- [ main] easy-es : Elasticsearch clientVersion:7.6.1 not equals jarVersion:7.14.0, It does not affect your use, but we still recommend keeping it consistent! 2023-05-31 21:38:01.547 INFO 5616 --- [ main] gbootDemo08ElasticsearchApplicationTests : Started SpringbootDemo08ElasticsearchApplicationTests in 4.097 seconds (JVM running for 5.374) 2023-05-31 21:38:01.607 INFO 5616 --- [ main] easy-es : ===> Smoothly process index mode activated 2023-05-31 21:38:01.688 INFO 5616 --- [onPool-worker-1] easy-es : ===> Index exists, automatically updating index by easy-es... 2023-05-31 21:38:02.095 INFO 5616 --- [onPool-worker-1] easy-es : ===> index has nothing changed 2023-05-31 21:38:02.095 INFO 5616 --- [onPool-worker-1] easy-es : ===> Congratulations auto process index by Easy-Es is done ! 2023-05-31 21:38:02.367 INFO 5616 --- [ main] easy-es : ===> Execute By Easy-Es: index-name: test_user DSL:{"size":10000,"query":{"bool":{"must":[{"term":{"name.keyword":{"value":"勒布朗詹姆斯","boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"_source":{"includes":["_id"],"excludes":[]},"track_total_hits":2147483647} 修改成功! 控制台成功打印了相应的DSL日志及操作执行结果。 查询操作 @Test void testQuery(){ //new构建查询条件,基础写法 LambdaEsQueryWrapper<TestUser> wrapper = new LambdaEsQueryWrapper<>(); //EsWrappers封装查询条件 //LambdaEsQueryWrapper<TestUser> queryWrapper = EsWrappers.lambdaQuery(TestUser.class).like(TestUser::getDescription, "历史"); wrapper.like(TestUser::getDescription,"历史"); wrapper.orderByDesc(TestUser::getAge); //用EsWrappers创建链式写法并模糊匹配查询 TestUser one = EsWrappers.lambdaChainQuery(testMapper).likeLeft(TestUser::getName, "库里").one(); SearchResponse search1 = testMapper.search(wrapper); System.out.println(Arrays.toString(search1.getHits().getHits())); System.out.println(one); } 查看执行结果: index-name: test_user DSL:{"size":10000,"query":{"bool":{"must":[{"wildcard":{"name.keyword":{"wildcard":"*库里","boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"track_total_hits":2147483647} 2023-05-31 21:52:56.800 INFO 812 --- [ main] easy-es : ===> Execute By Easy-Es: index-name: test_user DSL:{"size":10000,"query":{"bool":{"must":[{"wildcard":{"description":{"wildcard":"*历史*","boost":1.0}}}],"adjust_pure_negative":true,"boost":1.0}},"sort":[{"age":{"order":"desc"}}],"track_total_hits":2147483647} [{ "_index" : "test_user", "_type" : "_doc", "_id" : "irYVcogBrMcQl9SRH46R", "_score" : null, "_source" : { "age" : 38, "description" : "历史得分王", "name" : "勒布朗詹姆斯", "team" : "Los Angeles Lakers" }, "sort" : [ 38 ] }, { "_index" : "test_user", "_type" : "_doc", "_id" : "i7YVcogBrMcQl9SRH46R", "_score" : null, "_source" : { "age" : 35, "description" : "历史三分王", "name" : "斯蒂芬库里", "team" : "Golden State Warriors" }, "sort" : [ 35 ] }] TestUser(id=i7YVcogBrMcQl9SRH46R, name=斯蒂芬库里, description=历史三分王, age=35, team=Golden State Warriors) 可以基于like的模糊查询以及左匹配模糊查询结果也正确返回。 删除操作 @Test void testDelete(){ Integer delete = testMapper.delete(EsWrappers.lambdaQuery(TestUser.class).filter(age -> age.le(TestUser::getAge, 30))); System.out.println(delete>0?"删除成功!":"删除失败!"); } 这里通过filter过滤删除一条年龄小于30的数据: …… 2023-05-31 22:01:06.816 INFO 872 --- [onPool-worker-1] easy-es : ===> index has nothing changed 2023-05-31 22:01:06.817 INFO 872 --- [onPool-worker-1] easy-es : ===> Congratulations auto process index by Easy-Es is done ! 删除成功! 结果也显而易见。 整体上,确实相比于原生写操作简单了不少。当然,以上只是些基础的功能而已,高阶的功能还要慢慢看~ 高阶查询 通常在ES的使用中,在业务中的操作可能远不止简简单单的基础增删该查而已,因此Easy-Es也扩展了很多高阶功能,以便大家灵活使用。官网介绍了很多,但是这里笔者仅介绍几个我个人比较感兴趣的记录一下使用方法,其他读者可自行参照官网去学习。 高亮显示 高亮显示应该是ES作为数据搜索引擎用得最多的地方,但是在原生高亮使用中需要通过HighlightBuilder这个类来配置,但是在Easy-Es中,只需要在对应的实体类上添加高亮注解@HighLight就行了。并且省去了通过类配置自定义高亮样式的操作,只需要在注解对应的属性上设置就行了。 ———————————————— 原文链接:https://blog.csdn.net/qq_42263280/article/details/130993569
-
介绍 Easy-Es(简称EE)是一款基于ElasticSearch(简称Es)官方提供的RestHighLevelClient打造的ORM开发框架,在 RestHighLevelClient 的基础上,只做增强不做改变,为简化开发、提高效率而生。EE是Mybatis-Plus的Es平替版,在有些方面甚至比MP更简单,同时也融入了更多Es独有的功能,助力您快速实现各种场景的开发. (1)Elasticsearch java 客户端种类 Elasticsearch 官方提供了很多版本的 Java 客户端,包含但不限于: 【1】Transport 客户端 【2】Java REST 客户端 【3】Low Level REST 客户端 【4】High Level REST 客户端 【5】Java API 客户端 非官方的 Java 客户端,包含但不限于: 【1】Jest 客户端 【2】BBoss 客户端 【3】Spring Data Elasticsearch客户端 【4】easy-es客户端 (2)优势和特性分析 【1】全自动索引托管 全球开源首创的索引托管模式,开发者无需关心索引的创建更新及数据迁移等繁琐步骤,索引全生命周期皆可托管给框架,由框架自动完成,过程零停机,用户无感知,彻底解放开发者。该特性可以帮助我们在修改索引名称,索引配置,索引结构后自动更新,并迁移数据,减少运维成本。 【2】屏蔽语言差异。 提供类似Mybatis-Plus使用方式,相对于RestHighLevelClient使用便利不少,相对于Springdata-ElasticSearch的使用也有进一步的改进,更加符合国人的使用习惯。 【3】零魔法值和代码量极少。 这一点主要是针对RestHighLevelClient代码的臃肿问题解决,使用上更加简单。 【4】强大的 CRUD 操作。 内置通用 Mapper,仅仅通过少量配置即可实现大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求。 【5】支持 Lambda 形式调用 通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错段。 【6】内置分页插件。 基于RestHighLevelClient 物理分页,开发者无需关心具体操作,且无需额外配置插件,写分页等同于普通 List 查询,且保持和PageHelper插件同样的分页返回字段,无需担心命名影响。 【7】支持ES高阶语法 支持高亮搜索,分词查询,权重查询,Geo地理位置查询,IP查询,聚合查询等高阶语法。 【8】良好的拓展性,支持混合使用。 底层仍使用RestHighLevelClient,可保持其拓展性,开发者在使用EE的同时,仍可使用RestHighLevelClient的功能。 (3)性能、安全、拓展、社区 Easy-Es对于性能和安全问题专门做了文档描述,链接如下所示: https://www.easy-es.cn/pages/6e2197 根据文档描述整体的性能还是很好的,安全上已接入OSCS墨菲安全扫描无安全风险。单元测试用例综合覆盖率超95%,已上线的所有功能均有测试用例覆盖,且经过生产环境和开源社区大量用户使用验证。 EE底层用的就是Es官方提供的RestHighLevelClient,我们只是对RestHighLevelClient做了增强,并没有改变减少或是削弱它原有的功能。我们在项目中可以使用EE框架也可以根据需要直接使用RestHighLevelClient,是支持混合使用的。 目前该开源框架已经加入dromara开源社区,社区目前活跃,每年会发很多个版本,不断提升用户体验。 gitee仓库情况: github仓库情况: 附上同类型的产品spring-data-elasticsearch仓库情况: (2)ES版本及SpringBoot版本说明 Easy-Es底层用了ES官方的RestHighLevelClient,所以对ES版本有要求,要求ES和RestHighLevelClient JAR依赖版本必须为7.14.0,至于es客户端,实际 测下来7.X任意版本都可以很好的兼容. 值得注意的是,由于Springdata-ElasticSearch的存在,Springboot它内置了和ES及RestHighLevelClient依赖版本,这导致了不同版本的Springboot实际引入的ES及RestHighLevelClient 版本不同,而ES官方的这两个依赖在不同版本间的兼容性非常差,进一步导致很多用户无法正常使用Easy-Es,这实际上这是一个依赖冲突的问题. Easy-Es在项目启动时做了依赖校验,如果项目在启动时可以在控制台看到打印出级别为Error且内容为"Easy-Es supported elasticsearch and restHighLevelClient jar version is:7.14.0 ,Please resolve the dependency conflict!" 的日志时,则说明有依赖冲突待您解决. 解决方案其实很简单,可以像下面一样配置maven的exclude移除Springboot或Easy-Es已经声明的ES及RestHighLevelClient依赖,然后重新引入,引入时指定版本号为7.14.0即可解决. <dependency> <groupId>cn.easy-es</groupId> <artifactId>easy-es-boot-starter</artifactId> <version>1.1.0</version> <exclusions> <exclusion> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> </exclusion> <exclusion> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.14.0</version> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.14.0</version> </dependency> 也可以简单粗暴的把springboot版本调整到2.5.5,其它都不需要调整,也可以勉强正常使用. 索引处理 (一)索引别名策略 为了更好的理解和使用索引的更新原理,最好先了解下ES索引别名的机制。 索引别名是指给一个或者多个索引定义另外一个名称,使索引别名和索引之间可以建立某种逻辑关系。 可以用别名表示别名和索引之间的包含关系,假设我们当前有多个日期日志索引记录,比如log_index_01,log_index_02,log_index_03…,那么我们为了统一检索可以设置一个别名为log_index,然后请求索引为log_index,这样就可以通过log_index查询多个索引的数据,而不用一个一个的指定查询了。 需要指出的是,在默认情况下,当一个别名只指向一个索引时,写入数据的请求可以指向这个别名,如果这个别名指向多个索引,则写入数据的请求是不可以指向这个别名的。 引入别名之后,还可以用别名表示索引之间的替代关系。这种关系一般是在某个索引被创建后,有些参数是不能更改的(如主分片的个数),但随着业务发展,索引中的数据增多,需要更改索引参数进行优化。我们需要平滑地解决该问题,既要更改索引的设置,又不能改变索引名称,这时就可以使用索引别名。 假设一个酒店的搜索别名设置为hotel,初期创建索引hotel_1时,主分片个数设置为5,然后设置hotel_1的别名为hotel。此时客户端使用索引别名hotel进行搜索请求,该请求会转发到索引hotel_1中。假设此时酒店索引中的新增数据急剧增长,索引分片需要扩展,需要将其扩展成为10个分片的索引。但是一个索引在创建后,主分片个数已经不能更改,因此只能考虑使用索引替换来完成索引的扩展。这时可以创建一个索引hotel_2,除了将其主分片个数设置为10外,其他设置与hotel_1相同。当hotel_2的索引数据准备好后,删除hotel_1的别名hotel,同时,置hotel_2的别名为hotel。此时客户端不用进行任何改动,继续使用hotel进行搜索请求时,该请求会转发给索引hotel_2。如果服务稳定,最后将hotel_1删除即可。此时借助别名就完成了一次索引替换工作。如下图所示,在左图中,hotel索引别名暂时指向hotel_1,hotel_2做好了数据准备;在右图中,hotel索引别名指向hotel_2,完成了索引的扩展切换。 参考《Elasticsearch搜索引擎构建入门与实战》 索引别名在这种需要变更索引的情况下,搜索端不需要任何变更即可完成切换,这在实际的生产环境中是非常方便的。 (二)easy-es索引的自动托管之平滑模式实践 (1)介绍 提示:如果在使用过程中发现索引未得到更新,建议对照文章“ES版本及SpringBoot版本”部分检查下版本。 自动托管之平滑模式(自动挡-雪地模式) 默认开启此模式。 在此模式下,索引的创建更新数据迁移等全生命周期用户均不需要任何操作即可完成,过程零停机,用户无感知,可实现在生产环境的平滑过渡,类似汽车的自动档-雪地模式,平稳舒适,彻底解放用户! 需要值得特别注意的是,在自动托管模式下,系统会自动生成一条名为ee-distribute-lock的索引,该索引为框架内部使用,用户可忽略,若不幸因断电等其它因素极小概率下发生死锁,可删除该索引即可.另外,在使用时如碰到索引变更,原索引名称可能会被追加后缀_s0或_s1,不必慌张,这是全自动平滑迁移零停机的必经之路,索引后缀不影响使用,框架会自动激活该新索引.关于_s0和_s1后缀,在此模式下无法避免,因为要保留原索引数据迁移,又不能同时存在两个同名索引,凡是都是要付出代价的,如果您不认可此种处理方式,可以使用其他的方式。 (2)实践测试 【1】创建实体类并绑定索引名称为document,添加相关的字段属性,当前索引未创建。 【2】修改日志为debug模式以便查看DSL语句。启动项目,可以看到框架帮助我们自动创建了索引。 【3】默认创建的主分片为1,副本数为1 【4】现在我们添加一些测试数据,直接通过框架提供的API测试,现在已经创建了三条数据。 【5】接下来修改实体类的主分片大小,副本数大小,添加字段,然后再重启项目。 【6】下面观察输出的DSL语句,分析实现原理。 1:首先创建一个新的索引,索引名称为:原索引名称_s0 2:然后通过reindex命令将原索引的数据迁移到新的索引上,DSL语句如下所示 3:修改别名,将原先的别名下包含的旧的索引去除,然后添加刚刚创建的新的索引,这样通过原先的索引别名依然可以正常查询处理数据。 4:在完成上述文档迁移操作后,将旧的索引直接删除 5:以上执行流程中还包括了创建ee-distribute-lock索引,该索引为框架内部使用,可忽略。接着通过查询接口查询过往的数据,可以正常查询到历史的数据,并且其_settings属性和_mapping表结构都得到了更新。 索引文档的增删改查 插入记录 // 插入一条记录 Integer insert(T entity); // 批量插入多条记录 Integer insertBatch(Collection<T> entityList) 更新记录 //根据 ID 更新 Integer updateById(T entity); // 根据ID 批量更新 Integer updateBatchByIds(Collection<T> entityList); // 根据动态条件 更新记录 Integer update(T entity, LambdaEsUpdateWrapper<T> updateWrapper); 删除记录 // 根据 ID 删除 Integer deleteById(Serializable id); // 根据 entity 条件,删除记录 Integer delete(LambdaEsQueryWrapper<T> wrapper); // 删除(根据ID 批量删除) Integer deleteBatchIds(Collection<? extends Serializable> idList); keyword模糊查询 LambdaEsQueryWrapper<UserInfo> wrapper = new LambdaEsQueryWrapper<>(); wrapper.like(UserInfo::getUserName, userName); text分词查询 当我们需要对字段进行分词查询时,需要该字段的类型为text类型,并且指定分词器(不指定就用ES默认分词器,效果通常不理想). 比如EE中常用的API match()等都需要字段类型为text类型. 当使用match查询时未查询到预期结果时,可以先检查索引类型,然后再检查分词器,因为如果一个词没被分词器分出来,那结果也是查询不出来的. 中文需要提前在ES中安装分词器。 /** * 分词测试 */ @GetMapping("/match") public EsPageInfo<UserInfo> match(String word) { LambdaEsQueryWrapper<UserInfo> wrapper = new LambdaEsQueryWrapper<>(); wrapper.match(UserInfo::getContent, word); return EsPageInfo.of(userInfoMapper.selectList(wrapper)); } 条件构造器 query_string方式 // 假设我的查询条件是:创建者等于老王,且创建者分词匹配"隔壁"(比如:隔壁老汉,隔壁老王),或者创建者包含猪蹄 // 对应mysql语法是(creator="老王" and creator like "老王") or creator like "%猪蹄%",下面用es的queryString来演示实现一样的效果 // 足够灵活,非常适合前端页面中的查询条件列表字段及条件不固定,且可选"与或"的场景. LambdaEsQueryWrapper<Document> wrapper = new LambdaEsQueryWrapper<>(); String queryStr = QueryUtils.combine(Link.OR, QueryUtils.buildQueryString(Document::getCreator, "老王", Query.EQ, Link.AND), QueryUtils.buildQueryString(Document::getCreator, "隔壁", Query.MATCH)) + QueryUtils.buildQueryString(Document::getCreator, "*猪蹄*", Query.EQ); wrapper.queryStringQuery(queryStr); List<Document> documents = documentMapper.selectList(wrapper); System.out.println(documents); 对应的DSL语句和结果展示 分页查询 关于分页,支持了ES的三种分页模式,大家可参考下表,按需选择. // 物理分页 EsPageInfo<T> pageQuery(LambdaEsQueryWrapper<T> wrapper, Integer pageNum, Integer pageSize); 1 2 文档链接: https://www.easy-es.cn/pages/0cf11e/#浅分页 注意事项 【1】目前还不支持ElasticSearch8.X的版本,目前暂时只支持es7x,也不支持6.X版本。 【2】easy-es索引的自动托管之平滑模式使用起来很方便,但是注意它会把原索引删除,使用新的索引,虽然在框架内使用不受影响,但是如果其他地方依赖了该索引那么可能会造成影响,如果使用这种方式,建议采用别名策略,不直接访问索引。 【3】对比于spring-data-elasticsearch和easy-es,二者在使用上都相对于Es官方提供的RestHighLevelClient有着更简洁的使用操作性。spring-data-elasticsearch因为属于spring-data项目维护,社区更加活跃更新也比较频繁,目前已经支持8.X版本了。easy-es在使用上相对来说更加友好而且更加符合国人习惯。 【4】easy-es所支持的混合模式,当easy-es无法满足需求时可以使用原生的RestHighLevelClient这对于实际应用非常适用。 【5】ES查询功能非常强,特性众多,因时间问题无法测试所有的功能情况。但是目前针对于常用的功能进行的测试,从结果看,基本上是满足日常的开发使用的,而且操作起来还是非常简便的。根据issues的反馈,easy-es预计在今年推出2.X版本,该版本将会有很多的优化和新的功能点,进一步满足开发使用。 参考文档 https://www.easy-es.cn/pages/ec7460/ https://github.com/zwzhangyu/ZyCodeHub/tree/main/middleware/elasticsearch/easy-es ———————————————— 原文链接:https://blog.csdn.net/Octopus21/article/details/128988806
-
Spring6.1的M2版本带来了新特性RestClient,它是RestTemplate的现代替代品,提供类似WebClient的流畅API。RestClient简化了HTTP请求,支持GET、POST等操作,能直接转换响应为对象,且在遇到错误时抛出异常。通过exchange方法,开发者可以进行更复杂的请求处理。 摘要由CSDN通过智能技术生成 在最近发布的Spring 6.1 M2版本中,推出了一个全新的同步HTTP客户端:RestClient。用一句话来让Spring开发者认识RestClient的话:像WebClient一样具备流畅API的RestTemplate。所以,RestClient的使命就是淘汰已经有14年历史的RestTemplate。 关于WebClient和RestTemplate,之前在几种服务消费方式(RestTemplate、WebClient、Feign)这篇文章中有详细的介绍。如果您有一定的了解,那么对于RestClient一定可以快速上手。 RestClient案例 下面我们通过几个官方给出的案例一起来快速的认识一下RestClient。 HTTP请求 下面是一个最简单的GET请求,返回一个字符串。从这个例子中,我们可以看到API形式跟WebClient类似。不像以前用RestTemplate的时候那么麻烦。 RestClient restClient = RestClient.create(); String result = restClient.get() .uri("https://example.com") .retrieve() .body(String.class); System.out.println(result); 关于GET请求,很多时候我们返回的不仅仅是String,更多的时候是一些实体;同时我们有时候还需要获取HTTP状态码以及头信息。这个时候,我们可以使用toEntity方法来返回一个更为通用的ResponseEntity来进行后续操作,比如下面这样: ResponseEntity<String> result = restClient.get() .uri("https://example.com") .retrieve() .toEntity(String.class); System.out.println("Response status: " + result.getStatusCode()); System.out.println("Response headers: " + result.getHeaders()); System.out.println("Contents: " + result.getBody()); 在业务层面,为了更方便的解析业务数据。RestClient还支持对结果进行对象转换。比如下面的例子,就是把HTTP请求返回的JSON数据转化为Pet对象。这样就免去了开发者手动从ResponseEntity中获取内容,再进行消息转化的麻烦。 int id = ... Pet pet = restClient.get() .uri("https://petclinic.example.com/pets/{id}", id) .accept(APPLICATION_JSON) .retrieve() .body(Pet.class); 关于其他请求,也是类似的,比如下面的POST请求: Pet pet = ... ResponseEntity<Void> response = restClient.post() .uri("https://petclinic.example.com/pets/new") .contentType(APPLICATION_JSON) .body(pet) .retrieve() .toBodilessEntity(); 错误处理 默认情况下,RestClient在接收到4xx和5xx状态码的时候,会抛出一个RestClientException的子类。对于这个动作,我们可以通过onStatus方法去重写它,比如下面这样: String result = restClient.get() .uri("https://example.com/this-url-does-not-exist") .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) }) .body(String.class); 上面的例子是进一步做了包装,并重新抛出。当然您也可以做一些业务性的其他操作。 高级处理 Exchange 直接看下面的例子: Pet result = restClient.get() .uri("https://petclinic.example.com/pets/{id}", id) .accept(APPLICATION_JSON) .exchange((request, response) -> { if (response.getStatusCode().is4xxClientError()) { throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); } else { Pet pet = convertResponse(response); return pet; } }); exchange方法提供了更灵活且完整的请求处理入口。在这里,开发者里获取到request信息,也可以操作response信息。所以,如果您有复杂的处理逻辑上一节中的请求方法无法满足你需要的时候,就可以通过这里的exchange方法来定制复杂的处理逻辑。 小结 相信大家对RestTemplate一定都不陌生,但实际应用估计已经不是很多了,更多的会使用一些其他的客户端来实现HTTP的调用。如今Spring 6.1将推出的RestClient将很好的弥补这块不足,同时与WebClient互相补充。好了,今天的学习就到这里!如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!更多Spring Boot教程可以点击直达!,欢迎收藏与转发支持 ———————————————— 原文链接:https://blog.csdn.net/dyc87112/article/details/131776216
-
今天给大家简单的介绍一下SpringBoot如何集成Elasticsearch,并简单的介绍一下基于SpringBoot模式下怎么进行简单的增删改查操作,这边增删改查操作有点类似于JPA的模式。(什么是JPA模式,大家可以自行搜索答案) Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。史上最全Java技术栈面试题都在这里面了:Java面试题 废话不多说,现在马上开始我们今天的内容。如何新建Springboot项目我这边就不废话了,不会的同学可以看我以前写的教程。 文章地址:IDEA上面如何创建SpringBoot项目-CSDN博客 1.首先是引入相关的依赖,下面是我的pom文件。 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>ElasticSearch</name> <description>ElasticSearch project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-elasticsearch --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> <version>2.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> pom.xml文件中最重要的其实就是引入ES(Elasticsearch的简称后面我都这么叫),也就是spring-boot-starter-data-elasticsearch 依赖。 2.接下来就是对应的配置文件了,具体配置文件如下所示: # elasticsearch集群名称,默认的是elasticsearch spring.data.elasticsearch.cluster-name=my-application #节点的地址 注意api模式下端口号是9300,千万不要写成9200 spring.data.elasticsearch.cluster-nodes=192.168.11.24:9300 #是否开启本地存储 spring.data.elasticsearch.repositories.enable=true 3.索引对应的实体类如下所示: package com.elasticsearch.entity; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; /** * @author linzhiqiang * @date 2018/5/16 */ @Document(indexName = "company",type = "employee", shards = 1,replicas = 0, refreshInterval = "-1") public class Employee { @Id private String id; @Field private String firstName; @Field private String lastName; @Field private Integer age = 0; @Field private String about; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getAbout() { return about; } public void setAbout(String about) { this.about = about; } } 4.实体类对应的dao接口如下所示: package com.elasticsearch.dao; /** * Created by 19130 on 2018/5/16. */ import com.elasticsearch.entity.Employee; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Component; /** * @author linzhiqiang */ @Component public interface EmployeeRepository extends ElasticsearchRepository<Employee,String>{ /** * 查询雇员信息 * @param id * @return */ Employee queryEmployeeById(String id); } 5.实体类对应的控制类如下所示: package com.elasticsearch.controller; import com.elasticsearch.dao.EmployeeRepository; import com.elasticsearch.entity.Employee; import com.google.gson.Gson; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author linzhiqiang */ @RestController @RequestMapping("es") public class EmployeeController { @Autowired private EmployeeRepository employeeRepository; /** * 添加 * @return */ @RequestMapping("add") public String add() { Employee employee = new Employee(); employee.setId("1"); employee.setFirstName("xuxu"); employee.setLastName("zh"); employee.setAge(26); employee.setAbout("i am in peking"); employeeRepository.save(employee); System.err.println("add a obj"); return "success"; } /** * 删除 * @return */ @RequestMapping("delete") public String delete() { Employee employee = employeeRepository.queryEmployeeById("1"); employeeRepository.delete(employee); return "success"; } /** * 局部更新 * @return */ @RequestMapping("update") public String update() { Employee employee = employeeRepository.queryEmployeeById("1"); employee.setFirstName("哈哈"); employeeRepository.save(employee); System.err.println("update a obj"); return "success"; } /** * 查询 * @return */ @RequestMapping("query") public Employee query() { Employee accountInfo = employeeRepository.queryEmployeeById("1"); System.err.println(new Gson().toJson(accountInfo)); return accountInfo; } } 以上所有的增删改查操作都要基于搭建好的ES系统(关于Linux上面如何搭建ES系统大家可以自行google) 关于我踩过的坑: 1.ES中API的端口号是9300而不是9200。 2.ES系统中Elasticsearch.yml配置文件中要加入network.host: 0.0.0.0,否则外网地址访问不了。 3.最新的资料一定要去官网上面查看,博客上面好多都是过时的。官网地址:Elasticsearch Platform — Find real-time answers at scale | Elastic 4.注意JDK、ES、Springboot三者之间的版本,很多时候错误都是版本冲突引起的。 ———————————————— 原文链接:https://blog.csdn.net/linzhiqiang0316/article/details/80343401
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签