Java Note five (finished)

基本是完结了,不排除后面打补丁(补充内容)

day31-多线程&JUC

1. 概述

1.1 线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

单线程:一个进程如果只有一条执行路径,则称为单线程程序
多线程:一个进程如果有多条执行路径,则称为多线程程序

 

1.2 进程

进程是程序的基本执行实体。应用软件中互相独立,可以同时运行的功能。

独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
并发性:任何进程都可以同其他进程一起并发执行

 

1.3 并发

在同一时刻,有个指令在个CPU上交替执行。

 

1.4 并行

在同一时刻,有个指令在个CPU上同时执行。

 

2. 实现多线程

2.1 继承Thread类

方法名 说明
void run() 在线程开启后,此方法将被调用执行
void start() 使此线程开始执行,Java虚拟机会调用run方法()

 

实现步骤

  • 定义一个类MyThread继承Thread类
  • 在MyThread类中重写run()方法
  • 创建MyThread类的对象
  • 启动线程
public class ThreadDemo1 {
    public static void main(String[] args) {
        MyThread m1 = new MyThread();
        MyThread m2 = new MyThread();
        
        m1.setName("线程一");
        m2.setName("Thread two");

        // m1.run();  no this way
        m1.start();
        m2.start();

    }
}

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + " --> " + i);
        }
    }
}

 

两个小问题

  • 为什么要重写run()方法?因为run()是用来封装被线程执行的代码
  • run()方法和start()方法的区别?run():封装线程执行的代码,直接调用,相当于普通方法的调用start():启动线程;然后由JVM调用此线程的run()方法

 

2.2 实现Runnable接口

  • Thread构造方法
    方法名 说明
    Thread(Runnable target) 分配一个新的Thread对象
    Thread(Runnable target, String name) 分配一个新的Thread对象
  • 实现步骤
    • 定义一个类MyRunnable实现Runnable接口
    • 在MyRunnable类中重写run()方法
    • 创建MyRunnable类的对象
    • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
    • 启动线程
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 获取当前线程的对象
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "-->" + i);
        }
    }
}


public class ThreadDemo2 {
    public static void main(String[] args) {

        // 创建MyRunnable类的对象
        // 不用创建两个
        MyRunnable mr = new MyRunnable();

        new Thread(mr, "线程1").start();
        new Thread(mr, "Thread Two").start();
    }
}

 

2.3 实现Callable接口

  • 方法介绍
    方法名 说明
    V call() 计算结果,如果无法计算结果,则抛出一个异常
    FutureTask(Callable<V> callable) 创建一个 FutureTask,一旦运行就执行给定的 Callable
    V get() 如有必要,等待计算完成,然后获取其结果
  • 实现步骤
    • 定义一个类MyCallable实现Callable接口
    • 在MyCallable类中重写call()方法(具有返回值,表示多线程运行的结果)
    • 创建MyCallable类的对象
    • 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数(管理多线程的运行结果)
    • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
    • 启动线程
    • 再调用get方法,就可以获取线程结束之后的结果。
public class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}


public class ThreadDemo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        
 		MyCallable mc = new MyCallable();

        FutureTask<Integer> ft1 = new FutureTask<>(mc);
        FutureTask<Integer> ft2 = new FutureTask<>(mc);

        Thread t1 = new Thread(ft1);
        Thread t2 = new Thread(ft2);

        t1.start();
        t2.start();

        System.out.println(ft1.get());
        System.out.println(ft2.get());
    }
}

 

2.4 三种方式对比

多线程的第三种实现方式

  • 实现Runnable、Callable接口
    • 好处: 扩展性强,实现该接口的同时还可以继承其他的类
    • 缺点: 编程相对复杂,不能直接使用Thread类中的方法
  • 继承Thread类
    • 好处: 编程比较简单,可以直接使用Thread类中的方法
    • 缺点: 可以扩展性较差,不能再继承其他的类

 

2.5 常见的成员方法

方法名 说明
void setName(String name) 设置线程的名字(构造方法也可以设置名字)
String getName() 返回此线程的名称
static Thread currentThread() 获取当前线程的对象
Thread thread = Thread.currentThread();
System.out.println(thread.getName());  // main

MyThread m1 = new MyThread();
MyThread m2 = new MyThread();
MyThread m3 = new MyThread("THREAD THREE");

System.out.println(m1.getName());  // Thread-0  默认名字
System.out.println(m2.getName());  // Thread-1
System.out.println(m3.getName());  // THREAD THREE

m1.setName("线程一");
System.out.println(m1.getName());  // 线程一

//========================
//MyThread需要实现有参构造
public MyThread(String name) {
    super(name);
}

 

2.6 线程的休眠

  • 相关方法
方法名 说明
static void sleep(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数
// main方法
public static void main(String[] args) throws InterruptedException {
    System.out.println("睡觉前");
    Thread.sleep(3000); // 时间到后自动醒来
    System.out.println("睡觉后");
}

// 接口重写run方法
public class MyRunnable implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 获取当前线程的对象
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "-->" + i);
        }
    }
}

 

2.7 线程优先级

 

线程调度:

  • 两种调度方式
    • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
    • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
  • Java使用的是抢占式调度模型
  • 随机性假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的

 

  • 优先级相关方法
方法名 说明
final int getPriority() 返回此线程的优先级
final void setPriority(int newPriority) 更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10

 

2.8 守护线程

  • 相关方法
方法名 说明
final void setDaemon(boolean on) 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
MyThread1 m1 = new MyThread1("MT--Fast");
MyThread m = new MyThread("MT");


// 把第二个线程设置为守护线程
// 当普通线程(m1)执行完之后,那么守护线程也没有继续运行下去的必要了.
m.setDaemon(true);

m1.start();
m.start();

 

2.9 礼让线程and插入线程

方法名 说明
public static native void yield() 出让线程/礼让线程
public final void join() 插入线程/插队线程

 

yield():

public class MyThread extends Thread{

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + " --> " + i);
            // 静态方法,表示出让当前cpu的执行权
            Thread.yield();
        }
    }
}

// 测试类
MyThread t1 = new MyThread("THREAD one");
MyThread t2 = new MyThread("线程二");

t1.start();
t2.start();

// 结果尽可能的均匀

 

join():

public class ThreadJoin {
    public static void main(String[] args) throws InterruptedException {

        MyThread t = new MyThread("土豆");
        t.start();

        // 表示把t这个线程,插入到当前线程之前
        // t:土豆
        // 当前线程:main线程
        t.join();

        // main线程
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:" + i);
        }
    }

}

 

3. 线程同步

3.1 线程的6大状态

线程的生命周期

Thread.State 解释 jdk解释
NEW 创建线程对象 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
RUNNABLE start方法 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。
BLOCKED 无法获得锁对象 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING wait方法 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING sleep方法 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED 全部代码运行完毕 一个完全运行完成的线程的状态。也称之为终止状态、结束状态

多线程的6种状态

 

3.2 线程安全问题

3.2.1 卖票案例

  • 案例需求某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
  • 实现步骤
    • 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;
    • 在SellTicket类中重写run()方法实现卖票,代码步骤如下
    • 判断票数大于0,就卖票,并告知是哪个窗口卖的
    • 卖了票之后,总票数要减1
    • 票卖没了,线程停止
    • 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下
    • 创建SellTicket类的对象
    • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
    • 启动线程
public class SellTicket implements Runnable {

    // 如果用的是extends Thread则需要static修饰,因为会创建多个
    private int tickets = 100;

    @Override
    public void run() {
        while (tickets >= 0) {
            Thread thread = Thread.currentThread();

            // 加入线程休眠,可能导致卖票异常
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(thread.getName() + "卖出一张票,余票:" + (--tickets));
        }
    }
}

// 测试类=============================================================
public class SellTicketDemo {
    public static void main(String[] args) {
        //创建SellTicket类的对象
        SellTicket st = new SellTicket();

        //创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称
        Thread t1 = new Thread(st, "售票处1");
        Thread t2 = new Thread(st, "SELL TICKET ONE");
        Thread t3 = new Thread(st, "1");

        //启动线程
        t1.start();
        t2.start();
        t3.start();

    }
}

 

3.2.2 出现的问题

  • 卖票出现了问题
    • 相同的票出现了多次
    • 出现了负数的票
  • 安全问题出现的条件
    • 是多线程环境
    • 有共享数据
    • 有多条语句操作共享数据
  • 问题产生原因线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题

卖票问题原因

 

3.3 同步代码块

同步代码块格式:

synchronized(任意对象) { 
	多条语句操作共享数据的代码 
}

synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

 

同步的好处和弊端

  • 好处:解决了多线程的数据安全问题
  • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

 

  • 案例修改后的代码:

修改为while true而非 while tickets > 0,使得不会某一个线程抢到后知道卖完才跳出代码块。

而且如果while true写在循环代码块外面,也会单独走完。

锁对象,一定要是唯一的。

synchronized (Thread.currentThread()) {
}
// 锁对象不一样,则失效。

// 字节码文件对象,一定是唯一的
synchronized (SellTicket.class) {
}

 

public class SellTicket implements Runnable {


    private int tickets = 100;

    // 锁对象,一定要是唯一的
    static Object obj = new Object();

    @Override
    public void run() {
        // 同步代码块
        while (true) {
            synchronized (obj) {
                if (tickets > 0) {

                    Thread thread = Thread.currentThread();

                    // 加入线程休眠,可能导致卖票异常
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }

                    System.out.println(thread.getName() + "卖出一张票,余票:" + (--tickets));
                } else {
                    break;
                }
            }
        }
    }
}

 

3.4 同步方法

把synchronized关键字加到方法上

  • 同步方法的格式:
修饰符 (static) synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}

特点1:同步方法是锁住方法里面所有的代码

特点2:锁对象不能自己制定(非静态:this,静态:当前类的字节码文件对象)

public class SellTicket1 implements Runnable {

    private int tickets = 100;

    @Override
    public void run() {
        while (true) {
            if (method()) break;
        }
    }

    private synchronized boolean method() {
        if (tickets > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "卖出一张票,余票:" + (--tickets));
        } else {
            return true;
        }
        return false;
    }
}

StringBuilder用于多个线程是不安全的,同步建议使用StringBuffer,方法synchronized修饰。

 

3.5 Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。

  • ReentrantLock构造方法
    方法名 说明
    ReentrantLock() 创建一个ReentrantLock的实例
  • 加锁解锁方法
    方法名 说明
    void lock() 获得锁
    void unlock() 释放锁

 

使用try catch finally保证锁关闭

public class SellTicket2 implements Runnable {


    private int tickets = 100;

    // 如果用extends Thread,则会创建多个锁对象,需要static修饰
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        // 同步代码块
        while (true) {
            lock.lock();
            try {
                if (tickets > 0) {
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,余票:" + (--tickets));
                } else {
                    break;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
            // lock.unlock();  // 执行到最后一次时,线程break到下面的大括号后,没有执行解锁。
        }
    }
}

 

3.6 死锁

  • 概述线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
  • 什么情况下会产生死锁
    1. 资源有限
    2. 同步嵌套
public class MyThread2 extends Thread {

    static Object objA = new Object();
    static Object objB = new Object();

    @Override
    public void run() {
        //1.循环
        while (true) {
            if ("线程A".equals(getName())) {
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁");//A
                    synchronized (objB) {
                        System.out.println("线程A拿到了B锁,顺利执行完一轮");
                    }
                }
            } else if ("线程B".equals(getName())) {
                synchronized (objB) {
                    System.out.println("线程B拿到了B锁,准备拿A锁");//B
                    synchronized (objA) {
                        System.out.println("线程B拿到了A锁,顺利执行完一轮");
                    }
                }
            }
        }
    }
}



public class ThreadDemo7 {
    public static void main(String[] args) {
       /*
           需求:
                死锁
       */
        MyThread2 t1 = new MyThread2();
        MyThread2 t2 = new MyThread2();

        t1.setName("线程A");
        t2.setName("线程B");

        t1.start();
        t2.start();

    }
}

// 线程B拿到了B锁,准备拿A锁
// 线程A拿到了A锁,准备拿B锁

 

4. 生产者和消费者

4.1 概述

生产者消费者模式是一个十分经典的多线程协作的模式。

所谓生产者消费者问题,实际上主要是包含了两类线程:

一类是生产者线程用于生产数据
​ 一类是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为
消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

 

  • 等待和唤醒方法
方法名 说明
void wait() 使当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify() 唤醒正在等待对象监视器的单个线程(随机唤醒单个线程)
void notifyAll() 唤醒正在等待对象监视器的所有线程

 

4.2 案例实现

 

Desk类:

public class Desk {

    // 桌子上是否有食物,0没有,1有
    public static int foodFlag = 0;

    // 吃货最多能吃的总个数 (食物总量,吃完不补)
    public static int count = 10;

    // 锁对象
    public static Object lock = new Object();

}

 

Foodie类 消费者:

public class Foodie extends Thread{
    /*
     * 1. 循环
     * 2. 同步代码块
     * 3. 判断共享数据是否到了末尾(到了末尾)
     * 4. 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)
     * */

    @Override
    public void run() {

        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    //先判断桌子上是否有面条
                    if (Desk.foodFlag == 0) {
                        //如果没有,就等待
                        try {
                            Desk.lock.wait();  //让当前线程跟锁进行绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        //把吃的总数-1
                        Desk.count--;
                        //如果有,就开吃
                        System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗!!!");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}

 

Cook类 生产者:

public class Cook extends Thread{

    @Override
    public void run() {
        while (true) {
            synchronized (Desk.lock) {
                if (Desk.count == 0) {
                    break;
                } else {
                    //判断桌子上是否有食物
                    if(Desk.foodFlag == 1){
                        //如果有,就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else{
                        //如果没有,就制作食物
                        System.out.println("厨师做了一碗面条");
                        //修改桌子上的食物状态
                        Desk.foodFlag = 1;
                        //叫醒等待的消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

 

Demo测试类:

public class CookFoodieDemo {
    public static void main(String[] args) {
        //创建线程的对象
        Cook c = new Cook();
        Foodie f = new Foodie();

        //给线程设置名字
        c.setName("厨师");
        f.setName("吃货");

        //开启线程
        c.start();
        f.start();
    }
}

 

4.3 阻塞队列

  • 结构:

阻塞队列实现等待唤醒机制

ArrayBlockingQueue:底层是数组,有界

LinkedBlockingQueue:底层是链表,无界。但不是真正的无界,最大为int的最大值

 

BlockingQueue的核心方法:

put(anObject):将参数放入队列,如果放不进去会阻塞

take():取出第一个数据,取不到会阻塞

 

4.4 阻塞队列实现案例

Cook生产者:

public class Cook1 extends Thread{

    ArrayBlockingQueue<String> queue;

    public Cook1(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            //不断的把面条放到阻塞队列当中
            try {
                // put方法底层加了 锁
                queue.put("面条");
                System.out.println("厨师放了一碗面条");  // // 打印语句在锁的外面,输出不连续
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

Foodie消费者:

public class Foodie1 extends Thread {

    ArrayBlockingQueue<String> queue;

    public Foodie1(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            //不断从阻塞队列中获取面条
            try {
                String food = queue.take();
                System.out.println(food);  // 打印语句在锁的外面,输出不连续
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

测试类:

public class QueueDemo {
    public static void main(String[] args) {

        /*
         * 生产者和消费者必须使用同一个阻塞队列
         */

        // 创建阻塞队列的对象,容量为 1
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        //2.创建线程的对象,并把阻塞队列传递过去
        Cook1 c = new Cook1(queue);
        Foodie1 f = new Foodie1(queue);

        //3.开启线程
        c.start();
        f.start();
    }
}

 

5. 线程池

5.1 概述

线程池存在的意义:

系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理是对系统资源的消耗,这样就有点"舍本逐末"了。

针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。

 

线程池的核心原理 :

  1. 创建一个池子,池子中是空的
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可刚开始任务容器是空的,所以线程都在wait
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

 

5.2 Executors默认线程池

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象

方法名称 说明
public static ExecutorService newCachedThreadPool() 创建一个没有上限(int最大值)的线程池
public static newFixedThreadPool(int nThreads) 创建一个指定最多线程数量的线程池

 

newCachedThreadPool():

public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {


        // 1,创建一个默认的线程池对象.池子中默认是空的.默认最多可以容纳int类型的最大值.
        ExecutorService pool1 = Executors.newCachedThreadPool();

        // 2.提交任务
        pool1.submit(new MyRunnable());  // pool-1-thread-1----0

        Thread.sleep(1000);

        pool1.submit(new MyRunnable());  // 线程复用 pool-1-thread-1
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());
        Thread.sleep(1000);
        pool1.submit(new MyRunnable());

        // 3.销毁线程池
        // pool1.shutdown();
    }
}

public class MyRunnable implements Runnable{

    @Override
    public void run() {
//        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName());
//            System.out.println(Thread.currentThread().getName() + "----" + i);
//        }
    }
}

 

5.3 自定义线程池--ThreadPoolExecutor

 

创建线程池对象 :

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
    
corePoolSize:   核心线程的最大值,不能小于0
maximumPoolSize:最大线程数,不能小于等于0,maximumPoolSize >= corePoolSize
keepAliveTime:  空闲线程最大存活时间,不能小于0
unit:           时间单位
workQueue:      任务队列,不能为null
threadFactory:  创建线程工厂,不能为null      
handler:        任务的拒绝策略,不能为null 

注:明确线程池对多可执行的任务数 = 队列容量 + 最大线程数

 

任务拒绝策略:

RejectedExecutionHandler是jdk提供的一个任务拒绝策略接口,它下面存在4个子类。

拒绝策略 说明
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常。是默认的策略。
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常 这是不推荐的做法。
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中。
ThreadPoolExecutor.CallerRunsPolicy 调用任务的run()方法绕过线程池直接执行。

 

当队列满时才会创建临时线程

自定义线程池超详细解析

 

public class ThreadPoolExecutorDemo {
    public static void main(String[] args) {

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3, // 核心线程数量
                4, // 最大线程数量  >= 核心线程数量
                3, // 3s
                TimeUnit.SECONDS, // 单位
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );

        for (int i = 0; i < 7; i++) {
            // 定义一个变量,来指定当前执行的任务;这个变量需要被final修饰
            final int num = i;
            pool.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "---->> 执行了任务: "  + num);
            });
        }
    }
}

// 只执行了(0 - 5)6次,有一个任务被抛弃,并报异常java.util.concurrent.RejectedExecutionException
pool-1-thread-4---->> 执行了任务: 5
pool-1-thread-3---->> 执行了任务: 2
pool-1-thread-1---->> 执行了任务: 0
pool-1-thread-3---->> 执行了任务: 4
pool-1-thread-4---->> 执行了任务: 3
pool-1-thread-2---->> 执行了任务: 1
    
    
// DiscardOldestPolicy,任务4,作为队列第一个被抛弃
pool-1-thread-2---->> 执行了任务: 1
pool-1-thread-4---->> 执行了任务: 5
pool-1-thread-3---->> 执行了任务: 2
pool-1-thread-1---->> 执行了任务: 0
pool-1-thread-2---->> 执行了任务: 6
pool-1-thread-3---->> 执行了任务: 4
    
// CallerRunsPolicy 没有通过线程池中的线程执行任务,而是直接调用任务的run()方法绕过线程池直接执行。
// 假设执行 10 次
main---->> 执行了任务: 6
pool-1-thread-2---->> 执行了任务: 1
pool-1-thread-4---->> 执行了任务: 5
pool-1-thread-1---->> 执行了任务: 0
main---->> 执行了任务: 7
pool-1-thread-4---->> 执行了任务: 3
pool-1-thread-1---->> 执行了任务: 4
pool-1-thread-4---->> 执行了任务: 9
pool-1-thread-3---->> 执行了任务: 2
pool-1-thread-2---->> 执行了任务: 8

 

5.4 线程池多大合适

线程池最大计算

最大并行数:

4核8线程 最大并行数即为8

// 向Java虚拟机返回可用处理器的数目
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count);  // 8

 

6.尚未实现的内容

 

多线程02.md

 

尚未实现的内容

 

day32 网络编程

1. 网络入门

  • 网络编程:在网络通信协议下,不同计算机上运行的程序,可以进行数据传输

Java中可以使用java.net包下的技术轻松开发出常见的网络应用程序

 

1.1 软件架构

软件架构

 

BS优缺点:

  • 不需要开发客户端,只需要开发服务端
  • 用户不需要下载,打开浏览器就能使用
  • 如果应用过大,用户体验受到影响

 

CS优缺点:

  • 画面可以做的非常精美,用户体验好
  • 需要开发客户端,也需要开发服务端
  • 用户需要下载和更新的时候太麻烦

 

1.2 网络编程三要素

  • IP地址设备在网络中的地址,是唯一的标识
  • 端口应用程序在设备中唯一的标识。
  • 协议数据在网络中传输的规则,常见的协议有UDP、TCP、http、https、ftp。

 

1.3 IP地址

IP地址:互联网协议地址,是网络中设备的唯一标识

  • IPV4:每个IP地址长32bit,也就是4个字节。(点分十进制)
  • IPv4的地址分类形式
    • 公网地址(万维网使用)和私有地址(局域网使用)。
    • 192.168.开头的就是私有址址,范围即为192.168.0.0——192.168.255.255,专门为组织机构内部使用,以此节省IP。(如网吧所有主机共享同一个公网IP)
  • 特殊IP地址
    127.0.0.1,也可以是localhost:是回送地址也称本地回环地址,也称本机IP,永远只会寻找当前所在本机。(不经过路由器)

 

  • IPV6:为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16位一组,分成8组十六进制数(冒分十六进制)

IPV6

  • DOS常用命令:
    • ipconfig:查看本机IP地址
    • ping IP地址:检查网络是否连通。(ping www.baidu.com
  • 特殊IP地址:
    • 127.0.0.1:是回送地址,可以代表本机地址,一般用来测试使用

 

1.4 InetAddress类

此类表示Internet协议(IP)地址

  • 相关方法
方法名 说明
public static InetAddress getLocalHost() 返回本主机的地址对象
public static InetAddress getByName(String host) 得到指定主机的IP地址对象,参数是域名或者IP地址
public String getHostName() 获取此IP地址的主机名
public String getHostAddress() 返回IP地址字符串
public boolean isReachable(int timeout) 在指定毫秒内连通该IP地址对应的主机,连通返回true
public static void main(String[] args) throws UnknownHostException {
    // 获取InetAddress对象
    InetAddress address = InetAddress.getByName("192.168.0.101");
    System.out.println(address);   //   /192.168.0.101
    System.out.println(address.getHostName());   // George


    InetAddress address1 = InetAddress.getByName("George");
    System.out.println(address1);   // George/169.254.221.189
    // Exception in thread "main" java.net.UnknownHostException: 不知道这样的主机。 (GeorgeWatson)
    System.out.println(address1.getHostAddress());  // 169.254.221.189


}

 

1.5 端口

  • 端口设备上应用程序的唯一标识
  • 端口号用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。

    如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用 80,FTP占用21)
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。(如:Tomcat占 用8080,MySQL占用3306)
  • 动态端口:49152到65535,之所以称为动态端口,是因为它 一般不固定分配某种进程,而是动态分配。

 

1.6 协议

  • 计算机网络中,连接和通信的规则被称为网络通信协议

网络编程

 

1.6.1 UDP协议

  • 用户数据报协议(User Datagram Protocol)
  • UDP是面向无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
  • 速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据
  • 由于使用UDP协议消耗系统资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输
  • 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议

 

1.6.2 TCP协议

  • 传输控制协议 (Transmission Control Protocol)
  • TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”
  • 速度慢,没有大小限制,数据安全
  • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠第一次握手,客户端向服务器端发出连接请求,等待服务器确认第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求

    第三次握手,客户端再次向服务器端发送确认信息,确认连接

  • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等

 

2. UDP通信

2.1 UDP发送数据

  • Java中的UDP通信
    • UDP协议是一种不可靠的网络协议,它在通信的两端各建立一个Socket对象,但是这两个Socket只是发送,接收数据的对象,因此对于基于UDP协议的通信双方而言,没有所谓的客户端和服务器的概念
    • Java提供了DatagramSocket类作为基于UDP协议的Socket
  • 构造方法
    方法名 说明
    DatagramSocket() 创建发送端的Socket对象,系统会随机分配一个端口号。
    public DatagramSocket(int port) 创建接收端的Socket对象并指定端口号
    public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发送端数据包对象
    buf:要发送的内容,字节数组
    length:要发送内容的字节长度
    address:接收端的IP地址对象
    port:接收端的端口号
  • 相关方法
    方法名 说明
    void send(DatagramPacket p) 发送数据报包
    void close() 关闭数据报套接字
    void receive(DatagramPacket p) 从此套接字接受数据报包
  • 发送数据的步骤
    • 创建发送端的Socket对象(DatagramSocket)
    • 创建数据,并把数据打包
    • 调用DatagramSocket对象的方法发送数据
    • 关闭发送端
  • 示例代码
public class UDPSendDemo {
    public static void main(String[] args) throws IOException {
        // 发送数据

        // 1.创建DatagramSocket对象(快递公司)
        // 细节:
        // 绑定端口,以后我们就是通过这个端口往外发送
        // 空参:所有可用的端口中随机一个进行使用
        // 有参:指定端口号进行绑定
        DatagramSocket ds = new DatagramSocket();

        // 2.打包数据
        String str = "乔治华生";
        byte[] bytes = str.getBytes();
//        InetAddress localHost = InetAddress.getLocalHost();
        InetAddress address = InetAddress.getByName("127.0.0.1");
        int port = 10086;

        DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);

        //3.发送数据
        ds.send(dp);

        //4.释放资源
        ds.close();

    }
}

2.2 UDP接收数据

  • 接收数据的步骤
    • 创建接收端的Socket对象(DatagramSocket)
    • 创建一个数据包,用于接收数据
    • 调用DatagramSocket对象的方法接收数据
    • 解析数据包,并把数据在控制台显示
    • 关闭接收端
  • 构造方法
    方法名 说明
    public DatagramPacket(byte[] buf, int length) 创建接收端的数据包对象
    buf:用来存储接收的内容
    length:能够接收内容的长度
  • 相关方法
    方法名 说明
    byte[] getData() 返回数据缓冲区
    int getLength() 返回要发送的数据的长度或接收的数据的长度
  • 示例代码
public class UDPReceiveDemo {
    public static void main(String[] args) throws IOException {
        //接收数据

        // 1.创建DatagramSocket对象(快递公司)
        // 细节:
        // 在接收的时候,一定要绑定端口
        // 而且绑定的端口一定要跟发送的端口保持一致
        DatagramSocket ds = new DatagramSocket(10086);

        //2.接收数据包
        byte[] bytes = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bytes,bytes.length);


        // 该方法是阻塞的
        // 程序执行到这一步的时候,会在这里死等
        // 等发送端发送消息
        System.out.println("UDP Receive is wating....");
        ds.receive(dp);

        //3.解析数据包
        byte[] data = dp.getData();
        int len = dp.getLength();
        InetAddress address = dp.getAddress();
        SocketAddress socketAddress = dp.getSocketAddress();
        System.out.println("============================================");
        System.out.println("SocketAddress:   " + socketAddress);   // SocketAddress:   /127.0.0.1:57576
        System.out.println("InetAddress:  " + address);              // InetAddress:  /127.0.0.1
        System.out.println("============================================");

        int port = dp.getPort();  // SendDemo中DatagramSocket是空参的

        System.out.println("接收到数据:" + new String(data,0,len));
        System.out.println("该数据是从:" + address + ", 这台电脑中的:" + port + ", 这个端口发出的");

        //4.释放资源
        ds.close();
        // 接收到数据:乔治华生
        // 该数据是从:/127.0.0.1, 这台电脑中的:57576, 这个端口发出的
    }
}

 

2.3 案例

  • 案例需求UDP发送数据:数据来自于键盘录入,直到输入的数据是886,发送数据结束UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收

 

发送886,发送端and接收端都结束

发送端可以开多个,接收端不行!Exception in thread "main" java.net.BindException: Address already in use: bind

发送端:

public class UDPSendingDemo {
    public static void main(String[] args) throws IOException {

        DatagramSocket ds = new DatagramSocket();

        int port = 10010;
        InetAddress address = InetAddress.getByName("127.0.0.1");

        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请输入要发送的内容:");
            String input = sc.nextLine();

            byte[] bytes = input.getBytes();


            DatagramPacket dp = new DatagramPacket(bytes, bytes.length, address, port);
            ds.send(dp);  // 886也要发出去,保证接收端关闭
            if ("886".equals(input)) {
                break;
            }
        }

        ds.close();

    }
}

 

接收端:

异常:发送数据过长,超过1024字节,后面内容无法接收。

public class UDPReceivingDemo {
    public static void main(String[] args) throws IOException {

        DatagramSocket ds = new DatagramSocket(10010);

        byte[] bytes = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
        System.out.println("===========UDP is receiving===========");

        while (true) {
            
            ds.receive(dp);

            byte[] result = dp.getData();
            int len = dp.getLength();
            String data = new String(result, 0, len);
            if ("886".equals(data)) {
                break;
            }

            System.out.println("接收到的数据:  " + data);
        }

        System.out.println("===========UDP is ending===========");

        ds.close();
    }
}

 

2.4 UDP三种通讯方式

  • 单播单播用于两个主机之间的端对端通信(上面的代码即为单播)
  • 组播组播用于对一组特定的主机进行通信组播地址:224.0.0.0——239.255.255.255

    其中224.0.0.0——224.0.0.255为预留的组播地址

  • 广播广播用于一个主机对整个局域网上所有主机上的数据通信广播地址:255.255.255.255
// 广播实现,修改发送端IP为
InetAddress address = InetAddress.getByName("255.255.255.255");

 

 

2.5 UDP组播实现

实现步骤

  • 发送端
    1. 创建发送端的Socket对象(MulticastSocket)
    2. 创建数据,并把数据打包(DatagramPacket)
    3. 调用MulticastSocket对象的方法发送数据(在单播中,这里是发给指定IP的电脑但是在组播当中,这里是发给组播地址
    4. 释放资源
  • 接收端
    1. 创建接收端Socket对象 (MulticastSocket)
    2. 创建一个箱子,用于接收数据 DatagramPacket
    3. 把当前计算机绑定一个组播地址ms.joinGroup(address);
    4. 将数据接收到箱子中
    5. 解析数据包,并打印数据
    6. 释放资源

 

发送端:(来自old day22)

public class SendMessageDemo1 {
    public static void main(String[] args) throws IOException {
         /*
            组播发送端代码
        */

        //创建MulticastSocket对象
        MulticastSocket ms = new MulticastSocket() ;

        // 创建DatagramPacket对象
        String s = "你好, 鄭華生先生!" ;
        byte[] bytes = s.getBytes();
        InetAddress address = InetAddress.getByName("224.0.0.1");
        int port = 10000;

        DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, address, port) ;

        // 调用MulticastSocket发送数据方法发送数据
        ms.send(datagramPacket);

        // 释放资源
        ms.close();

    }
}

 

接收端:(来自old day22)

/**
 接收端
 */
public class ServerDemo3 {
    public static void main(String[] args) throws Exception {
        System.out.println("=====服务端启动======");
        // 1、创建接收端对象:注册端口(人)
        MulticastSocket socket = new MulticastSocket(9999);

//        socket.joinGroup(InetAddress.getByName("224.0.1.1"));
        // 注意:绑定组播地址(加群)
        // 把当前接收端加入到一个组播组中,绑定对应的组播消息的组播IP
        socket.joinGroup(new InetSocketAddress(InetAddress.getByName("224.0.1.1") , 9999),
                NetworkInterface.getByInetAddress(InetAddress.getLocalHost()));
//                NetworkInterface.getByInetAddress(InetAddress.getByName("192.168.0.103")));

        // 2、创建一个数据包对象接收数据(韭菜盘子)
        byte[] buffer = new byte[1024 * 64];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);


        while (true) {
            // 3、等待接收数据。
            socket.receive(packet);
            // 4、取出数据即可
            // 读取多少倒出多少
            int len = packet.getLength();
            String rs = new String(buffer,0, len);
            System.out.println("收到了来自:" + packet.getAddress() +", 对方端口是" + packet.getPort() +"的消息:" + rs);
        }
    }
}

 

3. TCP通信

TCP协议发送和接收数据

3.1 TCP发送数据

  • Java中的TCP通信
    • Java对基于TCP协议的的网络提供了良好的封装,使用Socket对象来代表两端的通信端口,通信之前要保证连接已经建立,并通过Socket产生IO流来进行网络通信。
    • Java为客户端提供了Socket类,为服务器端提供了ServerSocket类
  • 构造方法
    方法名 说明
    Socket(InetAddress address,int port) 创建流套接字并将其连接到指定IP指定端口号
    Socket(String host, int port) 创建流套接字并将其连接到指定主机上的指定端口号
  • Socket类成员方法
    方法名 说明
    InputStream getInputStream() 返回此套接字的输入流
    OutputStream getOutputStream() 返回此套接字的输出流

 

客户端发送数据(后运行,否则因无法建立连接而报错):(来自old day22)

Exception in thread "main" java.net.ConnectException: Connection refused: connect

public class ClientDemo1 {
    public static void main(String[] args) {
        System.out.println("============客户端启动成功=========");

        try {
            //1.创建Socket对象
            //细节:在创建对象的同时会连接服务端
            //      如果连接不上,代码会报错
            Socket socket = new Socket("127.0.0.1", 7777);

            //2.可以从连接通道中获取输出流
            OutputStream os = socket.getOutputStream();
            // os.write("郑华生666".getBytes());

            // 低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            //写出数据  (注意这里没有换行!!!)
            // 服务端:java.net.SocketException: Connection reset,
            // 因为此处没有发出换行符,服务端仍在等待,而客户端已经运行结束,服务端殉情
            // ps.print("我是TCP客户端,我已经与你对接,约吗?");
            ps.println("我是TCP客户端,我已经与你对接,约吗?");
            ps.flush();

            // 3.释放资源 (需要保证消息发完)
            // os.close();
//            socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

 

3.2 TCP接收数据

  • 构造方法
    方法名 说明
    ServletSocket(int port) 创建绑定到指定端口的服务器套接字
  • ServerSocket类成员方法
    方法名 说明
    Socket accept() 等待接收客户端的Socket通信连接
    连接成功返回Socket对象与客户端建立端到端通信
  • 注意事项
    1. accept方法是阻塞的,作用就是等待客户端连接
    2. 客户端创建对象并连接服务器,此时是通过三次握手协议,保证跟服务器之间的连接
    3. 针对客户端来讲,是往外写的,所以是输出流
      针对服务器来讲,是往里读的,所以是输入流
    4. read方法也是阻塞的
    5. 客户端在关流的时候,还多了一个往服务器写结束标记的动作
    6. 最后一步断开连接,通过四次挥手协议保证连接终止

 

服务端接收数据(先运行):(来自old day22)

public class ServerDemo1 {
    public static void main(String[] args) {
        //TCP协议,接收数据
        try {
            System.out.println("============服务器启动成功=========");


            //1.创建对象ServerSocker
            ServerSocket ss = new ServerSocket(7777);

            //2.监听客户端的链接
            Socket socket = ss.accept();  // 阻塞
            //  3.从连接通道中获取输入流读取数据
            // 字节流读中文乱码
            InputStream is = socket.getInputStream();

            // 包装为字符流
            // InputStreamReader isr = new InputStreamReader(is);
            // 还能继续包装为缓冲流
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            String msg;
            if ((msg = br.readLine()) != null) {
                System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
            }

            //4.释放资源
            // socket.close();
            // ss.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

3.3 三次握手和四次挥手

  • 三次握手

07_TCP三次握手

 

  • 四次挥手(确保连接断开,且数据处理完毕)

08_TCP四次挥手

 

3.4 案例(多发多收)

客户端与接收端使用死循环,实现多发多收,输入exit退出。

读取数据时,读到换行符结束,因此写出数据时要多写出个换行符。

 

客户端:

public class ClientTest {
    public static void main(String[] args) {
        System.out.println("============客户端启动成功=========");

        try {
            Socket socket = new Socket("127.0.0.1", 8848);

            OutputStream os = socket.getOutputStream();

            PrintStream ps = new PrintStream(os);

            Scanner sc = new Scanner(System.in);

            while (true) {
                // 发送消息
                System.out.println("请输入:");
                String msg = sc.nextLine();
                if ("exit".equals(msg)) {
                    System.out.println("====感谢使用,再见!=====");
                    ps.println(msg);
                    ps.flush();
                    break;
                }
                ps.println(msg);
                ps.flush();
            }
//            socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

服务端:(单个客户端)

本案例实现了多发多收,那么是否可以同时接收多个客户端的消息?

不可以的。因为服务端现在只有一个线程,只能与一个客户端进行通信。

public class ServerTest {
    public static void main(String[] args) {
        // 日期格式:
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("============服务端启动成功=========");

        try {
            ServerSocket serverSocket = new ServerSocket(8848);

            Socket socket = serverSocket.accept();

            InputStream is = socket.getInputStream();

            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            String msg;
            // 服务端while read
            while ((msg = br.readLine()) != null) {
                if ("exit".equals(msg.trim())) {
                    break;
                }
                Date date = new Date();
                System.out.println(sdf.format(date));
                System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
                System.out.println("-------------------------------------------");
            }
            System.out.println("Good Bye!!!!!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

3.5 服务端实现多线程

  1. 主线程定义了循环负责接收客户端Socket管道连接
  2. 每接收到一个Socket通信管道后分配一个独立的线程负责处理它。
  3. 客户端不用处理

服务端多线程

 

服务端线程池类:

  • 增加主动关闭下线逻辑
public class ServerSocketThread extends Thread{
    private Socket socket;
    public ServerSocketThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            // 时间定义
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            String msg;
            // 该消息不断接收消息,当该程序手动下线,会抛出异常
            while ((msg = br.readLine()) != null) {
                if ("exit".equals(msg.trim())) {
                    System.out.println(socket.getRemoteSocketAddress() + ",已经 exit 方式下线");
                    System.out.println("Good Bye!!!!!");
                    System.out.println("-------------------------------------------");
                    break;
                }
                Date date = new Date();
                System.out.println(sdf.format(date));
                System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
                System.out.println("-------------------------------------------");
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + ", 关闭下线!");
            System.out.println("-------------------------------------------");
        }
    }
}

 

服务端类:

  • 增加上线提示
public class ServerTest {
    public static void main(String[] args) {
        System.out.println("============服务端启动成功=========");

        try {
            ServerSocket serverSocket = new ServerSocket(8848);

            // a. 定义一个死循环由 (主线程) 负责不断的接收客户端的socket管道连接
            while (true) {
                // 没接收到一个客户端的socket管道,交给一个独立的子线程负责读取消息
                Socket socket = serverSocket.accept();
			   // 上线提示
                System.out.println(socket.getRemoteSocketAddress() + "已上线");
                System.out.println("-------------------------------------------");

                // 开始创建独立的线程处理 socket
                new ServerSocketThread(socket).start();

            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

运行结果:

============服务端启动成功=========
/127.0.0.1:4801已上线
-------------------------------------------
/127.0.0.1:4806已上线
-------------------------------------------
/127.0.0.1:4810已上线
-------------------------------------------
2023-05-21 17:17:51
/127.0.0.1:4806说了:hello
-------------------------------------------
/127.0.0.1:4806,已经 exit 方式下线
Good Bye!!!!!
-------------------------------------------
2023-05-21 17:18:16
/127.0.0.1:4801说了:byebye
-------------------------------------------
2023-05-21 17:18:16
/127.0.0.1:4801说了:
-------------------------------------------
/127.0.0.1:4801, 关闭下线!
-------------------------------------------
2023-05-21 17:18:37
/127.0.0.1:4810说了:ggg
-------------------------------------------
/127.0.0.1:4810,已经 exit 方式下线
Good Bye!!!!!
-------------------------------------------

 

3.6 线程池优化

目前的通信架构存在什么问题?
客户端与服务端的线程模型是: N-N的关系。
客户端并发越多,系统瘫痪的越快。

服务端可以复用线程处理多个客户端,可以避免系统瘫痪。
适合客户端通信时长较短的场景。

客户端不动

引入线程池处理多个客户端消息

 

Runnable线程:

public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            String msg;
            while ((msg = br.readLine()) != null) {
                if ("exit".equals(msg.trim())) {
                    System.out.println(socket.getRemoteSocketAddress() + "exit 方式下线");
                    System.out.println("Good Bye!!!!!");
                    System.out.println("-------------------------------------------");
                    break;
                }
                Date date = new Date();
                System.out.println(sdf.format(date));
                System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
                System.out.println("-------------------------------------------");
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "已下线!");
            System.out.println("-------------------------------------------");
        }
    }
}

 

服务端:(成员变量配置线程池)

public class ServerTest {

    // 使用静态变量存储一个线程池对象
    
    // 意味着,第四个客户端打开时,进入队列,发消息不会被接收。直到第6个客户端启动才会被接收。第8个触发拒绝策略。
    private static ExecutorService pool = new ThreadPoolExecutor(3, 5, 6,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(2),
            Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());


    public static void main(String[] args) {
        System.out.println("============服务端启动成功=========");

        try {
            ServerSocket serverSocket = new ServerSocket(8848);

            while (true) {
                // 每接收到一个socket
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress() + "已上线");
                // 线程池 处理 任务
//                Runnable target = new ServerReaderRunnable(socket);
//                pool.execute(target);
                pool.execute(new ServerReaderRunnable(socket));

            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

3.7 即时通信

即时通信,是指一个客户端的消息发出去,其他客户端可以接收到。

之前的消息都是发给服务端的。

即时通信需要进行端口转发的设计思想。

服务端需要把在线的Socket管道存储起来。一旦收到一个消息要推送给其他管道

 

客户端线程:

问题:这里输出的getRemoteSocketAddress()为同一个, 为客户端main线程定义的6868端口

socket.getLocalSocketAddress()得到的socket地址是自己的本地socket地址

public class ClientReaderThread extends Thread{
    
    private Socket socket;
    
    public ClientReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                // 这里输出的socket为同一个, 为客户端main线程定义的6868端口
                System.out.println(socket.getRemoteSocketAddress() + "收到了: " + msg);
                // getLocalSocketAddress得到的socket地址是自己的本地socket地址
                System.out.println(socket.getLocalSocketAddress() + "收到了: " + msg);
            }
        } catch (Exception e) {
            System.out.println("服务端把你踢出去了~~");
        }
    }
}

客户端代码:

客户端上线时,为客户端分配一个独立的线程负责读取它收到的消息

/**
    拓展:即时通信

    客户端:发消息的同时,随时有人发消息过来。
    服务端:接收消息后,推送给其他所有的在线socket
 */
public class ClientDemo1 {
    public static void main(String[] args) {
        try {
            System.out.println("====客户端启动===");
            // 1、创建Socket通信管道请求有服务端的连接
            // public Socket(String host, int port)
            // 参数一:服务端的IP地址
            // 参数二:服务端的端口
            Socket socket = new Socket("127.0.0.1", 6868);

            // 马上为客户端分配一个独立的线程负责读取它收到的消息
            new ClientReaderThread(socket).start();

            // 2、从socket通信管道中得到一个字节输出流 负责发送数据
            OutputStream os = socket.getOutputStream();
            // 3、把低级的字节流包装成打印流
            PrintStream ps = new PrintStream(os);

            Scanner sc =  new Scanner(System.in);
            while (true) {
                System.out.println("请说:");
                String msg = sc.nextLine();
                // 4、发送消息
                ps.println(msg);
                ps.flush();
            }
            // 关闭资源。
            // socket.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

服务器代码:

定义集合,将上线的客户端socket加入集合

/**
   目标: 即时通信
 */
public class ServerDemo2 {

    // 定义静态list集合存储当前全部在线的socket管道
    public static List<Socket> onLineSockets = new ArrayList<>();

    public static void main(String[] args) {
        try {
            System.out.println("===服务端启动成功===");
            // 1、注册端口
            ServerSocket serverSocket = new ServerSocket(6868);
            // a.定义一个死循环由主线程负责不断的接收客户端的Socket管道连接。
            while (true) {
                // 2、每接收到一个客户端的Socket管道,交给一个独立的子线程负责读取消息
                Socket socket = serverSocket.accept();
                System.out.println(socket.getRemoteSocketAddress()+ "它来了,上线了!");
                // 把当前客户端管道Socket加入到在线集合中去
                onLineSockets.add(socket);

                // 3、开始创建独立线程处理socket
                new ServerReaderThread(socket).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

服务器线程:

接收所有客户端的消息,并将消息转发给所有在线socket

public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 3、从socket通信管道中得到一个字节输入流
            InputStream is = socket.getInputStream();
            // 4、把字节输入流包装成缓冲字符输入流进行消息的接收
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            // 5、按照行读取消息
            String msg;
            while ((msg = br.readLine()) != null){
                System.out.println(socket.getRemoteSocketAddress() + "说了:" + msg);
                // 把这个消息发给当前所有在线socket
                sendMsgToAll(msg);
            }
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
            // 从在线集合中抹掉本客户端socket
            ServerDemo2.onLineSockets.remove(socket);
        }
    }

    private void sendMsgToAll(String msg) {
        try {
            // 遍历全部的在线 socket给他们发消息
            for (Socket onLineSocket : ServerDemo2.onLineSockets) {
                // 除了自己的socket,其他socket我都发!!
                if(onLineSocket != socket){
                    PrintStream ps = new PrintStream(onLineSocket.getOutputStream());
                    ps.println(msg);
                    ps.flush();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

服务器输出结果:
===服务端启动成功===
/127.0.0.1:11186它来了,上线了!
/127.0.0.1:11186说了:你好
/127.0.0.1:11195它来了,上线了!
/127.0.0.1:11199它来了,上线了!
/127.0.0.1:11195说了:我是
/127.0.0.1:11199说了:华生

(即使通讯客户端界面及私聊及在线人数实现,见old day13中文包代码)

 

4. BS架构

BS架构

 

服务器线程:

public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            // 浏览器 已经与本线程建立了Socket管道
            // 响应消息给浏览器显示
            PrintStream ps = new PrintStream(socket.getOutputStream());
            // 必须响应HTTP协议格式数据,否则浏览器不认识消息
            ps.println("HTTP/1.1 200 OK"); // 协议类型和版本 响应成功的消息!
            ps.println("Content-Type:text/html;charset=UTF-8"); // 响应的数据类型:文本/网页

            ps.println(); // 必须发送一个空行

            // 才可以响应数据回去给浏览器
            ps.println("<span style='color:red;font-size:90px'>你好,乔治华生先生!</span>");
            ps.close();
        } catch (Exception e) {
            System.out.println(socket.getRemoteSocketAddress() + "下线了!!!");
        }
    }
}

 

服务器代码:

public class BSserverDemo {
    // 使用静态变量记住一个线程池对象
    private static ExecutorService pool = new ThreadPoolExecutor(3,
            5, 6, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(2)
            , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        try {
            // 1.注册端口
            ServerSocket ss = new ServerSocket(8080);
            // 2.创建一个循环接收多个客户端的请求。
            while(true){
                Socket socket = ss.accept();
                // 3.交给一个独立的线程来处理!
                pool.execute(new ServerReaderRunnable(socket));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

 

5. 综合练习

5.1 接收并反馈

  • 案例需求客户端:发送数据,接受服务器反馈服务器:收到消息后给出反馈
  • 案例分析
    • 客户端创建对象,使用输出流输出数据
    • 服务端创建对象,使用输入流接受数据
    • 服务端使用输出流给出反馈数据
    • 客户端使用输入流接受反馈数据

socket可以getInputStream()也可以getOutputStream()

客户端:

public class ClientTest2 {
    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("127.0.0.1", 10010);

        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        OutputStream os = socket.getOutputStream();

        Scanner sc = new Scanner(System.in);

        while (true) {
            System.out.println("请输入要发送的内容:");
            String input = sc.nextLine();

            os.write((input + "\n").getBytes());
//            socket.shutdownOutput();

            if ("success".equals(br.readLine())) {
                System.out.println("服务器接收成功!");
            }

            if ("886".equals(input)) {
                break;
            }
        }
        socket.close();
    }
}

 

服务端:

public class ServerTest2 {
    public static void main(String[] args) throws IOException {
        System.out.println("服务器等待连接...");

        ServerSocket ss = new ServerSocket(10010);

        Socket acceptSocket = ss.accept();
        System.out.println("服务器连接成功!");

        // 回写数据
        OutputStream os = acceptSocket.getOutputStream();

        BufferedReader br = new BufferedReader(new InputStreamReader(acceptSocket.getInputStream()));

        String str;

        while ((str = br.readLine()) != null) {
            System.out.println("收到消息: " + str);
            os.write("success\n".getBytes());
        }

        acceptSocket.close();
        ss.close();

        System.out.println("服务器已断开...");
        
    }
}

 

5.2 上传文件

  • 案例需求客户端:数据来自于本地文件,接收服务器反馈服务器:接收到的数据写入本地文件,给出反馈
  • 案例分析
    • 创建客户端对象,创建输入流对象指向文件,每读一次数据就给服务器输出一次数据,输出结束后使用shutdownOutput()方法告知服务端传输结束
    • 创建服务器对象,创建输出流对象指向文件,每接受一次数据就使用输出流输出到文件中,传输结束后。使用输出流给客户端反馈信息
    • 客户端接受服务端的回馈信息
  • 相关方法
    方法名 说明
    void shutdownInput() 将此套接字的输入流放置在“流的末尾”
    void shutdownOutput() 禁止用此套接字的输出流

 

客户端:

import java.io.*;
import java.net.Socket;

public class FileUploadTest6 {
    public static void main(String[] args) throws IOException {
        Socket filenameSocket = new Socket("127.0.0.1", 10000);

        File file = new File("srcdir/0.webm");
        String filename = file.getName();
        System.out.println("文件名: " + filename);  // 文件名: 0.webm

        // 发送文件名给服务器
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(filenameSocket.getOutputStream()));
        bw.write(filename);
        bw.newLine();
        bw.flush();

        // ======================================================



        Socket fileSocket = new Socket("127.0.0.1", 10000);
        System.out.println("已连接到服务器");

        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
        BufferedOutputStream bos = new BufferedOutputStream(fileSocket.getOutputStream());

        int len;
        byte[] bytes = new byte[1024 * 8];
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }

        System.out.println("文件上传完成");
        // 及时关流
        bos.close();
        bis.close();
        // ===============================================================

        // 读取服务端的发送成功信息:
        Socket successSocket = new Socket("127.0.0.1", 10000);
        System.out.println("已连接到服务器");
        BufferedReader br = new BufferedReader(new InputStreamReader(successSocket.getInputStream()));
        System.out.println(br.readLine());
        successSocket.close();


        bis.close();
        bos.close();
        fileSocket.close();
        filenameSocket.close();
    }
}

 

服务端:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

public class FileDownloadTest6 {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(10000);
        System.out.println("等待客户端连接...");
        Socket filenameSocket = ss.accept();
        System.out.println("客户端已连接...");

        BufferedReader bis1 = new BufferedReader(new InputStreamReader(filenameSocket.getInputStream()));
        String fileDescription = bis1.readLine();
        if (fileDescription == null) {
            System.out.println("未接收到文件描述");
            filenameSocket.close();
            ss.close();
            return;
        }
        System.out.println("接收到文件描述: " + fileDescription);  //接收到文件描述: 0.webm

        filenameSocket.shutdownInput();

        // ===================================================================



        Socket fileSocket = ss.accept();
        System.out.println("已接收到文件内容");

        BufferedInputStream bis = new BufferedInputStream(fileSocket.getInputStream());

        // 解析文件描述获取文件名和扩展名
        int dotIndex = fileDescription.lastIndexOf(".");
        String filename = fileDescription.substring(0, dotIndex);
        String extension = fileDescription.substring(dotIndex + 1);

        String str = UUID.randomUUID().toString().replace("-", "");

        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("destdir/" + filename + "_" + str + "." + extension));

        int len;
        byte[] bytes = new byte[1024 * 8];
        while ((len = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, len);
        }
        bos.close();
        System.out.println("文件下载完成");

        // =======================================================
        // 回写成功信息
        Socket successSocket = ss.accept();
        
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(successSocket.getOutputStream()));
        bw.write("文件传输成功!");
        bw.newLine();
        bw.flush();

        successSocket.close();

        bis1.close();
        bw.close();
        fileSocket.close();
        filenameSocket.close();
        ss.close();
    }
}

bw.newLine(); bw.flush();

使用newLine()方法添加换行符,确保服务器可以通过readLine()方法读取到完整的内容

 

5.3 服务器改写为多线程

服务器只能处理一个客户端请求,接收完一个图片之后,服务器就关闭了。

优化方案一:使用循环

弊端:第一个用户正在上传数据,第二个用户就来访问了,此时第二个用户是无法成功上传的。所以,使用多线程改进

优化方案二:每来一个用户,就开启多线程处理

 

MyRunnable.java:

import java.io.*;
import java.net.Socket;
import java.util.UUID;

public class MyRunnable implements Runnable{

    Socket socket;

    public MyRunnable(Socket socket){
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //3.读取数据并保存到本地文件中
            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            String name = UUID.randomUUID().toString().replace("-", "");
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("mysocketnet\\serverdir\\" + name + ".jpg"));
            int len;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
            bos.close();
            //4.回写数据
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            bw.write("上传成功");
            bw.newLine();
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //5.释放资源
           if(socket != null){
               try {
                   socket.close();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
        }
    }
}

 

客户端:

import java.io.*;
import java.net.Socket;

public class Client {
    public static void main(String[] args) throws IOException {
        //客户端:将本地文件上传到服务器。接收服务器的反馈。
        //服务器:接收客户端上传的文件,上传完毕之后给出反馈。


        //1. 创建Socket对象,并连接服务器
        Socket socket = new Socket("127.0.0.1",10000);

        //2.读取本地文件中的数据,并写到服务器当中
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("mysocketnet\\clientdir\\a.jpg"));
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        byte[] bytes = new byte[1024];
        int len;
        while ((len = bis.read(bytes)) != -1){
            bos.write(bytes,0,len);
        }

        //往服务器写出结束标记
        socket.shutdownOutput();


        //3.接收服务器的回写数据
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = br.readLine();
        System.out.println(line);


        //4.释放资源
        socket.close();

    }
}

 

服务端:


import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        //客户端:将本地文件上传到服务器。接收服务器的反馈。
        //服务器:接收客户端上传的文件,上传完毕之后给出反馈。


        //1.创建对象并绑定端口
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            //2.等待客户端来连接
            Socket socket = ss.accept();

            //开启一条线程
            //一个用户就对应服务端的一条线程
            new Thread(new MyRunnable(socket)).start();
        }

    }
}

服务端(线程池版本):

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Server {
    public static void main(String[] args) throws IOException {
        //客户端:将本地文件上传到服务器。接收服务器的反馈。
        //服务器:接收客户端上传的文件,上传完毕之后给出反馈。

        //创建线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,//核心线程数量
                16,//线程池总大小
                60,//空闲时间
                TimeUnit.SECONDS,//空闲时间(单位)
                new ArrayBlockingQueue<>(2),//队列
                Executors.defaultThreadFactory(),//线程工厂,让线程池如何创建线程对象
                new ThreadPoolExecutor.AbortPolicy()//阻塞队列
        );

        //1.创建对象并绑定端口
        ServerSocket ss = new ServerSocket(10000);

        while (true) {
            //2.等待客户端来连接
            Socket socket = ss.accept();

            //开启一条线程
            //一个用户就对应服务端的一条线程
            //new Thread(new MyRunnable(socket)).start();
            pool.submit(new MyRunnable(socket));
        }

    }
}

 

6. 即时通讯升级版

增加上下线全服广播,添加私聊及群发选择,添加系统回复错误。

代码 作用
ClientMain 客户端主界面,用于发送消息
ClientReceiveThread 客户端运行后,为每个客户端socket分配线程,死循环用于接收服务端转发的消息
ServerMain 服务端主界面,死循环接收客户端socket,为其分配线程进行消息转发
ServerRunnable 实现群发及消息私聊,校验用户发出的内容及目的地是否存在问题

 

ClientMain:

public class ClientMain {
    public static void main(String[] args) {

        try {
            Socket socket = new Socket("127.0.0.1", 9999);

            // 新建线程用于为当前客户端接收消息
            new ClientReceiveThread(socket).start();

            // 当前客户端界面输出
            PrintStream ps = new PrintStream(socket.getOutputStream());

            Scanner sc = new Scanner(System.in);
            while (true) {
                System.out.println("请输入你要发送的内容:");
                String text = sc.nextLine();
                // 将信息发给服务端进行中转
                ps.println(text);
                ps.flush();

                System.out.println("请输入发送地址(all为群发):");
                String address = sc.nextLine();
                // 将信息发给服务端进行中转
                ps.println(address);
                ps.flush();

                System.out.println("=========消息已上传服务器=========");
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

 

ClientReceiveThread:

public class ClientReceiveThread extends Thread{

    private Socket socket;

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EE");

    public ClientReceiveThread(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {

        System.out.println("------------------------------------------------------------");
        System.out.println("您的本地Socket地址为:" + socket.getLocalSocketAddress());
        System.out.println("------------------------------------------------------------");

        try {
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));

            String address;
            String text;


            while ((address = br.readLine()) != null && (text = br.readLine()) != null) {
                // 先收到 发送者地址  后收到  文本内容
                System.out.println(sdf.format(new Date()) + "   Socket地址:" + address  + "说了:\n" + text);
                System.out.println("------------------------------------------------------------");
            }


        } catch (IOException e) {
            System.out.println("===========服务端已下线===========");
        }

    }
}

 

ServerMain:

public class ServerMain {

    public static HashMap<String, Socket> allOnlineSocket = new HashMap<>();

    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EE");
        System.out.println("==================服务器端已启动==================");

        try {
            ServerSocket ss = new ServerSocket(9999);

            while (true) {
                Socket socket = ss.accept();
                String socketAddress = socket.getRemoteSocketAddress().toString();

                // 日期时间
                Date date = new Date();
                String time = sdf.format(date);

                System.out.println("------------------------------------------------------------");
                System.out.println(time + "\n" + socketAddress + "已经上线!");
                System.out.println("------------------------------------------------------------");

                allOnlineSocket.put(socketAddress, socket);

                // 用线程处理该socket发送的信息,实现转发
                new Thread(new ServerRunnable(socket)).start();

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 

ServerRunnable:

public class ServerRunnable implements Runnable{

    private Socket socket;

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss EE");

    private String localSocketAddress;  //    /127.0.0.1:5140

    public ServerRunnable(Socket socket) {
        this.socket = socket;
        this.localSocketAddress = socket.getRemoteSocketAddress().toString();
    }


    @Override
    public void run() {

        try {

            sendMessageToAll("【自动群发】 【" + localSocketAddress + "】已经上线啦~~~");

            // 接收每个客户端发来的消息
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 第一个为内容,第二个为地址,输入all为群发


            while (true) {
                String text = br.readLine();
                String address = br.readLine();

                boolean flag = checkMessageAndAddress(text, address);

                if (flag) {
                    // 文本不为空,并且发送的地址存在
                    // 用户群发
                    if (address.trim().equalsIgnoreCase("all")) {
                        // 服务端输出
                        System.out.println(sdf.format(new Date()) + "   " + localSocketAddress + ",【群发】了:\n" + text);
                        System.out.println("------------------------------------------------------------");

                        sendMessageToAll(text);
                    } else {
                        // 服务端输出
                        System.out.println(sdf.format(new Date()) + "   " + localSocketAddress + ",向" + address + "【私聊了】了:");
                        System.out.println(text);
                        System.out.println("------------------------------------------------------------");
                        sendMessageToOne(text, address);
                    }

                } else {
                    // 文本为空,或地址不存在
                    sendMessageToOne("【系统回复】文本为空或指定Socket地址不存在!!", localSocketAddress);
                }
            }



        } catch (IOException e) {
            System.out.println(localSocketAddress + "下线了!!!");
            sendMessageToAll("【自动群发】 【" + localSocketAddress + "】已经下线啦,再见~~~");
            System.out.println("------------------------------------------------------------");
            // 从在线集合中抹掉本客户端socket
            ServerMain.allOnlineSocket.remove(localSocketAddress);
        }
    }

    /**
     * 群发
     * @param text 文本
     */
    private void sendMessageToAll(String text) {
        Collection<Socket> allSockets = ServerMain.allOnlineSocket.values();

        for (Socket receiveSocket : allSockets) {
            // 本地不再次接收自己所发出的群发消息
            if (receiveSocket != socket) {
                try {
                    PrintStream ps = new PrintStream(receiveSocket.getOutputStream());
                    // 先发送 自己(发送者)的地址
                    ps.println(localSocketAddress);
                    ps.flush();
                    // 再发送  文本
                    ps.println(text);
                    ps.flush();

                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * 发送信息 给 指定地址
     * @param text 文本
     * @param address 地址
     */
    private void sendMessageToOne(String text, String address) {
        Socket destSocket = ServerMain.allOnlineSocket.get(address);


        try {
            PrintStream ps = new PrintStream(destSocket.getOutputStream());
            // 先发送 自己(发送者)的地址
            ps.println(localSocketAddress);
            ps.flush();

            // 在发送  文本
            ps.println(text);
            ps.flush();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 检查 发送文本 或 socket地址 是否符合
     * @param text 文本
     * @param address socket地址
     * @return flag
     */
    private boolean checkMessageAndAddress(String text, String address) {
        if ("".equals(text.trim()) || "".equals(address.trim())) {
            System.out.println("text or address is NULL!!!!!!!!");
            return false;
        }
        // all 特殊情况
        if (address.trim().equalsIgnoreCase("all")) {
            return true;
        }

        Set<String> addressSet = ServerMain.allOnlineSocket.keySet();
        for (String s : addressSet) {
            if (address.equals(s)) {
                return true;
            }
        }
        System.out.println("No address Match!!!!!!!");
        return false;
    }
}

 

day33 反射&动态代理

1.反射

1.1 概述

反射允许对封装类的字段(成员变量),方法(成员方法)和构造函数的信息进行编程访问。

专业的解释:

  • 是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
  • 对于任意一个对象,都能够调用它的任意属性和方法;
  • 这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

通俗的理解:

  • 利用反射创建的对象可以无视修饰符调用类里面的内容
  • 可以跟配置文件结合起来使用,把要创建的对象信息和方法写在配置文件中。
  • 读取到什么类,就创建什么类的对象
  • 读取到什么方法,就调用什么方法
  • 此时当需求变更的时候不需要修改代码,只要修改配置文件即可。

反射的作用:(面试题)

你觉得反射好不好?好,有两个方向

第一个方向:无视修饰符访问类中的内容。但是这种操作在开发中一般不用,都是框架底层来用的。(获取一个类里面所有的信息,获取到了之后,再执行其他的业务逻辑)

第二个方向:反射可以跟配置文件结合起来使用,动态的创建对象,动态的调用方法。

反射的概述

 

1.2 获取字节码文件对象的三种方式

 

java文件:就是我们自己编写的java代码。

字节码文件:就是通过java文件编译之后的class文件(是在硬盘上真实存在的,用眼睛能看到的)

字节码文件对象:当class文件加载到内存之后,虚拟机自动创建出来的对象。

这个对象里面至少包含了:构造方法,成员变量,成员方法。

而我们的反射获取的是什么?字节码文件对象,这个对象在内存中是唯一的。

 

三种方式:

  • Class.forName(“全类名”)
  • 类名.class
  • 对象.getClass()

获取class对象的三种方式

 

代码:

public class ReflectDemo1 {

    public static void main(String[] args) throws ClassNotFoundException {

        //1.Class这个类里面的静态方法forName
        //Class.forName("类的全类名"): 全类名 = 包名 + 类名

        //源代码阶段获取 --- 先把Student加载到内存中,再获取字节码文件的对象
        //clazz 就表示Student这个类的字节码文件对象。
        //就是当Student.class这个文件加载到内存之后,产生的字节码文件对象
        Class<?> clazz = Class.forName("day35.Student");

        System.out.println(clazz);  // class day35.Student

        //2.通过class属性获取
        //类名.class  加载阶段 (一般当做参数进行传递,synchronized)
        Class<Student> clazz2 = Student.class;

        //因为class文件在硬盘中是唯一的,所以,当这个文件加载到内存之后产生的对象也是唯一的
        System.out.println(clazz == clazz2);  // true

        // 3.通过Student对象获取字节码文件对象  运行阶段
        // 当我们已经有了这个类的对象时
        Student s = new Student();
        Class<? extends Student> clazz3 = s.getClass();

        System.out.println(clazz2 == clazz3);  // true

    }
}

 

1.3 获取构造方法

规则:

  • get表示获取
  • Declared表示私有
  • 最后的s表示所有,复数形式
  • 如果当前获取到的是私有的,必须要临时修改访问权限,否则无法使用

 

Class类中用于获取构造方法的方法:

方法名 说明
Constructor<?>[] getConstructors() 获得所有的构造方法对象的数组(只能public修饰)
Constructor<?>[] getDeclaredConstructors() 获得所有的构造方法对象的数组(包含private修饰)
Constructor<T> getConstructor(Class<?>... parameterTypes) 获取指定公共构造方法对象(只能public修饰)
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 获取指定构造方法对象(包含private修饰)
// 获取class字节码文件对象
Class<?> clazz = Class.forName("day35.Student");

// 获取构造方法 public
Constructor<?>[] cons = clazz.getConstructors();
for (Constructor<?> con : cons) {
    System.out.println(con);
    // public day35.Student()
    // public day35.Student(java.lang.String)
}
System.out.println("========================================");
Constructor<?>[] cons2 = clazz.getDeclaredConstructors();
for (Constructor<?> con : cons2) {
    System.out.println(con);
    // public day35.Student()
    // private day35.Student(java.lang.String,int)
    // protected day35.Student(int)
    // public day35.Student(java.lang.String)
}
System.out.println("========================================");
// 获取空参构造
Constructor<?> con1 = clazz.getDeclaredConstructor();
System.out.println(con1);  // public day35.Student()
System.out.println("========================================");
Constructor<?> con2 = clazz.getDeclaredConstructor(String.class, int.class);
System.out.println(con2);  // private day35.Student(java.lang.String,int)
System.out.println("========================================");
// Constructor<?> con3 = clazz.getConstructor(int.class); // protected修饰
// Exception in thread "main" java.lang.NoSuchMethodException: day35.Student.<init>(int)
// System.out.println(con3);
System.out.println("========================================");
Constructor<?> con4 = clazz.getConstructor(String.class); // public修饰
System.out.println(con4);  // public day35.Student(java.lang.String)

 

Constructor类中用于获取和创建对象的方法:

构造方法创建对象!

方法名 说明
T newlnstance(Object... initargs) 根据指定的构造方法创建对象
setAccessible(boolean flag) 设置为true,表示取消访问检查
int getModifiers() 获取权限修饰符
int getParameterCount() 获取构造方法参数个数
Parameter[] getParameters() 父类Executable方法,返回参数对象数组

代码如下:

// 获取权限修饰符  Modifier.class定义
int modifiers1 = con1.getModifiers();  // public
System.out.println(modifiers1);   // 1

int modifiers2 = clazz.getDeclaredConstructor(int.class).getModifiers();  // protected
System.out.println(modifiers2);  // 4

int modifiers3 = con2.getModifiers();  // private
System.out.println(modifiers3);   // 2

// 获取构造方法参数个数
int parameterCount = con2.getParameterCount();
System.out.println(parameterCount);  // 2
// 父类Executable方法,返回参数对象数组
Parameter[] parameters = con2.getParameters();
for (Parameter parameter : parameters) {
    System.out.println(parameter);
    // java.lang.String arg0
    // int arg1
}

// 暴力反射:表示临时取消权限校验
con2.setAccessible(true);
Student stu = (Student) con2.newInstance("张三", 23);
// 没有setAccessible报错
// Exception in thread "main" java.lang.IllegalAccessException:
// Student with modifiers "private"
System.out.println(stu);  // Student{name = 张三, age = 23}

 

1.4 获取成员变量

Class类中用于获取成员变量的方法:

方法名 说明
Field[] getFields() 返回所有成员变量对象的数组(只能拿public的)
Field[] getDeclaredFields() 返回所有成员变量对象的数组,存在就能拿到
Field getField(String name) 返回单个成员变量对象(只能拿public的)
Field getDeclaredField(String name) 返回单个成员变量对象,存在就能拿到

 

Field类中用于创建对象的方法:

方法 说明
void set(Object obj, Object value) 赋值
Object get(Object obj) 获取值

 

代码如下:

Class<?> clazz = Class.forName("day35.Student1");

// 获取所有
Field[] fields = clazz.getFields();// public
for (Field field : fields) {
    System.out.println(field);  // public java.lang.String day35.Student1.gender
}

System.out.println("============================================");

Field[] fields1 = clazz.getDeclaredFields();
for (Field field : fields1) {
    System.out.println(field);
    // private java.lang.String day35.Student1.name
    // private int day35.Student1.age
    // public java.lang.String day35.Student1.gender
}

System.out.println("============================================");
// 获取单个
Field gender = clazz.getField("gender");
System.out.println(gender);  // public java.lang.String day35.Student1.gender
System.out.println("============================================");
Field name = clazz.getDeclaredField("name");
System.out.println(name);   // private java.lang.String day35.Student1.name
System.out.println("============================================");

// 权限修饰符
System.out.println(name.getModifiers());  // 2
System.out.println(gender.getModifiers());  // 1

// 获取成员变量名
System.out.println(name.getName());  // name

// 获取数据类型
System.out.println(name.getType());   // class java.lang.String

// 获取成员变量记录的值
Student1 stu = new Student1("Watson", 22, "男");

name.setAccessible(true);
Object o = name.get(stu);
// setAccessible():
// Exception in thread "main" java.lang.IllegalAccessException
System.out.println(o);  // Watson

// 修改对象里面记录的值
name.set(stu, "GeorgeWatson");

System.out.println(stu.getName());  // GeorgeWatson

 

1.5 获取成员方法

Class类获取成员方法的方法:

方法名 说明
Method[] getMethods() 返回所有成员方法对象的数组(只能拿public的),包括继承的
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,存在就能拿到,不包括继承的
Method getMethod(String name, Class<?>... parameterTypes) 返回单个成员方法对象(只能拿public的)
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回单个成员方法对象,存在就能拿到

 

Method类中用于创建对象的方法:

Object invoke(Object obj, Object... args) :运行方法

参数一:用obj对象调用该方法

参数二:调用方法的传递的参数(如果没有就不写)

返回值:方法的返回值(如果没有就不写)

Class<?> clazz = Class.forName("day35.Student2");

// 获取所有方法
Method[] methods = clazz.getMethods();
for (Method method : methods) {
    System.out.println(method);
}
// 包含父类中所有的公共方法
// public void day35.Student2.sleep()
// public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
// public final void java.lang.Object.wait() throws java.lang.InterruptedException
// public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
// public boolean java.lang.Object.equals(java.lang.Object)
// public native int java.lang.Object.hashCode()
// public final native java.lang.Class java.lang.Object.getClass()
// public final native void java.lang.Object.notify()
// public final native void java.lang.Object.notifyAll()
System.out.println("===============================================");

// 无父类方法, 可以获取私有方法
Method[] methods1 = clazz.getDeclaredMethods();
for (Method method : methods1) {
    System.out.println(method);
}
System.out.println("===============================================");

// 获取单一方法
Method m = clazz.getDeclaredMethod("eat", String.class, int.class);
System.out.println(m);  // private void day35.Student2.eat(java.lang.String,int)
// 修饰符
System.out.println(m.getModifiers()); //  2
// 获取名字
System.out.println(m.getName());   // eat
// 获取形参
Parameter[] parameters = m.getParameters();
for (Parameter parameter : parameters) {
    System.out.println(parameter.toString());
    // java.lang.String arg0
    // int arg1
}

// 获取方法抛出的异常
Class<?>[] exceptionTypes = clazz.getDeclaredMethod("eat", String.class)
        .getExceptionTypes();
// [class java.io.IOException, class java.lang.NullPointerException, class java.lang.ClassCastException]
System.out.println(Arrays.toString(exceptionTypes));


// 方法运行
// Object invoke(Object obj, Object... args)
Student2 stu = new Student2();
// 参数一:用obj对象调用该方法
// 参数二:调用方法的传递的参数(如果没有就不写)
m.setAccessible(true);
Object peanut = m.invoke(stu, "花生", 2);  // 在吃花生
// 获取返回值
System.out.println(peanut);  // null 因为该方法没有返回值

 

2. 反射练习

2.1 保存数据

保存任意对象数据

代码如下:

public class MyReflectDemo {
    public static void main(String[] args) throws IllegalAccessException, IOException {
    /*
        对于任意一个对象,都可以把对象所有的字段名和值,保存到文件中去
    */
       Student s = new Student("乔治",20,'男',180.6,"打代码");
       Teacher t = new Teacher("Watson",10000);
       saveObject(s);
       saveObject(t);
    }

    //把对象里面所有的成员变量名和值保存到本地文件中
    public static void saveObject(Object obj) throws IllegalAccessException, IOException {

        String allClassName = obj.getClass().getName(); // day35_test.Student
        String className = allClassName.substring(allClassName.lastIndexOf(".") + 1); // Student
        System.out.println("类名是:" + className);

        //1.获取字节码文件的对象
        Class clazz = obj.getClass();
        //2. 创建IO流
        BufferedWriter bw = new BufferedWriter(new FileWriter("destdir/" + className + ".txt"));
        //3. 获取所有的成员变量
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 一律暴力获取
            field.setAccessible(true);
            //获取成员变量的名字
            String fieldName = field.getName();
            //获取成员变量的值
            Object value = field.get(obj);
            //写出数据
            bw.write(fieldName + "=" + value);
            bw.newLine();
        }
        bw.close();

    }
}

 

2.2 结合配置文件动态创建对象

 

/*
反射可以跟配置文件结合的方式,动态的创建对象,并调用方法
*/

// prop.properties文件内容:
// classname=day35_test.Teacher1
// method=teach

//1.读取配置文件中的信息
Properties prop = new Properties();
FileInputStream fis = new FileInputStream("destdir/prop.properties");
prop.load(fis);
fis.close();
System.out.println(prop);   // {classname=day35_test.Teacher1, method=teach}


//2.获取全类名和方法名
String className = (String) prop.get("classname");
String methodName = (String) prop.get("method");

System.out.println(className);  // day35_test.Teacher1
System.out.println(methodName);  // teach

//3.利用反射创建对象并运行方法
Class clazz = Class.forName(className);

//获取构造方法
Constructor con = clazz.getDeclaredConstructor();
Object o = con.newInstance();
System.out.println(o);  // Teacher{name = null, salary = 0.0}

//获取成员方法并运行
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
method.invoke(o);   /// 老师在教书!

 

2.3 利用反射擦除泛型

集合中的泛型只在java文件中存在,当编译成class文件之后,就没有泛型了。


public class ReflectDemo3 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

        ArrayList<Integer> list = new ArrayList<>();
        list.add(123);

        // list.add("aaa");  无法add

        // 2.利用反射运行add方法去添加字符串
        // 因为反射使用的是class字节码文件

        // 获取class对象
        Class clazz = list.getClass();

        // 获取add方法对象
        Method method = clazz.getDeclaredMethod("add", Object.class);

        //运行方法
        method.invoke(list,"aaa");

        //打印集合
        System.out.println(list);  // [123, aaa]

    }
}

 

2.4 修改字符串的内容

字符串不能修改的真正原因。

字符串,在底层是一个byte类型的字节数组,名字叫做value

private final byte[] value;

真正不能被修改的原因:final 和 private

final修饰value表示value记录的地址值不能修改。

private修饰value而且没有对外提供getvalue和setvalue的方法。所以,在外界不能获取或修改value记录的地址值。

如果要强行修改可以用反射:

String s = "abc";
String ss = "abc";
// private final byte[] value= {97,98,99};
// 没有对外提供getvalue和setvalue的方法,不能修改value记录的地址值
// 如果我们利用反射获取了value的地址值。
// 也是可以修改的,final修饰的value
// 真正不可变的value数组的地址值,里面的内容利用反射还是可以修改的,比较危险

//1.获取class对象
Class clazz = s.getClass();

//2.获取value成员变量(private)
Field field = clazz.getDeclaredField("value");
//但是这种操作非常危险
//JDK高版本已经屏蔽了这种操作,低版本还是可以的
//临时修改权限
field.setAccessible(true);

//3.获取value记录的地址值
byte[] bytes = (byte[]) field.get(s);
bytes[0] = 100;

System.out.println(s);//dbc
System.out.println(ss);//dbc

 

3. 动态代理

 

好处:无侵入式的给方法增强功能(调用者 → 代理 → 对象)

 

代理里面就是对象要被代理的方法

通过接口保证,后面的对象和代理需要实现同一个接口,接口中就是被代理的所有方法

 

动态代理三要素:

  1. 真正干活的对象
  2. 代理对象
  3. 利用代理调用方法

切记一点:代理可以增强或者拦截的方法都在接口中,接口需要写在newProxyInstance的第二个参数里。

动态代理的思想分析

 

java.lang.reflect,Proxy类:提供了为对象产生代理对象的方法

public static Obiect newProxyInstance(Classloader loader, Class<?>[] interfaces, InvocationHandler h)

参数一:用于指定用哪个类加载器,去加载生成的代理类
参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
参数三:用来指定生成的代理对象要干什么事情

 

接口:

public interface Star {
    //我们可以把所有想要被代理的方法定义在接口当中

    //唱歌
    public abstract String sing(String name);

    //跳舞
    public abstract void dance();
}

 

被代理的类:

public class BigStar implements Star {
    private String name;
	// ............
    @Override
    public String sing(String name) {
        System.out.println(this.name + "正在唱:" + name);
        return "Oshi no ko";
    }

    @Override
    public void dance() {
        System.out.println(this.name + "正在跳舞");
    }
}

 

代理工具类:

public class ProxyUtil {
    /*
     *
     * 方法的作用:
     *       给一个明星的对象,创建一个代理
     *
     *  形参:
     *       被代理的明星对象
     *
     *  返回值:
     *       给明星创建的代理
     *
     * 需求:
     *   外面的人想要大明星唱一首歌
     *   1. 获取代理的对象
     *      代理对象 = ProxyUtil.createProxy(大明星的对象);
     *   2. 再调用代理的唱歌方法
     *      代理对象.唱歌的方法("《偶像》");
     * */
    public static Star createProxy(BigStar bigStar){
       /* java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法:

        public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
        参数一:用于指定用哪个类加载器,去加载生成的代理类
        参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
        参数三:用来指定生成的代理对象要干什么事情*/
        Star star = (Star) Proxy.newProxyInstance(
                ProxyUtil.class.getClassLoader(),  //参数一:用于指定用哪个类加载器,去加载生成的代理类
                new Class[]{Star.class},  //参数二:指定接口,这些接口用于指定生成的代理长什么,也就是有哪些方法
                //参数三:用来指定生成的代理对象要干什么事情
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        /*
                         * 参数一:代理的对象
                         * 参数二:要运行的方法 sing
                         * 参数三:调用sing方法时,传递的实参
                         * */
                        if("sing".equals(method.getName())){
                            System.out.println("准备话筒,收钱");
                        }else if("dance".equals(method.getName())){
                            System.out.println("准备场地,收钱");
                        }
                        //去找大明星开始唱歌或者跳舞
                        //代码的表现形式:调用大明星里面唱歌或者跳舞的方法
                        return method.invoke(bigStar,args);
                    }
                }
        );
        return star;
    }
}

 

测试类:

public class Test {
    public static void main(String[] args) {
    /*
        需求:
            外面的人想要大明星唱一首歌
             1. 获取代理的对象
                代理对象 = ProxyUtil.createProxy(大明星的对象);
             2. 再调用代理的唱歌方法
                代理对象.唱歌的方法("只因你太美");
     */
        //1. 获取代理的对象
        BigStar bigStar = new BigStar("爱");
        Star proxy = ProxyUtil.createProxy(bigStar);

        //2. 调用唱歌的方法
        String result = proxy.sing("《偶像》");
        System.out.println(result);
        // 准备话筒,收钱
        // 爱正在唱:《偶像》
        // Oshi no ko
    }
}

 

执行逻辑流程:

动态代理的代码实现

 

day34 其他

1. 日志

1.1 概述

作用:

跟输出语句一样,可以把程序在运行过程中的详细信息都打印在控制台上。

利用log日志还可以把这些详细信息保存到文件和数据库中。

 

优势:

输出语句 日志技术
输出位置 只能是控制台 可以将日志信息写入到文件或者数据库中
取消日志 需要修改代码,灵活性较差 不需要修改代码,灵活性较好
多线程 性能较差 性能较好

 

体系结构:

日志体系

 

1.2 Logback日志框架

Logback是基于slf4j的日志规范实现的框架,性能比之前使用的log4j要好。

官网:https://logback.qos.ch/index.html

 

Logback主要分为三个技术模块:

  • logback-core:该模块为其他两个模块提供基础代码,必须有。
  • logback-classic:完整实现了slf4j API的模块
  • logback-access模块与TomcatJettyServlet容器集成,以提供 HTTP 访问日志功能

 

1.3 logback.xml

需要在src目录下:

(Maven则放在src/main/resources中)

 

Loaback日志输出位置、格式设置:

  • 通过logback.xml中的<appender>标签可以设置输出位置和日志信息的详细格式
  • 通常可以设置2个日志输出位置:一个是控制台、一个是系统文件中

输出到控制台的配置标志:

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">

输出到系统文件的配置标志:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

 

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
        CONSOLE :表示当前的日志信息是可以输出到控制台的。
    -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--输出流对象 默认 System.out 改为 System.err-->
        <target>System.out</target>
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
                %msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level]  %c [%thread] : %msg%n</pattern>
        </encoder>
    </appender>

    <!-- File是输出的方向通向文件的 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            <charset>utf-8</charset>
        </encoder>
        <!--日志输出路径-->
        <file>F:\MyCode\new-java\java-learn-6\data.log</file>
        <!--指定日志文件拆分和压缩规则-->
        <rollingPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--通过指定压缩文件名称,来确定分割文件方式-->
            <fileNamePattern>F:\MyCode\new-java\java-learn-6\data2-%d{yyyy-MMdd}.log%i.gz</fileNamePattern>
            <!--文件拆分大小-->
            <maxFileSize>1MB</maxFileSize>
        </rollingPolicy>
    </appender>

    <!--

    level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF
   , 默认debug
    <root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。
    -->
    <root level="ALL">
        <appender-ref ref="CONSOLE"/>
        <!--下方若被注释,则文件输出不到-->
        <appender-ref ref="FILE" />
    </root>
</configuration>

 

1.4 日志级别

可以通过设置日志的输出级别来控制哪些日志信息输出或者不输出。只输出级别不低于设定级别的日志信息。

TRACE, DEBUG, INFO, WARN, ERROR

还有两个特殊的:

ALL:输出所有日志

OFF:关闭所有日志

日志级别从小到大的关系:TRACE < DEBUG < INFO < WARN < ERROR 【默认级别是debug】(忽略大小写)

 

1.5 获取日志对象

// 写在方法外
public static final Logger LOGGER = LoggerFactory.getLogger("类对象");

代码:

// 注意导包
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Date;
import java.util.Scanner;

public class Test {

    public static final Logger LOGGER = LoggerFactory.getLogger("Test.class");

    public static void main(String[] args) {

        // 登录
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String name = sc.nextLine();

        System.out.println("请输入密码:");
        String pw = sc.nextLine();
        LOGGER.info("用户于" + new Date() + ", 输入用户名了:" + name + ", 密码:" + pw);
        // 2023-05-23 20:48:16.172 [INFO ]  Test.class [main] : 用户于Tue May 23 20:48:16 CST 2023, 输入用户名了:George, 密码:Hello World
    }
}

 

2. 类加载器

2.1 概述

作用:

负责将.class文件(存储的物理文件)加载在到内存中

01_类加载器

 

2.2 类加载的完整过程

  • 类加载时机简单理解:字节码文件什么时候会被加载到内存中?有以下的几种情况:
    • 创建类的实例(对象)
    • 调用类的类方法
    • 访问类或者接口的类变量,或者为该类变量赋值
    • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
    • 初始化某个类的子类
    • 直接使用java.exe命令来运行某个主类

    总结而言:用到了就加载,不用不加载

     

  • 类加载过程
    1. 加载
      • 通过包名 + 类名,获取这个类,准备用流进行传输
      • 在这个类加载到内存中
      • 加载完毕创建一个class对象

03-类加载的过程-加载

02_类加载过程加载

  1. 链接
    • 验证确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全(文件中的信息是否符合虚拟机规范有没有安全隐患)

    03_类加载过程验证

    • 准备负责为类的类变量(被static修饰的变量)分配内存,并设置默认初始化值(初始化静态变量)

    04_类加载过程准备

    • 解析将类的二进制数据流中的符号引用替换为直接引用(本类中如果用到了其他类,此时就需要找到对应的类)

    05_类加载过程解析

  2. 初始化根据程序员通过程序制定的主观计划去初始化类变量和其他资源(静态变量赋值以及初始化其他资源)

    06_类加载过程初始化

  • 小结
    • 当一个类被使用的时候,才会加载到内存
    • 类加载的过程:加载、验证、准备、解析、初始化

2.3 类加载的分类

  • 分类
    • Bootstrap class loader:虚拟机的内置类加载器,通常表示为null ,并且没有父null
    • Platform class loader:平台类加载器,负责加载JDK中一些特殊的模块
    • System class loader:系统类加载器,负责加载用户类路径上所指定的类库
  • 类加载器的继承关系
    • System的父加载器为Platform
    • Platform的父加载器为Bootstrap

 

2.4 双亲委派模型

  • 介绍如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

07_双亲委派模型

  • 代码示例:
public class ClassLoaderDemo1 {
    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

        //获取系统类加载器的父加载器 --- 平台类加载器
        ClassLoader classLoader1 = systemClassLoader.getParent();

        //获取平台类加载器的父加载器 --- 启动类加载器
        ClassLoader classLoader2 = classLoader1.getParent();

        // 系统类加载器jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
        System.out.println("系统类加载器" + systemClassLoader);
        // 平台类加载器jdk.internal.loader.ClassLoaders$PlatformClassLoader@2d98a335
        System.out.println("平台类加载器" + classLoader1);
        // 启动类加载器null
        System.out.println("启动类加载器" + classLoader2);

    }
}

 

2.5 ClassLoader 中的两个方法

  • 方法介绍
    方法名 说明
    public static ClassLoader getSystemClassLoader() 获取系统类加载器
    public InputStream getResourceAsStream(String name) 加载某一个资源文件
  • 示例代码
public class ClassLoaderDemo2 {
    public static void main(String[] args) throws IOException {
        //static ClassLoader getSystemClassLoader() 获取系统类加载器
        //InputStream getResourceAsStream(String name)  加载某一个资源文件

        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

        //利用加载器去加载一个指定的文件
        //参数:文件的路径(放在src的根目录下,默认去那里加载) maven在src/main/resources下
        //返回值:字节流。
        InputStream is = systemClassLoader.getResourceAsStream("prop.properties");

        Properties prop = new Properties();
        prop.load(is);

        // {classname=day35_test.Teacher1, method=teach}
        System.out.println(prop);

        is.close();
    }
}

 

3. xml

3.1 概述

  • xml概述XML的全称为(EXtensible Markup Language),是一种可扩展的标记语言
    标记语言:通过标签来描述数据的一门语言(标签有时我们也将其称之为元素)
    可扩展:标签的名字是可以自定义的,XML文件是由很多标签组成的,而标签名是可以自定义的
  • 作用
    • 用于进行存储数据和传输数据
    • 作为软件的配置文件
  • 作为配置文件的优势
    • 可读性好
    • 可维护性高
    • 可以配置成组出现的数据

 

3.2 xml语法规则

标签的规则:

  • 标签由一对尖括号和合法标识符组成
    <student>
    
  • 标签必须成对出现
    <student> </student>
    前边的是开始标签,后边的是结束标签
    
  • 特殊的标签可以不成对,但是必须有结束标记
    <address/>
    
  • 标签中可以定义属性,属性和标签名空格隔开,属性值必须用引号引起来。两属性中间用空格隔开
    <student id="1"> </student>
    
  • 标签需要正确的嵌套
    这是正确的: <student id="1"> <name>张三</name> </student>
    这是错误的: <student id="1"><name>张三</student></name>
    

语法规则:

  • XML文件的后缀名为:xml
  • 文档声明必须是第一行第一列
    <?xml version="1.0" encoding="UTF-8"?>
    version:该属性是必须存在的
    encoding:该属性不是必须的
    打开当前xml文件的时候应该是使用什么字符编码表(一般取值都是UTF-8)
    
    standalone: 该属性不是必须的,描述XML文件是否依赖其他的xml文件,取值为yes/no
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    
  • 必须存在一个根标签(最外面的标签),有且只能有一个
  • XML文件中可以定义注释信息
  • XML文件中可以存在以下特殊字符
    &lt; < 小于
    &gt; > 大于
    &amp; & 和号
    &apos; ' 单引号
    &quot; " 引号
    
  • XML文件中可以存在CDATA区
    <![CDATA[ …内容… ]]>
    
  • 示例代码
    <?xml version="1.0" encoding="UTF-8" ?>
    <!--注释的内容-->
    <!--本xml文件用来描述多个学生信息-->
    <students>
    
        <!--第一个学生信息-->
        <student id="1">
            <name>张三</name>
            <age>23</age>
            <info>学生&lt; &gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;的信息</info>
            <message> <![CDATA[内容 <<<<<< >>>>>> ]]]></message>
        </student>
    
        <!--第二个学生信息-->
        <student id="2">
            <name>李四</name>
            <age>24</age>
        </student>
    
    </students>
    

 

3.3 dtd约束

DTD(Document Type Definition)文档类型定义。不能约束具体的数据类型。

  • 什么是约束用来限定xml文件中可使用的标签以及属性
  • 约束的分类
    • DTD
    • schema
  • 编写DTD约束
    • 步骤
      1. 创建一个文件,这个文件的后缀名为.dtd
      2. 看xml文件中使用了哪些元素<!ELEMENT> 可以定义元素
      3. 判断元素是简单元素还是复杂元素简单元素:没有子元素。
        复杂元素:有子元素的元素;
    • 代码实现
      <!ELEMENT persons (person)>
      <!ELEMENT person (name,age)>
      <!ELEMENT name (#PCDATA)>
      <!ELEMENT age (#PCDATA)>
      
  • 引入DTD约束
    • 引入DTD约束的三种方法,引用后可以快速生成
      • 引入本地dtd
        <!DOCTYPE 根元素名称 SYSTEM 'DTD文件的路径'>
        
      • 在xml文件内部引入
        <!DOCTYPE 根元素名称 [ dtd文件内容 ]>
        
      • 引入网络dtd
        <!DOCTYPE 根元素的名称 PUBLIC "DTD文件名称" "DTD文档的URL">
        
    • 代码实现
      • 引入本地DTD约束
        // 这是persondtd.dtd文件中的内容,已经提前写好
        <!ELEMENT persons (person+)>
        <!ELEMENT person (name,age)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT age (#PCDATA)>
        
        // 在person1.xml文件中引入persondtd.dtd约束
        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE persons SYSTEM 'persondtd.dtd'>
        
        <persons>
            <person>
                <name>张三</name>
                <age>23</age>
            </person>
        </persons>
        
      • 在xml文件内部引入
        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE persons [
                <!ELEMENT persons (person+)>
                <!ELEMENT person (name,age)>
                <!ELEMENT name (#PCDATA)>
                <!ELEMENT age (#PCDATA)>
                ]>
        
        <persons>
            <person>
                <name>张三</name>
                <age>23</age>
            </person>
        </persons>
        
      • 引入网络dtd
        <?xml version="1.0" encoding="UTF-8" ?>
        <!DOCTYPE persons PUBLIC "dtd文件的名称" "dtd文档的URL">
        
        <persons>
            <person>
                <name>张三</name>
                <age>23</age>
            </person>
        </persons>
        

 

3.4 schema约束

 

schema和dtd的区别:

  1. schema约束文件也是一个xml文件,符合xml的语法,这个文件的后缀名.xsd
  2. 一个xml中可以引用多个schema约束文件,多个schema使用名称空间区分(名称空间类似于java包名)
  3. dtd里面元素类型的取值比较单一常见的是PCDATA类型,但是在schema里面可以支持很多个数据类型
  4. schema 语法更加的复杂

04_schema约束介绍.png

 

编写schema约束:

  • 步骤1,创建一个文件,这个文件的后缀名为.xsd。
    2,定义文档声明
    3,schema文件的根标签为: <schema>
    4,在<schema>中定义属性:(当前schema文件被谁约束着)
    xmlns=http://www.w3.org/2001/XMLSchema
    5,在<schema>中定义属性 :(如果别人想要本schema文件去约束xml,那么当前xml文件必须要使用到这个地址,可以理解为名字)
    targetNamespace =唯一的url地址,指定当前这个schema文件的名称空间。
    6,在<schema>中定义属性 :
    ​ elementFormDefault="qualified“,表示当前schema文件是一个质量良好的文件。
    7,通过element定义元素
    8,判断当前元素是简单元素还是复杂元素
<?xml version="1.0" encoding="UTF-8" ?>
<schema
    xmlns="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://www.itheima.cn/javase"
    elementFormDefault="qualified"
>

    <!--定义persons复杂元素-->
    <element name="persons">
        <complexType>  <!--复杂的元素-->
            <sequence maxOccurs='unbounded'> <!--person数量没有上限-->
                <!--定义person复杂元素-->
                <element name = "person">
                    <complexType>
                        <sequence>
                            <!--定义name和age简单元素-->
                            <element name = "name" type = "string"></element>
                            <element name = "height" type = "double"></element>
                        </sequence>
                        
                    </complexType>
                </element>
            </sequence>
        </complexType>

    </element>

</schema>

 

引入schema约束:

  • 步骤1,在根标签上定义属性xmlns="http://www.w3.org/2001/XMLSchema-instance"
    2,通过xmlns引入约束文件的名称空间
    3,给某一个xmlns属性添加一个标识,用于区分不同的名称空间
    ​ 格式为: xmlns:标识=“名称空间地址” ,标识可以是任意的,但是一般取值都是xsi
    4,通过xsi:schemaLocation指定名称空间所对应的约束文件路径
    ​ 格式为:xsi:schemaLocation = "名称空间url 文件路径“
<?xml version="1.0" encoding="UTF-8" ?>

<persons
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.george.com/"
    xsi:schemaLocation="http://www.george.com/javase person.xsd">
    <person>
        <name>张三</name>
        <age>23</age>
    </person>

</persons>

 

3.5 xml解析--Dom4j

  • 概述xml解析就是从xml中获取到数据
  • 常见的解析思想
    • DOM(Document Object Model)文档对象模型:就是把文档的各个组成部分看做成对应的对象。
      会把xml文件全部加载到内存,在内存中形成一个树形结构,再获取对应的值。优点:可以读取,可以添加,可以删除,可以做任何事情。
    • SAX:不会把整体的xml文件都加载到内存,而是从上往下逐行进行扫描。
      缺点:只能读取,不能添加,不能删除。
      优点:因为他是逐行扫描不需要把整体的xml文件都加载到内存,所以他可以解析比较大的xml文件。

 

DOM常见的解析工具:

  • JAXP:SUN公司提供的一套XML的解析的API
  • JDOM:开源组织提供了一套XML的解析的API-jdom
  • DOM4J:开源组织提供了一套XML的解析的API-dom4j,全称:Dom For Java

 

02_dom解析概述.png

使用Dom4j解析XML文件

  1. 通过网站:https://dom4j.github.io/ 去下载dom4j(dom4j-1.6.1.jar)
  2. 在idea中当前模块下新建一个lib文件夹,将jar包复制到文件夹中
  3. 选中jar包 -> 右键 -> 选择add as library即可

 

SAXReader类:

构造器/方法 说明
public SAXReader() 创建Dom4J的解析器对象
Document read(String url) 加载XML文件成为Document对象

Document类:

方法名 说明
Element getRootElement() 获得根元素对象

Element类:

方法名 说明
List<Element> elements() 得到当前元素下所有子元素(同一个名字有三个则输出三次)
List<Element> elements(String name) 得到当前元素下指定名字的子元素返回集合
Element element(String name) 得到当前元素下指定名字的子元素,如果有很多名字相同的返回第一个
String getName() 得到元素名字
String attributeValue(String name) 通过属性名直接得到属性值
String elementText(子元素名) 得到指定名称的子元素的文本
String getText() 得到文本
public class Dom4jDemo1 {
    public static void main(String[] args) throws DocumentException {
        // 1. 创建解析器对象
        SAXReader saxReader = new SAXReader();
        // 2. 利用解析器去读取xml文件,返回文档对象
//        File file = new File("d36/b.xml");
        File file = new File("src\\main\\java\\d36\\b.xml");
//        System.out.println(file.exists());
        Document document = saxReader.read(file);
        // org.dom4j.tree.DefaultDocument@77468bd9 [Document: name file:///F:/MyCode/new-java/java-learn-6/src/main/java/d36/b.xml]
        System.out.println(document);

        // 根标签
        Element rootElement = document.getRootElement();
        System.out.println(rootElement.getName());  // persons

        // 获取子标签 (不含孙子)
        List<Element> elements = rootElement.elements("person");
//        elements.forEach(s -> System.out.println(s.getName()));  // person * 3
        for (Element element : elements) {
            Attribute id = element.attribute("id");
            Element name = element.element("name");
            Element height = element.element("height");

            // id: 1, name: zhangsan, height: 188.8
            // id: 2, name: lisi, height: 177.7
            // id: 3, name: wangwu, height: 199.8
            System.out.println("id: " + id.getText() + ", name: " + name.getText() + ", height: " + height.getText());
            // new Person(...)
        }
    }
}

 

3.6 Xpath

  • Dom4j需要进行文件的全部解析,然后再寻找数据。
  • Xpath技术更加适合做信息检索。

概述:

  • XPath在解析XML文档方面提供了独树一帜的路径思想,更加优雅,高效
  • XPath使用路径表达式来定位XML文档中的元素节点或属性节点。

示例:

/元素/子元素/孙元素
//子元素//孙元素

 

需求:使用Dom4J把一个XML文件的数据进行解析

分析:

  1. 导入jar包(dom4j和jaxen-1.1.2.jar),Xpath技术依赖Dom4j技术
  2. 通过dom4j的SAXReader获取Document对象
  3. 利用XPath提供的API,结合XPath的语法完成选取XML文档元素节点进行解析操作。
  4. Document中与Xpath相关的API如下:(结果是node,但是可以强转为Element or Attribute
方法名 说明
Node selectSingleNode("表达式") 获取符合表达式的唯一元素
List<Node> selectNodes("表达式") 获取符合表达式的元素集合

 

Xpath的四大检索方案:

  • 绝对路径:采用绝对路径获取从根节点开始逐层的查找/contactList/contact/name节点列表并打印信息
方法名 说明
/根元素/子元素/孙元素 从根元素开始,一级一级向下查找,不能跨级
public class XPathDemo {
    public static void main(String[] args) throws DocumentException {

        SAXReader saxReader = new SAXReader();

        Document document = saxReader.read(new File("src/main/java/d36/b.xml"));

        //  List<Node> 改为  List<Element>
        List<Node> nodes = document.selectNodes("/persons/person/name");
        System.out.println(nodes.size());  // 3
        
        for (Node node : nodes) {
            System.out.println(node.getText());
        }
        // zhangsan
        // lisi
        // wangwu
        
        Node node = document.selectSingleNode("/persons/person/name");
        System.out.println(node.getText());  // zhangsan
    }
}

 

  • 相对路径:先得到根节点contactList,再采用相对路径获取下一级contact节点的name子节点并打印信息
方法名 说明
./子元素/孙元素 从当前元素开始,一级一级向下查找,不能跨级
Element rootElement = document.getRootElement();

Node node = rootElement.selectSingleNode("./person/name");
System.out.println(node.getText());  // zhangsan

Element person = rootElement.element("person");
Node node1 = person.selectSingleNode("./name");
System.out.println(node1.getText());  // zhangsan

 

  • 全文检索:直接全文搜索所有的name元素并打印
方法名 说明
//contact 找contact元素,无论元素在哪里
//contact/name 找contact,无论在哪一级,但name一定是contact的子节点
//contact//name contact无论在哪一种,name只要是contact的子孙元素都可以找到
List<Node> nodes = document.selectNodes("//name");
nodes.forEach(s -> System.out.println(s.getText()));  // 所有

List<Node> nodes1 = document.selectNodes("//person/name");
nodes1.forEach(s -> System.out.println(s.getText()));  // person下子标签

List<Node> nodes2 = document.selectNodes("//persons//name");
nodes2.forEach(s -> System.out.println(s.getText()));  // persons下的name,无论几层

 

  • 属性查找:在全文中搜索属性,或者带属性的元素
方法名 说明
//@属性名 查找属性对象,无论是哪个元素,只要有这个属性即可。
//元素[@属性名] 查找元素对象,全文搜索指定元素名和属性名。
//元素[@属性名=‘值’] 查找元素对象,全文搜索指定元素名和属性名,并且属性值相等。
// 任何有id属性
List<Node> nodes = document.selectNodes("//@id");
for (Node node : nodes) {
    Attribute attribute = (Attribute) node;
    System.out.println(attribute.getText());
}

// person下的id属性
List<Node> nodes1 = document.selectNodes("//person[@id]");
for (Node node : nodes1) {
    Element element = (Element) node;
    System.out.println(element.attributeValue("id"));
}

// 指定属性值
List<Node> nodes = document.selectNodes("//person[@id='3']");
System.out.println(nodes.size());

 

4. junit

4.1 概述

单元测试:单元测试就是针对最小的功能单元编写测试代码,Java程序最小的功能单元是方法,因此,单元测试就是针对Java方法的测试,进而检查方法的正确性。

 

优点:

  • JUnit可以灵活的选择执行哪些测试方法,可以一键执行全部测试方法。
  • Junit可以生成全部方法的测试报告。 (成功绿色,失败红色)
  • 单元测试中的某个方法测试失败了,不会影响其他测试方法的测试。

 

4.2 基本使用

  • 将JUnit的jar包导入到项目中
    • IDEA通常整合好了Junit框架,一般不需要导入。
    • 如果IDEA没有整合好,需要自己手工导入如下2个JUnit的jar包到模块(hamcrest-core-1.3.jar, junit-4.13.1.jar)
  • 编写测试方法:该测试方法必须是公共的无参数无返回值的非静态方法
  • 在测试方法上使用@Test注解:标注该方法是一个测试方法
  • 在测试方法中完成被测试方法的预期正确性测试。
  • 选中测试方法,选择Run Method(以前无main无法运行) ,如果测试良好则是绿色;如果测试失败,则是红色。(多个方法去类名上右键run类名)
public class JunitDemo1 {
    @Test
    public void method() {
        int a = 10;
        int b = 20;
        int result = a + b;
        System.out.println(result);
    }
}

 

4.3 常用注解

(Junit 4.xxxx版本):

注解(Junit4) 注解(Junit5) 说明
@Test @Test 测试方法
@Before @BeforeEach 用来修饰实例方法,该方法会在每一个测试方法执行之前执行一次。
@After @AfterEach 用来修饰实例方法,该方法会在每一个测试方法执行之后执行一次。
@BeforeClass @BeforeAll 用来静态修饰方法,该方法会在所有测试方法之前只执行一次。
@AfterClass @AfterAll 用来静态修饰方法,该方法会在所有测试方法之后只执行一次。

开始执行的方法:初始化资源。

执行完之后的方法:释放资源。

 

功能类:

/**
   业务方法
 */
public class UserService {
    public String loginName(String loginName , String passWord){
        if("admin".equals(loginName) && "123456".equals(passWord)){
            return "登录成功";
        }else {
            return "用户名或者密码有问题";
        }
    }

    public void selectNames(){
        System.out.println(10/2);
        System.out.println("查询全部用户名称成功~~");
    }
}

 

测试方法:

import org.junit.*;

/**
   测试类
 */
public class TestUserService {

    // 修饰实例方法的
    // 假如我要测试删掉一个文件功能,那可以先用该方法拷贝一个,这样测试不会影响实际数据
    @Before
    public void before(){
        System.out.println("===before方法执行一次===");
    }

    @After
    public void after(){
        System.out.println("===after方法执行一次===");
    }

    // 修饰静态方法
    @BeforeClass
    public static void beforeClass(){
        System.out.println("===beforeClass方法执行一次===");
    }

    @AfterClass
    public static void afterClass(){
        System.out.println("===afterClass方法执行一次===");
    }


    /**
       测试方法
       注意点:
            1、必须是公开的,无参数 无返回值的方法
            2、测试方法必须使用@Test注解标记。
     */
    @Test
    public void testLoginName(){
        UserService userService = new UserService();
        String rs = userService.loginName("admin","123456");

        // 进行预期结果的正确性测试:断言。
        // 参数一:当两个结果不一样时提示的信息
        // 参数二:期望值
        // 实际返回结果
        Assert.assertEquals("您的登录业务可能出现问题", "登录成功", rs );
    }

    @Test
    public void testSelectNames(){
        UserService userService = new UserService();
        userService.selectNames();
    }
}

运行结果:
===beforeClass方法执行一次===
===before方法执行一次===
===after方法执行一次===
===before方法执行一次===
5
查询全部用户名称成功~~
===after方法执行一次===
===afterClass方法执行一次===

 

5. 注解

5.1 概述

Annotation表示注解。是JDK1.5的新特性。

注解的主要作用:对我们的程序进行标注。通过注解可以给类增加额外的信息。

注解是给编译器或JVM看的,编译器或JVM可以根据注解来完成对应的功能。

@Deprecated:已过时的方法

@SuppressWarning("all"):压制警告

 

5.2 自定义注解

格式:

public @interface 注解名称 {
	public 属性类型 属性名() default 默认值 ;
}

// 使用自定义注解格式
@注解名(属性名1=值1, 属性名2=值2)
// 使用自定义注解时要保证注解每个属性都有值
// 注解可以使用默认值

特殊属性:
value属性,如果只有一个value属性的情况下,使用value属性的时候可以省略value名称不写。(如:@SuppressWarning("all")
但是如果有多个属性, 且多个属性没有默认值,那么value名称是不能省略的。

 

5.3 元注解

元注解:就是注解注解的注解。 (写在注解上面的注解)

元注解有两个:

@Target: 约束自定义注解只能在哪些地方使用
@Retention:申明注解的生命周期

@Target中可使用的值定义在ElementType枚举类中,常用值如下:

  • TYPE,类,接口
  • FIELD, 成员变量
  • METHOD, 成员方法
  • PARAMETER, 方法参数
  • CONSTRUCTOR, 构造器
  • LOCAL_VARIABLE, 局部变量

@Retention中可使用的值定义在RetentionPolicy枚举类中,常用值如下:

  • SOURCE: 注解只作用在源码阶段,生成的字节码文件中不存在
  • CLASS: 注解作用在源码阶段,字节码文件阶段,运行阶段不存在,默认值.
  • RUNTIME:注解作用在源码阶段,字节码文件阶段,运行阶段(开发常用)

 

5.4 注解的解析

注解的解析:注解的操作中经常需要进行解析,注解的解析就是判断是否存在注解,存在注解就解析出内容。

与注解解析相关的接口:

  • Annotation:注解的顶级接口,注解都是Annotation类型的对象
  • AnnotatedElement:该接口定义了与注解解析相关的解析方法
方法 说明
Annotation[] getDeclaredAnnotations() 获得当前对象上使用的所有注解,返回注解数组。
T getDeclaredAnnotation(Class<T> annotationClass) 根据注解类型获得对应注解对象
boolean isAnnotationPresent(Class<Annotation> annotationClass) 判断当前对象是否使用了指定的注解,如果使用了则返回true,否则false
  • 所有的类成分Class, Method, Field, Constructor, 都实现了AnnotatedElement接口他们都拥有解析注解的能力

 

解析注解的技巧:

注解在哪个成分上,我们就先拿哪个成分对象。

比如注解作用成员方法,则要获得该成员方法对应的Method对象,再来拿上面的注解

比如注解作用在类上,则要该类的Class对象,再来拿上面的注解

比如注解作用在成员变量上,则要获得该成员变量对应的Field对象,再来拿上面的注解

 

5.5 模拟Junit

利用反射,获取所有方法,遍历判断方法是否用了指定注解,是的话激活该方法

注解类:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

}

被测试类:

public class MyTestDemo {

    // 程序运行之后,需要自动运行 方法 1 3
    @MyTest
    public void method1(){
        System.out.println("method1");
    }

    public void method2(){
        System.out.println("method2");
    }

    @MyTest
    public void method3(){
        System.out.println("method3");
    }
}

运行测试:

public class MyTestUseCase {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException {
        // 1,获取class对象
        Class clazz = Class.forName("d36.MyTestDemo");

        //获取对象
        Object o = clazz.newInstance();

        //2.获取所有方法
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            //method依次表示类里面的每一个方法
            method.setAccessible(true);
            //判断当前方法有没有MyTest注解
            if(method.isAnnotationPresent(MyTest.class)){
                method.invoke(o);
            }
        }
    }
}