log
目录
Java Base
public class MyPublicClass {
public int publicField; // 公共字段,任何类都可以访问
protected int protectedField; // 受保护字段,同一包或子类可以访问
private int privateField; // 私有字段,只有本类可以访问
void defaultField() {} // 默认(包级私有),同一包内的类可以访问
}
用final修饰class可以阻止被继承:
用final修饰method可以阻止被子类覆写:
用final修饰field可以阻止被重新赋值:
用final修饰局部变量可以阻止被重新赋值:
一个.java文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同。
整形类型:
byte: 1byte
short: 2bytes
int: 4bytes
long: 8bytes
float: 4bytes2
double:8bytes
Integer:
Integer可以为null, int不能
用于数据库字段,或API参数可返回。因为它们可能为null
数值比较用:a.equals(b), 不能用a==b
自动装箱(Autoboxing)
自动拆箱(unboxing)
Integer a = 10; 相当于 Integer.valueOf(10)
int b = a; 相当于 b = a.intValue()
int[] numbers = {1,2,3,4,5}
String[] names = {}
基础数据类型boolean byte short int long float double char
基础数据类型在栈上,类数据类型在堆上
----------
String str = "hi," + "lao" + "zhang";
str1.equals(str2)
格式化
String str = String.format("my name is %s, my ages is %d", "wang", 20);
String str = "hello";
str = "world"; 堆上新增加一个区域保存world
原来的字符串"hello"还在堆上,只是我们无法通过变量str访问它而已。
因此,字符串的不可变是指字符串内容不可变
String str = null; 不指向堆上的区域
StringJoiner
String.join()
String[] names = { "Bob" , "Alice" , "Grace" };
StringJoiner sj = new StringJoiner(",", "Hello", "!");
String str = String.join(",", names);
System.out.println(sj.toString()); // Hello,Bob,Alice,Grace!
System.out.println(str); // Bob,Alice,Grace
String: 是不可变类型,函数传值是一个副本,下面两个是可变类型
StringBuffer: 字符串拼接时synchronized保障线程安全
StringBuilder: 和StringBuffer一样继承自AbstractStringBuilder
函数只是传值
String str = "laowang";
change(str)
System.out.println(str); 输出还是laowang
public static void change(str) {
str = "xiaowang";
}
StringBuffer sb = new StringBuffer("xiaowang");
change(sb)
System.out.println(sb);
因为不创建副本,StringBuffer性能优于String, 但不如StringBuilder
StringBuffer 线程安全
StringBuilder线程不安全
String类是final类型,不可继承
public final class String {
}
String不可变的好处如下。
·只有当字符串是不可变的,字符串常量池才能实现,字符串池的实现可以在运行时节约很多堆空间,因为不同的字符串变量都指向池中的同一个字符串;
可以避免一些安全漏洞,比如在 Socket 编程中,主机名和端口都是以字符串的形式传入,因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞;
多线程安全,因为字符串是不可变的,所以同一个字符串实例可以被多个线程共享,保证了多线程的安全性,
适合做缓存的 key,因为字符串是不可变的,所以在它创建的时候哈希值就被缓存了,不需要重新计算速度更快,所以字符串很适合作缓存的中的 key。
int[] intarr = new int[10];
int[] intarr1 = {1, 2, 3, 4, 5};
int len = intarr.length(); 数组长度
是不可变数组,要管理可变数组使用ArrayList
java泛型不支持基础类型int, 需要用对象类型Integer
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
Java中主要的日期类型包括java.util.Date、
java.time包下的新API(如LocalDate、LocalTime、LocalDateTime、Instant等)
以及java.util.Calendar类,分别适用于不同场景的日期时间处理需求。
----------------
异或运算
n = 0 ^ 0; // 0
n = 0 ^ 1; // 1
n = 1 ^ 0; // 1
n = 1 ^ 1; // 0
// 多行字符串
String s = """
SELECT * FROM
users
WHERE id > 100
ORDER BY name DESC
""";
--------------
double d = 3.1415926;
System.out.printf("%.2f\n", d); // 显示两位小数3.14
System.out.printf("%.4f\n", d); // 显示4位小数3.1416
Scanner scanner = new Scanner(System.in);
System.out.print("Input your name: ");
String name = scanner.nextLine();
System.out.print("Input your age: ");
int age = scanner.nextInt(); // 读取一行输入并获取整数
数组和ArrayList都可以用枚举
int[] ns = { 1, 4, 9, 16, 25 };
for (int n : ns) {
System.out.println(n);
}
int[] ns = { 28, 12, 89, 73, 65, 18, 96, 50, 8, 36 };
Arrays.sort(ns);
System.out.println(Arrays.toString(ns));
---------------------
class Student extends Person {}
class Action inplements ActionInterface {}
final class 禁止继承
sealed指定三个类可继承
public sealed class Shape permits Rect, Circle, Triangle {
}
强制转型
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
也可以这样写:
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
抽象类
class Person {
public abstract void run();
}
如果一个抽象类没有字段,所有方法全部都是抽象方法:
就可以把该抽象类改写为接口:interface。
interface Person {
void run();
String getName();
}
类只能继承自一个抽象类
类可以继承自多个接口
一个interface可以继承自另一个interface。interface继承自interface使用extends
在接口中,可以定义default方法
interface Person {
String getName();
default void run() {
System.out.println(getName() + " run");
}
}
interface是可以有静态字段的,并且静态字段必须为final类型:
public interface Person {
public static final int MALE = 1;
public static final int FEMALE = 2;
}
匿名内部类,实现了Runable接口并实例化
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}
-------------------
cp 就是classpath
java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
在linux中,路径用:分开
自带“依赖关系”的class容器就是模块module
src/module-info.java 表明本模块依赖
module hello.world {
requires java.base; // 可不写,任何模块都会自动引入java.base
requires java.xml;
}
从Java 9开始,原有的Java标准库已经由一个单一巨大的rt.jar分拆成了几十个模块,这些模块以.jmod扩展名标识,可以在$JAVA_HOME/jmods目录下找到它们:
java.base.jmod
java.compiler.jmod
java.datatransfer.jmod
java.desktop.jmod
---------------
枚举和枚举类
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
public class Weekday {
public static final int SUN = 0;
public static final int MON = 1;
public static final int TUE = 2;
public static final int WED = 3;
public static final int THU = 4;
public static final int FRI = 5;
public static final int SAT = 6;
}
java14开始:
记录类record:
定义class时使用final,无法派生子类;
每个字段使用final,保证创建实例后无法修改任何字段。
record Point(int x, int y) {}
相当于下面代码:
final class Point extends Record {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return this.x;
}
public int y() {
return this.y;
}
public String toString() {
return String.format("Point[x=%s, y=%s]", x, y);
}
public boolean equals(Object o) {
...
}
public int hashCode() {
...
}
}
-------------
File f1 = new File ( "C:\\Windows");
System.out.println(f1.isFile());
System.out.println(f1.isDirectory());
File file = new File ( "/path/to/file");
if (file.createNewFile()) {
// 文件创建成功:
// TODO:
if (file.delete()) {
// 删除文件成功:
}
}
File f = File.createTempFile( "tmp-" , ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
System.out.println(f.isFile());
System.out.println(f.getAbsolutePath());
File f = new File ( "C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有文件和子目录
File[] fs2 = f.listFiles(new FilenameFilter () { // 仅列出.exe文件
public boolean accept (File dir, String name) {
return name.endsWith( ".exe"); // 返回true表示接受该文件
} }
);
printFiles(fs1);
printFiles(fs2);
boolean mkdir() : 创建当前File对象表示的目录;
boolean mkdirs() : 创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
InputStream buffered = new BufferedInputStream (file);
InputStream gzip = new GZIPInputStream (buffered); //直接读取解压缩的内容
//Filter模式(装饰器模式)
public class Main {
public static void main(String[] args) throws IOException {
byte[] data = "hello, world!".getBytes("UTF-8");
try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
System.out.println("Total read " + input.getBytesRead() + " bytes");
}
}
}
class CountInputStream extends FilterInputStream {
private int count = 0;
CountInputStream(InputStream in) {
super(in);
}
public int getBytesRead() {
return this.count;
}
public int read() throws IOException {
int n = in.read();
if (n != -1) {
this.count ++;
}
return n;
}
public int read(byte[] b, int off, int len) throws IOException {
int n = in.read(b, off, len);
if (n != -1) {
this.count += n;
}
return n;
}
}
从classpath读取文件
try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
if (input != null) {
// TODO:
}
}
序列化与反序列化
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
output.writeInt(12345);
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456));
}
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。
更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
使用Files工具类
byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));
// 默认使用UTF-8编码读取:
String content1 = Files.readString(Path.of("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Path.of("/path", "to", "file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Path.of("/path/to/file.txt"));
Java提供的System.currentTimeMillis()返回的就是以毫秒表示的当前时间戳。
当前时间戳在java.time中以Instant类型表示,我们用Instant.now()获取当前时间戳,
效果和System.currentTimeMillis()类似:
public final class Instant implements ... {
private final long seconds;
private final int nanos;
}
一个是以秒为单位的时间戳,一个是更精确的纳秒精度
// 以指定时间戳创建Instant, 并转换为时区:
Instant ins = Instant.ofEpochSecond(1568568760);
ZonedDateTime zdt = ins.atZone(ZoneId.systemDefault());
System.out.println(zdt); // 2019-09-16T01:32:40+08:00[Asia/Shanghai]
Instant now = Instant.now();
now.getEpochSecond(); // 秒
now.toEpochMilli(); // 毫秒
哈希碰撞
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("MD5");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
byte[] result = md.digest(); // 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6
System.out.println(HexFormat.of().formatHex(result));
用户输入的口令,通常还需要使用PBE算法,采用随机数杂凑计算出真正的密钥,再进行加密。
PBE就是Password Based Encryption的缩写,它的作用如下:
key = generate(userPassword, secureRandomPassword);
PBE的作用就是把用户输入的口令和一个安全随机的口令采用杂凑后计算出真正的密钥。i
以AES密钥为例,我们让用户输入一个口令,然后生成一个随机数,通过PBE算法计算出真正的AES口令
DH算法:Diffie-Hellman算法应运而生。
DH算法解决了密钥在双方不直接传递密钥的情况下完成密钥交换
-------------
Java规定:
1.必须捕获的异常,包括Exception及其子类,但不包括RuntimeException及其子类,这种类型的异常称为Checked Exception。
2.不需要捕获的异常,包括Error及其子类,RuntimeException及其子类。
下面代码编译时会抛出异常:
public class Main {
public static void main(String[] args) {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
static byte[] toGBK(String s) {
return s.getBytes("GBK");
}
}
如下编写可以通过编译:
static byte[] toGBK(String s) throws UnsupportedEncodingException {
return s.getBytes("GBK");
}
可以在main中捕获异常:
public class Main {
public static void main(String[] args) {
try {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
} catch (UnsupportedEncodingException e) {
System.out.println(e);
}
}
static byte[] toGBK(String s) throws UnsupportedEncodingException {
return s.getBytes("GBK");
}
}
让main抛出异常
public static void main(String[] args) throws Exception {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
}
同时处理两个异常:
try {
process1();
process2();
process3();
} catch (IOException | NumberFormatException e) {
// IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
通过printStackTrace()可以打印出方法的调用栈
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
多级抛出异常,方便跟踪异常栈
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
try {
process2();
} catch (NullPointerException e) {
throw new IllegalArgumentException(e);
}
}
static void process2() {
throw new NullPointerException();
}
}
在catch中抛出异常,不会影响finally的执行。JVM会先执行finally,然后抛出异常。
自定义异常:
在一个大型项目中,可以自定义新的异常类型,但是,保持一个合理的异常继承体系是非常重要的。
一个常见的做法是自定义一个BaseException作为“根异常”,然后,派生出各种业务类型的异常。
BaseException需要从一个适合的Exception派生,通常建议从RuntimeException派生:
public class BaseException extends RuntimeException {
}
public class UserNotFoundException extends BaseException {
}
public class LoginFailedException extends BaseException {
}
多个构造方法:
public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
这种增强的NullPointerException详细信息是Java 14新增的功能,但默认是关闭的,我们可以给JVM添加一个-XX:+ShowCodeDetailsInExceptionMessages参数启用它:
java -XX:+ShowCodeDetailsInExceptionMessages Main.java
------------
// assert
public class Main {
public static void main(String[] args) {
int x = -1;
assert x > 0;
System.out.println(x);
}
}
//assert失败时,会带上信息x must >= 0
assert x >= 0 : "x must >= 0" ;
JVM默认关闭断言指令,即遇到assert语句就自动忽略了,不执行。
要执行assert语句,必须给Java虚拟机传递-enableassertions(可简写为-ea)参数启用断言
java -ea Main.java
//NullPointerException 空指针异常,用Optional.isPresent()判断
public Optional<String> readFromFile (String file) {
if (!fileExist(file)) {
return Optional.empty();
}
...
}
Reflect
Class cls = new Class(String);
Class类的构造方法是private,只有JVM能创建Class实例,
我们自己的Java程序是无法创建Class实例的。
获取类的三种方法:
Class cls1 = String.class;
String s = "Hello";
Class cls2 = s.getClass();
Class cls3 = Class.forName("java.lang.String");
boolean sameClass = cls1 == cls2; // true
判断类型和子类:
Integer n = new Integer(123);
boolean b1 = n instanceof Integer; // true,因为n是Integer类型
boolean b2 = n instanceof Number; // true,因为n是Number类型的子类
打印类的信息
public static void main(String[] args) {
printClassInfo("".getClass());
printClassInfo(Runnable.class);
printClassInfo(java.time.Month.class);
printClassInfo(String[].class);
printClassInfo(int.class);
}
static void printClassInfo(Class cls) {
System.out.println("Class name: " + cls.getName());
System.out.println("Simple name: " + cls.getSimpleName());
if (cls.getPackage() != null) {
System.out.println("Package name: " + cls.getPackage().getName());
}
System.out.println("is interface: " + cls.isInterface());
System.out.println("is enum: " + cls.isEnum());
System.out.println("is array: " + cls.isArray());
System.out.println("is primitive: " + cls.isPrimitive());
}
访问字段:
Class stdClass = Student.class;
// 获取public字段"score":
System.out.println(stdClass.getField("score"));
// 获取继承的public字段"name":
System.out.println(stdClass.getField("name"));
// 获取private字段"grade":
System.out.println(stdClass.getDeclaredField("grade"));
调用方法:
Class stdClass = Student.class;
// 获取public方法getScore,参数为String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 获取继承的public方法getName,无参数:
System.out.println(stdClass.getMethod("getName"));
// 获取private方法getGrade,参数为int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
打印JavaBean
import java.beans.*;
public class Main {
public static void main (String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println( " " + pd.getReadMethod());
System.out.println( " " + pd.getWriteMethod());
}
}
}
Log
因为对Commons Logging的接口不满意,有人就搞了SLF4J。
因为对Log4j的性能不满意,有人就搞了Logback。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Main {
final Logger logger = LoggerFactory.getLogger(getClass());
int score = 99;
p.setScore(score);
logger.info( "Set score {} for Person {} ok." , score, p.getName());
}
Annotation
Java语言使用@interface语法来定义注解(Annotation)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
元注解
@Target: 定义Annotation能够被应用于源码的哪些位置
@Retention: 定义了Annotation的生命周期
@Repeatable:定义Annotation是否可重复
@Inherited: 定义子类是否可继承父类定义的Annotation
定义:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
Generics
public interface Comparable<T> {
int compareTo(T o);
}
class Person implements Comparable<Person> {
String name;
int score;
Person(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return this.name + "," + this.score;
}
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
}
比较接口:
Person[] ps = new Person[] {
new Person("Bob", 61),
new Person("Alice", 88),
new Person("Lily", 75),
};
Arrays.sort(ps);
System.out.println(Arrays.toString(ps));
泛型中静态函数返回值要加一个<T>
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
// 可以编译通过:
public static <T> Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
}
Java的泛型是由编译器在编译时实行的,编译器内部永远把所有类型T视为Object处理,但是,在需要转型的时候,编译器会根据T的类型自动为我们实行安全地强制转型。
局限一:<T>不能是基本类型,例如int,因为实际类型是Object,Object类型无法持有基本类型:
局限二:无法取得带泛型的Class
所有泛型实例,无论T的类型是什么,getClass()返回同一个Class实例,因为编译后它们全部都是Pair<Object>
局限三:不能判断带泛型类型的类型,例如: x instanceof Pair<String> ;
局限四:不能实例化 T 类型,例如: new T() 。
泛型方法要防止重复定义方法,例如: public boolean equals(T obj), 要改为same(T obj); ;
子类可以获取父类的泛型类型 <T>
Pair<Integer>不是Pair<Number>的子类
<? extends Number>的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T的上界限定在Number了。
<? extends Number> getFirst();
即返回值是Number或Number的子类,因此,可以安全赋值给Number类型的变量:
Number x = p.getFirst();
int sumOfList(List<? extends Integer> list) {
int sum = 0;
for (int i=0; i<list.size(); i++) {
Integer n = list.get(i);
sum = sum + n;
}
return sum;
}
这是一个对参数List<? extends Integer>进行只读的方法
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
Pair<? super Integer>表示,方法参数接受所有泛型类型为Integer或Integer父类的Pair类型。
1.<? extends T>允许调用读方法T get()获取T的引用,但不允许调用写方法set(T)传入T的引用(传入null除外);
2.<? super T>允许调用写方法set(T)传入T的引用,但不允许调用读方法T get()获取T的引用(获取Object除外)。
一个是允许读不允许写,另一个是允许写不允许读。
PECS原则
何时使用extends,何时使用super?为了便于记忆,我们可以用PECS原则:Producer Extends Consumer Super。
即:如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。
无限定通配符
void sample(Pair<?> p) {
}
因为<?>通配符既没有extends,也没有super,因此:
不允许调用set(T)方法并传入引用(null除外);
不允许调用T get()方法并获取T引用(只能获取Object引用)。
换句话说,既不能读,也不能写,那只能做一些null判断:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
Pair<?>是所有Pair<T>的超类:
Properties
读取配置文件
String f = "setting.properties";
Properties props = new Properties();
props.load(new java.io.FileInputStream(f));
String filepath = props.getProperty("last_open_file");
String interval = props.getProperty("auto_save_interval", "120");
写入配置文件
Properties props = new Properties();
props.setProperty("url", "http://www.liaoxuefeng.com");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
Stream
List<Integer> list = List.of(1, 2, 5);
但是List.of()方法不接受null值,如果传入null,会抛出NullPointerException异常。
转数组:
List<String> list = List.of("apple", "pear", "banana");
Object[] array = list.toArray();
for (Object s : array) {
System.out.println(s);
}
自动转为类型
List<Integer> list = List.of(12, 34, 56);
Integer[] array = list.toArray(new Integer[3]);
List定义方法:
class Person {
public boolean equals(Object o) {
if (o instanceof Person p) {
return this.name.equals(p.name) && this.age == p.age;
}
return false;
}
}
ArrayList<>()
Map<String, Student> map = new HashMap<>();
Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
map.put(DayOfWeek.MONDAY, "星期一");
map.put(DayOfWeek.TUESDAY, "星期二");
Map不但需要正确覆写equals()方法,还要正确覆写hashCode()方法。
InputStream和Reader的区别是一个是字节流,一个是字符流。字符流在内存中已经以char类型表示了,不涉及编码问题。
Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
Set接口并不保证有序,而SortedSet接口则保证元素是有序的:
HashSet是无序的,因为它实现了Set接口,并没有实现SortedSet接口;
TreeSet是有序的,因为它实现了SortedSet接口。
Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序表
放入PriorityQueue的元素,必须实现Comparable接口,PriorityQueue会根据元素的排序顺序决定出队的优先级。
Deque来实现一个双端队列,它的功能是:
既可以添加到队尾,也可以添加到队首;
既可以从队首获取,又可以从队尾获取。
不可变集合
Collections还提供了一组方法把可变集合封装成不可变集合:
封装成不可变List:List<T> unmodifiableList(List<? extends T> list)
封装成不可变Set:Set<T> unmodifiableSet(Set<? extends T> set)
封装成不可变Map:Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)
这种封装实际上是通过创建一个代理对象,拦截掉所有修改方法实现的
线程安全集合
Collections还提供了一组方法,可以把线程不安全的集合变为线程安全的集合:
变为线程安全的List:List<T> synchronizedList(List<T> list)
变为线程安全的Set:Set<T> synchronizedSet(Set<T> s)
变为线程安全的Map:Map<K,V> synchronizedMap(Map<K,V> m)
--------
InputStream / OutputStream
IO流以byte(字节)为最小单位,因此也称为字节流。
Reader / Writer
字符流传输的最小数据单位是char
包java.io提供了同步IO,而java.nio则是异步IO
FileInputStream、FileOutputStream、
FileReader和FileWriter。
序列化
public interface Serializable {
}
反序列化
和ObjectOutputStream相反,ObjectInputStream负责从一个字节流读取Java对象:
测试:
public class CalculatorTest {
Calculator calculator;
@BeforeEach
public void setUp() {
this.calculator = new Calculator();
}
@AfterEach
public void tearDown() {
this.calculator = null;
}
@Test
void testAdd() {
assertEquals(100, this.calculator.add(100));
assertEquals(150, this.calculator.add(50));
assertEquals(130, this.calculator.add(-20));
}
}
JUnit提供assertThrows()来期望捕获一个指定的异常
条件测试:
使用config.getConfigFile()
@Test
void testWindows() {
assertEquals("C:\\test.ini", config.getConfigFile("test.ini"));
}
@Test
void testLinuxAndMac() {
assertEquals("/usr/local/test.cfg", config.getConfigFile("test.cfg"));
}
@Test
@DisabledOnOs(OS.WINDOWS)
void testOnNonWindowsOs() {
// TODO: this test is disabled on windows
}
@Test
@DisabledOnJre(JRE.JAVA_8)
void testOnJava9OrAbove() {
// TODO: this test is disabled on java 8
}
@Test
@EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true")
void testOnlyOnDebugMode() {
// TODO: this test is only run on DEBUG=true
}
参数化测试
@ParameterizedTest
@ValueSource(ints = { 0, 1, 5, 100 })
void testAbs(int x) {
assertEquals(x, Math.abs(x));
}
@ParameterizedTest
@ValueSource(ints = { -1, -5, -100 })
void testAbsNegative(int x) {
assertEquals(-x, Math.abs(x));
}
@MethodSource注解,它允许我们编写一个同名的静态方法来提供测试参数:
@ParameterizedTest
@MethodSource
void testCapitalize(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}
static List<Arguments> testCapitalize() {
return List.of( // arguments:
Arguments.of("abc", "Abc"), //
Arguments.of("APPLE", "Apple"), //
Arguments.of("gooD", "Good"));
}
使用@CsvSource,它的每一个字符串表示一行,一行包含的若干参数用,分隔,因此,上述测试又可以改写如下:
@ParameterizedTest
@CsvSource({ "abc, Abc", "APPLE, Apple", "gooD, Good" })
void testCapitalize(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}
Thread
// 多线程同步
public class Main {
public static void main(String[] args) throws Exception {
var add = new AddThread();
var dec = new DecThread();
add.start();
dec.start();
add.join();
dec.join();
System.out.println(Counter.count);
}
}
class Counter {
public static int count = 0;
}
class AddThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count += 1; }
}
}
class DecThread extends Thread {
public void run() {
for (int i=0; i<10000; i++) { Counter.count -= 1; }
}
}
public class Counter {
public void add(int n) {
synchronized(this) {
count += n;
}
}
...
}
写法二:
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。
JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。
ReentrantLock用于替代synchronized加锁
public class Counter {
private int count;
public void add(int n) {
synchronized(this) {
count += n;
}
}
}
改造:
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
//lock.lock();
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
count += n;
} finally {
lock.unlock();
}
}
}
}
使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
Condition提供的await()、signal()、signalAll()原理和synchronized锁对象的wait()、notify()、notifyAll()是一致的,
并且其行为也是一样的:
await()会释放当前锁,进入等待状态;
signal()会唤醒某个等待线程;
signalAll()会唤醒所有等待线程;
唤醒线程从await()返回后需要重新获得锁。
此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来:
if (condition.await(1, TimeUnit.SECOND)) {
// 被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
可见,使用Condition配合Lock,我们可以实现更灵活的线程同步。
使用ReadWriteLock可以提高读取效率:
- ReadWriteLock只允许一个线程写入;
- ReadWriteLock允许多个线程在没有写入时同时读取;
- ReadWriteLock适合读多写少的场景。
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
// 注意: 一对读锁和写锁必须从同一个rwlock获取:
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
悲观锁则是读的过程中拒绝有写入,也就是写入必须等待
乐观锁的意思就是乐观地估计读的过程中大概率不会有写入
StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;
StampedLock是不可重入锁。
// lock downgrading 降级
public void downgradeWriteLock() {
writeLock.lock();
try {
doWrite();
readLock.lock();
} finally {
writeLock.unlock();
}
try {
doRead();
} finally {
readLock.unlock();
}
}
如果要对某一受限资源进行限流访问,可以使用Semaphore,保证同一时间最多N个线程访问受限资源。
使用Semaphore先调用acquire()获取,然后通过try ... finally保证在finally中释放。
调用acquire()可能会进入等待,直到满足条件为止。也可以使用tryAcquire()指定等待时间:
使用java.util.concurrent包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
interface | non-thread-safe | thread-safe |
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet / TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
java.util.Collections工具类还提供了一个包装类包装旧的线程安全集合转换器,可以这么用:
Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
如果我们自己通过CAS编写incrementAndGet(),它大概长这样:
public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while ( ! var.compareAndSet(prev, next));
return next;
}
我们利用AtomicLong可以编写一个多线程安全的全局唯一ID生成器:
class IdGenerator {
AtomicLong var = new AtomicLong(0);
public long getNextId() {
return var.incrementAndGet();
}
}
使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:
原子操作实现了无锁的线程安全;
适用于计数器,累加器等。
对线程池提交一个Callable任务,可以获得一个Future对象;
可以用Future在将来某个时刻获取结果。
CompletableFuture可以指定异步处理流程:
thenAccept()处理正常结果;
exceptional()处理异常结果;
thenApplyAsync()用于串行化另一个CompletableFuture;
anyOf()和allOf()用于并行化多个CompletableFuture。
Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。
使用Fork/Join模式可以进行并行计算以提高效率。
可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:
ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
使用ThreadLocal要用try ... finally结构,并在finally中清除。
Closeable接口继承了AutoCloseable接口,故原有实现Closeable接口的类,均能在try-with-resources结构中使用。
也可以将自定义的类实现AutoCloseable接口,然后在try-with-resources结构中使用。
byte[] b = new byte[1024];
try (FileInputStream fis = new FileInputStream("my.txt")) {
int data = fis.read();
while (data != -1) {
data = fis.read(b);
}
throw new RuntimeException();
}
System.out.println(new String(b));
Java 19引入的虚拟线程是为了解决IO密集型任务的吞吐量,它可以高效通过少数线程去调度大量虚拟线程;
虚拟线程在执行到IO操作或Blocking操作时,会自动切换到其他虚拟线程执行,从而避免当前线程等待,能最大化线程的执行效率;
虚拟线程使用普通线程相同的接口,最大的好处是无需修改任何代码,就可以将现有的IO操作异步化获得更大的吞吐能力。
计算密集型任务不应使用虚拟线程,只能通过增加CPU核心解决,或者利用分布式计算资源。
Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下:
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
因为ExecutorService只是接口,Java标准库提供的几个常用实现类有:
FixedThreadPool:线程数固定的线程池;
CachedThreadPool:线程数根据任务动态调整的线程池;
SingleThreadExecutor:仅单线程执行的线程池。
Runnable VS Callable
Runnable接口有个问题,它的方法没有返回值。如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不便。所以,Java标准库还提供了一个Callable接口,和Runnable接口比,它多了一个返回值:
class Task implements Callable<String> {
public String call() throws Exception {
return longTimeCalculation();
}
}
进程与线程有什么区别
进程是操作系统分配资源的最小单位,线程是CPU调度的最小单位;
一个进程中可以包含多个线程;
进程与进程之间是相对独立的,进程中的线程之间并不完全独立,可以共享进程中的堆内存、方法区内存、系统资源等;
进程上下文的切换要比线程的上下文切换慢很多;
某个进程发生异常,不会对其它进程造成影响,但,某个线程发生异常,可能会对此进程中的其它线程造成影响;
线程池
避免了频繁创建和销毁线程所带来的性能开销
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
submit()和execute()方法都是用来提交任务到线程池中执行的,但它们之间存在一些区别:
① 参数类型,execute()方法只能接收实现了Runnable接口类型的任务,而submit()方法可以接收Runnable类型的任务和Callable接口的实现类。
② 返回类型,execute()方法没有返回值,submit()方法则返回一个Future对象,通过这个对象可以获取任务的执行结果或者取消任务执行。
③ 异常处理,execute()方法在执行任务出现异常时会直接抛出异常,而submit()方法则会捕获异常并封装到Future对象中,可以通过调用Future对象的get()方法来获取执行过程中的异常。
④ 对线程池的影响,当线程池已满时,execute()方法会直接抛出RejectedExecutionException异常,而submit()方法会将任务放入阻塞队列中,等待有空闲的线程时再执行。
Runtime.getRuntime().availableProcessors()获取的是CPU核心线程数,也就是计算资源。
CPU密集型,线程池大小设置为N,也就是和cpu的线程数相同,可以尽可能地避免线程间上下文切换,但在实际开发中,一般会设置为N+1,为了防止意外情况出现线程阻塞,如果出现阻塞,多出来的线程会继续执行任务,保证CPU的利用效率。
IO密集型,线程池大小设置为2N,这个数是根据业务压测出来的,如果不涉及业务就使用推荐。
只要有一个用户线程在运行,守护线程就会一直运行。只有所有的用户线程都结束的时候,守护线程才会退出。
编写代码时,也可以通过thread.setDaemon(true)指定线程为守护线程。
守护线程和用户线程的创建方式相同,只是在用户线程的基础上将setDaemon设置为true即可。
yield的主要作用是让当前正在执行的线程从运行状态变为就绪状态。
调用 yield 方法并不会释放线程持有的任何监视器锁,这与 wait 方法不同,后者会释放锁并导致线程进入等待状态。
wait() 必须在同步方法或同步代码块中使用,因为它需要释放锁;yield() 则可以在任何地方使用。
java.util.concurrent.locks包提供了更灵活的锁机制,如ReentrantLock。与synchronized相比,Lock提供了更多的功能,如可中断的获取锁、尝试获取锁以及定时获取锁等。
/**
* 线程不安全的单例模式
*/
public class SingleInstance {
private static SingleInstance instance;
public static SingleInstance getInstance(){
if(instance == null){
synchronized (SingleInstance.class){
if(instance == null){
instance = new SingleInstance();
}
}
}
return instance;
}
}
对于上面的代码包含三个步骤:
① 分配内存空间 ② 初始化对象 ③ 将instance引用指向内存空间
正常执行的CPU指令顺序为①②③,CPU对程序进行重排序后的执行顺序是①③②,此时就会出现问题。
private static volatile Singleton instance; // 关键:volatile修饰
volatile的作用?:
禁止指令重排序:确保写操作前的所有操作不会重排到写操作之后
保证内存可见性:修改立即对其他线程可见
当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
volatile 关键字在 Java 中确保了变量的更新操作会立即被其他线程看到,但它并不能保证原子性。
每次线程访问volatile变量时,它都必须从主内存中读取该变量的最新值,而不是使用线程本地缓存中的值。
对于复合操作(如 i++)并不能保证其原子性。在这种情况下,应该使用其他同步机制(如 synchronized 或原子类)来确保操作的原子性。
在多核处理器的系统中,由于缓存一致性问题,即使在一个线程中对 volatile 变量的写入操作可能会被其他线程延迟看到,这是因为不同核心的缓存需要时间来同步。
37、Java中提供了哪些类解决多线程特性问题?
在Java中解决原子性问题的方案包括synchronized、Lock、ReentranLock、ReadWriteLock、CAS操作、Java中提供的原子类等。
解决可见性和有序性问题,可以禁用CPU缓存和编译器优化。
JVM提供了禁用缓存和编译优化的方法,包括volatile关键字、synchronized、final关键字以及Java内存模型中的Happens-Before原则。
@Async的作用就是异步处理任务。
在方法上添加@Async,表示此方法是异步方法;
在类上添加@Async,表示类中的所有方法都是异步方法;
使用此注解的类,必须是Spring管理的类;
需要在启动类或配置类中加入@EnableAsync注解,@Async才会生效;
在使用@Async时,如果不指定线程池的名称,也就是不自定义线程池,@Async是有默认线程池的,使用的是Spring默认的线程池SimpleAsyncTaskExecutor。
为了提高程序的执行性能,编译器和CPU会对程序的指令进行重排序,可以分为编译器重排序和CPU重排序,CPU重排序又可以分为指令级重排序和内存系统重排序。
48、as-if-serial原则是什么?
编译器和CPU对程序代码的重排序必须遵循as-if-serial原则,as-if-serial原则规定编译器和CPU无论对程序代码如何重排序,都必须保证程序在单线程环境下运行的正确性。
ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
Semaphore(信号量)在Java中是一个非常重要的并发工具,它主要用于限制可以同时访问某些资源的线程数量。
Callable接口的call()方法允许有返回值,而Runnable接口的run()方法则不返回任何结果。
Callable的call()方法允许抛出异常,而Runnable的run()方法则不能抛出任何被检查的异常。
Future是一个代表异步计算结果的接口。它提供了检查计算是否完成的方法以及获取计算结果的方法。通过Future,我们可以了解Callable任务是否已经完成,如果完成了,还可以获取它的返回值。
线程安全和并发安全的数据结构
数据结构选择,选择适合并发环境的数据结构,
如ConcurrentHashMap、CopyOnWriteArrayList等,它们内部已经实现了必要的同步机制。
Thread dump可以帮助开发人员诊断和解决多线程应用程序中的问题,例如死锁、资源竞争等。通过分析Thread dump,可以了解每个线程的状态、调用栈、锁信息等,从而找出问题的根源。
调用线程的interrupt()方法可以设置线程的中断状态。
在线程的运行过程中,可以使用Thread.currentThread().isInterrupted()方法检查中断状态,如果为true,则退出循环,从而停止线程。
乐观锁 自旋
编译器优化,将锁消除,前提是Java必须运行在server模式,同时必须开启逃逸分析;
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
锁粗化就是把多次的锁请求合并成一个请求,扩大锁的范围,降低锁请求、同步、释放带来的性能损耗。
什么是内置锁?
偏向锁
可以通过FileChannel类的lock或者tryLock方法进行加锁解锁。
// 以可写的方式打开一个文件 nezha.txt 的通道
FileChannel channel = FileChannel.open(Paths.get("nezha.txt"), StandardOpenOption.WRITE);
// 阻塞直至获取锁
FileLock lock = channel.lock();
// 会立即返回,要么返回锁,要么返回null
FileLock tryLock = channel.tryLock();
为了重量级锁synchronized提高性能,Java虚拟机(JVM)对synchronized的实现进行了优化,引入了锁升级的概念。
synchronized 锁升级的原理是基于锁状态的变化,从无锁状态开始,逐步升级到偏向锁、轻量级锁,最终到重量级锁。
CountDownLatch和CyclicBarrier在Java并发编程中都是常用的同步工具,但它们在使用场景和特性上存在明显的区别。
Optional
定义Optional对象
import java.util.Optional
@Value("${myValue:#{null}}")
private Optional<String> value;
if (value.isPresent()) {
// do something cool
}
-------------------------
@Entity
public class Book {
@Column
private LocalDate publishingDate;
...
public Optional getPublishingDate() {
return Optional.ofNullable(publishingDate);
}
public void setPublishingDate(LocalDate publishingDate) {
this.publishingDate = publishingDate;
}
}
Session session = em.unwrap(Session.class);
Optional<Book> book = session.byId(Book.class).loadOptional(1L);
-------------------------
使用@Entity(name=***)时: Repository中的@Query(***)中,只能写sql语句(当然也不用写nativeQuery=true了!)
使用@Entity+@Table(name=***)时: Repository中的@Query(***)中,可以写hql与sql(而且写sql时候必须加上nativeQuery=true)
nativeQuery = true时 有nativeQuery = true时,是可以执行原生sql语句
nativeQuery = false时
select * from xxx中xxx也不是数据库对应的真正的表名,而是对应的实体名,
并且sql中的字段名也不是数据库中真正的字段名,而是实体的字段名。
Optional.of():传递参数,如果of中的对象是null,就报空指针异常。
Optional.ofNullable():允许ofNullable传递null对象
Optional.empty():返回空的Optional实例
optional.isPresent():判断Optional实例是否为空
optional.orElse():如果optional为空的话返回orElse中的对象
optional.get():获取optional中的T对象
optional.map():如果optional不为null,则执行map方法中的映射函数得到返回值。
optional.map(Function<? super T,? extends U> mapper)
常规写法
User user = .....
if(user != null) {
String name = user.getUsername();
if(name != null) {
return name.toUpperCase();
} else {
return null;
}
} else {
return null;
}
使用optional
Optional<User> user = ...
return user.map(u -> u.getUsername())
.map(name -> name.toUpperCase())
.orElse(null);
常规写法
if (map.get("user")!=null){
Map<String,Object> user = (Map<String, Object>) map.get("user");
if (user.get("info")!=null){
Map<String,Object> info = (Map<String, Object>) user.get("info");
if (info.get("address")!=null){
String address = (String) info.get("address");
System.out.println(address);
}
}
}
使用optional
String address=Optional.ofNullable(map)
.map(m->(Map<String,Object>)m.get("user"))
.map(user->(Map<String,Object>)user.get("info"))
.map(info->(String)info.get("address"))
.orElse(null);
常规写法
if (user!=null){
UserInfo info = user.getInfo();
if (info!=null){
String address = info.getAddress();
}
}
使用optional
String address = Optional.ofNullable(user)
.map(u -> u.getInfo())
.map(info -> info.getAddress())
.orElse(null);
当某个值为空时设置默认值:
User resultUser = Optional.ofNullable(user).orElse(new User());
Maven
Maven定义了几种依赖关系,分别是compile、test、runtime和provided:
scope 说明 示例
compile 编译时需要用到该jar包(默认) commons-logging
test 编译Test时需要用到该jar包 junit
runtime 编译时不需要,但运行时需要用到 mysql
provided 编译时需要用到,但运行时由JDK或某个服务器提供 servlet-api
使用Maven发布一个Artifact时:
可以发布到本地,然后推送到远程Git库,由静态服务器提供基于网页的repo服务,使用方必须声明repo地址;
可以发布到central.sonatype.org,并自动同步到Maven中央仓库,需要前期申请账号以及本地配置;
可以发布到GitHub Packages作为私有仓库使用,必须提供Token以及正确的权限才能发布和使用。
spring
IoC
Spring的IoC容器同时支持属性注入和构造方法注入,并允许混合使用。
无侵入的设计有以下好处:
应用程序组件既可以在Spring的IoC容器中运行,也可以自己编写代码自行组装配置;
测试的时候并不依赖Spring容器,可单独进行测试,大大提高了开发效率。
Spring的IoC容器接口是ApplicationContext,并提供了多种实现类;
通过XML配置文件创建IoC容器时,使用ClassPathXmlApplicationContext;
持有IoC容器后,通过getBean()方法获取Bean的引用。
使用Annotation可以大幅简化配置,每个Bean通过@Component和@Autowired注入;
必须合理设计包的层次结构,才能发挥@ComponentScan的威力。
Spring默认使用Singleton创建Bean,也可指定Scope为Prototype;
可将相同类型的Bean注入List或数组;
可用@Autowired(required=false)允许可选注入;
可用带@Bean标注的方法创建Bean;
可使用@PostConstruct和@PreDestroy对Bean进行初始化和清理;
相同类型的Bean只能有一个指定为@Primary,其他必须用@Qualifier("beanName")指定别名;
注入时,可通过别名@Qualifier("beanName")指定某个Bean;
可以定义FactoryBean来使用工厂模式创建Bean。
Spring提供了Resource类便于注入资源文件。
最常用的注入是通过classpath以classpath:/path/to/file的形式注入。
@Value("classpath:/logo.txt")
private Resource resource;
Spring容器可以通过@PropertySource自动读取配置,并以@Value("${key}")的形式注入;
可以通过${key:defaultValue}指定默认值;
以#{bean.property}形式注入时,Spring容器自动把指定Bean的指定属性值注入。
Spring允许通过@Profile配置不同的Bean;
Spring还提供了@Conditional来进行条件装配,Spring Boot在此基础上进一步提供了基于配置、Class、Bean等条件进行装配。
@Component
@ConditionalOnProperty(name = "app.storage", havingValue = "s3")
public class S3Uploader implements Uploader {
...
}
AOP
在Spring容器中使用AOP非常简单,只需要定义执行方法,并用AspectJ的注解标注应该在何处触发并执行。
Spring通过CGLIB动态创建子类等方式来实现AOP代理模式,大大简化了代码。
使用注解实现AOP需要先定义注解,然后使用@Around("@annotation(name)")实现装配;
使用注解既简单,又能明确标识AOP装配,是使用AOP推荐的方式。
由于Spring通过CGLIB实现代理类,我们要避免直接访问Bean的字段,以及由final方法带来的“未代理”问题。
遇到CglibAopProxy的相关日志,务必要仔细检查,防止因为AOP出现NPE异常。
spring boot
Spring Boot提供了一个Actuator,可以方便地实现监控,并可通过Web访问特定类型的监控。
Spring本身提供了条件装配@Conditional,但是要自己编写比较复杂的Condition来做判断,比较麻烦。Spring Boot则为我们准备好了几个非常有用的条件:
@ConditionalOnProperty:如果有指定的配置,条件生效;
@ConditionalOnBean:如果有指定的Bean,条件生效;
@ConditionalOnMissingBean:如果没有指定的Bean,条件生效;
@ConditionalOnMissingClass:如果没有指定的Class,条件生效;
@ConditionalOnWebApplication:在Web环境中条件生效;
@ConditionalOnExpression:根据表达式判断条件是否生效。