前程有光

前程有光 查看完整档案

上海编辑武汉大学  |  计算机科学与技术 编辑蚂蚁金服  |  高级软件开发师 编辑填写个人主网站
编辑

欢迎关注我的公众号【前程有光】

分享架构实战经验,面试资料,分享各类电子书,面试宝典

个人动态

前程有光 发布了文章 · 1月21日

分享一个图书馆管理系统的java实战项目,建议做java开发的都看看!

前言

该项目核心需求:

实现简单登录

管理员端实现下列功能

①查找书籍
②增加书籍
③删除书籍
④展示全部书籍
⑤退出系统

通用户实现下列功能

①查询书籍
②借阅书籍
③归还书籍
④退出系统

项目类的设计展示

图书相关的类

Book:定义书籍的信息 BookList:表示书库,里面存放书籍

package book;

/**
 * Created with IntelliJ IDEA
 * Details about unstoppable_t:
 * User: Administrator
 * Date: 2021 -01-19
 * Time: 16:54
 */
public class Book {

    //定义成员变量
    private String name;
    private String author;
    private int price;
    private String type;
    private boolean isBorrowed = false; //表示书的借阅状态

    public Book(String name, String author, int price, String type) {
        this.name = name;
        this.author = author;
        this.price = price;
        this.type = type;
//        this.isBorrowed = isBorrowed;
    }

    public String getName() {
        return name;
    }

    public String getAuthor() {
        return author;
    }

    public int getPrice() {
        return price;
    }

    public String getType() {
        return type;
    }

    public boolean isBorrowed() {
        return isBorrowed;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public void setType(String type) {
        this.type = type;
    }

    public void setBorrowed(boolean borrowed) {
        isBorrowed = borrowed;
    }

//    @Override
//    public String toString() {
//        return "Book{" +
//                "name='" + name + '\'' +
//                ", author='" + author + '\'' +
//                ", price=" + price +
//                ", type='" + type + '\'' +
//                ", isBorrowed=" + isBorrowed +
//                '}';
//    }

    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", author='" + author + '\'' +
                ", price=" + price +
                ", type='" + type + '\'' +
                ((isBorrowed == true) ? " 借阅状态: 已借出" : " 借阅状态: 未借出") +
                '}';
    }
}
package book;

/**
 * Created with IntelliJ IDEA
 * Details about unstoppable_t:
 * User: Administrator
 * Date: 2021 -01-19
 * Time: 16:54
 */
public class BookList {

    public int usedSize = 3;  //书架
    public Book[] books = new Book[10]; //书的类型为Book,用顺数组book去存储

    public BookList() {
        books[0] = new Book("三国演义","罗贯中", 100, "小说");
        books[1] = new Book("水浒传", "施耐庵", 100, "小说");
        books[2] = new Book("西游记", "吴承恩", 100, "小说");
    }

    //给指定位置放书
    public void setBooks(int pos,Book book) {
        this.books[pos] = book;
    }

    //拿到指定位置的书
    public Book getBooks(int pos){
        return this.books[pos];
    }

    public int getUsedSize() {
        return usedSize;
    }

    public void setUsedSize(int usedSize) {
        this.usedSize = usedSize;
    }
}

对书库(顺序表)操作的类

//新增
public class AddOperation implements IOperation{
    @Override
    public void work(BookList booklist) {
        System.out.println("添加书籍");
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入书名");
        String name = sc.nextLine();
        System.out.println("请输入书的作者");
        String author = sc.nextLine();
        System.out.println("请输入书的价格");
        int price = sc.nextInt();
        System.out.println("请输入书的类型");
        String type = sc.next();

        Book newBook = new Book(name,author,price,type); //构建新书(对象)
        int size = booklist.getUsedSize(); //通过bookList引用访问当前顺序表长度
        booklist.setBooks(size,newBook); //将新书放在顺序表最后面
        booklist.setUsedSize(size+1); //顺序表放了新书之后,长度加1

    }
}

//借阅
public class BorrowOperation implements IOperation{
    @Override
    public void work(BookList booklist) {
        System.out.println("借阅书籍");
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入书名");
        String name = sc.nextLine();  //name为所要借阅书名

        for (int i = 0; i < booklist.getUsedSize(); i++) {
            //通过booklist下标遍历每一本书
            Book book = booklist.getBooks(i);
            if(book.getName().equals(name)){
                //如果为true,说明要借阅的书存在,我们需要做的是修改书的借阅状态
                book.setBorrowed(true);  //为true表示书已经结出
                return ;
            }
        }
        System.out.println("非常抱歉,本馆没有您要借阅的书!"); //
    }
}

//删除
public class DelOperation implements IOperation{
    @Override
    public void work(BookList booklist) {
        System.out.println("删除书籍");
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入书名");
        String name = sc.nextLine();  //name为所要删除的书名

        int i = 0;
        for (; i < booklist.getUsedSize(); i++) {
            Book book = booklist.getBooks(i);
            if(book.getName().equals(name)){
                break;
            }
        }

        if(i >= booklist.getUsedSize()){
            System.out.println("没有要删除的这本书!");
            return ;
        }

        //此时i为所要删除书的下标
        for (int j = i; j < booklist.getUsedSize()-1; j++) {
            Book book = booklist.getBooks(j+1); //获得j+1位置的书
            booklist.setBooks(j,book);  //将j+1位置的书给j位置
        }
        int size = booklist.getUsedSize();  //获得顺序表长度
        booklist.setUsedSize(size-1);   //删除书后,长度减去1
        System.out.println("书已被删除!");
    }
}

//展示
public class DisplayOperation implements IOperation{
    @Override
    public void work(BookList booklist) {
        System.out.println("展示书籍");

        for (int i = 0; i < booklist.getUsedSize(); i++) {
            Book book = booklist.getBooks(i);
            System.out.println(book);
        }
    }
}

//查找
public class FindOperation implements IOperation{
    @Override
    public void work(BookList booklist) {
        System.out.println("查找书籍");
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入书名");
        String name = sc.nextLine();  //name为所要查找书名

        for (int i = 0; i < booklist.getUsedSize(); i++) {
            //通过booklist下标遍历每一本书
            Book book = booklist.getBooks(i);
            if(book.getName().equals(name)){
                System.out.println("该书存在!");
                System.out.println(book);  //直接打印书的信息,toString方法已被重写
                return ;
            }
        }
        System.out.println("没有这本书!");
    }
}

//归还
public class ReturnOperation implements IOperation{
    @Override
    public void work(BookList booklist) {
        System.out.println("归还书籍");
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入书名");
        String name = sc.nextLine();  //name为所要归还的书名

        for (int i = 0; i < booklist.getUsedSize(); i++) {
            Book book = booklist.getBooks(i);
            if(book.getName().equals(name)){
                book.setBorrowed(false);
                System.out.println(book);  //直接打印书的信息,toString方法已被重写
                return ;
            }
        }
        System.out.println("没有你要归还的这本书!");
    }
}

//退出
public class ExitOperation implements IOperation{
    @Override
    public void work(BookList booklist) {
        System.out.println("退出系统");

        System.exit(1); //表示退出系统
    }
}

//总接口
public interface IOperation {
    void work(BookList booklist);
}

用户相关类

<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">用户类</mark>

package user;

import book.BookList;
import operation.IOperation;

/**
 * Created with IntelliJ IDEA
 * Details about unstoppable_t:
 * User: Administrator
 * Date: 2021 -01-19
 * Time: 16:55
 */
abstract public class User {

    public String name;
    public IOperation[] operations;

    public User(String name){
        this.name = name;
    }

    /*
    新建menu方法,理解为用户菜单
    因为SpecialPerson和OrdinaryPerson继承了User,所以让两个子类重写menu方法,二者的菜单展示不一致
    此时menu可以没有具体实现,因而将它设计为抽象方法,因此User类成为抽象类

     */
    abstract public int menu();

    //operations中存放的是哪些操作方法,得看子类
    public void doOperation(int choice, BookList bookList){
        this.operations[choice].work(bookList);
    }
}

<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">管理员</mark>

package user;
import operation.*;

import java.util.Scanner;
/**
 * Created with IntelliJ IDEA
 * Details about unstoppable_t:
 * User: Administrator
 * Date: 2021 -01-19
 * Time: 16:56
 * 管理员
 */
public class SpecialPerson extends User{

    //构造方法
    public SpecialPerson(String name){
        super(name);
        this.operations = new IOperation[]{
        new ExitOperation(), //0退出系统
        new FindOperation(),
        new AddOperation(),
        new DelOperation(),
        new DisplayOperation()
        };
    }

    /*
    重写父类User的menu方法
    menu作用:返回你要执行的操作
     */
    @Override
    public int menu() {
        System.out.println("管理员菜单!");

        System.out.println("=================================");
        System.out.println("hello " + this.name + " 热诚欢迎使用本校图书馆管理系统!");
        System.out.println("1.查找图书");
        System.out.println("2.新增图书");
        System.out.println("3.删除图书");
        System.out.println("4.显示所有图书");
        System.out.println("0.退出系统");
        System.out.println("================================");
        System.out.println("请按照提示选择相应操作: ");
        Scanner sc = new Scanner(System.in);
        int choice = sc.nextInt();
        return choice;

    }
}

<mark style="box-sizing: border-box; outline: 0px; background-color: rgb(248, 248, 64); color: rgb(0, 0, 0); overflow-wrap: break-word;">普通用户</mark>

package user;

import operation.*;

import java.util.Scanner;

/**
 * Created with IntelliJ IDEA
 * Details about unstoppable_t:
 * User: Administrator
 * Date: 2021 -01-19
 * Time: 16:57
 * 普通用户
 */
public class OrdinaryPerson extends User{

    //构造方法
    public OrdinaryPerson(String name){
        super(name);

        this.operations = new IOperation[]{
                new ExitOperation(), //0退出系统
                new FindOperation(),
                new BorrowOperation(),
                new ReturnOperation()
        };
    }

    //重写父类User的menu方法
    @Override
    public int menu() {
        System.out.println("普通用户菜单!");

        System.out.println("=================================");
        System.out.println("hello" + this.name + "热诚欢迎使用本校图书馆管理系统!");
        System.out.println("1.查找图书");
        System.out.println("2.借阅图书");
        System.out.println("3.归还图书");
        System.out.println("0.退出系统");
        System.out.println("================================");
        System.out.println("请按照提示选择相应操作: ");
        Scanner sc = new Scanner(System.in);
        int choice = sc.nextInt();
        return choice;
    }
}

主函数程序

import book.BookList;
import user.OrdinaryPerson;
import user.SpecialPerson;
import user.User;

import java.util.Scanner;

/**
 * Created with IntelliJ IDEA
 * Details about unstoppable_t:
 * User: Administrator
 * Date: 2021 -01-19
 * Time: 16:30
 */
public class TrqTest {

    //login(登录)方法返回用户实例,类型为User
    public static User login(){
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入你的姓名: ");
        String name = sc.nextLine();
        System.out.println("请输入你的身份> 1:管理员 0:普通用户");
        int number = sc.nextInt();
        if(number == 1){
            return new SpecialPerson(name);
        }else{
            return new OrdinaryPerson(name);
        }
    }

    public static void main(String[] args) {

        BookList bookList = new BookList();

        //登录
        User user = login(); // 父类引用指向子类实例(向上转型)
        while(true) {
            //user.menu() 调用哪个menu方法,根据登录身份决定
            int choice = user.menu();

            //此doOperation方法是当前user自己的方法
            //choice选择当前用户operations数组中的相应的类,该类产生的对象调用自己的work方法
            user.doOperation(choice, bookList);
        }

    }
}

运行一下看看具体怎么样吧
在这里插入图片描述


感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!
欢迎关注公众号:前程有光,领取一线大厂Java面试题总结+各知识点学习思维导+一份300页pdf文档的Java核心知识点总结!

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 1月13日

深入浅出!2020年春招+秋招JVM面试题整理(附答案)

运行时数据区是什么?

虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干不同的数据区,这些区域有各自的用途、创建和销毁时间。

线程私有:程序计数器、Java 虚拟机栈、本地方法栈。

线程共享:Java 堆、方法区。

程序计数器是什么?

程序计数器是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器完成。是唯一在虚拟机规范中没有规定内存溢出情况的区域。

如果线程正在执行 Java 方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为 Undefined。

Java 虚拟机栈的作用?

Java 虚拟机栈来描述 Java 方法的内存模型。每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。

有两类异常:① 线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError。② 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OutOfMemoryError(HotSpot 不可动态扩展,不存在此问题)。

本地方法栈的作用?

本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法。

虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如 HotSpot 将虚拟机栈和本地方法栈合二为一。

本地方法栈在栈深度异常和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError。

堆的作用是什么?

是虚拟机所管理的内存中最大的一块,被所有线程共享的,在虚拟机启动时创建。堆用来存放对象实例,Java 里几乎所有对象实例都在堆分配内存。堆可以处于物理上不连续的内存空间,逻辑上应该连续,但对于例如数组这样的大对象,多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。

堆既可以被实现成固定大小,也可以是可扩展的,可通过 -Xms-Xmx 设置堆的最小和最大容量,当前主流 JVM 都按照可扩展实现。如果堆没有内存完成实例分配也无法扩展,抛出 OutOfMemoryError。

方法区的作用是什么?

方法区用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

JDK8 之前使用永久代实现方法区,容易内存溢出,因为永久代有 -XX:MaxPermSize 上限,即使不设置也有默认大小。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中永久代完全废弃,改用在本地内存中实现的元空间代替,把 JDK 7 中永久代剩余内容(主要是类型信息)全部移到元空间。

虚拟机规范对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收。垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载。如果方法区无法满足新的内存分配需求,将抛出 OutOfMemoryError。

运行时常量池的作用是什么?

运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。

运行时常量池相对于 Class 文件常量池的一个重要特征是动态性,Java 不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是 String 的 intern 方法。

运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError。

直接内存是什么?

直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用,而且可能导致内存溢出。

JDK1.4 中新加入了 NIO 这种基于通道与缓冲区的 IO,它可以使用 Native 函数库直接分配堆外内存,通过一个堆里的 DirectByteBuffer 对象作为内存的引用进行操作,避免了在 Java 堆和 Native堆来回复制数据。

直接内存的分配不受 Java 堆大小的限制,但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使内存区域总和大于物理内存限制,导致动态扩展时出现 OOM。

由直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果发现内存溢出后产生的 Dump 文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是 NIO),那么就可以考虑检查直接内存方面的原因。

内存溢出和内存泄漏的区别?

内存溢出 OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用。

内存泄露 Memory Leak,指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。

堆溢出的原因?

堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,例如在 while 死循环中一直 new 创建实例。

堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出。

如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具*置。

如果不是内存泄漏,即内存中对象都必须存活,应当检查 JVM 堆参数,与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

栈溢出的原因?

由于 HotSpot 不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss 参数来设定,存在两种异常:

StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。

OutOfMemoryError: 如果 JVM 栈可以动态扩展,当扩展无法申请到足够内存时会抛出 OutOfMemoryError。HotSpot 不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的。

运行时常量池溢出的原因?

String 的 intern 方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用,否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。

在 JDK6 及之前常量池分配在永久代,因此可以通过 -XX:PermSize-XX:MaxPermSize 限制永久代大小,间接限制常量池。在 while 死循环中调用 intern 方法导致运行时常量池溢出。在 JDK7 后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。

方法区溢出的原因?

方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。

JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如 -XX:MetaspaceSize 指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。

创建对象的过程是什么?

字节码角度

  • NEW: 如果找不到 Class 对象则进行类加载。加载成功后在堆中分配内存,从 Object 到本类路径上的所有属性都要分配。分配完毕后进行零值设置。最后将指向实例对象的引用变量压入虚拟机栈顶。
  • DUP: 在栈顶复制引用变量,这时栈顶有两个指向堆内实例的引用变量。两个引用变量的目的不同,栈底的引用用于赋值或保存局部变量表,栈顶的引用作为句柄调用相关方法。
  • INVOKESPECIAL: 通过栈顶的引用变量调用 init 方法。

执行角度

① 当 JVM 遇到字节码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。

② 在类加载检查通过后虚拟机将为新生对象分配内存。

③ 内存分配完成后虚拟机将成员变量设为零值,保证对象的实例字段可以不赋初值就使用。

④ 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。

⑤ 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

对象分配内存的方式有哪些?

对象所需内存大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。

指针碰撞: 假设 Java 堆内存规整,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。

空闲列表: 如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。

选择哪种分配方式由堆是否规整决定,堆是否规整由垃圾收集器是否有空间压缩能力决定。使用 Serial、ParNew 等收集器时,系统采用指针碰撞;使用 CMS 这种基于清除算法的垃圾收集器时,采用空间列表。

对象分配内存是否线程安全?

对象创建十分频繁,即使修改一个指针的位置在并发下也不是线程安全的,可能正给对象 A 分配内存,指针还没来得及修改,对象 B 又使用了指针来分配内存。

解决方法:① CAS 加失败重试保证更新原子性。② 把内存分配按线程划分在不同空间,即每个线程在 Java 堆中预先分配一小块内存,叫做本地线程分配缓冲 TLAB,哪个线程要分配内存就在对应的 TLAB 分配,TLAB 用完了再进行同步。

对象的内存布局了解吗?

对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。

对象头占 12B,包括对象标记和类型指针。对象标记存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等,这部分占 8B,称为 Mark Word。Mark Word 被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。

类型指针是对象指向它的类型元数据的指针,占 4B。JVM 通过该指针来确定对象是哪个类的实例。

实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。

对齐填充不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。

对象的访问方式有哪些?

Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。

句柄: 堆会划分出一块内存作为句柄池,reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只会改变句柄的实例数据指针,而 reference 本身不需要修改。

直接指针: 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 存储对象地址,如果只是访问对象本身就不需要多一次间接访问的开销。优点是速度更快,节省了一次指针定位的时间开销,HotSpot 主要使用直接指针进行对象访问。

如何判断对象是否是垃圾?

引用计数:在对象中添加一个引用计数器,如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。原理简单,效率高,但是在 Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。

可达性分析:主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。

Java 的引用有哪些类型?

JDK1.2 后对引用进行了扩充,按强度分为四种:

强引用: 最常见的引用,例如 Object obj = new Object() 就属于强引用。只要对象有强引用指向且 GC Roots 可达,在内存回收时即使濒临内存耗尽也不会被回收。

软引用: 弱于强引用,描述非必需对象。在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。

弱引用: 弱于软引用,描述非必需对象。弱引用关联的对象只能生存到下次 YGC 前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于 YGC 具有不确定性,因此弱引用何时被回收也不确定。

虚引用: 最弱的引用,定义完成后无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列。

有哪些 GC 算法

标记-清除算法

分为标记和清除阶段,首先从每个 GC Roots 出发依次标记有引用关系的对象,最后清除没有标记的对象。

执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。

存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发 Full GC。

标记-复制算法

为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。

实现简单、运行高效,解决了内存碎片问题。 代价是可用内存缩小为原来的一半,浪费空间。

HotSpot 把新生代划分为一块较大的 Eden 和两块较小的 Survivor,每次分配内存只使用 Eden 和其中一块 Survivor。垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%。

标记-整理算法

标记-复制算法在对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法

老年代使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。

标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且移动必须全程暂停用户线程。如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和访问器解决。

你知道哪些垃圾收集器?

Serial

最基础的收集器,使用复制算法、单线程工作,只用一个处理器或一条线程完成垃圾收集,进行垃圾收集时必须暂停其他所有工作线程。

Serial 是虚拟机在客户端模式的默认新生代收集器,简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的,对于处理器核心较少的环境,Serial 由于没有线程交互开销,可获得最高的单线程收集效率。

ParNew

Serial 的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。

ParNew 是虚拟机在服务端模式的默认新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS 配合。自从 JDK 9 开始,ParNew 加 CMS 不再是官方推荐的解决方案,官方希望它被 G1 取代。

Parallel Scavenge

新生代收集器,基于复制算法,是可并行的多线程收集器,与 ParNew 类似。

特点是它的关注点与其他收集器不同,Parallel Scavenge 的目标是达到一个可控制的吞吐量,吞吐量就是处理器用于运行用户代码的时间与处理器消耗总时间的比值。

Serial Old

Serial 的老年代版本,单线程工作,使用标记-整理算法

Serial Old 是虚拟机在客户端模式的默认老年代收集器,用于服务端有两种用途:① JDK5 及之前与 Parallel Scavenge 搭配。② 作为CMS 失败预案。

Parellel Old

Parallel Scavenge 的老年代版本,支持多线程,基于标记-整理算法。JDK6 提供,注重吞吐量可考虑 Parallel Scavenge 加 Parallel Old。

CMS

以获取最短回收停顿时间为目标,基于标记-清除算法,过程相对复杂,分为四个步骤:初始标记、并发标记、重新标记、并发清除。

初始标记和重新标记需要 STW(Stop The World,系统停顿),初始标记仅是标记 GC Roots 能直接关联的对象,速度很快。并发标记从 GC Roots 的直接关联对象开始遍历整个对象图,耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。

缺点:① 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。② 无法处理浮动垃圾,有可能出现并发失败而导致 Full GC。③ 基于标记-清除算法,产生空间碎片。

G1

开创了收集器面向局部收集的设计思路和基于 Region 的内存布局,主要面向服务端,最初设计目标是替换 CMS。

G1 之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。而 G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大。

跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。

G1 运作过程:

  • 初始标记:标记 GC Roots 能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。需要 STW 但耗时很短,在 Minor GC 时同步完成。
  • 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理 SATB 记录的在并发时有变动的对象。
  • 最终标记:对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录。
  • 筛选回收:对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。

可由用户指定期望停顿时间是 G1 的一个强大功能,但该值不能设得太低,一般设置为100~300 ms。

ZGC 了解吗?

JDK11 中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。

基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。

ZGC 的 Region 具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。

你知道哪些内存分配与回收策略?

对象优先在 Eden 区分配

大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。

大对象直接进入老年代

大对象指需要大量连续内存空间的对象,典型是很长的字符串或数量庞大的数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获得足够的连续空间。

HotSpot 提供了 -XX:PretenureSizeThreshold 参数,大于该值的对象直接在老年代分配,避免在 Eden 和 Survivor 间来回复制。

长期存活对象进入老年代

虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头。如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1。对象在 Survivor 中每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。对象晋升老年代的阈值可通过 -XX:MaxTenuringThreshold 设置。

动态对象年龄判定

为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。

空间分配担保

MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。

如果不满足,虚拟机会查看 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 Minor GC,否则改成一次 FullGC。

冒险是因为新生代使用复制算法,为了内存利用率只使用一个 Survivor,大量对象在 Minor GC 后仍然存活时,需要老年代进行分配担保,接收 Survivor 无法容纳的对象。

你知道哪些故障处理工具?

jps:虚拟机进程状况工具

功能和 ps 命令类似:可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)。LVMID 与操作系统的进程 ID(PID)一致,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,必须依赖 jps 命令。

jstat:虚拟机统计信息监视工具

用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。

参数含义:S0 和 S1 表示两个 Survivor,E 表示新生代,O 表示老年代,YGC 表示 Young GC 次数,YGCT 表示 Young GC 耗时,FGC 表示 Full GC 次数,FGCT 表示 Full GC 耗时,GCT 表示 GC 总耗时。

jinfo:Java 配置信息工具

实时查看和调整虚拟机各项参数,使用 jps 的 -v 参数可以查看虚拟机启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用 jinfo 的 -flag 查询。

jmap:Java 内存映像工具

用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和 jinfo 一样,部分功能在 Windows 受限,除了生成堆转储快照的 -dump 和查看每个类实例的 -histo 外,其余选项只能在 Linux 使用。

jhat:虚拟机堆转储快照分析工具

JDK 提供 jhat 与 jmap 搭配使用分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后可以在浏览器查看。

jstack:Java 堆栈跟踪工具

用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停顿时通过 jstack 查看各个线程的调用堆栈,可以获知没有响应的线程在后台做什么或等什么资源。

Java 程序是怎样运行的?

  • 首先通过 Javac 编译器将 .java 转为 JVM 可加载的 .class 字节码文件。

    Javac 是由 Java 编写的程序,编译过程可以分为: ① 词法解析,通过空格分割出单词、操作符、控制符等信息,形成 token 信息流,传递给语法解析器。② 语法解析,把 token 信息流按照 Java 语法规则组装成语法树。③ 语义分析,检查关键字使用是否合理、类型是否匹配、作用域是否正确等。④ 字节码生成,将前面各个步骤的信息转换为字节码。

    字节码必须通过类加载过程加载到 JVM 后才可以执行,执行有三种模式,解释执行、JIT 编译执行、JIT 编译与解释器混合执行(主流 JVM 默认执行的方式)。混合模式的优势在于解释器在启动时先解释执行,省去编译时间。

  • 之后通过即时编译器 JIT 把字节码文件编译成本地机器码。

    Java 程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会认定其为"热点代码",热点代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率,虚拟机会把它们编译成本地机器码,尽可能对代码优化,在运行时完成这个任务的后端编译器被称为即时编译器。

  • 还可以通过静态的提前编译器 AOT 直接把程序编译成与目标机器指令集相关的二进制代码。

类加载是什么?

Class 文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制。

与编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持 Java 的动态绑定。

类初始化的情况有哪些?

① 遇到 newgetstaticputstaticinvokestatic 字节码指令时,还未初始化。典型场景包括 new 实例化对象、读取或设置静态字段、调用静态方法。

② 对类反射调用时,还未初始化。

③ 初始化类时,父类还未初始化。

④ 虚拟机启动时,会先初始化包含 main 方法的主类。

⑤ 使用 JDK7 的动态语言支持时,如果 MethodHandle 实例的解析结果为指定类型的方法句柄且句柄对应的类还未初始化。

⑥ 接口定义了默认方法,如果接口的实现类初始化,接口要在其之前初始化。

其余所有引用类型的方式都不会触发初始化,称为被动引用。被动引用实例:① 子类使用父类的静态字段时,只有父类被初始化。② 通过数组定义使用类。③ 常量在编译期会存入调用类的常量池,不会初始化定义常量的类。

接口和类加载过程的区别:初始化类时如果父类没有初始化需要初始化父类,但接口初始化时不要求父接口初始化,只有在真正使用父接口时(如引用接口中定义的常量)才会初始化。

类加载的过程是什么?

加载

该阶段虚拟机需要完成三件事:① 通过一个类的全限定类名获取定义类的二进制字节流。② 将字节流所代表的静态存储结构转化为方法区的运行时数据区。③ 在内存中生成对应该类的 Class 实例,作为方法区这个类的数据访问入口。

验证

确保 Class 文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击。验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。

准备

为类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量,不包括实例变量。如果变量被 final 修饰,编译时 Javac 会为变量生成 ConstantValue 属性,准备阶段虚拟机会将变量值设为代码值。

解析

将常量池内的符号引用替换为直接引用。

符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。

直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。

初始化

直到该阶段 JVM 才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的 <client> 方法,该方法是 Javac 自动生成的。

有哪些类加载器?

自 JDK1.2 起 Java 一直保持三层类加载器:

  • 启动类加载器

    在 JVM 启动时创建,负责加载最核心的类,例如 Object、System 等。无法被程序直接引用,如果需要把加载委派给启动类加载器,直接使用 null 代替即可,因为启动类加载器通常由操作系统实现,并不存在于 JVM 体系。

  • 平台类加载器

    从 JDK9 开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等。

  • 应用类加载器

    也称系统类加载器,负责加载用户类路径上的类库,可以直接在代码中使用。如果没有自定义类加载器,一般情况下应用类加载器就是默认的类加载器。自定义类加载器通过继承 ClassLoader 并重写 findClass 方法实现。

双亲委派模型是什么?

类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器。

一个类加载器收到了类加载请求,它不会自己去尝试加载,而将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试。

类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性。

如何判断两个类是否相等?

任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。

两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要类加载器不同,这两个类就必定不相等。

总结

感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!
欢迎关注公众号:前程有光,领取一线大厂Java面试题总结+各知识点学习思维导+一份300页pdf文档的Java核心知识点总结!

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 1月7日

【粉丝投稿】三年经验成功拿到京东offer,附上大佬的Java社招面经分享

前言

今天这篇文章我本来是不打算更新的,在上午的时候一个粉丝朋友给我打了个微信电话,我当时还纳闷,都好久不联系了突然打电话给我不会是借钱吧(开个玩笑),电话接通后他跟我说自己拿到京东offer了,我当时是真的吃惊!在印象中这位粉丝一直不温不火,工作3年了!这次突然一鸣惊人,然后就说要请我吃饭什么的,说多亏了我给他的资料和指导,下面我把他的经历详细说一下吧!

开始之前,记得点赞收藏加关注哦!我这里准备了一线大厂面试资料和我原创的超硬核PDF技术文档,以及我为大家精心准备的多套简历模板(不断更新中),希望大家都能找到心仪的工作!需要的朋友点击这里备注思否自行下载即可!

一面

1、线程池用过哪些?线程池有哪些参数?然后问我几个常用线程池的用法和实际场景问题。

2、集合框架的知识,hashmap,ArrayList,LinkedList源码相关知识,基本整个介绍了 一遍,与hastable,concurrenthashmap相互的关联和区别;

3、说几个垃圾回收器,cms回收器有哪几个过程,停顿几次,会不会产生内存碎片。老 年代产生内存碎片会有什么问题。

4、讲讲快速排序,分析一下时间复杂度?

5、双亲委派模型介绍一下;

6、java中同步、volatile关键字;

7、jvm内存分区,为什么要有新生代和老年代?

8、有做个VM内存优化吗?

9、数据库索引主键和唯一索引有什么区别?

10、聚集索引和非聚集索引的区别?

11、MySQL存储引擎innoDB和MylSAM的区别?

12、innoDB的B+树索引叶子节点的Data域存储的是什么?MylSAM的B+树索引叶子节点的Data域存储的是主键还是物理地址?

二面

1、在一个静态方法内调用一个非静态成员为什么是非法的?

2、MySQL innodb的b+树索引,主键索引,聚簇索引有什么区别。

3、数据库四大特性

4、事务的四大隔离级别

5、jvm场景问题,标记清除多次后老年代产生内存碎片,引起full gc,接下来可能发生什么问题?

6、MySQL里有哪些锁,行锁表锁,乐观锁呢?

7、MySQL的死锁怎么产生的,举了两个例子。

8、dubbo里的zookeeper是做什么的?

9、aio,nio,bio的了解,NIO的核心概念有哪些?

10、常用的NIO框架有哪些?优劣势?

11、手撕代码。牛客题霸上的原题,可以去看看:NC9 二叉树中是否存在节点和为指定值的路径;

三面

1、分布式下redis如何保证线程安全?

2、redis持久化的方式以及区别;

3、zookeeper如何实现分布式锁、其他分布式锁怎么实现?

4、kafka的架构,如何用kafka保证消息的有序性?

5、数据库的优化包含哪些?MySQL的优化,谈两个你优化的例子。

6、最有技术难度的项目,介绍下相关核心设计流程。

7、工作中,遇见了技术瓶颈无法解决,你的解决思路?

8、未来你的职业规划是怎么样?

HR面

这个最终人事面,我感觉没什么好说的,反正就是巴拉巴拉那老一套。大家感兴趣的话我下次可以单独出一篇针对人事面的小篇章帮大家应对人事小姐姐的灵魂拷问!

最后

最后提供免费的Java架构学习资料,学习技术内容包含有:Spring,Dubbo,MyBatis, RPC, 源码分析,高并发、高性能、分布式,性能优化,微服务 高级架构开发等等。

还有Java核心知识点+全套架构师学习资料和视频+一线大厂面试宝典+面试简历模板可以领取+阿里美团网易腾讯小米爱奇艺快手哔哩哔哩面试题+Spring源码合集+Java架构实战电子书。需要的朋友点击这里备注思否自行下载即可,希望对大家有帮助!
在这里插入图片描述

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 1月7日

建议java开发人员都看看!深入浅出复盘我做java开发这几年,附上去年秋招的一些经验!

秋招

实习

这个可能是秋招中最大的亮点,一个不错的实习经历可以给秋招简历增加不少分数, 当然如果能通过实习顺利转正,那么在秋招中会给自己增加更多的底气.
实习中其实可能会打杂的活比较多,有些时间会很烦, 但是也应该意识到作为新人,leader可能并不放心把一些线上重要的代码交给你.你要做的就是努力做好这些琐碎的工作,赢得leader的信任

竞赛

特别是ACM,当时实习时,恰好坐在leader旁边也协助筛选过简历,筛简历时leader说有几个标准, 学校,实习,竞赛.项目 ,几乎各占四分之一,如果两项都没有的话,那么简历筛选想要通过就身十分困难.

项目

无论是网上项目,还是学校实际的项目,都要自己动手做过, 不要云,因为你没有做过,或者直接拿别人简历上的项目,只要被提问到重点肯定会露馅, 最好的来说是学校的项目或者一些社会上有偿的开发项目, 这些项目的开发过程中你会有自己的思考和知道一些问题常见的解决方案.

阿里9面 (三部门总结)

  • redis集群如何保证线程安全(这个问题是个陷阱 , 因为redis是单线程不存在线程安全问题, 当然楼主跳进去了)
  • 系统中redis 过期时间的设置,为何这么设置
  • zk的心跳机制,以及选择leader的过程
  • ZooKeeper的ZAB算法与Paxos的本质区别是什么?
  • hashmap 问题
  • 三次握手 四次挥手
  • 网络安全的一些问题
  • 实习中遇到的最大的问题是什么?如何解决
  • 线程个数如何设置?(这个问题最终是想问 IO密集 以及CPU密集线程个数的设置)
  • 垃圾回收算法
  • G1
  • 偏向锁
  • 内存分配策略
  • Java对象的引用的方法? 有哪些方法? Java为什么使用指针?
  • TCP与UDP的区别
  • 消息中间件的好处

    • mysqlB+树的访问流程
  • 索引
  • 算法题
  • 多线程输出abc
  • 非递归快排
  • 先序遍历中序遍历 生成二叉树

字节面试

  • poll select epoll 区别
  • 输入url发生了什么
  • hashmap
  • currenthashmap
  • java与c区别
  • 面向对象三大特征
  • redi过期策略
  • redis 持久化
  • redis跳表
  • lru算法实现
  • 实习项目难点
  • kafka如何保证消息一致性
  • kafka跟 rb区别? 为什么使用kafka
  • dubbo原理
  • 负载均衡策略
  • redis数据结构
  • zset底层数据结构

网易

  • 线程间的通信
  • 匿名管道与有名管道不同之处
  • 堆,栈,队列数据结构讲解
  • 三次握手四次挥手
  • 操作系统相关知识
  • 关于一些图的算法
  • java垃圾收集器
  • 对于网易的看法

写一些回答HR的套话

有问题先google再问

作为新人有问题很正常, 可以询问leader或者带你的人, 当然你要自己先确认确实无法自己解决

开朗比沉默好

在工作中你愿意跟沉默的人合作还是跟开朗健谈的人合作? 显而易见, 工作中千万不要太沉默,会吃大亏

跟随大流

这个不用多说大家也懂

总结

最后我为大家准备了一些Java架构学习资料,学习技术内容包含有:Spring,Dubbo,MyBatis, RPC, 源码分析,高并发、高性能、分布式,性能优化,微服务 高级架构开发等等给你领取。有需要的朋友点这里备注csdn免费下载
作为开发人员来说,不说要求自己成为业内顶尖,但也要保证自己不被市场所淘汰,学习对于程序员来说是最基本的事情。

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 1月7日

深度分析!深入浅出总结一下手撕springMVC的要点,建议收藏起来慢慢看!

1、创建过程与文件目录

1.1、创建Maven工程

image

2、pom依赖与配置文件

2.1、pom依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zhz</groupId>
    <artifactId>simulation-springmvc</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>simulation-springmvc Maven Webapp</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.1</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>simulation-springmvc</finalName>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
            <plugins>
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.22.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.2.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.2</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

2.2、springmvc配置类

<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <!--配置创建容器时要扫描的包-->
    <component-scan base-package="com.zhz.controller,com.zhz.service"></component-scan>
</beans>

2.3、web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <!--配置前端控制器-->
  <servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>com.zhz.springmvc.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <!--Web服务器一旦启动,Servlet就会实例化创建对象,然后初始化(预备创建对象)-->
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

3、实体类(User)

实体类:User.java

package com.zhz.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author :zhz
 * @date :Created in 2021/01/04
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 实体类
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
   private Integer id;
   private String name;
   private String password;
}

4、控制器(UserController)

package com.zhz.controller;

import com.zhz.bean.User;
import com.zhz.service.UserService;
import com.zhz.springmvc.annotation.Autowired;
import com.zhz.springmvc.annotation.Controller;
import com.zhz.springmvc.annotation.RequestMapping;
import com.zhz.springmvc.annotation.ResponseBody;

/**
 * @author :zhz
 * @date :Created in 2021/01/03
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 控制器
 **/
@Controller
public class UserController {

    @Autowired(value = "userService")
    private UserService userService;

    @RequestMapping("/listUsers")
    public String listUsers(){
        userService.listUsers();
        return "forward:/success.jsp";
    }

    @RequestMapping("/getData")
    @ResponseBody  //返回json格式的数据
    public User getData(){
        //调用服务层
        return userService.getUser();
    }
}

5、业务处理类与实现类(UserService,UserServiceImpl)

package com.zhz.service;

import com.zhz.bean.User;

/**
 * @author :zhz
 * @date :Created in 2021/01/03
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description:
 **/
public interface UserService {
    void listUsers();

    User getUser();
}
package com.zhz.service.impl;

import com.zhz.bean.User;
import com.zhz.service.UserService;
import com.zhz.springmvc.annotation.Service;

/**
 * @author :zhz
 * @date :Created in 2021/01/03
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description:
 **/
@Service("userService")
public class UserServiceImpl implements UserService {

    @Override
    public void listUsers() {
        System.out.println("===调用===UserServiceImpl===listUser===");
    }

    @Override
    public User getUser() {
        return new User(1,"zhz","123456");
    }
}

6、手撕mvc具体代码

6.1、核心annotation注解

package com.zhz.springmvc.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Retention注解表示Annotation的保留策略 RetentionPolicy.Class:运行时不保留,不可以通过反射读取。
 * RetentionPolicy.RUNTIME:运行是保留,可以通过反射读取。
 * RetentionPolicy.SOURCE:丢弃。
 */
@Target(value = ElementType.FIELD)  //作用在属性上
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Autowired {
    String value();
}
package com.zhz.springmvc.annotation;

import java.lang.annotation.*;

@Target(ElementType.TYPE)  //  //作用在类上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {

    String value() default "";
}
package com.zhz.springmvc.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)    //作用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {

    String value() default "";
}
package com.zhz.springmvc.annotation;

import java.lang.annotation.*;

/**
 * @BelongsProject: SpringMvc
 */
@Target(ElementType.METHOD)    //作用在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
package com.zhz.springmvc.annotation;

import java.lang.annotation.*;

/**
 * @Description: 自定义注解
 */
@Target(ElementType.TYPE)  //作用在类上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {

    String value();
}

6.2、context上下文(SpringMVC容器)

package com.zhz.springmvc.context;

import com.zhz.springmvc.annotation.Autowired;
import com.zhz.springmvc.annotation.Controller;
import com.zhz.springmvc.annotation.Service;
import com.zhz.springmvc.xml.XmlParse;

import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author :zhz
 * @date :Created in 2021/01/03
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: SpringMVC容器
 **/
public class WebApplicationContext {

    //classpath:springmvc.xml
    String contextConfigLocation;

    //定义集合  用于存放 bean 的权限名|包名.类名
    List<String> classNameList = new ArrayList<>();

    //创建Map集合用于扮演IOC容器:  key存放bean的名字   value存放bean实例
    public Map<String, Object> iocMap = new ConcurrentHashMap<>();

    public WebApplicationContext() {
    }

    public WebApplicationContext(String contextConfigLocation) {
        this.contextConfigLocation = contextConfigLocation;
    }

    /**
     * 初始化Spring容器
     */
    public void onRefresh() {
        //1、进行解析spring mvc配置文件操作  ==》 com.zhz.controller,com.zhz.service
        String basePackage = XmlParse.getBasePackage(contextConfigLocation.split(":")[1]);
        //通过","来分割com.zhz.controller,com.zhz.service 获得对应的包名
        String[] packs = basePackage.split(",");
        //2、进行包扫描
        for (String pack : packs) {
            executeScanPackage(pack);
        }
        //3、实例化容器中的bean
        executeInstance();

        //4、进行自动注入操作
        executeAutowired();
    }

    /**
     * 进行包扫描
     *
     * @param pack
     */
    private void executeScanPackage(String pack) {
        //1、把com.zhz.controller====>com/zhz/controller   com.zhz.service====>com/zhz/controller
        URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/"));
        String path = url.getFile();
        //2、/com/zhz/service
        File dir = new File(path);
        for (File file : dir.listFiles()) {
            if (file.isDirectory()) {//说明是com.zhz.service.impl层
                executeScanPackage(pack + "." + file.getName());
            } else {
                //文件目录下文件  获取全路径   UserController.class  ==> com.zhz.controller.UserController
                String className = pack + "." + file.getName().replaceAll(".class", "");
                classNameList.add(className);
            }
        }

    }

    /**
     * 实例化容器
     */
    private void executeInstance() {
        try {
            // com.zhz.controller.UserController      com.zhz.service.impl.UserServiceImpl
            for (String className : classNameList) {
                Class<?> clazz = Class.forName(className);
                if (clazz.isAnnotationPresent(Controller.class)) {
                    //控制层的bean,得到类的简写名称也就是UserController
                    String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase()+clazz.getSimpleName().substring(1);//首位变为小写
                    iocMap.put(beanName, clazz.newInstance());
                } else if (clazz.isAnnotationPresent(Service.class)) {
                    //Service层,主要是为了获得他的value
                    Service service = clazz.getAnnotation(Service.class);
                    String beanName = service.value();
                    iocMap.put(beanName, clazz.newInstance());
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

    /**
     * 进行自动注入操作
     */
    private void executeAutowired() {
        try {
            //从容器中取出bean,然后判断bean中是否有属性上使用Autowired,如果使用了该注解,就需要进行自动注入操作
            for (Map.Entry<String, Object> entry : iocMap.entrySet()) {
                //取出容器中的bean
                Object bean = entry.getValue();
                //从bean中获取属性
                Field[] fields = bean.getClass().getDeclaredFields();
                for (Field field : fields) {
                    if (field.isAnnotationPresent(Autowired.class)) {
                        //获取注解中的value值,该值是bean的name
                        Autowired autowired = field.getAnnotation(Autowired.class);
                        String beanName = autowired.value();
                        ;
                        //取消检查机制
                        field.setAccessible(true);
                        field.set(bean, iocMap.get(beanName));
                    }
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

6.3、映射处理(url与ccontroller之间的映射)

package com.zhz.springmvc.handler;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.lang.reflect.Method;

/**
 * @author :zhz
 * @date :Created in 2021/01/04
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description:
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HandlerMapping {

    //请求URL地址
    private String url;
    //控制器
    private Object controller;
    //控制器的方法
    private Method method;
}

6.4、前端处理器

package com.zhz.springmvc.servlet;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhz.springmvc.annotation.Controller;
import com.zhz.springmvc.annotation.RequestMapping;
import com.zhz.springmvc.annotation.ResponseBody;
import com.zhz.springmvc.context.WebApplicationContext;
import com.zhz.springmvc.handler.HandlerMapping;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author :zhz
 * @date :Created in 2021/01/03
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 前端控制器
 **/
public class DispatcherServlet extends HttpServlet {

    //指定SpringMvc容器
    private WebApplicationContext webApplicationContext;
    //创建集合,用于存放映射关系、映射地址与控制器.方法,用于发送请求直接从该集合中进行匹配
    List<HandlerMapping> handList = new ArrayList<>();

    @Override
    public void init() throws ServletException {
        //1、从web.xml中获得加载初始化参数contextConfigLocation的值classpath:springmvc.xml
        String configLocation = this.getServletConfig().getInitParameter("contextConfigLocation");
        //2、创建SpringMVC容器
        webApplicationContext = new WebApplicationContext(configLocation);

        //3、进行初始化操作
        webApplicationContext.onRefresh();

        //4、初始化请求映射关系   /findUser   ===》控制器.方法
        initHandlerMapping();
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //进行请求分发处理
        doDispatcher(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }

    /**
     * 初始化请求映射关系(获取链接地址0
     */
    private void initHandlerMapping() {
        //遍历map, key存放bean的名字   value存放bean实例
        for (Map.Entry<String, Object> entry : webApplicationContext.iocMap.entrySet()) {
            //获得bean的class类型
            Class<?> clazz = entry.getValue().getClass();
            if (clazz.isAnnotationPresent(Controller.class)) {
                //获取bean中所有的方法,为这些方法建立映射关系
                Method[] methods = clazz.getDeclaredMethods();
                for (Method method : methods) {
                    if (method.isAnnotationPresent(RequestMapping.class)) {
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        //获取注解中的值
                        String url = requestMapping.value();
                        //建立映射地址,与控制器 方法
                        HandlerMapping handlerMapping = new HandlerMapping(url, entry.getValue(), method);
                        handList.add(handlerMapping);
                    }
                }
            }
        }
    }

    /**
     * 进行请求分发处理
     *
     * @param request
     * @param response
     */
    private void doDispatcher(HttpServletRequest request, HttpServletResponse response) {
        try {
            //根据用户的请求地址(/listUsers)查找Controller
            HandlerMapping handlerMapping = getHandler(request);

            if (handlerMapping == null) {
                response.getWriter().print("<h1>404 NOT  FOUND!</h1>");
            } else {
                //调用处理方法之前 进行参数的注入

                //调用目标方法---》获得方法的返回值类型
                Object result = handlerMapping.getMethod().invoke(handlerMapping.getController());
                if (result instanceof String){
                    //跳转到JSP中
                    String viewName = (String)result;
                    //forward:/success.jsp重定向
                    if (viewName.contains(":")){
                        String viewType=viewName.split(":")[0];//也就是forward或者redirect
                        String viewPage=viewName.split(":")[1];//跳转的页面
                        if (viewType.equals("forward")){//请求转发
                            request.getRequestDispatcher(viewPage).forward(request,response);
                        }else{//重定向
                            response.sendRedirect(viewPage);
                        }
                    }else{
                        //默认请求转发
                        request.getRequestDispatcher(viewName).forward(request,response);
                    }
                }else{
                    //返回JSON格式数据
                    Method method=handlerMapping.getMethod();
                    if (method.isAnnotationPresent(ResponseBody.class)){
                        //将返回值转换成 json格式数据
                        ObjectMapper objectMapper = new ObjectMapper();
                        String json = objectMapper.writeValueAsString(result);
                        response.setContentType("text/html;charset=utf-8");
                        PrintWriter writer= response.getWriter();
                        writer.print(json);
                        writer.flush();
                        writer.close();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (ServletException e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据用户请求查找对应的Handler======>获取请求对应的handler(也就是从handList中取出)
     *
     * @param request
     * @return
     */
    private HandlerMapping getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        for (HandlerMapping handlerMapping : handList) {
            //从容器的Handle取出URL  和  用户的请求地址进行匹配,找到满足条件的Handler(controller)
            if (handlerMapping.getUrl().equals(requestURI)) {
                return handlerMapping;
            }
        }
        return null;
    }

}

6.5、解析XML

package com.zhz.springmvc.xml;

import lombok.val;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.File;
import java.io.InputStream;

/**
 * @author :zhz
 * @date :Created in 2021/01/04
 * @version: V1.0
 * @slogan: 天下风云出我辈,一入代码岁月催
 * @description: 解析spring mvc.xml
 **/
public class XmlParse {

    public static String getBasePackage(String xml){
        try {
            SAXReader saxReader=new SAXReader();
            // 通过reader对象的read方法加载spring mvc.xml文件,获取docuemnt对象。
            InputStream inputStream = XmlParse.class.getClassLoader().getResourceAsStream(xml);
            Document document = saxReader.read(inputStream);
            // 通过document对象获取根节点beans
            Element rootElement = document.getRootElement();
            // 通过element对象的返回给定本地名称和任何名称空间的第一个元素
            Element componentScan = rootElement.element("component-scan");
            //返回componentScan的参数
            Attribute attribute = componentScan.attribute("base-package");
            //返回base-package的值
            String basePackage = attribute.getText();
            return basePackage;
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return "";
    }
}

7、前端页面

7.1、index.jsp

<html>
<body>
<h2>Hello World!</h2>
</body>
</html>

7.2、succes.jsp

<html>
<body>
<h2>Hello World!</h2>
   跳转至success.jsp页面
</body>
</html>

总结

这篇关于手撕springMVC的文章就先更到这里了,感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!
springMVC其实并不算是一个很难的知识点,用点心多看就很简单,最后点击这里即可领取Java架构大礼包哦!!!

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 1月7日

阿里二面:小伙子说一下循环依赖吧?Spring又是如何解决循环依赖的?

什么是循环依赖?

顾名思义,循环依赖就是A依赖B,B又依赖A,两者之间的依赖关系形成了一个圆环,通常是由于不正确的编码所导致。Spring只能解决属性循环依赖问题,不能解决构造函数循环依赖问题,因为这个问题无解。

接下来我们首先写一个Demo来演示Spring是如何处理属性循环依赖问题的。

Talk is cheap. Show me the code

第一步:定义一个类ComponentA,其有一个私有属性componentB。

package com.tech.ioc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author 君战
 * **/
@Component
public class ComponentA {

    @Autowired
    private ComponentB componentB;

    public void say(){
        componentB.say();
    }

}

第二步:定义一个类ComponentB,其依赖ComponentA。并定义一个say方法便于打印数据。

package com.tech.ioc;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
 * @author 君战
 * **/
@Component
public class ComponentB {

    @Autowired
    private ComponentA componentA;

    public void say(){
        System.out.println("componentA field " + componentA);
        System.out.println(this.getClass().getName() + " -----> say()");
    }

}

第三步:重点,编写一个类-SimpleContainer,模仿Spring底层处理循环依赖。如果理解这个代码,再去看Spring处理循环依赖的逻辑就会很简单。

package com.tech.ioc;

import java.beans.Introspector;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 演示Spring中循环依赖是如何处理的,只是个简版,真实的Spring依赖处理远比这个复杂。
 * 但大体思路都相同。另外这个Demo很多情况都未考虑,例如线程安全问题,仅供参考。
 * @author 君战
 *
 * **/
public class SimpleContainer {

    /***
     * 用于存放完全初始化好的Bean,Bean处于可状态
     * 这个Map定义和Spring中一级缓存命名一致
     * */
    private Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

    /***
     * 用于存放刚创建出来的Bean,其属性还没有处理,因此存放在该缓存中的Bean还不可用。
     * 这个Map定义和Spring中三级缓存命名一致
     * */
    private final Map<String, Object> singletonFactories = new HashMap<>(16);

    public static void main(String[] args) {
        SimpleContainer container = new SimpleContainer();
        ComponentA componentA = container.getBean(ComponentA.class);
        componentA.say();
    }

    public <T> T getBean(Class<T> beanClass) {
        String beanName = this.getBeanName(beanClass);
        // 首先根据beanName从缓存中获取Bean实例
        Object bean = this.getSingleton(beanName);
        if (bean == null) {
            // 如果未获取到Bean实例,则创建Bean实例
            return createBean(beanClass, beanName);
        }
        return (T) bean;
    }
    /***
     * 从一级缓存和二级缓存中根据beanName来获取Bean实例,可能为空
     * */
    private Object getSingleton(String beanName) {
        // 首先尝试从一级缓存中获取
        Object instance = singletonObjects.get(beanName);
        if (instance == null) { // Spring 之所以能解决循环依赖问题,也是靠着这个singletonFactories
            instance = singletonFactories.get(beanName);
        }
        return instance;
    }

    /***
     * 创建指定Class的实例,返回完全状态的Bean(属性可用)
     *
     * */
    private <T> T createBean(Class<T> beanClass, String beanName) {
        try {
            Constructor<T> constructor = beanClass.getDeclaredConstructor();
            T instance = constructor.newInstance();
            // 先将刚创建好的实例存放到三级缓存中,如果没有这一步,Spring 也无法解决三级缓存
            singletonFactories.put(beanName, instance);
            Field[] fields = beanClass.getDeclaredFields();
            for (Field field : fields) {
                Class<?> fieldType = field.getType();
                field.setAccessible(true); 
                // 精髓是这里又调用了getBean方法,例如正在处理ComponentA.componentB属性,
                // 执行到这里时就会去实例化ComponentB。因为在getBean方法首先去查缓存,
                // 而一级缓存和三级缓存中没有ComponentB实例数据,所以又会调用到当前方法,
                // 而在处理ComponentB.componentA属性时,又去调用getBean方法去缓存中查找,
                // 因为在前面我们将ComponentA实例放入到了三级缓存,因此可以找到。
                // 所以ComponentB的实例化结束,方法出栈,返回到实例化ComponentA的方法栈中,
                // 这时ComponentB已经初始化完成,因此ComponentA.componentB属性赋值成功!
                field.set(instance, this.getBean(fieldType));
            }
            // 最后再将初始化好的Bean设置到一级缓存中。
            singletonObjects.put(beanName, instance);
            return instance;
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException();
    }

    /**
     * 将类名小写作为beanName,Spring底层实现和这个差不多,也是使用javaBeans的
     * {@linkplain Introspector#decapitalize(String)}
     **/
    private String getBeanName(Class<?> clazz) {
        String clazzName = clazz.getName();
        int index = clazzName.lastIndexOf(".");
        String className = clazzName.substring(index);
        return Introspector.decapitalize(className);
    }
}

如果各位同学已经阅读并理解上面的代码,那么接下来我们就进行真实的Spring处理循环依赖问题源码分析,相信再阅读起来就会很容易。

底层源码分析

分析从AbstractBeanFactory的doGetBean方法着手。可以看到在该方法首先调用transformedBeanName(其实就是处理BeanName问题),和我们自己写的getBeanName方法作用是一样的,但Spring考虑的远比这个复杂,因为有FactoryBean、别名问题。

// AbstractBeanFactory#doGetBean
protected <T> T doGetBean(
            String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
            throws BeansException {

        String beanName = transformedBeanName(name);
        Object bean;

        // !!!重点是这里,首先从缓存中beanName来获取对应的Bean。
        Object sharedInstance = getSingleton(beanName);
        if (sharedInstance != null && args == null) {
            // 执行到这里说明缓存中存在指定beanName的Bean实例,getObjectForBeanInstance是用来处理获取到的Bean是FactoryBean问题
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
        else {
            try {
                // 删除与本次分析无关代码....
                // 如果是单例Bean,则通过调用createBean方法进行创建
                if (mbd.isSingleton()) {
                    sharedInstance = getSingleton(beanName, () -> {
                        try {
                            return createBean(beanName, mbd, args);
                        } catch (BeansException ex) {
                            destroySingleton(beanName);
                            throw ex;
                        }
                    });

                }    
            catch (BeansException ex) {
                cleanupAfterBeanCreationFailure(beanName);
                throw ex;
            }
        }
        return (T) bean;
    }

getSingleton方法存在重载方法,这里调用的是重载的getSingleton方法,注意这里传递的boolean参数值为true,因为该值决定了是否允许曝光早期Bean。

// DefaultSingletonBeanRegistry#getSingleton
public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
}
// DefaultSingletonBeanRegistry#getSingleton
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // 首先从一级缓存中获取
        Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            // 如果一级缓存中未获取到,再从二级缓存中获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 如果未从二级缓存中获取到并且allowEarlyReference值为true(前面传的为true)
            if (singletonObject == null && allowEarlyReference) {
                synchronized (this.singletonObjects) {
                   //Double Check
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
                            // 最后尝试去三级缓存中获取
                            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
                                singletonObject = singletonFactory.getObject();
                                // 保存到二级缓存
                                this.earlySingletonObjects.put(beanName, singletonObject);
                                // 从三级缓存中移除
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }
        return singletonObject;
    }

ok,看完Spring是如何从缓存中获取Bean实例后,那再看看creatBean方法是如何创建Bean的

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
        throws BeanCreationException {
    // 删除与本次分析无关的代码...
    try {// createBean方法底层是通过调用doCreateBean来完成Bean创建的。
        Object beanInstance = doCreateBean(beanName, mbdToUse, args);
        if (logger.isTraceEnabled()) {
            logger.trace("Finished creating instance of bean '" + beanName + "'");
        }
        return beanInstance;
    } catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) {
        throw ex;
    } catch (Throwable ex) {
        throw new BeanCreationException(
                mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex);
    }
}
// AbstractAutowireCapableBeanFactory#doCreateBean
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
            throws BeanCreationException {

        BeanWrapper instanceWrapper = null;
        if (mbd.isSingleton()) {
            instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
        }
        if (instanceWrapper == null) {
            // 创建Bean实例
            instanceWrapper = createBeanInstance(beanName, mbd, args);
        }
        Object bean = instanceWrapper.getWrappedInstance();
        // 如果允许当前Bean早期曝光。只要Bean是单例的并且allowCircularReferences 属性为true(默认为true)
        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
            // 这里调用了addSingletonFactory方法将刚创建好的Bean保存到了三级缓存中。
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }

        // 删除与本次分析无关的代码.....
        Object exposedObject = bean;
        try {// Bean属性填充
            populateBean(beanName, mbd, instanceWrapper);
            // 初始化Bean,熟知的Aware接口、InitializingBean接口.....都是在这里调用
            exposedObject = initializeBean(beanName, exposedObject, mbd);
        } catch (Throwable ex) {

        }
        // 删除与本次分析无关的代码.....
        return exposedObject;
    }

先分析addSingletonFactory方法,因为在该方法中将Bean保存到了三级缓存中。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        // 如果一级缓存中不存在指定beanName的key
        if (!this.singletonObjects.containsKey(beanName)) {
            // 将刚创建好的Bean示例保存到三级缓存中
            this.singletonFactories.put(beanName, singletonFactory);
            // 从二级缓存中移除。
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

处理Bean的依赖注入是由populateBean方法完成的,但整个执行链路太长了,这里就不展开讲了,只说下IoC容器在处理依赖时是如何一步一步调用到getBean方法的,这样就和我们自己写的处理字段注入的逻辑对上了。

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    // 删除与本次分析无关代码...
    PropertyDescriptor[] filteredPds = null;
    if (hasInstAwareBpps) {
        if (pvs == null) {
            pvs = mbd.getPropertyValues();
        }
        // 遍历所有已注册的BeanPostProcessor接口实现类,如果实现类是InstantiationAwareBeanPostProcessor接口类型的,调用其postProcessProperties方法。
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof InstantiationAwareBeanPostProcessor) {
                InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
                // 删除与本次分析无关代码...
                pvs = pvsToUse;
            }
        }
        // 删除与本次分析无关代码...
    }

}

在Spring 中,@Autowired注解是由AutowiredAnnotationBeanPostProcessor类处理,而@Resource注解是由CommonAnnotationBeanPostProcessor类处理,这两个类都实现了InstantiationAwareBeanPostProcessor接口,都是在覆写的postProcessProperties方法中完成了依赖注入。这里我们就分析@Autowired注解的处理。

// AutowiredAnnotationBeanPostProcessor#postProcessProperties
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
        // 根据beanName以及bean的class去查找Bean的依赖元数据-InjectionMetadata 
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {// 调用inject方法
            metadata.inject(bean, beanName, pvs);
        } catch (BeanCreationException ex) {
            throw ex;
        } catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
        }
        return pvs;
    }

在InjectionMetadata的inject方法中,获取当前Bean所有需要处理的依赖元素(InjectedElement),这是一个集合,遍历该集合,调用每一个依赖注入元素的inject方法。

// InjectionMetadata#inject
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    // 获取当前Bean所有的依赖注入元素(可能是方法,也可能是字段)
    Collection<InjectedElement> checkedElements = this.checkedElements;
    Collection<InjectedElement> elementsToIterate =
            (checkedElements != null ? checkedElements : this.injectedElements);
    if (!elementsToIterate.isEmpty()) {
        // 如果当前Bean的依赖注入项不为空,遍历该依赖注入元素
        for (InjectedElement element : elementsToIterate) {
            // 调用每一个依赖注入元素的inject方法。
            element.inject(target, beanName, pvs);
        }
    }
}

在AutowiredAnnotationBeanPostProcessor类中定义了两个内部类-AutowiredFieldElement、AutowiredMethodElement继承自InjectedElement,它们分别对应字段注入和方法注入。

image

以大家常用的字段注入为例,在AutowiredFieldElement的inject方法中,首先判断当前字段是否已经被处理过,如果已经被处理过直接走缓存,否则调用BeanFactory的resolveDependency方法来处理依赖。

// AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
        Field field = (Field) this.member;
        Object value;
        if (this.cached) {// 如果当前字段已经被处理过,直接从缓存中获取
            value = resolvedCachedArgument(beanName, this.cachedFieldValue);
        } else {
            // 构建依赖描述符
            DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
            desc.setContainingClass(bean.getClass());
            Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
            Assert.state(beanFactory != null, "No BeanFactory available");
            TypeConverter typeConverter = beanFactory.getTypeConverter();
            try {// 调用BeanFactory的resolveDependency来解析依赖
                value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
            } catch (BeansException ex) {
                throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
            }
            // 删除与本次分析无关代码....
        }
        if (value != null) {
            // 通过反射来对属性进行赋值
            ReflectionUtils.makeAccessible(field);
            field.set(bean, value);
        }
    }
}

在DefaultListableBeanFactory实现的resolveDependency方法,最终还是调用doResolveDependency方法来完成依赖解析的功能。在Spring源码中,如果存在do什么什么方法,那么该方法才是真正干活的方法。

// DefaultListableBeanFactory#resolveDependency
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
            @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
        // .....
        // 如果在字段(方法)上添加了@Lazy注解,那么在这里将不会真正的去解析依赖
        Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
                descriptor, requestingBeanName);
        if (result == null) {
            // 如果添加@Lazy注解,那么则调用doResolveDependency方法来解析依赖
            result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
        }
        return result;
}
// DefaultListableBeanFactory#doResolveDependency
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
            @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

    //.....
    try {
        // 根据名称以及类型查找合适的依赖
        Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
        if (matchingBeans.isEmpty()) {// 如果未找到相关依赖
            if (isRequired(descriptor)) { // 如果该依赖是必须的(@Aautowired的required属性),直接抛出异常
                raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
            }
            return null;
        }

        String autowiredBeanName;
        Object instanceCandidate;
        // 如果查找到的依赖多于一个,例如某个接口存在多个实现类,并且多个实现类都注册到IoC容器中。
        if (matchingBeans.size() > 1) {// 决定使用哪一个实现类,@Primary等方式都是在这里完成
            autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
            if (autowiredBeanName == null) {
                if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
                    return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
                } else { 
                    return null;
                }
            }
            instanceCandidate = matchingBeans.get(autowiredBeanName);
        } else {
            // We have exactly one match.
            Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
            autowiredBeanName = entry.getKey();
            instanceCandidate = entry.getValue();
        }

        if (autowiredBeanNames != null) {
            autowiredBeanNames.add(autowiredBeanName);
        }
        // 如果查找到的依赖是某个类的Class(通常如此),而不是实例,
        //调用描述符的方法来根据类型resolveCandidate方法来获取该类型的实例。
        if (instanceCandidate instanceof Class) {
            instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
        }
        //...
}

在依赖描述符的resolveCandidate方法中,是通过调用BeanFactory 的getBean方法来完成所依赖Bean实例的获取。

// DependencyDescriptor#resolveCandidate
public Object resolveCandidate(String beanName, Class<?> requiredType, BeanFactory beanFactory)
            throws BeansException {

    return beanFactory.getBean(beanName);
}

而在getBean方法实现中,依然是通过调用doGetBean方法来完成。这也和我们自己写的依赖处理基本一致,只不过我们自己写的比较简单,而Spring要考虑和处理的场景复杂,因此代码比较繁杂,但大体思路都是一样的。

// AbstractBeanFactory#getBean
public Object getBean(String name) throws BeansException {
    return doGetBean(name, null, null, false);
}

重点是前面我们写的处理循环依赖的Demo,如果理解那个代码,再看Spring的循环依赖处理,就会发现很简单。

总结

循环依赖就是指两个Bean之间存在相互引用关系,例如A依赖B,B又依赖A,但Spring只能解决属性循环依赖,不能解决构造函数循环依赖,这种场景也无法解决。

Spring解决循环依赖的关键就是在处理Bean的属性依赖时,先将Bean存到三级缓存中,当存在循环依赖时,从三级缓存中获取到相关Bean,然后从三级缓存中移除,存入到二级缓存中。
看懂了,其实循环依赖就这样简单,最后点击这里即可领取Java架构大礼包哦!!!

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 1月7日

面试官:小伙子,你给我简单的说一下不使用 MQ 如何实现 pub/sub 场景?

前言

在配置中心中,有一个经典的 pub/sub 场景:某个配置项发生变更之后,需要实时的同步到各个服务端节点,同时推送给客户端集群。

在之前实现的简易版配置中心中是通过 redis 的 pub/sub 来实现的。这种实现虽然简单,但却强依赖了 redis。

配置中心作为一个基础组件,如果能尽可能的减少外部依赖,那对使用方来说一定是更友好的。那么,有没有可能不使用 MQ 来实现 pub/sub 的场景呢?答案是肯定的。

基于 DB 的 pub/sub 方案

Apollo 在实现上述场景时,并没有选用基于 MQ 来进行实现,而是通过数据库实现了一个简单的消息队列。示意图如下:

大致实现方式如下:

  1. Admin Service 在配置发布后会往 ReleaseMessage 表插入一条消息记录
  2. Config Service 中有一个线程会每秒扫描一次 ReleaseMessage 表,看是否有新的消息记录(怎么判断是不是新消息呢,怎么保证每个 client 不会重复消费呢?)
  3. Config Service 如果发现有新的消息记录,就会通知给客户端(怎么保证通知给每个客户端呢?每个 Config Service 都通知,不会重复通知吗?)

下面,就让我们带着这几个问题来学习一下源码吧。(画外音:思路比源码更重要

DatabaseMessageSender

Admin Service 在配置发布后会调用 DatabaseMessageSender#sendMessage 方法,该方法主要做了两件事情:

  1. 创建 ReleaseMessage ,然后将其保存到数据库中
  2. 记录当前保存的 ReleaseMessage Id,将其放到 DatabaseMessageSender#toClean 队列中。

为什么要记录当前保存的 ReleaseMessage Id 呢?

DatabaseMessageSender 中有个定时任务,会去清除比当前 ID 小的 ReleaseMessage。

ReleaseMessageScanner

Config Service 中通过 ReleaseMessageScanner 组件会每秒(默认配置下)扫描一次 ReleaseMessage 表,来获取最新的消息。

有了这个基于 DB 的 pub/sub,Admin Service 在配置发布之后,每个 Config Service 都会通过 DB 来感知到这个消息,然后再通知给客户端。

那 Config Service 又是如何通知客户端的呢?

基于长轮询的实时消息

在 Apollo 的设计中,配置发生更新之后,并不是服务端主动推给客户端的,而且客户端通过长轮询的方式向服务端询问是否有配置发生了变更。大致思路为:如果在 60 秒内没有该客户端关心的配置发布,那么会返回 Http 状态码 304 给客户端;如果有该客户端关心的配置发布,请求就会立即返回,客户端从返回的结果中获取到配置变化的 namespace 后,会立即请求 Config Service 获取该 namespace 的最新配置

客户端的相关代码在 RemoteConfigLongPollService#doLongPollingRefresh,代码比较简单,感兴趣的同学可以自行查阅。

这里我们重点看一下服务端是如何实现的。

在传统的 servlet 模型中,每个请求都是由某个线程处理的,如果一个请求处理的时间较长,那么这种基于线程池的同步模型很快就会把所有线程耗尽,导致服务器无法响应新的请求。

servlet 3.0 中引入了异步支持,允许对一个请求进行异步处理,工作线程在此期间不会被阻塞,可以继续处理传入的客户端请求。

从 Spring 3.2 开始,可以使用 DeferredResult 来实现异步处理。使用 DeferredResult 时,可以设置超时,超时之后自动返回超时错误响应。同时,可以在另一个线程中,可以调用其 setResult()写入结果返回。

在 Apollo 客户端长轮询的地址为 /notifications/v2,对应的服务端代码为 NotificationControllerV2

NotificationControllerV2 中就使用了 Spring 的 DeferredResult来实现的。本文重在解决问题的思路,就不展示源码了,感兴趣的同学可以自己阅读一下源码。不过,小编写了一个简单的 demo 来帮助我们理解一下 DeferredResult 的使用。

@Slf4j
@RestController
public class DeferredResultDemoController {

    private final Multimap<String, DeferredResult<String>> deferredResults = ArrayListMultimap.create();

    @GetMapping("/info")
    public DeferredResult<String> info(String key) {
        // 设置 1 秒超时时间,设置超时是返回的结果
        DeferredResult<String> result = new DeferredResult<>(1000L, "key not change");
        // 将 result 放到 deferredResults 中, key 即为当前请求所关心的配置项
        deferredResults.put(key, result);
        // 如果超时,移除当前 DeferredResult,并打印日志,同时返回 DeferredResult 构造器中传入的结果
        result.onTimeout(() -> {
            deferredResults.remove(key, result);
            log.info("time out key not change");
        });
        // 如果完成了,则从 deferredResults 中移除当前 DeferredResult
        result.onCompletion(() -> deferredResults.remove(key, result));
        return result;
    }

    @PostConstruct
    public void init() {
        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.MILLISECONDS.sleep(700);
                } catch (InterruptedException e) {
                    log.info(e.getMessage(), e);
                }
                // 定时任务,模拟配置更新
                // 当 hello key 发生变更之后,从 deferredResults 获取到相关的 DeferredResult,通过 setResult 方法设置返回结果,同时移除 deferredResults
                if (deferredResults.containsKey("hello")) {
                    Collection<DeferredResult<String>> results = deferredResults.removeAll("hello");
                    results.forEach(stringDeferredResult -> stringDeferredResult.setResult("hello key change :" + System.currentTimeMillis()));
                }
            }
        }).start();
    }
}

总结

感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!
其实不管怎么说,思路绝对比你去死读源码要重要的多,不管干什么先把思路捋清楚,最后点击这里即可领取Java架构大礼包哦!!!

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 1月6日

spring源码总结笔记!深入浅出从入门讲到源码,建议先收藏再看!

本篇主要内容

  • Spring 概述(基本情况)
  • 核⼼思想 IoC 和 AOP
  • ⼿写实现 IoC 和 AOP(⾃定义spring框架)
  • Spring IoC ⾼级应⽤
  • 基础知识
  • ⾼级特性
  • Spring IoC 源码深度剖析
  • 设计⾮常优雅
  • 设计模式
  • 注意:原则、⽅法和技巧
  • Spring AOP ⾼级应⽤
  • 声明式事务控制
  • Spring AOP 源码深度剖析
  • 必要的笔记、必要的图、通俗易懂的语⾔化解知识难点

Spring 概述

Spring 是分层的 full-stack(全栈) 轻量级开源框架,以 IoC 和 AOP 为内核,提供了展现层 SpringMVC 和业务层事务管理等众多的企业级应⽤技术,还能整合开源世界众多著名的第三⽅框架和类库,已经成为使⽤最多的 Java EE 企业应⽤开源框架。

Spring 发展历程

  • 1997年 IBM 提出了EJB的思想;
  • 1998年,SUN 制定开发标准规范EJB1.0;
  • 1999年,EJB 1.1发布;
  • 2001年,EJB 2.0发布;
  • 2003年,EJB 2.1发布;
  • 2006年,EJB 3.0发布;
  • 2017 年 9 ⽉份发布了 Spring 的最新版本 Spring 5.0 通⽤版(GA)

    Spring 的优势

  • ⽅便解耦,简化开发
  • AOP编程的⽀持
  • 声明式事务的⽀持
  • ⽅便程序的测试
  • ⽅便集成各种优秀框架
  • 降低JavaEE API的使⽤难度
  • 源码是经典的 Java 学习范例

    Spring 的核⼼结构

Spring是⼀个分层⾮常清晰并且依赖关系、职责定位⾮常明确的轻量级框架,主要包括⼏个⼤模块:数据处理模块、Web模块、AOP(Aspect Oriented Programming)/Aspects模块、Core Container模块和 Test 模块,如下图所示,Spring依靠这些基本模块,实现了⼀个令⼈愉悦的融合了现有解决⽅案的零侵⼊的轻量级框架。

  • Spring核⼼容器(Core Container) 容器是Spring框架最核⼼的部分,它管理着Spring应⽤中bean的创建、配置和管理。在该模块中,包括了Spring bean⼯⼚,它为Spring提供了DI的功能。基于bean⼯⼚,我们还会发现有多种Spring应⽤上下⽂的实现。所有的Spring模块都构建于核⼼容器之上。
  • ⾯向切⾯编程(AOP)/Aspects Spring对⾯向切⾯编程提供了丰富的⽀持。这个模块是Spring应⽤系统中开发切⾯的基础,与DI⼀样,AOP可以帮助应⽤对象解耦。
  • 数据访问与集成(Data Access/Integration)Spring的JDBC和DAO模块封装了⼤量样板代码,这样可以使得数据库代码变得简洁,也可以更专注于我们的业务,还可以避免数据库资源释放失败⽽引起的问题。 另外,Spring AOP为数据访问提供了事务管理服务,同时Spring还对ORM进⾏了集成,如Hibernate、MyBatis等。该模块由JDBC、Transactions、ORM、OXM 和 JMS 等模块组成。
  • Web 该模块提供了SpringMVC框架给Web应⽤,还提供了多种构建和其它应⽤交互的远程调⽤⽅案。 SpringMVC框架在Web层提升了应⽤的松耦合⽔平。
  • Test 为了使得开发者能够很⽅便的进⾏测试,Spring提供了测试模块以致⼒于Spring应⽤的测试。 通过该模块,Spring为使⽤Servlet、JNDI等编写单元测试提供了⼀系列的mock对象实现。

什么是IoC?

  • IoC Inversion of Control (控制反转/反转控制),注意它是⼀个技术思想,不是⼀个技术实现描述的事情:Java开发领域对象的创建,管理的问题
  • 传统开发⽅式:⽐如类A依赖于类B,往往会在类A中new⼀个B的对象
  • IoC思想下开发⽅式:我们不⽤⾃⼰去new对象了,⽽是由IoC容器(Spring框架)去帮助我们实例化对象并且管理它,我们需要使⽤哪个对象,去问IoC容器要即可我们丧失了⼀个权利(创建、管理对象的权利),得到了⼀个福利(不⽤考虑对象的创建、管理等⼀系列事情)

为什么叫做控制反转?

  • 控制:指的是对象创建(实例化、管理)的权利
  • 反转:控制权交给外部环境了(spring框架、IoC容器)

IoC解决了什么问题

  • IoC解决对象之间的耦合问题

IoC和DI的区别

  • DI:Dependancy Injection(依赖注⼊)

怎么理解:

  • IOC和DI描述的是同⼀件事情,只不过⻆度不⼀样罢了

什么是AOP

  • AOP: Aspect oriented Programming ⾯向切⾯编程/⾯向⽅⾯编程
  • AOP是OOP的延续,从OOP说起
  • OOP三⼤特征:封装、继承和多态
  • oop是⼀种垂直继承体系

AOP在解决什么问题

在不改变原有业务逻辑情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复

为什么叫做⾯向切⾯编程

  • 「切」:指的是横切逻辑,原有业务逻辑代码我们不能动,只能操作横切逻辑代码,所以⾯向横切逻辑
  • 「⾯」:横切逻辑代码往往要影响的是很多个⽅法,每⼀个⽅法都如同⼀个点,多个点构成⾯,有⼀个⾯的概念在⾥⾯

Spring IOC源码深度剖析

好处:

提⾼培养代码架构思维、深⼊理解框架

原则

定焦原则:抓主线
宏观原则:站在上帝视⻆,关注源码结构和业务流程(淡化具体某⾏代码的编写细节)

读源码的⽅法和技巧

断点(观察调⽤栈)
反调(Find Usages)
经验(spring框架中doXXX,做具体处理的地⽅)

Spring源码构建

下载源码(github)
安装gradle 5.6.3(类似于maven) Idea 2019.1 Jdk 11.0.5
导⼊(耗费⼀定时间)
编译⼯程(顺序:core-oxm-context-beans-aspects-aop)
⼯程—>tasks—>compileTestJava
# Spring IoC的容器体系
IoC容器是Spring的核⼼模块,是抽象了对象管理、依赖关系管理的框架解决⽅案。Spring 提供了很多的容器,其中 BeanFactory 是顶层容器(根容器),不能被实例化,它定义了所有 IoC 容器 必须遵从的⼀套原则,具体的容器实现可以增加额外的功能,⽐如我们常⽤到的ApplicationContext,其下更具体的实现如 ClassPathXmlApplicationContext 包含了解析 xml 等⼀系列的内容,AnnotationConfigApplicationContext 则是包含了注解解析等⼀系列的内容。Spring IoC 容器继承体系⾮常聪明,需要使⽤哪个层次⽤哪个层次即可,不必使⽤功能⼤⽽全的。BeanFactory 顶级接⼝⽅法栈如下

BeanFactory 容器继承体系

Spring AOP 应⽤

在讲解AOP术语之前,我们先来看⼀下下⾯这两张图,它们就是第三部分案例需求的扩展(针对这些扩展的需求,我们只进⾏分析,在此基础上去进⼀步回顾AOP,不进⾏实现)

上图描述的就是未采⽤AOP思想设计的程序,当我们红⾊框中圈定的⽅法时,会带来⼤量的重复劳动。程序中充斥着⼤量的重复代码,使我们程序的独⽴性很差。⽽下图中是采⽤了AOP思想设计的程序,它把红框部分的代码抽取出来的同时,运⽤动态代理技术,在运⾏期对需要使⽤的业务逻辑⽅法进⾏增强。

AOP 术语


# Spring中AOP的代理选择
Spring 实现AOP思想使⽤的是动态代理技术
默认情况下,Spring会根据被代理对象是否实现接⼝来选择使⽤JDK还是CGLIB。当被代理对象没有实现任何接⼝时,Spring会选择CGLIB。当被代理对象实现了接⼝,Spring会选择JDK官⽅的代理技术,不过我们可以通过配置的⽅式,让Spring强制使⽤CGLIB。

Spring中AOP的配置⽅式

  • 在Spring的AOP配置中,也和IoC配置⼀样,⽀持3类配置⽅式。
  • 第⼀类:使⽤XML配置
  • 第⼆类:使⽤XML+注解组合配置
  • 第三类:使⽤纯注解配置

Spring中AOP实现

需求:横切逻辑代码是打印⽇志,希望把打印⽇志的逻辑织⼊到⽬标⽅法的特定位置(service层transfer⽅法)

总结

感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞!

看懂了,这些项目其实也就这回事,程序也就这回事,最后点击这里即可领取Java架构大礼包哦!!!

查看原文

赞 1 收藏 1 评论 0

前程有光 发布了文章 · 2020-12-29

简单梳理一下Redis实现分布式Session,建议做java开发的都看看!

Redis实现分布式Session管理

Memcached管理机制

Redis管理机制

  1. redis的session管理是利用spring提供的session管理解决方案,将一个应用session交给Redis存储,整个应用中所有session的请求都会去redis中获取对应的session数据。

SpringBoot项目开发Session管理

引入依赖pop.xml


        <!--springboot-redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--spring-data-redis session 管理-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <!--排除内嵌tomcat-->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
        </dependency>

开发Session管理配置类(使用注解)

@Configuration
@EnableRedisHttpSession  //将整个应用中使用session的数据全部交给redis处理
public class RedisSessionManager {

}

Controller层设计

package com.xizi.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping("test")
public class TestController {

    //使用redis 的session管理  注意:当session中数据发生变化时必须将session中变化的数据同步到redis中
    @RequestMapping("test")
    public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
        List<String> list = (List<String>) request.getSession().getAttribute("list");
        if(list==null){
            list = new ArrayList<>();
        }
        list.add("xxxx");
        request.getSession().setAttribute("list",list);//每次session变化都要同步session

        response.getWriter().println("size: "+list.size());
        response.getWriter().println("sessionid: "+request.getSession().getId());
    }

    @RequestMapping("logout")
    public void logout(HttpServletRequest request){
        //退出登录
        request.getSession().invalidate();//失效
    }
}

打包测试

Nginx+Tomcat集群+Redis测试

Nginx相关配置

Tomcat集群

//这是tom4 后面的两个端口号依次+1
//关闭端口
<Server port="8003" shutdown="SHUTDOWN">

//连接端口
<Connector port="8989" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

<Connector port="10010" protocol="AJP/1.3" redirectPort="8443" />

改变初始页面index.jsp

在这里插入图片描述

3.Redis集群

已经开启了,不会的去看我前面的Redis集群搭建博客

测试

上传war包到三个Tomcat的Webapps目录下

直接访问Nginx页面,反向代理了Tomcat集群

GiF演示一波

最后

欢迎关注公众号:前程有光,领取一线大厂Java面试题总结+各知识点学习思维导+一份300页pdf文档的Java核心知识点总结!

查看原文

赞 0 收藏 0 评论 0

前程有光 发布了文章 · 2020-12-28

HashMap知识点总结,这一篇算是总结的不错的了,建议看看!

HashMap存储结构

内部包含了⼀个 Entry 类型的数组 Entry[] table。transient Entry[] table;(transient:表示不能被序列化)Entry类型存储着键值对。它包含了四个字段, Entry 是⼀个链表。即数组中的每个位置被当成⼀个桶,⼀个桶存放⼀个Entry链表。HashMap 使⽤拉链法来解决冲突,同⼀个链表中存放哈希值和散列桶取模运算结果相同的 Entry。

常规操作

  • final K getKey();
  • final V getValue();
  • final V setValue(V newValue);
  • final boolean equals(Object o);
  • final int hashCode();
  • final String toString();
static class Entry<K,V> implements Map.Entry<K,V> {
 final K key;
 V value;
 Entry<K,V> next;
 int hash;
 Entry(int h, K k, V v, Entry<K,V> n) {
 value = v;
 next = n;
 key = k;
 hash = h;
2. 拉链法的⼯作原理
 }
 public final K getKey() {
 return key;
 }
 public final V getValue() {
 return value;
 }
 public final V setValue(V newValue) {
 V oldValue = value;
 value = newValue;
 return oldValue;
 }
 public final boolean equals(Object o) {
 if (!(o instanceof Map.Entry))
 return false;
 Map.Entry e = (Map.Entry)o;
 Object k1 = getKey();
 Object k2 = e.getKey();
 if (k1 == k2 || (k1 != null && k1.equals(k2))) {
 Object v1 = getValue();
 Object v2 = e.getValue();
 if (v1 == v2 || (v1 != null && v1.equals(v2)))
 return true;
 }
 return false;
 }
 public final int hashCode() {
 return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
 }
 public final String toString() {
 return getKey() + "=" + getValue();
 }
}

插入put操作

  • 插入数组是对hash值与散列表使用除留余数的方法计算得到对应桶序号;
  • 插入时采用链表的头插法进行插入;
  • HashMap 允许插⼊键为 null 的键值对。但是因为 null 的 hashCode() ⽅法,也就⽆法确定该键值对应桶下标,只能通过强制指定第 0 个桶存放键为 null 的键值对;
public V put(K key, V value) {
 if (table == EMPTY_TABLE) {
 inflateTable(threshold);
 }
 // 键为 null 单独处理
 if (key == null)
 return putForNullKey(value);
 int hash = hash(key);
 // 确定桶下标
 int i = indexFor(hash, table.length);
 // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
 Object k;
 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
 V oldValue = e.value;
 e.value = value;
 e.recordAccess(this);
 return oldValue;
 }
 }
 modCount++;
 // 插⼊新键值对
 addEntry(hash, key, value, i);
 return null; }
private V putForNullKey(V value) {
 for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 if (e.key == null) {
 V oldValue = e.value;
 e.value = value;
 e.recordAccess(this);
 return oldValue;
 }
 }
 modCount++;
 addEntry(0, null, value, 0);
 return null; }
void addEntry(int hash, K key, V value, int bucketIndex) {
 if ((size >= threshold) && (null != table[bucketIndex])) {
 resize(2 * table.length);
 hash = (null != key) ? hash(key) : 0;
 bucketIndex = indexFor(hash, table.length);
 }
 createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
 Entry<K,V> e = table[bucketIndex];
 // 头插法,链表头部指向新的键值对
 table[bucketIndex] = new Entry<>(hash, key, value, e);
 size++; }

扩容

设 HashMap 的 table ⻓度为 M,需要存储的键值对数量为 N,如果哈希函数满⾜均匀性的要求,那么每条链表的⻓度⼤约为 N/M,因此查找的复杂度为 O(N/M)。为了让查找的成本降低,应该使 N/M 尽可能⼩,因此需要保证 M 尽可能⼤,也就是说 table 要尽可能⼤。

  • HashMap 采⽤动态扩容来根据当前的键值对数量来调整数组长度,使得空间效率和时间效率都能得到保证。(HashMap扩容并不是等到数组满了才扩容,因为元素是插入到链表中,永远也不会满,所以有一个阈值–threshold,当等于它时就进行扩容操作)
  • capacity一般为2的n次方,即使用户传入的不是2的n次方,它也可以⾃动地将传⼊的容量转换为 2 的n 次⽅。原因是除留余数取模时采用的是位运算来代替取模运算,能够极⼤降低重新计算桶下标操作的复杂度。(位运算只用于2进制,所以需要2的n次方);
  • 当需要扩容时,使⽤ resize() 实现,令 capacity 为原来的两倍,扩容操作需要把oldTable 的所有键值对重新插入newTable 中,因此这⼀步是很费时的。
  • 当⼀个桶存储的链表⻓度⼤于等于 8 时会将链表转换为红⿊树。
参数含义
capacitytable 的容量⼤⼩,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次⽅。
size键值对数量。
thresholdsize 的临界值,当 size ⼤于等于 threshold 就必须进⾏扩容操作。
loadFactor装载因⼦,table 能够使⽤的⽐例,threshold = (int)(capacity* loadFactor)。
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;
void addEntry(int hash, K key, V value, int bucketIndex) {
 Entry<K,V> e = table[bucketIndex];
 table[bucketIndex] = new Entry<>(hash, key, value, e);
 if (size++ >= threshold)
 resize(2 * table.length);
}
void resize(int newCapacity) {
 Entry[] oldTable = table;
 int oldCapacity = oldTable.length;
 if (oldCapacity == MAXIMUM_CAPACITY) {
 threshold = Integer.MAX_VALUE;
 return;
 }
 Entry[] newTable = new Entry[newCapacity];
 transfer(newTable);
 table = newTable;
 threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
 Entry[] src = table;
 int newCapacity = newTable.length;
 for (int j = 0; j < src.length; j++) {
 Entry<K,V> e = src[j];
 if (e != null) {
 src[j] = null;
 do {
 Entry<K,V> next = e.next;
 int i = indexFor(e.hash, newCapacity);
 e.next = newTable[i];
 newTable[i] = e;
 e = next;
 } while (e != null);
 }
 }
}

与 Hashtable 的⽐较**

Hashtable 使⽤ synchronized 来进⾏同步。
HashMap 可以插⼊键为 null 的 Entry。
HashMap 的迭代器是 fail-fast 迭代器。
HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。

总结

欢迎关注公众号:前程有光,领取一线大厂Java面试题总结+各知识点学习思维导+一份300页pdf文档的Java核心知识点总结!

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 274 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-04-24
个人主页被 3.8k 人浏览