给前端同学的 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 * |
枚举 | enum | string |
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
,先读取后写入。
解决方法:
- 提供原子操作的线程安全的数据机构 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