i@yujinyan.me

Blog

给前端同学的 Java 后端开发介绍

关于 Java 语言

The Java language is …

  • Compiled: 需要用 javac 将 .java 源代码文件编译成 .class 字节码文件
  • Object-oriented: 面向对象

Hello World

// HelloWorld.java
class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}
$ javac HelloWorld.java
$ java HelloWorld

面向对象 Object-oriented

Java:

@Value
public class Person implements A {
    String name;

    String introduce() {
        return "I'm " + name;
    }

    // Run this programmin with `java Person`
    public static void main(String[] args) {
        A harry = new Person("Harry");
        String a = "abc";

        System.out.println(harry.introduce());
    }
}

interface A {
  String introduce();
}

Duck typing vs. Nominative typing

TypeScript:

type Person = {
  name: string
}

const harry = {
  name: "Harry"
}

function introduce(person: Person) {
  return `I'm ${person.name}`
}

console.log(introduce(harry))
🔑

Java 和 TypeScript 类型系统的区别

Nominative typing: 对象类型、继承关系必须显式声明. An object is of a given type if it is declared to be (or if a type’s association with the object is inferred through mechanisms such as object inheritance).

Duck typing: 对象满足类型的约束条件(含有特定属性或方法)即可. An object is of a given type if it has all methods and properties required by that type.

🦆

能像鸭子一样叫的对象就是一只鸭子

Java 虚拟机

The runtime environment

JVM 是 Java 的运行时,类似 Node.js (或者说 V8) 和 JavaScript 之间的关系

其他广泛使用的 Java 运行时:

  • Android 的 Dalvik 以及后来的 Android Runtime (ART)
🔑

Difference between java HelloWorld and node hello.js

interpreting vs. compiling

  • V8 compiles JavaScript source code to native machine code at runtime. As of 2016, it also includes Ignition, a bytecode interpreter. (Wikipedia - Node.js)
  • JVM interprets bytecode produced by javac. It also uses JIT (just-in-time compilation) to translate part of Java bytecode into native machine code.

其他主流 JVM 语言

  • Kotlin:改良版 Java、Android 开发首选语言
  • Groovy:弱类型、动态、Gradle
  • Scala:学院派、函数式、大数据、Flink
  • Clojure:动态、函数式、Lisp

Spring Boot

快速上手

Spring Initializr 创建一个项目脚手架,导入 IDE 即可

JSON 序列化、反序列化

🔑

前端传给后端的 JSON 字符串是如何变成对象的?

JavaScript:

const c = JSON.stringify({id: 1, title: "hi"})
JSON.parse(c)

类比 Java:

@Value
class Campaign {
  int id;
  String title;
}

ObjectMapper objectMapper = new ObjectMapper();

Campaign campaign = new Campaign(1, "hi");
String json = objectMapper.writeValueToString(campaign); 
Campaign c = objectMapper.readValueFromString(json, Campaign.class); 
🔍

不同于 JavaScript,Java 在反序列化成对象的时候需要指定类型

有多种 JSON 序列化/反序列化的库,如 Jackson、Gson 等。Java 后端项目推荐用 Jackson(也是 Spring Boot 默认的 JSON 库)。

常见数据类型对应关系:

Java 类型JS 类型
日期java.time.ZonedDateTime原生 Date、moment.js、day.js *
枚举enumstring

LocalDateTime vs ZonedDateTime

  • ZonedDateTime 带有时区
  • LocalDateTime 不带时区
$ node
Welcome to Node.js v16.17.0.
Type ".help" for more information.
> new Date()
2022-09-13T02:10:14.100Z

JS 的 Date 带时区,所以直接对应 Java 的 ZonedDateTime

注解 Annotation

Java 的注解和 JavaScript 的 Decorator 语法相似,但语义不同。Java 注解的作用是给 class、方法、字段等添加元信息(Metadata)。

用途主要场景
编译器提示给编译器额外信息@Override
编译时处理代码生成Lombok
运行时处理路由配置、Bean 校验JSR-303: Bean Validation

编译器提示

public class OverrideDemo {

    static class Shape {
        String name() {
            return "Shape";
        }
    }

    static class Circle extends Shape {
        @Override 
        String name() { 
            return "Circle";
        }
    }
}

假设 Circle#name 方法注解了 @Override,但是实际上并没有覆写父类的任何方法,会报编译错误。@Override 注解是可选的, 给编译器额外的提示信息。

编译时处理

Project Lombok 是一个利用 Java 的注解处理器机制, 通过在编译期生成代码帮助我们减少样板代码(boilerplate)的工具类库。

我用得最多的:@Value,用于 immutable data class

JDK 17 的话可以直接用 record

运行时处理

也可以在运行时获取注解上标注的信息。常见的 Bean 数据校验(JSR-303)、Spring MVC 基于注解的路由配置都是采用这种机制。

在 Spring Boot 项目中使用 Hibernate 的 Bean 校验库:

implementation 'org.springframework.boot:spring-boot-starter-validation'

API Demo:

import lombok.Value;
import org.hibernate.validator.HibernateValidatorFactory;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.Min;
import java.util.Set;

/**
 * https://hibernate.org/validator/documentation/getting-started/
 */
public class BeanValidationDemo {

    @Value
    static class Article {
        @Min(5)
        String title;
    }

    public static void main(String[] args) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Set<ConstraintViolation<Article>> violations =
            validator.validate(new Article("Hi")); 
        System.out.println(violations);
    }
}

假设我们自己写一个这样的库:

import lombok.Value;
import lombok.extern.slf4j.Slf4j;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;

@Slf4j
public class RuntimeDemo {
    // 自定义注解,需要保留到运行时
    @Retention(RetentionPolicy.RUNTIME) 
    @interface Range { 
        int min();
        int max();
    }

    @Value
    static class Article {
        @Range(min = 1, max = 100)
        String title;
    }

    public static void main(String[] args) {
        Article article = new Article("Hello World");
        Field[] fields = article.getClass().getDeclaredFields();
        for (Field field : fields) {
            // 获取 Range 类型的注解
            Range range = field.getAnnotation(Range.class); 
            if (range != null) {
                log.info("range: min is {}, max is {}, field {}",
                    range.min(),
                    range.max(),
                    field);
            }
        }
    }
}

并发编程

线程 API

public class ThreadDemo {
    @SneakyThrows
    public static void main(String[] args) {
        Thread aThread = new Thread(() -> {
            try {
                Thread.sleep(1000L);
                System.out.printf("Inside %s%n",
                    Thread.currentThread().getName()); 
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        // 调用 start 方法后,aThread 开始运行
        // aThread 和 main 线程并发
        aThread.start();
        System.out.printf("Inside %s%n",
            Thread.currentThread().getName()); 
        // 等待 aThread 执行完成
        aThread.join();
    }
}

JVM 线程对应操作系统线程,是比较昂贵的资源。实际开发中几乎不会直接 new Thread,而是使用线程池做多任务并发。

🚀

JEP 425 将为 JVM 带来虚拟线程,即在一个操作系统线程上复用多个虚拟线程,从而提供应用吞吐量。 关于虚拟线程推荐阅读 Brian Goetz 的 Virtual Threads: New Foundations for High-Scale Java Applications

线程池

public class ThreadPoolDemo {

    // 创建一个线程池
    private static final ExecutorService POOL =
        Executors.newCachedThreadPool();

    @SneakyThrows
    static String writeFile() {
        Thread.sleep(1000);
        return "file.csv";
    }

    @SneakyThrows
    public static void main(String[] args) {
        Future<String> filename = POOL.submit(ThreadPoolDemo::writeFile);
        System.out.println(filename.get());
    }
}

Request Per Thread

传统 Spring MVC 使用的是一个 http 请求对应一个线程的模型

@RestController
class NaiveCounterDemoController {
    // 线程不安全的示范
    private int counter = 0; 

    @PostMapping("/inc")
    public void inc() {
        counter++; 
    }

    @GetMapping("/get")
    public int get() {
        return counter;
    }
}

Shared Mutable State

在上面的 NaiveCounterDemoController 中,多个线程各自处理自己的请求,并发修改 counter 会造成问题。

假如有 100 个并发请求增加计数器,最终计数器的结果可能小于 100。

原因是 counter++ 并非原子的操作,相当于 counter = counter + 1,先读取后写入。

Race condition between two clients concurrently incrementing a counter

并发加计数器的问题图解(引自 DDIA)

解决方法:

  • 提供原子操作的线程安全的数据机构 Thread-safe data structures: AtomicInteger (counter 场景首选)
  • 互斥锁 Mutual exclusion
  • 线程隔离 Thread confinement

参考:Shared Mutable State and Concurrency in Kotlin

数据库持久化

  • Raw SQL / Template Engine: MyBatis XML
  • SQL DSL: JOOQ
SELECT AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, COUNT(*)
FROM AUTHOR
JOIN BOOK ON AUTHOR.ID = BOOK.AUTHOR_ID
WHERE BOOK.LANGUAGE = 'DE'
AND BOOK.PUBLISHED > DATE '2008-01-01'
GROUP BY AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME
HAVING COUNT(*) > 5
ORDER BY AUTHOR.LAST_NAME ASC NULLS FIRST
LIMIT 2
OFFSET 1
create.select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, count())
      .from(AUTHOR)
      .join(BOOK).on(AUTHOR.ID.equal(BOOK.AUTHOR_ID))
      .where(BOOK.LANGUAGE.eq("DE"))
      .and(BOOK.PUBLISHED.gt(date("2008-01-01")))
      .groupBy(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
      .having(count().gt(5))
      .orderBy(AUTHOR.LAST_NAME.asc().nullsFirst())
      .limit(2)
      .offset(1)
  • ORM: Hibernate / prisma.io