Java体系总结——从基础语法到微服务

写在前面

大半年时间里,我从Java的基础语法开始,学了面向对象、异常处理、集合、IO流、数据库、多线程、还有稍微进阶点的技术,反射和注解,以及高级框架SSM,最后到微服务,基础框架的学习可以说都结束了,但心里还是没底,对这些技术的掌握程度和使用场景仍然没那么清楚,所以十分有必要回过头来再梳理一遍整个知识体系。

另外我认为学习要知其然,更要知其所以然,在解决了how的基础上问一个why,这才是真正决定上限拉开差距的地方。虽然对我个人来说,可能差的更多的是实践项目,但能把技术的发展历程,以及每次迭代要解决的问题和尚存的局限都想清楚,我认为这同样重要,甚至更为关键。只会简单用工具、调API的开发者和脚本小子又有什么区别?所以这篇文章会更专注于理论和知识体系,而非具体方法。

Java基础

初识Java

Java的构成

  • JVM:Java程序运行的虚拟机,用于识别Java编译的.class文件并将其转换为主机可识别的机器码,是Java代码跨平台的关键。

  • JRE:Java运行环境,全称Java Runtime Environment,包括JVM和运行的核心库,如果只需要运行Java程序,主机中只需要安装JRE。

  • JDK:Java开发工具包,全称Java Development Kit,包含开发工具和JRE,开发工具有编译的javac.exe和打包的jar.exe等,用于开发Java程序

三者关系是包含关系,可以简单概括为:

JRE=JVM+核心库

JDK=JRE+开发工具

跨平台的横向比较

Java的跨平台是借助虚拟机JVM屏蔽底层不同架构和操作系统的主机细节实现,那C++和python能否实现跨平台?又是如何实现的呢?

  • C++:C++是编译型语言,编译成exe文件后即可直接运行,其能运行的平台在编译时就已经确定,不同的处理器架构或操作系统需要编译多次,产生不同的版本才能运行,所以结论是:C++的二进制文件不能跨平台,但源码是可以跨平台的。
  • python:python作为一种解释型语言,其可执行性完全依赖于解释器,所以python是天然支持跨平台 的,其解析代码并转换为机器码的过程与JVM有些类似,但底层原理并不相同,最主要的是动态解释和静态编译的差别,这也是二者性能差距的主要原因。
    同时虽然python也会生成pyc中间文件,但这种文件更像是一种缓存,解释器也能直接识别.py源代码,而Java编译产生的class文件只针对JVM,所以可见Java是一种兼具解释型和编译型的语言,其性能也在C和python之间。

基础语法

命名规则

大驼峰表示变量名的每个单词首字母大写,如UserName,通常用于类的命名;

小驼峰表示中间单词的首字母大写,只有一个单词的不大写,如userName,通常用于变量和方法的命名。

补充python的命名方式,类同样采取大驼峰,变量和方法名用蛇形命名法,多个单词用下划线隔开,如user_name

输入输出

控制台的输出使用System.out.print()实现,若要输出换行则使用println

控制台输入对应可以使用System.in,但该方法使用十分繁琐,通常借助Scanner类实现,首先创建类Scanner sc=new Scanner(System.in),后续使用实例读取,如sc.nextInt()sc.nextLine()

通常所有输入统一使用sc.nextLine()读取为字符串,否则读取了指定类型的字符后会在缓冲区留下换行符,仍然需要手动读取清空,十分不优雅。
程序结构

老三样,顺序、选择和循环,使用起来和C完全一样,比python就是多了括号,不再赘述,直接展示一个简单案例,九九乘法表:

java 复制代码
public class demo01 {
    public static void main(String[] args) {
        for(int i=1;i<10;i++){
            for(int s=1;s<=i;s++)
                System.out.print(s+"*"+i+"="+i*s+'\t');
            System.out.print('\n');
        }
    }
}

方法

就是函数,通过权限修饰符 返回值 方法名(参数列表){方法体}的形式定义。

面向对象

面向对象的三大特性,封装、继承和多态

封装

封装是将类的属性(成员变量)和行为(方法)隐藏在类内部,仅对外提供有限的、受控的访问接口的思想 。通过访问权限修饰符控制类成员的对外可见性,常见的封装形式包括:定义属性、普通方法、构造方法,get/set方法、toString方法等。

要注意的是类默认有无参构造函数,编写任意构造函数都会覆盖原有无参构造方法,所以可以通过只写有参构造覆盖无参。

static(静态)关键字可修饰属性、方法、代码块,效果如下:

  • 修饰的属性:属于 "类级别的变量",在类加载时初始化并放入方法区,生命周期与类的加载周期一致(早于对象存在,不依赖具体对象)。
  • 修饰的方法:属于 "类级别的方法",可通过类名.方法名直接调用,无需创建对象;方法在类加载时就被加载到方法区,仅加载一次。
  • 修饰的代码块(静态代码块):在类加载时执行,执行时机早于main方法(因为main方法需等类加载完成后才运行),且只执行一次。

继承

继承是复用父类代码,并扩展功能的方法,Java中只有单继承,且子类创建时会自动创建父类,子类可重写父类方法,达到扩展功能的目的。

父类不希望被继承的方法或属性可以用final关键字修饰,该关键字修饰的效果如下:

  • 修饰的类不能被继承
  • 修饰的基本属性值不变,引用数据类型的地址不能发生变化
  • 修饰的方法不能被覆写

多态

多态直观上指同一行为有不同的表现形式 ,具体上表现为父类引用接收子类实例 ,如Animal animal=new Dog(),当接收对象为不同种类的动物时,执行相同的eat方法会表现出不同的形式。

如果要调用子类的特有方法,则需要经过instanceof判断类型后再强制转换。

多态的作用是提高程序可扩展性和健壮性 ,程序设计要满足ocp(对扩展开放,对修改封闭)原则,新增的类不能在原有代码上修改,所以先使用父类作为占位接收子类实例,新增子类无需修改原代码,达到不修改原方法的效果还能扩展功能的效果。

同时实际场景中也很少出现严格父类接收子类实例的场景,通常出现在参数、返回值和成员变量中,称为隐式多态。

接口

首先介绍抽象类abstract修饰,无法单独实例化,只能通过非抽象子类继承实现,其中被abstract修饰的方法必须被重写,仍然有普通方法,设计之初的目的是希望通过该类限制子类方法名,但受单继承限制,使用起来不够灵活,由此产生了接口。

接口是完全隐藏其内部,只有常量和抽象方法的结构(也可以有静态方法,但很少用),因为接口中只能包含抽象方法,故定义抽象方法的public abstract可以省略(Java8以后也有default修饰的默认方法,可不必重写)。允许一个类实现多个接口,目前编程的规范就是基于接口编程,在类中实现接口功能,利用多态调用。

从结构来说接口确实名副其实,上游根据接口名称编写调用逻辑,下层开发者编写 "实现类" 重写接口的抽象方法,实现具体功能,接口起到一个中间层的作用,确实像是接口,连接上下双方。

简单来说:接口本质是方法规范,当一个类要完成多方面工作时,继承关系无法满足,则需要借助接口实现。

异常处理

Java代码在执行错误时会中断,即从错误处以后的代码都不会执行,为了使非致命错误后的程序仍然能够执行,引入异常处理机制。

异常

处理的异常用Exception表示,不可处理的异常用Error表示,具体又分为编译时异常和运行时异常,编译异常要求开发者必须进行处理,运行时异常可处理可不处理,区分方法是看其是继承了RunTimeException类还是Exception,开发者定义异常的原则应该是:出现异常的概率大,用编译异常,强制要求方法调用者处理,出现异常的概率小,使用运行时异常。

异常抛出

程序执行出现异常后,立刻创建异常对象,并用throw抛出,终止当前程序;开发者编写方法时若认为很大可能出现异常,也可使用throws修饰方法,抛出异常,要求上层调用者酌情处理,可捕获或继续向上抛出,直到主函数,此时仍然可以向上抛出到虚拟机,但并不解决任何问题。

异常处理

处理机制通常为try catch finally语句块,catch可嵌套多层使用,处理多种不同异常,finally内的语句任何情况下都执行(除强制退出虚拟机System.exit(0)),通常用于资源回收。

集合

List

具体实现类有ArrayListLinkedList,分别基于数组链表实现。

  • ArrayList有序,可重复,基于数组,随机访问快,增删慢,但对多线程不安全(安全可用vector,但效率低),需要手动同步,默认容量为10,装满后自动触发扩容机制,扩为原来的1.5倍,扩容时创建新数组并复制原有数据。
  • LinkedList同样有序可重复,但底层封装了双向链表,且通过节点方式存储数据,故已知节点增删效率高(相比数组无需复制移动),随机访问慢,需从头遍历,无扩容机制,存储一个元素就创建一个新节点。

Map

HashMap保存具有映射关系的数据,即保存key---value对应的结构,底层实现基于链表和数组,即根据keyhashcode确定存储的数组索引,在链表内存储具体的value

相关知识使用不多,详细可见:Java异常处理与集合

进阶技术

流处理

程序执行的数据都保存在内存中,将临时存储(内存)中的数据保存到长期存储(磁盘)的过程称为持久化,目的是保证数据在系统关闭或崩溃后仍然可用。

从磁盘读取数据到内存称为输入,内存内容保存到磁盘称为输出,Java处理输入输出的机制称为流。

按照处理数据单位的不同,Java的流可分为字节流和字符流,通常分别用于处理二进制文件(图像,音频等)和文本文件;按照数据流向的不同,Java流可分为输入流和输出流。

使用方式也很简单,获取对应文件的流,通过缓冲区读或写即可,详细使用方法可见:Java处理IO流

连接数据库

随着数据量的增大与关系复杂程度的日益上升,使用流处理文件进行持久化的方式已经变得捉襟见肘了,文件无法高效查询,更难以管理数据间的关系,由此诞生了数据库。

JDBC

Java通过JDBC规范连接市面上的不同数据库,这就是前面提到接口作为中间层的应用,Java制定JDBC连接规范,具体过程由各数据库厂商自行实现,开发者在开发过程中只需调用指定方法,而无需关注实现细节。

JDBC连接数据库分为以下六步:

  1. 注册驱动。注册数据库驱动,让 JDBC 框架知晓要使用哪个数据库的驱动程序,为后续建立连接做准备。
  2. 获取数据库连接对象。使用数据库的地址、用户名和密码等信息,向数据库发起连接请求,获取连接对象connection
  3. 获取数据库操作对象。该对象专注执行,是分层设计、单一职责的体现,并且在此基础上提供了多种不同的操作对象(如StatementPreparedStatement等),以完成不同场景下的持久化操作。
  4. 编写sql语句。其中查询和增删改调用不同的方法(如executeQuery()executeUpdate()),此处重点是确保sql语句的正确。
  5. 获取结果。增删改返回影响记录的条数,查询语句返回结果集ResultSet,需遍历获取。
  6. 资源回收。数据库操作结束,释放获取的连接对象、结果集和操作对象,避免资源泄露。

防注入

sql语句往往要和用户输入作拼接,如果登陆验证用户输入为' or '1'='1则可以跳过验证,甚至可以使用';drop table;--触发删库操作,所以十分有必要对用户输入作合法性检查。

JDBC中可使用预编译的操作对象PreparedStatement执行数据库操作,该操作对象会将初始语句预编译为框架,用户输入仅作为字符串填充,最大程度上避免了恶意注入带来的危害。

这种机制将 "SQL 结构" 与 "用户数据" 严格分离,数据库不会将参数内容解析为 SQL 语句的一部分,即使输入包含单引号、分号等特殊字符,也只会被视为普通字符串,从而彻底避免了注入风险。

事务

数据库操作的某些场景对完整性有极高要求,比如银行的转账,扣款和收款必须保持一致,JDBC针对这种场景设置了事务处理。

JDBC默认设置为指令自动提交,程序执行中途发生错误可能造成数据库状态不一致,增强程序健壮性的写法应该是:

  1. 首先关闭自动提交connection.setAutoCommit(false)
  2. 事务写完统一提交connection.commit()
  3. 异常不过中设置回滚connection.rollback()

更多详细介绍可见:JDBC数据库接口

多线程

进程与线程

进程是系统资源分配的基本单位,随着计算机的发展,各软件的功能越来越复杂,如果将每个功能都拆分为独立进程,会极大增加管理进程的开销,另外进程彼此资源独立,其通信成本也将大大提升,由此产生了线程。

线程是高并发需求下的产物,一个进程可以有多个线程,线程间共享内存,是cpu调度的最小单位,减小管理开销与通信成本的同时还能更有效地利用资源。

简单来说,进程像是彼此独立的生产车间,需要单独的车间(资源)和车间主任(管理者),而线程则是车间内的工人,能同时工作(并发)的同时又无需新建车间(省资源),且说话方便(通信成本低)。

创建线程

Java启动默认开启两个线程,垃圾回收线程Garbage Collector和主线程Main

创建新线程有以下三种方法:

  • 继承Thread类。继承类extends Thread后,重写run方法,创建实例后调用start方法即可启动线程,执行内容就是重写的run方法。
  • 实现Runnable接口。由于Java单继承的特性,继承Thread类会占用类的继承名额,因此可通过实现接口implements Runnable实现多线程。实现类也需重写run方法,使用Thread t1=new Thread(Runnable对象)创建线程,使用这种方法能避免单继承的限制,提高程序的扩展性。
  • 实现Callable接口。这种方式创建的线程可以有返回值 ,首先实现接口implements Callable<返回值包装类>并重写call方法;随后将实现类传入FutureTask实例FutureTask<包装类> ft = new FutureTask<包装类>(new Callable实现类);;将实例传入Thread启动线程new Thread(ft).start(),最后可通过ft.get()获取返回值,要注意的是get会阻塞主线程,直到返回结果。

常用方法

方法名 作用 备注
setPriority 设置线程优先级 参数最高位10,最低为1,默认为5,提高线程执行的概率
sleep 线程睡眠 参数为毫秒
join 等待该线程结束 等待进程内设置join的线程执行完毕后程序再向后执行
yield 线程让步 当前线程暂停,让出 CPU 时间片给同优先级或更高优先级的线程,随机性较强(仅让出机会,不释放锁)
isAlive 判断线程是否活动 返回true表示线程已启动且未终止
interrupt 设置中断标志 并非直接终止线程,而是设置中断状态;线程可通过isInterrupted()检查标志,决定是否终止(阻塞状态下会抛出InterruptedException)
wait 挂起线程 必须在synchronized块中调用,会释放对象锁,使线程进入等待队列;需配合notify()唤醒
notify 唤醒线程 必须在synchronized块中调用,随机唤醒一个在该对象上等待的线程,使其进入就绪状态
setDaemon(true) 守护线程 守护线程为用户线程服务,当所有用户线程结束时,守护线程会自动终止(如垃圾回收线程)

线程安全

多线程带来性能提升的同时也带来了数据安全问题,多个不同线程同时读写共享数据时,可能因执行顺序混乱造成数据错乱,常规思路是对共享数据加锁,限制同一时间只有一个线程可以修改数据,保证操作原子性。

Java中可以通过设置同步块 synchronized实现线程安全,使用共享对象作为参数,线程进入同步块内需获取锁,方法体执行后释放锁,核心思想类似互斥锁(PV操作)。

线程通信
volatile是Java的一种弱同步机制,该关键字修饰的变量可保证可见性和有序性(禁止指令重排,即JVM自行决定执行顺序),即一个线程对变量的修改对所有线程可见,这种方式仅能处理简单状态变换,如标志位truefalse的切换可对所有线程可见,故可用于线程通信,但不可用于同步。

另一种可靠的通信方式是同步块内的挂起wait和唤醒notify操作,其中wait默认挂起当前线程,唤醒notify则是从等待池中随机唤醒一个线程,并且操作系统可能在没有notify的情况下执行唤醒操作,所以挂起需要在循环中检查判断。

另外这种几乎完全随机的唤醒机制效率很低,有时可能需要多次唤醒不需要的线程,再通过其检查判定机制重新挂起,最好是能根据线程分类,唤醒指定类型的线程。

ReentrantLockCondition是能按线程类型或条件精确唤醒 的方法,ReentrantLock锁不依赖共享对象,该锁可作为一个实例单独创建并实现锁功能。同时可根据该锁创建不同状态的等待队列Condition,调用await挂起,signal唤醒。

该部分有必要给出代码示例,以Java多线程文档中的生产者消费者为例:

java 复制代码
package ThreadTask.misson707;

import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class Product{
    private String name;
    public Product(String name){
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
class Producer implements Runnable{
    // 生产者线程,store作为锁对象
    private Store store;
    private Product product;
    public Producer(Store store,String name){
        this.store = store;
        this.product = new Product(name);
    }
    @Override
    public void run() {
        ArrayList<Product> products = store.getProducts();
        while(true){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            store.lock.lock();
            if(products.size()>store.getCapacity()-5){
                System.out.println("容量快满了继续休息");
                try {
                    store.conditionc.signalAll();
                    store.conditionp.await();
                    System.out.println("唤醒了生产者线程"+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally{
                    store.lock.unlock();
                }
            }else{
                store.getProducts().add(product);
                System.out.println("增加了商品"+product.getName());
                store.lock.unlock();
            }
        }
    }
}

class Consumer implements Runnable{
    private Store store;
    public Consumer(Store store){
        this.store = store;
    }
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            store.lock.lock();
            if(store.getProducts().size()<5){
                System.out.println("商品数量太少不能消费");
                try {
                    store.conditionp.signalAll();
                    store.conditionc.await();
                    System.out.println("唤醒了消费者线程"+Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }finally{
                    store.lock.unlock();
                }
            }else{
                Product product = store.getProducts().get(store.getProducts().size()-1);
                store.getProducts().remove(product);
                System.out.println("消费了商品"+product.getName());
                store.lock.unlock();
            }
        }
    }
}

public class Store {
    private int capacity=20;
    public ReentrantLock lock = new ReentrantLock();
    public Condition conditionp = lock.newCondition();
    public Condition conditionc = lock.newCondition();
    private ArrayList<Product> products = new ArrayList<Product>();

    public int getCapacity() {
        return capacity;
    }

    public ArrayList<Product> getProducts() {
        return products;
    }

    public static void main(String[] args) {
        Store store = new Store();
        Producer producer1 = new Producer(store,"小米su7");
        Producer producer2 = new Producer(store,"iPhone16");
        Producer producer3 = new Producer(store,"星巴克");

        Consumer consumer1=new Consumer(store);
        Consumer consumer2=new Consumer(store);

        Thread thread1 = new Thread(producer1);
        Thread thread2 = new Thread(producer2);
        Thread thread3 = new Thread(producer3);
        Thread thread4 = new Thread(consumer1);
        Thread thread5 = new Thread(consumer2);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();
    }
}

注解

注解是一种描述程序元素(类、属性、方法等)的机制,注解本身不影响程序运行,但程序可以获取注解以进行更改运行模式等操作,简单来说就是程序可读的注释

注解的使用方法为@注解名,我们在多线程部分重写run方法时,方法上的@override就是重写注解。

注解内可自定义属性,并可指定变量值,也可以定义元注解(修饰注解的注解),可限制被修饰的注解能作用的程序元素类型。

该部分在后续框架部分可用于标识所属组件、绑定功能等,开发者自行编写注解的场景较少,故不作赘述。

反射

通常创建实例的方法都是在代码中new出来,这种写法要求我们在程序执行之初就确定好要使用的类和具体方法,扩展性和灵活性不足。

反射机制(Reflect)则是一种程序执行时动态获取类和执行方法的能力,该方法突破了Java编译时确定类型的限制,为动态编程提供了底层支持,很多主流框架如Spring的底层都使用反射实现。

JVM会为每一个类型生成一个元数据对象class,该对象存储了类型的完整信息,成员、方法等,反射机制通过该元数据对象实现对类的操作,进而实现动态编程,因为该过程需要借助class进行,好像一面镜子,所以该操作称为反射。

获取元数据对象 class有三种方法:

  • Class.forName()全限定类名。示例为Class c=Class.forName("Reflect.Person");,全限定类名表示包含包位置的类路径,类似相对位置,但省略表示路径的符号和文件类型标记。
  • 实例的getclass方法。示例Class p = new Person().getClass();使用实例对象的getClass方法获取元数据对象。
  • 类的class属性。实例Class p1=Person.class;,使用类的class属性赋值。

还有一些获取属性的方法如下:

  • 获取构造函数getConstructor(),默认无参构造,参数内传入类型的元数据对象即为有参构造,如int.class
  • 创建实例构造函数.newInstance(),返回创建的实例。
  • 获取公开属性元数据.getField("属性名")
  • 获取私有属性元数据.getDeclaredField("属性名")
  • 获取属性列表元数据.getFields,返回Field数组。
  • 打破私有封装属性.setAccessible(true)
  • 修改实例属性属性.set(实例,修改值)
  • 获取方法元数据.getMethod(方法名,参数类型.class)
  • 执行方法方法.invoke(实例,参数)

反射与多态

反射提供了不同于传统正向先定义再使用类的方法,允许开发者逆向构建实例,并根据注解选择要执行的操作,其本质仍然是基于提高扩展性的设计。

提高扩展性比较简单的方法是使用多态,二者之间有什么区别和联系呢?

首先回顾多态。允许父类接受子类实例,这也允许我们无需精准定义类型即可允许程序,只是不能调用子类方法,我们通过instanceof判断类型并向下转型即可解决,好像也很方便,但多态的扩展性是有前提的------继承链

多态的类型宽松仅针对已知继承链内的类,这要求开发者在开发时就设计好所有类的继承关系,并体现在代码中,反射机制可以实现真正突破编译限制,允许在​​运行时动态操作任意类的元数据,直接调用子类方法而无需转型,甚至直接操作私有成员,是更自由的操作手段。

反射的问题

反射在提供自由的同时也带来了安全风险:

  1. 打破封装。封装性是面向对象三大核心特征之一,反射操作私有成员的能力直接打破了这一特性,私有成员的更改可能导致类的内部问题。
  2. 代码注入。恶意代码可能通过Class.forName("恶意类名")加载和执行其他类,绕过编译检查。
  3. 性能开销。反射结构需要动态解析类的结构,带来效率问题,性能敏感场景可能不符合要求。

所以总的来说,反射还是应该尽可能少用,不得已要用的地方也要做好校验,更详细的介绍可见Java反射与注解

web开发

前端相关知识可见:前端基础

Servlet

在 Web 开发中,Java 主要作为后端应用提供服务,其原生 Web 开发方案的核心是 Servlet------Servlet 可用于接收前端 HTTP 请求、处理业务逻辑(如交互式浏览和修改数据),并生成动态 Web 响应内容。

使用流程为:

  1. 实现 Servlet 接口(或继承其子类如HttpServlet),重写核心方法,包括初始化、请求处理、销毁等相关方法,以处理前端传来的请求;
  2. 配置web.xml(或通过注解@WebServlet),指定 URL 路径与 Servlet 类的映射关系;
  3. 借助 Servlet 的各级域对象(如ServletRequest、HttpSession、ServletContext)存储数据,将处理结果传递给前端;
  4. 前端解析后端返回的响应数据,动态生成页面。

模板引擎 JSP

JSP(JavaServer Pages)是一种模板技术,允许在 HTML 中嵌入 Java 代码;但其本质是 "JSP 文件会被 Web 服务器(如 Tomcat)先编译成 Servlet 类,Servlet 执行时生成完整的 HTML 内容,再逐行发送给前端",最终由浏览器解析并展示为网页。

具体使用时,通过<% %>(Java 代码块)、<%= %>(输出变量)等标签将 Java 代码嵌入 HTML 文件,JSP 通过 Servlet 域对象(如request)接收后端传递的数据,处理后生成 HTML 页面。

与 JavaScript(纯粹在前端执行、用于动态展示页面)不同,JSP 的 Java 代码在后端服务器执行,执行后仅将生成的 HTML 文件发送给前端 ------ 前端无法感知后端的 Java 逻辑,仅能看到最终的 HTML 内容。

该原生开发方案十分繁琐,目前大多被 Spring Boot、Spring MVC 等高级框架替代,仅作简单了解和概念辨析,更详细的介绍可见:JavaWeb开发,这里不作赘述。

设计模式

设计模式是代码设计的经验总结,用于解决特定场景下的代码复用、解耦、扩展等设计问题,核心目的是增强代码可读性、可重用性和可扩展性

七大原则

  1. 单一职责原则 (Single Responsibility Principle)

    一个类只负责一项职责,避免一个类因负责多个职责,一项功能的修改导致另一项职责故障。

  2. 开放-关闭原则 (Open-Closed Principle)

    对扩展开放,对修改关闭。程序扩展不修改原有代码,使用接口或继承等手段实现热插拔效果。

  3. 里氏替换原则 (Liskov Substitution Principle)

    基类可以存在的地方,子类一定可以出现。即任意子类均可以替换其基类,而程序仍能正常运行,这要求所有子类与基类的语义和异常定义一致,本质是对开闭原则的补充,详细可见细说里氏替换原则。

  4. 依赖倒转原则 (Dependence Inversion Principle)

    依赖抽象而非具体,是开闭原则的基础,面向接口编程。

  5. 接口隔离原则 (Interface Segregation Principle)

    实现最小的接口,不多余实现不需要的方法,可以有效降低依赖,降低耦合。

  6. 迪米特法则(Law Of Demeter)

    对象间尽量少了解,仅和朋友交流,包括成员变量,返回值,参数,是降低耦合性的另一原则。

  7. 组合/聚合复用原则 (Composite/Aggregate Reuse Principle)

    类尽量使用聚合的方式,而非继承,与迪米特法则互补,降低耦合度。

其实我认为还可以再精简一点,甚至只要两条就够了,就是维护可扩展性,降低耦合性,相对应的,七大原则可根据这两条核心目标划分为两类:
维护扩展性

  1. 开闭原则
  2. 里氏替换,子类要可以替换基类而不出错,要求语义和异常定义一致
  3. 依赖倒转

降低耦合度

  1. 单一职责
  2. 接口隔离
  3. 迪米特法则,对象间少了解,仅和成员、返回值、参数交流
  4. 聚合复用

下面介绍几种常用的设计模式。

单例模式

单例模式是指在一个JVM中单例对象只有一个实例存在,也只提供一个取得实例的方法,这种方法有几个好处:

  1. 减少频繁创建对象的开销;
  2. 省去new操作,降低了系统内存的使用频率;
  3. 保证核心对象唯一,避免产生多个核心对象导致的操作冲突。

创建单例有基于静态代码块的饿汉方法,和检测单例是否已经存在的饱汉方法,饱汉方法需要额外保证线程安全 ,而饿汉式也容易被反射机制突破私有构造方法的限制获取多个实例,所以最稳妥的方法是使用枚举。

即在枚举中获取实例,枚举首先是线程安全 的;并且每个枚举常量对应一个静态final实例,无法被重新创建 ;同时构造方法默认私有,编译后无法通过反射调用 (会抛出IllegalArgumentException)。

工厂方法模式

传统通过new关键字或反射创建实例的方式,仍需开发者直接管理对象的创建过程,设计模式的工厂方法模式则是将实例的生产过程抽象出来作为单独一个类,让该类管理实例的生产。

生产过程可以传入字符串匹配实例,或定义不同的生产方法名实现,但这种方式都有一个问题------扩展性不足。当实例增加时,必须修改原有工厂类的代码,这违背了开闭原则,此时需要采用抽象工厂模式。

抽象工厂指真正生产实例的工厂都基于一个抽象工厂创建,此时每个工厂只负责生产一类实例,但当实例增加时也只需增加工厂实现类即可,无需修改原有代码,维护了开闭原则,同时也是依赖倒转的应用。

代理模式

代理模式是借助一个 "代理类" 间接访问另一个 "被代理类" 功能的设计模式,这种方法主要用于对类的功能扩展,实际生产中也用于区分核心代码,将收集日志等非核心功能写入代理对象。

实现方案有以下三种:

  1. 静态代理

代理类与被代理类实现相同接口或继承相同父类,代理类聚合被代理类成员,同名方法中调用被代理类方法的同时增加自己的逻辑,实现对功能的扩展,示例代码如下:

java 复制代码
// 接口文件
public interface house {
    public void findHouse();
}
// 房东核心业务
public class HouseOwner implements house {
    public void findHouse() {
        System.out.println("房东收钱");
    }
}
// 中介及测试代码
public class Agent implements house {
    private HouseOwner owner;
    public Agent(HouseOwner owner) {
        this.owner = owner;
    }
    public void findHouse() {
        System.out.println("中介找到租客");
        owner.findHouse();
        System.out.println("中介后续维护");
    }

    public static void main(String[] args) {
        Agent agent = new Agent(new HouseOwner());
        agent.findHouse();
    }
}
  1. JDK动态代理

静态代理要求代理类与被代理类都实现相同的接口,当接口功能扩展时,所有实现类都要维护,使用较为不便,因此产生了动态代理。

JDK动态代理的本质是:动态生成代理类字节码,加载并生成对象,执行具体方法时调用InvocationHandler 实现类的增强方法,代理类本身不参与增强过程,只用于类型匹配与调用转发。

实际使用的大致过程为:

  • 实现InvocationHandler接口并重写invoke增强方法,其中增强方法可通过方法名method.getName()分别判断执行不同逻辑,实现对指定方法的增强;
  • 获取动态代理对象(参数为:类加载器、被代理类实现的接口数组、InvocationHandler接口的实现类);
  • 将动态代理类转型为目标接口类型,调用增强后的方法。

上述过程要求被代理类至少实现一个接口 ,JVM动态生成的代理类需要实现该接口保持类型一致。相比静态代理每次变更接口都需要修改代理类的方法,动态代理的代理类由JVM动态生成,相当于彻底消除了代理侧的维护成本 ,只需要维护被代理类实现接口的义务即可;同时代理逻辑模板可用于任何类InvocationHandler的实现类是与类型无关的增强模板,只要增强逻辑通用(如打印日志),就可以增强任何实现了接口的类。

  1. CGLIB动态代理

当被代理对象未实现接口时,可用CGLIB动态代理,即动态生成被代理类的子类实现代理。

底层依赖ASM字节码工具,通过核心类Enhancer动态生成子类,增强逻辑与JDK代理类似,通过实现MethodInterceptor接口并重写intercept方法定义。

要注意的是,受继承原理限制,final修饰的类和方法无法被CGLIB动态代理,本质上可以认为该方法是动态生成子类的静态代理。

使用方法示例如下:

java 复制代码
public class CglibProxy implements MethodInterceptor {
	// 重写intercept增强方法
    @Override
    public Object intercept(Object o, Method method, Object[] params, MethodProxy methodProxy) throws Throwable {
    	System.out.println("CGLIB 动态代理");
        Object result = methodProxy.invokeSuper(o, params);
        return result;
    }
}

public class Test {
    public static void main(String[] args) {
    	// 创建增强对象
    	Enhancer enhancer = new Enhancer();
        // 指定要增强的类,即生成该类的子类,需要父类的字节码
        enhancer.setSuperclass(Agent.class);
        // 给被代理类的所有方法设置回调,可以是实例或匿名内部类,回调实例内需要重写intercept方法
        enhancer.setCallback(new CglibProxy());
        // create生成代理对象
        Agent agent=(Agent) enhancer.create();
    }
}

有关设计模式更详细的介绍可见:Java设计模式

高级框架

SSM框架

SSM框架由Spring、pring MVC和MyBatis三个组件构成,形成 Spring 管理实例、Spring MVC 处理表现层请求、MyBatis 操作持久层数据的分层协作模式,实现从 Web 请求到数据库操作的全流程闭环,构成完整、全栈的开发框架。

下面详细介绍每个组件的核心思想和原理。

Spring

Spring核心容器负责管理实例,以 IOC(依赖反转 )和 AOP(面向切面编程)为核心思想。IOC 将对象的创建、管理权交给容器,通过依赖注入(DI)降低代码耦合;AOP 通过 "切面(Aspect)、切点(Pointcut)、通知(Advice)" 实现无侵入式功能扩展(如日志、事务),无需修改原有业务代码。

IOC依赖反转

不管是new还是基于反射创建实例,都是开发者管理实例的正向过程,依赖反转是指将代码中创建、管理的实例的控制权转交给容器,同时通过简单配置即可实现单例等设计模式,提高了开发效率。

直观上体现为:将原本由开发者创建资源的过程反转为资源准备好后注入到我们需要的实例中。

实现手段有三种:基于xml配置文件、基于注解和基于核心配置类。

基于xml配置容器对象

该方法需要将实例的类名,构造参数等信息在配置文件中指定,程序运行时自动将指定的实例加入容器,主函数中获取容器对象后,可通过getBean方法获取实例。

要注意的是其本质还是调用类自身的getset和构造方法,所以这些方法必须在类中声明。

基于注解配置容器对象

该方法通过注解描述类及成员,如@Component("id"),把资源交给spring管理,@Autowired匹配容器中已有的实例自动注入成员等,最后在xml配置文件中开启包扫描,告知容器扫描注解的范围,实现于xml配置相同的效果。

基于核心配置类配置容器对象

该方法可以完全脱离XML文件,仅通过一个核心配置类管理容器,配置类通过注解@Configuration声明,内部定义返回实例的方法并通过@Bean修饰加入容器,容器可在主函数内使用AnnotationConfigApplicationContext(配置类.class)获取。

AOP面向切面

面向切面是指在不修改原代码的基础上实现对功能的扩展,是动态代理技术的进一步封装。

AOP中将增强目标(通常是被增强的方法)称为切点,增强的内容(通常为日志处理等增强方法)称为通知,二者的组合称为切面。

通知根据执行时机不同分为前置、后置、返回、异常和可覆盖多种执行时机的环绕通知,共五种通知类型。

基于xml配置AOP

将通知类和目标类加入容器后,在xml配置文件建立二者关系。使用aop标签指定,绑定实例如下:

xml 复制代码
    <aop:config>
<!-- 引入切面,绑定通知   -->
        <aop:aspect ref="advice">
<!--            绑定通知与增强的方法-->
<!--            execution切点表达式,参数为:返回值类型 包名 类名 方法名-->
            <aop:before method="before" pointcut="execution(* AOP.Service.service())"/>
            <aop:after method="after" pointcut="execution(* AOP.Service.service())"/>
            <aop:after-returning method="afterReturning" pointcut="execution(* AOP.Service.service())"/>
            <aop:after-throwing method="afterThrowing" pointcut="execution(* AOP.Service.service())"/>
        </aop:aspect>
    </aop:config>

环绕通知不能与上述四种通知同时出现,因为环绕通知本身就可以实现四种不同场景下的增强功能,其简单增强示例如下:

java 复制代码
public void around(ProceedingJoinPoint joinPoint){
    System.out.println("前置通知触发");
    // 获取目标方法参数
    Object[] args = joinPoint.getArgs();
    try {
        // 执行目标方法
        joinPoint.proceed();
        System.out.println("后置通知");
    } catch (Throwable e) {
        System.out.println("异常通知");
        throw new RuntimeException(e);
    }finally {
        System.out.println("返回通知");
    }
}

基于注解配置AOP

开启相关支持和扫描包后,使用注解修饰类和方法,具体使用为:

复制代码
@Aspect 声明当前类为通知类
@Pointcut(表达式) 修饰方法为切入点,方法名为切入点id
@Before(表达式或id) 声明方法为前置通知
@AfterReturning 声明方法为后置通知,仅在正常执行后返回
@AfterThrowing 声明方法为异常通知,仅在抛出异常时通知
@After 声明方法为最终通知,无论是否抛出异常,都会通知
@Around 声明方法为环绕通知

增强AOP下Service类的service方法,使用实例为:

java 复制代码
package AOP;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Advice {
    @Pointcut("execution(* AOP.Service.service())")
    public void pointcut() {}

    @Before("pointcut()")
    public void before(){
        System.out.println("前置通知");
    }

    @AfterReturning("pointcut()")
    public void after(){
        System.out.println("后置通知");
    }
    @AfterThrowing("pointcut()")
    public void afterReturning(){
        System.out.println("返回通知");
    }
    @After("pointcut()")
    public void afterThrowing(){
        System.out.println("异常通知");
    }
    // @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint){
        System.out.println("前置通知触发");
        // 获取目标方法参数
        Object[] args = joinPoint.getArgs();
        try {
            // 执行目标方法
            joinPoint.proceed();
            System.out.println("返回通知");
        } catch (Throwable e) {
            System.out.println("异常通知");
            throw new RuntimeException(e);
        }finally {
            System.out.println("后置通知");
        }
    }
}

Spring MVC

MVC是一种架构模式,代表模型Model、视图View和控制器Controller,分别用于处理逻辑、展示信息和消息传递,是服务器端分层的核心模式。

该组件消息转发使用前端控制器DispatcherServlet实现,所有请求首先经过该调度器,再根据配置文件确定转发给对应的方法,底层封装Servlet,提供xml和注解两种配置方案。

基于xml配置资源调度器
web.xml内声明资源调度器,并将其绑定路径设置为/即所有路径,初始化中指定配置文件springmvc.xml,即具体转发规则,配置示例如下:

xml 复制代码
    <web-app>
        <!--SpringMVC前端控制器,本质是一个Servlet,接收所有请求,在容器启动时就会加载-->
        <servlet>
            <servlet-name>dispatcherServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
			<!--自动加载mvc的xml-->
                <param-value>classpath:springmvc.xml</param-value>
            </init-param>
			<!--Tomcat启动级别,1为随容器启动-->
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
			<!--绑定处理调度的url,/标识所有包都由该调度器转发-->
            <servlet-name>dispatcherServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
    </web-app>

随后在springmvc.xml中配置前后缀等信息建立映射,再在beans.xml中开启对mvc的识别和支持,最后实现Controller接口并重写handleRequest,通过ModelAndView对象即可实现跳转,示例代码如下:

xml 复制代码
beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<!--    开启包扫描-->
    <context:component-scan base-package="com.ssm"/>

<!--    开启对注解的支持-->
    <mvc:annotation-driven/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--匹配前缀为jsp文件路径下-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <!--匹配所有后缀为.jsp-->
        <property name="suffix" value=".jsp"/>
    </bean>

<!--    配置访问指定路径时调用的类-->
    <bean id="/t1" class="com.ssm.Service.Main"/>

</beans>
java 复制代码
package com.ssm.Service;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class Main implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        System.out.println("access success");
        ModelAndView mav = new ModelAndView();
        // 域信息内添加key为msg,value为Hello World的字段
        mav.addObject("msg", "Hello World");
        // 自动跳转到/WEB-INF/jsp/index.jsp
        mav.setViewName("index");
        return mav;
    }
}

基于注解配置资源调度器

web项目最重要的就是映射资源和路径,注解方式可以通过@Controller将类加入容器,@RequestMapping()映射资源路径,该注解可修饰类也可修饰方法,访问路径按从高到底依次嵌套。

简单示例代码如下:

java 复制代码
@Controller
@RequestMapping("/test")
public class TestController {
	// 访问url为/test/01
    @RequestMapping("/01")
    public ModelAndView test01(){

        //创建视图模型对象
        ModelAndView modelAndView = new ModelAndView();
        //往model中添加数据
        modelAndView.addObject("data","hello");
        //设置视图名称
        modelAndView.setViewName("list");

        return modelAndView;
    }

    @RequestMapping("/02")
    // 访问test/02自动跳转,model由spring自动注入
    public String test02(Model model){
        model.addAttribute("name","Tom");
			// 自动跳转list.jsp
        return "list"; 
    }

    @RequestMapping("/03")
    @ResponseBody
    public String test03(){
			// 访问test/03 页面显示hello world
        return "hello world";
    }

MyBatis

MyBatis组件为我们免除了几乎所有的JDBC代码,包括设置参数和获取结果集等工作,可实现半自动的持久层工作。

其核心使用一个SqlSession会话实例,该实例通过映射文件和方法接口动态生成一个代理实例,该代理实例根据配置文件可自动绑定方法与sql语句,而无需手动编写实现类

会话实例基于SqlSessionFactory会话工厂实例创建,工厂实例首先通过SqlSessionFactoryBuilder获取,SqlSessionFactoryBuilder通过预先配置的xml文件构建工厂SqlSessionFactory实例。

用于连接数据库的配置如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--    配置环境,默认选择id为development-->
    <environments default="development">
<!--        具体环境配置-->
        <environment id="development">
<!--            开启类型为jdbc的事务-->
            <transactionManager type="JDBC"/>
<!--            数据源对象,连接池-->
            <dataSource type="POOLED">
<!--                指定驱动、url、用户名和密码-->
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<!--                xml中&应写为&amp;-->
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
</configuration>

获取会话工厂实例时先用流读取该配置文件,再通过工厂构建实例获取工厂实例,代码流程如下:

java 复制代码
// 获取输入流对象,输入来自总配置文件"Mybatis-config.xml
InputStream inputStream= Resources.getResourceAsStream("Mybatis-config.xml");
// 使用SqlSessionFactoryBuilder生成工厂实例
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取sqlSession实例
SqlSession sqlSession=sqlSessionFactory.openSession();

基于xml的使用方法

新建与数据库格式相同的类用于读取数据,和要执行的方法接口(如查询和添加),在resource下的对应目录创建xml映射文件,用于绑定sql语句,同时在主配置文件中声明映射文件路径,大致过程为:

  1. 配置核心文件,用于获取数据库连接;
  2. 编写方法接口,及对应的sql语句,再编写映射文件绑定二者;
  3. 核心配置文件中增加映射文件;
  4. 通过工厂生产类获取工厂,再通过工厂获取数据库会话实例;
  5. 通过数据库会话实例获得接口的实现类;
  6. 调用接口实现类的方法实现对数据库的操作。

最终整体的工作流程如下图:

基于注解的使用方法

相比起xml配置,注解的使用方法就太简单了,直接在方法上加@操作(sql语句)注解即可,无需配置映射文件,示例代码如下:

java 复制代码
public interface UserDao {
    @Select("select * from userinfo")
    @Results({
            // 主键映射
            @Result(column = "id",property = "uid",id = true),
            @Result(column = "name",property = "name")
    })
    List<User> findAll();

    @Insert("insert into userinfo (id,name,sex,phone,hobby,description) values (#{uid},#{name},#{sex},#{phone},#{hobby},#{description})")
    int insert(User user);

补充说明

MyBatis有缓存机制 ,一级缓存默认开启,sqlSession级别,二级缓存需要手动开启,sqlSessionFactory级别。

关联查询 ,其实就是多表联查,比如一次查询用户记录的同时获取其中地址的成员。可通过association修饰形成嵌套查询,将上一次查询的结果传递给下一次查询,示例如下:

xml 复制代码
<resultMap id="one" type="com.Dao.User">
        <id column="id" property="uid"/>
        <!--       通过property指定聚合的成员,javaType指定聚合的全类名 select指定嵌套查询的方法-->
        <!--        将本次查询结果的id作为参数传给findAddress方法-->
        <association property="address" column="id" javaType="com.Dao.Address" select="findAddress"/>
    </resultMap>
<select id="findAll" resultType="com.Dao.User" resultMap="one">
     select * from userinfo
 </select>

也可以使用join连接查询,用association将查询结果拼接到聚合类内,示例代码如下:

xml 复制代码
<!-- 1. 定义包含关联对象的 resultMap -->
<resultMap id="userWithAddressMap" type="com.Dao.User">
    <!-- 映射User自身的属性 -->
    <id column="user_id" property="uid"/> <!-- user表的id用别名user_id避免冲突 -->
    <result column="user_name" property="name"/> <!-- 假设user表的name字段别名user_name -->
    
    <!-- 映射关联的Address对象 -->
    <association property="address" javaType="com.Dao.Address">
        <id column="addr_id" property="id"/> <!-- address表的id别名addr_id -->
        <result column="city" property="city"/> <!-- address表的city字段 -->
        <result column="street" property="street"/> <!-- address表的street字段 -->
    </association>
</resultMap>

<!-- 2. 一次SQL查询关联所有数据(通过JOIN) -->
<select id="findUserWithAddress" resultMap="userWithAddressMap">
    SELECT 
        u.id AS user_id,  -- 别名区分用户ID
        u.name AS user_name,
        a.id AS addr_id,  -- 别名区分地址ID
        a.city,
        a.street
    FROM user u
    LEFT JOIN address a ON u.id = a.user_id  -- 关联条件:用户ID=地址表的user_id
    WHERE u.id = #{id}  -- 按用户ID查询
</select>

延迟加载,上面关联查询的操作都是立即执行的,有些关联对象的结果我们没必要立即获取,可以等到需要时再加载,这种技术就是延迟加载。延迟加载开启粒度很细,可以在全局配置文件开启,也可以在关联查询内开启,甚至可以在单条sql语句内开启。

SSM涉及三个框架的底层原理和具体方法,这里不进一步展开,更详细的介绍可见:
SSM框架

微服务

微服务是一种架构风格。随着软件规模不断扩大,集成功能越来越复杂,可维护性和可扩展性都变得越来越差,并且无法通过引入新框架解决,须采取一种新的架构模式,对项目进行更细粒度的拆分,实现解耦业务模块,服务独立迭代,微服务架构就此应运而生了。

把完成特定业务的模块剥离为可独立运行的服务,再通过注册中心管理这些服务状态,确保服务健康可用,就形成了微服务架构。

但微服务并非万能的解决方案,因为每个微服务独立运行,故可根据场景采用不同技术栈,同时其也可使用独立的数据存储,大型项目使用更灵活;但也因此带来了额外的服务通信开销,部署和运维成本,要求服务单独部署也提高了团队合作的门槛。

SpringBoot

SpringBoot是Spring框架的进一步封装,内部整合了Spring、SpringMVC 等核心组件,还内嵌了Tomcat服务器。同时通过 "约定优于配置" 理念,大幅简化了传统Spring的 XML 配置 ,优先采用注解 +yml/properties配置;同时通过启动器starter整合相关依赖(如spring-boot-starter-web整合了 Web 开发所需的spring-web、spring-webmvc、嵌入式 Tomcat、JSON 解析),只需一个启动器即可完成相关功能的开发,简化了依赖引入过程。此外,SpringBoot可通过Maven继承父工程的方式管理依赖版本,减少了依赖版本冲突的风险。

同时允许打包后的jar独立部署运行,完美契合微服务的自治特征,是微服务中服务实现的主流选择。

Dubbo

Dubbo是阿里开发的服务治理框架,该框架负责服务远程通信及管理,服务提供者将自己的服务注册到注册中心,消费者可从服务中心获取服务地址,进而发起调用。

Dubbo通常采用ZooKeeper作为服务中心,ZooKeeper本质是分布式协调服务,作为注册中心时,核心是为分布式系统提供 "服务注册、发现、配置同步、健康监测" 等基础协调能力。

结构上 :要求提供者实现接口消费者聚合接口类,并使用容器自动注入,最后通过调用接口类的方法实现远程调用。

通信上:服务提供者需要到服务中心注册,包括自身的地址,服务端口信息等,消费者到注册中心查询服务地址,其调用会借助Dubbo转换为RPC远程请求,提供的服务和调用服务的类都需要使用Dubbo的注解修饰,分别用于暴露提供的服务和注入远程服务的代理对象。

其整体调用结构如下图:

Spring Cloud

Dubbo更侧重于服务间的 RPC 通信与服务治理,Spring Cloud则是分布式微服务架构的一站式解决方案,它将市面上成熟的微服务框架整合起来,使用Spring Boot的思想再次封装,提供的功能有:

  1. 分布式/版本化配置
  2. 服务注册与发现
  3. 服务间调用
  4. 负载均衡
  5. 熔断降级、API 网关等。

该框架使用Eureka注册中心 治理服务,引入依赖后无需手动配置可视化页面,提供者注册服务后,消费者使用远程调用RestTemplate类并结合Spring Cloud注解即可实现服务调用,相比原生RestTemplate的硬编码方法(本地URL映射远程URL),使用服务中心可以实现地址的自动映射,即只需指定服务名称,注册中心自动匹配地址列表和端口,客户端的负载均衡组件自动完成负载均衡并选择具体地址和端口,这在服务器集群部署、动态扩缩容以及服务迁移等场景下极大减少了维护开销。

经过注册中心使用服务名称替换URL+端口的一步封装仍有局限性,访问服务指定方法的URL仍需维护,在此基础上,Spring Cloud提供了远程调用组件Feign 对服务再一步剥离,将服务名与要访问的方法封装成一个接口,用户引入该组件依赖后即可通过组件直接调用服务,而彻底无需关心URL及参数设置,实现更彻底的服务解耦。

直观理解可见Feign封装及调用代码:

java 复制代码
// 封装Feign组件,绑定服务与方法的URL
@FeignClient("USER-SERVER")
public interface UserClient {

    @GetMapping("/user/{uid}")
    User findUserById(@PathVariable("uid") int uid);
}
// 调用逻辑,只需远程调用组件Feign的执行方法,无需传入具体URL
@Service
public class cotroller{
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private UserClient userClient;
		
	@GetMapping ("/order/{oid}")
	@ResponseBody
    public User findOrderById(int id) {
        //远程调用 根据用户id查询用户信息
        //User user = restTemplate.getForObject("http://USER-SERVER/user/" + order.getUserId(), User.class);

        User user = userClient.findUserById(order.getUserId());
        return user;
    }
}

更详细的介绍可见:Java微服务

总结

核心概念辨析

多态与反射

二者都是提高可扩展性的设计,多态提高扩展性的方式是继承链(实现链),类之间有的继承(实现)关系时才能使用多态扩展功能;而反射则更灵活,该机制允许我们在程序运行时操作任意类型的类,甚至获取私有成员和方法,但其灵活性也是一把双刃剑,需要酌情使用。

JDK代理与CGLIB代理

代理是在不修改原方法的基础上实现功能扩展的设计模式,静态代理是基于继承(实现接口)和聚合的方法重写,使用起来很不灵活,后续维护成本高。
JDK代理 只需被代理类实现接口,使用时根据接口类型动态生成代理类,增强逻辑通过单独的实现类实现,代理类仅用于类型匹配和调用转发,同时增强实现类与被代理类类型无关,只要增强逻辑通用即可多次复用,既彻底消除了代理侧维护成本,又提高了通用性。
CGLIB代理 是弥补JDK代理仅能代理实现接口类不足的代理方法,该方法会动态生成被代理类的子类,通过子类调用增强模板的增强方法,但受限于继承原理,被final修饰的类无法被CGLIB代理。

那是不是没有实现接口,又被final修饰的类就无法被代理了呢?并非,此时就要请静态代理出场了。

静态代理聚合被代理类,直接在自身方法内调用被代理类的方法,本质是一种万能方法,只是因为其可维护性差而不被首选。

技术发展路径

经过总结可以发现,Java在服务端的技术演变大体经历了原生支持、SSM框架和微服务三个阶段。不同阶段对各类功能的实现呈现出 "层层封装、逐级递进" 的特点:原生技术聚焦底层原理的落地,而封装后的框架则成为面向开发者的高效工具 ------ 工具的熟练使用与底层原理的深入理解,对技术人员而言都十分重要,需兼顾掌握。

以下是各阶段核心技术的演变流程总结:

实例管理 表现层 持久层
原生支持 new/反射 Servlet JDBC
SSM Spring IoC Spring MVC MyBatis
微服务 SpringBoot SpringBoot MyBatis

个人总结

经过这几天的回顾总结,可以下结论说Java后端体系下的基础框架部分学习已经结束了,基础语法,小规模开发的SSM框架,大规模项目的微服务,这些基本目前操作都已经掌握,剩下的就是场景化的精进,高并发下的性能提升,缓存击穿的解决方法等。

如果要再进一步提升,做项目我认为是远远不够的,个人能做的项目很杂,如果要纯手写工作量巨大,、到头来也只是做了个玩具,对真正生产环境中的问题还是一窍不通,所以我的下一步想法是,直接找实习。用自己学的知识尝试解决真正的问题,才能快速完成认识到实践的第二次飞跃。

相关推荐
SUPER52669 小时前
FastApi项目启动失败 got an unexpected keyword argument ‘loop_factory‘
java·服务器·前端
咕噜咕噜啦啦10 小时前
Eclipse集成开发环境的使用
java·ide·eclipse
光军oi13 小时前
全栈开发杂谈————关于websocket若干问题的大讨论
java·websocket·apache
weixin_4196583113 小时前
Spring 的统一功能
java·后端·spring
小许学java13 小时前
Spring AI-流式编程
java·后端·spring·sse·spring ai
Light6014 小时前
领码方案|微服务与SOA的世纪对话(5):未来已来——AI 驱动下的智能架构哲学
微服务·智能双生体·ai 增强 ddd·自驱动 mesh·预测型 ci/cd·自演进闭环
haogexiaole14 小时前
Java高并发常见架构、处理方式、api调优
java·开发语言·架构
EnCi Zheng14 小时前
@ResponseStatus 注解详解
java·spring boot·后端
Fency咖啡14 小时前
高效学习方法——知识关联性
学习方法