发新话题
打印

[开发] 《Java教程》廖雪峰

集合-编写equals和hashCode

Map通过key查到value,其内部存在一个巨大的数组用于存放所有value。通过key计算出索引取得指定的value。
而计算是通过调用key对象的hashCode()方法,因此正确使用Map必须保证:

  • key对象必须正确覆写equals()方法,相当的两个key实例调用equals()必须返回true
  • key独享必须正确覆写hashCode()方法,且hashCode()方法遵循以下规范
    • 如果两个对象相等,则两个对象的hashCode()必须相等
    • 如果两个对象不相等,则两个对象的hashCode()尽量不要相等


举例来说:

  • a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode()
  • a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不相等

编写equals()方法在60楼已经总结:
规划需要比较的字段,用instanceof判断类型,然后用equals比较引用类型,用==比较基本类型

编写hashCode
在equals中比较的字段,必须在hashCode中计算,根据计算出的索引来获取value
代码:
class Person {
    String firstName;
    String lastName;
    int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public boolean equals(Object o) {
        if (o instanceof Person p) {
            return Objects.equals(this.firstName, p.firstName) && Objects.equals(this.lastName, p.lastName) && this.age == p.age;
        }
        return false;
    }

    public int hashCode() {
        return Objects.hash(firstName, lastName, age);
    }
}
编写原则:

  • equals()用到的每个字段,都必须在hashCode()中用于计算
  • equals()中没有用到的字段,绝不可放在hashCode()中计算


延伸问题
HashMap初始化时,内部默认数组只有16。
此时任何key,无论它的hashCode()返回值有多大,都可以简单的通过以下方法,把索引确定在0到15
代码:
int index = key.hashCode() & 0xf; // 0xf = 15
如果超过16个key-value添加到HashMap,数组会在内部自动扩容,每次扩容一倍。
相应的需要重新确定hashCode()计算的索引位置,例如对长度32位的数组计算hashCode()变成了
代码:
int index = key.hashCode() & 0x1f; // 0x1f = 31
由于每次扩容都会重新计算,导致重新分布已有的key-value,频繁扩容对性能影响很大,如果我们确定了需要使用的HashMap容量,最好创建时指定容量
代码:
Map<String, Integer> map = new HashMap<>(10000);
这里指定了10000,但内部数组时2的n次方,此时内部数组是比10000大的16384(2的14次方)

如果两个key通过hashCode()计算除的索引值刚好相同,它们对应的value并不影响。
因为在HashMap内部的数组中,在这两个相同值的索引存放的元素不是实际的value对象,而是一个List,包含了这两个Entry
代码:
List<Entry<String, Person>>
HashMap找到该索引对应的List<Entry<String, Person>>,还需要遍历这个List,找到此次真正需要的key对应的value,然后返回
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用EnumMap

如果作为key的对象是enum,可以使用EnumMap
EnumMap内部以一个非常紧凑的数组存储value,并且根据enum类型的key直接定位到内部数组的索引,不需要计算hashCode(),因此效率最高,也没有空间浪费
代码:
Map<DayOfWeek, String> map = new EnumMap<>(DayOfWeek.class);
map.put(DayOfWeek.MONDAY, "星期一");
map.put(DayOfWeek.TUESDAY, "星期二");
map.put(DayOfWeek.WEDNESDAY, "星期三");
map.put(DayOfWeek.THURSDAY, "星期四");
map.put(DayOfWeek.FRIDAY, "星期五");
map.put(DayOfWeek.SATURDAY, "星期六");
map.put(DayOfWeek.SUNDAY, "星期日");
System.out.println(map);
System.out.println(map.get(DayOfWeek.MONDAY));
使用EnumMap时,利用向上转型,我们使用Map接口来引用,因此和HashMap替换时客户端看来没有区别
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用TreeMap

HashMap是一种以空间换时间的映射表,内部Key是无序的。
另一种SortedMap内部会对Key进行排序,SortedMap是接口,实现类是TreeMap
代码:
Map<String, Integer> map = new TreeMap<>();
map.put("orange", 1);
map.put("apple", 2);
map.put("pear", 3);
for (String key : map.keySet()) {
    System.out.println(key);
}
// apple, orange, pear
使用TreeMap时,Key对象必须实现Comparable接口,Value对象没有任何要求
如果Key的class没有实现Comparable,俺么必须在创建TreeMap时,同时指定一个自定义排序算法
代码:
Map<Person, Integer> map = new TreeMap<>(new Comparator<Person>() {
    public int compare(Person p1, Person p2) {
        return p1.name.compareTo(p2.name);
    }
});
map.put(new Person("Tom"), 1);
map.put(new Person("Bob"), 2);
map.put(new Person("Lily"), 3);
for (Person key : map.keySet()) {
    System.out.println(key);
}
// {Person: Bob}, {Person: Lily}, {Person: Tom}
System.out.println(map.get(new Person("Bob"))); // 2
Comparator接口要求实现一个比较方法,负责比较传入的两个元素a和b

  • a<b,返回负数(通常为-1)
  • a==b,返回0
  • a>b,返回正数(通常为1)

TreeMap不使用equals()和hashCode(),因此传入的Key类无需覆写这两个方法。

TreeMap在比较两个Key是否相等时,依赖Key的compareTo()方法或者Comparator.compare()方法。不要漏掉在两个Key相等时,必须返回0
代码:
public class Main {
    public static void main(String[] args) {
        Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
            public int compare(Student p1, Student p2) {
                return p1.score > p2.score ? -1 : 1; //漏掉了相等返回0的情况,可以改为Integer.compare(p1.score, p2.score)
            }
        });
        map.put(new Student("Tom", 77), 1);
        map.put(new Student("Bob", 66), 2);
        map.put(new Student("Lily", 99), 3);
        for (Student key : map.keySet()) {
            System.out.println(key);
        }
        System.out.println(map.get(new Student("Bob", 66))); // null?
    }
}

class Student {
    public String name;
    public int score;
    Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public String toString() {
        return String.format("{%s: score=%d}", name, score);
    }
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用Properties

配置文件的key-value一般都是String-String,虽然可以用Map<String,String>来表示,但因为配置文件非常有用,Java集合库提供了Properties来表示一组“配置”(历史遗留原因,Properties内部本质是一个Hashtable)

读取配置文件
Java配置文件以.properties为扩展名,每行以key=value表示,以#开头的是注释
代码:
# setting.properties

last_open_file=/data/hello.txt
auto_save_interval=60
读取配置文件
代码:
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实例
  • 调用load()读取文件
  • 调用getProperty()获取配置

getProperty获取配置,如果key不存在会返回null,也可以设置一个默认值
因为load(InputStream)接收一个InputStream实例,它表示一个字节流,不一定是文件流,因此可以从jar包中或内存中读取资源流
代码:
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/common/setting.properties"));

String settings = "# test" + "\n" + "course=Java" + "\n" + "last_open_date=2019-08-07T12:35:01";
ByteArrayInputStream input = new ByteArrayInputStream(settings.getBytes("UTF-8"));
props.load(input);

props.load(new FileInputStream("C:\\conf\\setting.properties"));
由于Properties由Hashtable派生,因此继承下来的get()和set()方法不推荐使用

写入配置文件
使用setProperty()修改Properties实例,然后使用store()方法写入文件
代码:
Properties props = new Properties();
props.setProperty("url", "http://www.liaoxuefeng.com");
props.setProperty("language", "Java");
props.store(new FileOutputStream("C:\\conf\\setting.properties"), "这是写入的properties注释");
编码
早期版本.Properties文件编码是ASCII编码,涉及中文必须用name=\u4e2d\u6587表示。JDK9开始可以使用UTF-8编码
由于load(InputStream)默认总以ASCII编码读取字节流,因此会导致读到乱码。这里需要用另一个重载方法load(Reader)读取
代码:
Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
String author = props.getProperty("author");

props.setProperty("author", "看天的陌路人");
props.store(new FileWriter("settings.properties", StandardCharsets.UTF_8), "这是写入的properties注释");
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用Set

Set用于存储不重复的元素集合。它包含方法:

  • boolean add(E e) - 添加元素到Set<E>
  • boolean remove(Object e) - 从Set<E>删除元素
  • boolean contains(Object e) - 判断是否包含元素

Set相当于只存储key,不存储value的Map,因此需要实现Map对应的方法

  • HashSet需要实现equals()和hashCode()方法
  • TreeSet需要实现Comparable接口(如果没有实现Comparable接口,那么创建TreeSet需要传入一个Comparator对象)

遍历时,hashSet是无序的,而TreeSet是有序的
代码:
Set<String> set1 = new HashSet<>();
set1.add("apple");
set1.add("banana");
set1.add("pear");
set1.add("orange");
for (String s : set1) {
    System.out.println(s);
}

System.out.println("-------------");

Set<String> set2 = new TreeSet<>();
set2.add("apple");
set2.add("banana");
set2.add("pear");
set2.add("orange");
for (String s : set2) {
    System.out.println(s);
}
使用Set对一个List去重(根据message中的sequence字段)
代码:
List<Message> received = List.of(
        new Message(1, "Hello!"),
        new Message(2, "发工资了吗?"),
        new Message(2, "发工资了吗?"),
        new Message(3, "去哪吃饭?"),
        new Message(3, "去哪吃饭?"),
        new Message(4, "Bye")
);
Set<Integer> r = new HashSet<>();
List<Message> messageList = new ArrayList<>(received); //received是个普通的List没有实现remove方法,需要重新构造ArrayList(包含重写的add和remove方法)
messageList.removeIf(message -> !r.add(message.sequence));
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用Queue

Queue实现了一个先进先出(FIFO)的有序表,和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:

  • 向队列尾部添加元素
  • 从队列头部取出元素

队列接口Queue定义的如下方法:

  • int size() - 获取队列长度
  • boolean add(E) / boolean offer(E) - 添加元素到队列尾部
  • E remove() / E poll() - 获取队首元素并从队列中删除
  • E element() / E peek() - 获取队首元素并不删除

其中poll()和peek()在添加或获取失败时,会返回false或null,而remove()和element()则会在失败时直接抛出异常

不要把null加入队列,否则使用poll()取出时,不知道取出的元素是null还是队列为空。

另外,LinkedList既实现了List接口,也实现了Queue接口,使用的时候如果当作List,就获取List的引用,如果当作Queue,就获取Queue的引用
代码:
// 这是一个List:
List<String> list = new LinkedList<>();
// 这是一个Queue:
Queue<String> queue = new LinkedList<>();
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用PriorityQueue

PriorityQueue与Queue的区别是,调用remove()或poll()方法,返回的总是优先级最高的元素

为了返回优先级最高的元素,需要实现Comparable接口。
如果放入的元素并没有实现Comparable接口,则需要为PriorityQueue传入一个Comparator对象
代码:
Queue<User> q = new PriorityQueue<>(new UserComparator());
// 添加3个元素到队列:
q.offer(new User("Bob", "A1"));
q.offer(new User("Alice", "A2"));
q.offer(new User("Boss", "V1"));
System.out.println(q.poll()); // Boss/V1
System.out.println(q.poll()); // Bob/A1
System.out.println(q.poll()); // Alice/A2
System.out.println(q.poll()); // null,因为队列为空
传入的Comparator对象
代码:
class UserComparator implements Comparator<User> {
    public int compare(User u1, User u2) {
        if (u1.number.charAt(0) == u2.number.charAt(0)) {
            // 如果两人的号都是A开头或者都是V开头,比较号的大小:
            return u1.number.compareTo(u2.number);
        }
        if (u1.number.charAt(0) == 'V') {
            // u1的号码是V开头,优先级高:
            return -1;
        } else {
            return 1;
        }
    }
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用Deque

Deque(双端队列Double Ended Queue),允许两头都进,两头都出。它实现的功能是:

  • 既可以添加到队尾,也可以添加到队首
  • 既可以从队首获取,也可以从队尾获取

它出队和入队的方法:

  • boolean addLast(E) / boolean offerLast(E) - 添加元素到队列尾部(相当于Queue的add()和offer())
  • E removeFirst() / E pollFirst() - 获取队首元素并从队列中删除(相当于Queue的remove()和poll())
  • E getFirst() / E peekFirst() - 获取队首元素并不删除(相当于Queue的element()和peek())
  • boolean addFirst(E) / boolean offerFirst(E) - 添加元素到队列头部
  • E removeLast() / E pollLast() - 获取队尾元素并从队列中删除
  • E getLast() / E peekLast() - 获取队尾元素并不删除

因为Deque扩展自Queue,所以也可以使用Queue的add()/offer()等方法,但最好用自己的方法addLast()/offerLast()

Deque和Queue一样只是一个接口,它的实现类有ArrayDeque和LinkedList

这里LinkedList既实现了List接口,又实现了Queue接口,同时也实现了Deque接口。使用时,尽量持有接口,而不是具体的实现类:
代码:
// 不推荐的写法:
LinkedList<String> d1 = new LinkedList<>();
d1.offerLast("z");
// 推荐的写法:
Deque<String> d2 = new LinkedList<>();
d2.offerLast("z");
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用Stack

栈(Stack)是一种后进先出(LIFO)的数据结构,它只有入栈和出栈的操作:

  • 把元素压栈:push(E)
  • 把栈顶的元素弹出:pop()
  • 取栈顶的元素但不弹出:peek()

Java中用Deque实现Stack的功能(历史遗留原因,没有单独的Stack接口),只调用Deque的push()/pop()/peek()方法

  • boolean push(E) - 把元素压栈(相当于addFirst())
  • E pop() - 把栈顶的元素弹出(相当于removeFirst())
  • E peek() - 取栈顶的元素但不弹出(相当于peekFirst())
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用Iterator

for each循环使用了需要遍历的对象的Iterator
代码:
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
     String s = it.next();
     System.out.println(s);
}
因为需要遍历的对象,本身知道自己应该如何高效的循环,for循环调用需遍历对象的Iterator进行遍历,获得了统一的代码,因此可以使用for each的简单写法,编译器会自动转换为上面调用Iterator的遍历
代码:
for (String s : list) {
    System.out.println(s);
}
对于自己编写的集合类,如果需要实现for each循环,需要实现:

  • 集合类实现Iterable接口,该返回一个Iterator对象
  • 用Iterator对象迭代集合内部数据(hasNext和next方法)

下面是以倒序遍历集合的Iterator
代码:
import java.util.*;

public class Main {
    public static void main(String[] args) {
        ReverseList<String> rlist = new ReverseList<>();
        rlist.add("Apple");
        rlist.add("Orange");
        rlist.add("Pear");
        for (String s : rlist) {
            System.out.println(s);
        }
    }
}

class ReverseList<T> implements Iterable<T> {

    private List<T> list = new ArrayList<>();

    public void add(T t) {
        list.add(t);
    }

    @Override
    public Iterator<T> iterator() {
        return new ReverseIterator(list.size());
    }

    class ReverseIterator implements Iterator<T> {
        int index;

        ReverseIterator(int index) {
            this.index = index;
        }

        @Override
        public boolean hasNext() {
            return index > 0;
        }

        @Override
        public T next() {
            index--;
            return ReverseList.this.list.get(index);
        }
    }
}
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

集合-使用Collections

创建空集合

  • List<T> emptyList() - 创建空List
  • Map<K, V> emptyMap() - 创建空Map
  • Set<T> emptySet() - 创建空Set

也可以用集合自身的of方法创建
代码:
List<String> list1 = List.of();
List<String> list2 = Collections.emptyList();
创建单元素集合

  • List<T> singletonList(T o) - 创建一个元素的List
  • Map<K, V> singletonMap(K key, V value) - 创建一个元素的Map
  • Set<T> singleton(T o) - 创建一个元素的Set

同样可以用集合自身的of方法创建
代码:
List<String> list1 = List.of("apple");
List<String> list2 = Collections.singletonList("apple");
这里返回的是不可变集合,如果需要创建可变集合,需要用ArrayList包装
代码:
List<String> list = new ArrayList<>(List.of("Orange", "Apple", "Pear"));
排序
可以对List进行排序,需要传入可变List
代码:
List<String> list = new ArrayList<>(List.of("Orange", "Apple", "Pear"));
Collections.sort(list);
洗牌
可以对List进行随机化
代码:
List<String> list = new ArrayList<>(List.of("Orange", "Apple", "Pear"));
Collections.shuffle(list);
不可变集合

  • List<T> unmodifiableList(List<? extends T> list) - 封装成不可变List
  • Set<T> unmodifiableSet(Set<? extends T> set) - 封装成不可变Set
  • Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m) - 封装成不可变Map

将一个集合变为不可变集合
代码:
List<String> mutable = new ArrayList<>(List.of("Orange", "Apple", "Pear"));
List<String> immutable = Collections.unmodifiableList(mutable);
immutable.add("Banana"); // UnsupportedOperationException!
mutable.add("Banana"); //会影响immutable
对原先的集合的引用的变更,会影响到变更后的不可变集合,所以一旦封装为不可变集合后,立刻扔掉不可变集合原先的引用
代码:
List<String> immutable = Collections.unmodifiableList(mutable);
mutable = null; // 立刻扔掉mutable的引用:
线程安全集合
引用:
变为线程安全的List:List<T> synchronizedList(List<T> list)
变为线程安全的Set:Set<T> synchronizedSet(Set<T> s)
变为线程安全的Map:Map<K,V> synchronizedMap(Map<K,V> m)
从Java5开始,引用了更高效的并发集合类,因此上面的同步方法基本没用了
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-IoC容器-IoC原理

Spring核心就是提供了一个IoC容器,可以管理所有轻量级的JavaBean组件。

IoC(Inversion of Control)控制反转
主要用于解决,对系统中多个组件共享组件的生命周期及依赖关系的维护,主要解决的核心问题:

  • 谁负责创建组件
  • 谁负责根据以来关系组装组件
  • 销毁时,如何按照以来顺序正确销毁


传统应用程序中,控制权在程序本身,程序流程由开发者控制。IoC模式下,控制权发生了反转(从应用程序转移到了IoC容器):
所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样应用程序只需要直接使用已经创建好并配置好的组件。

为了让组件在IoC容器中被“装配”出来,需要某种“注入”机制
例如BookService自己不创建DataSource,而是等待外部通过setDataSource()方法来注入一个DataSource
代码:
public class BookService {
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
这样不直接new一个DataSource,而是注入一个DataSource,好处是:

  • BookService不在关心如何创建DataSource,因此不必编写读取数据库配置之类的代码
  • DataSource实例被注入到BookService,同样也可以注入到UserService,因此共享组件非常简单
  • 测试BookService更容易,因为注入的是DataSource,可以使用内存数据库,而不是真实的MySQL配置

IoC又称为依赖注入(DI: Dependency Injection),解决的问题是:将组件的创建+配置与组件的使用分离,并且由IoC容器负责管理组件的生命周期。

IoC容器实例化所有组件,就需要通过配置告诉容器如何创建组件,以及各组件的依赖关系。
代码:
<beans>
    <bean id="dataSource" class="HikariDataSource" />
    <bean id="bookService" class="BookService">
        <property name="dataSource" ref="dataSource" />
    </bean>
    <bean id="userService" class="UserService">
        <property name="dataSource" ref="dataSource" />
    </bean>
</beans>
在Spring的IoC容器中,我们把所有组件统称为JavaBean,即配置一个组件就是配置一个Bean

依赖注入方式
依赖注入可以通过set()方法实现,也可以通过构造方法实现。
代码:
public class BookService {
    private DataSource dataSource;

    public BookService(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
无侵入容器
Spring的IoC容器是一个高度可扩展的无侵入容器。
无侵入:指应用程序的组件无需实现Spring的特定端口(组件不知道自己在Spring容器中运行)
好处是:

  • 应用程序组件既可以在Spring的IoC容器中运行,也可以自己编写代码自行组装配置
  • 测试时不依赖Spring容器,可以单独测试
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-IoC容器-装配Bean

编写xml配置文件,用来装配所需的Bean,以及相应依赖的Bean。

例如当我们编写用户注册或登录给用户发送邮件的功能时,我们编写UserService和MailService两个服务实现,
UserService实现登陆和注册功能时,通过setMailService注入MailService,这时通过编写配置文件application.xml来告诉Spring的IoC如何创建并组装Bean:
代码:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.itranswarp.learnjava.service.UserService">
        <property name="mailService" ref="mailService" />
    </bean>

    <bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
</beans>
其中:

  • <bean ...>的id标识,是bean的唯一ID
  • userService的Bean中,通过<property name="..." ref="..." /> 注入了另一个Bean
  • Bean顺序不用在意,Spring会根据依赖正确初始化

上面的配置相当于
代码:
UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);
只是Spring容器是通过读取xml后使用反射完成

如果注入的不是Bean,而是boolean、int、String这样的数据类型,则通过value注入
代码:
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
    <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
    <property name="username" value="root" />
    <property name="password" value="password" />
    <property name="maximumPoolSize" value="10" />
    <property name="autoCommit" value="true" />
</bean>
完成xml配置后,我们需要创建一个Spring的IoC容器实例,加载这个配置文件(让Spring容器为我们创建并装配好配置中的所有Bean)
代码:
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
接着我们就可以从Spring的IoC容器中“取出”装配好的Bean并使用
代码:
// 获取Bean:
UserService userService = context.getBean(UserService.class);
// 正常调用:
User user = userService.login("bob@example.com", "password");
ApplicationContext
Spring容器就是ApplicationContext,它是一个接口,
我们一般用ClassPathXmlApplicationContent实现类自动从classpath中查找指定的XML配置文件

从ApplicationContext中我们可以根据Bean的ID获取Bean,但通常我们根据Bean类型来获取
代码:
UserService userService = context.getBean(UserService.class);
Spring的另一种IoC容器时BeanFactory
代码:
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
MailService mailService = factory.getBean(MailService.class);
BeanFactory的实现是按需创建,而ApplicationContext时一次性创建所有Bean(它还提供了一些额外功能)。
通常我们用ApplicationContext
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-IoC容器-使用Annotation配置

使用Annotation配置,可以代替XML配置IoC容器,Spring会自动扫描Bean并组装它们

添加@Componet注解,定义Bean
代码:
@Component
public class MailService {
    ...
}
添加@Autowired注解,将指定类型的Bean主导到指定字段中
代码:
@Autowired
MailService mailService;
也可以写在构造方法中
代码:
public UserService(@Autowired MailService mailService) {
    this.mailService = mailService;
}
一般我们会写在字段上

添加@Configuration注解和@ComponentScan注解到AppConfig类

  • @Configuration注解定义AppConfig类为配置类
  • @ComponentScan注解让让容器自动搜索当前类所有的包以及子包,把所有标注@Component的Bean自动创建出来,并根据@Autowired进行装配
代码:
@Configuration
@ComponentScan
public class AppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        User user = userService.login("bob@example.com", "password");
        System.out.println(user.getName());
    }
}
这时我们创建ApplicationContext时,使用的实现类时AnnotationConfigApplicationContext,并传入一个标注了@Configuration的类名
代码:
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-IoC容器-定制Bean

Scope
正常对一个Bean添加@Component注解,Spring容器会自动创建一个单例(Singleton)(容器初始化时创建Bean,容器关闭前销毁Bean),运行期间调用getBean(Class)返回的总是同一个实例
额外添加@Scopde注解,这种Bean叫原型(Prototype),每次调用getBean(Class)容器都返回一个新的实例
代码:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
    ...
}
注入List
对于接口相同,不同实现类的Bean,可以以泛型List的方式注入这些Bean
代码:
public interface Validator {
    void validate(String email, String password, String name);
}
代码:
@Component
public class EmailValidator implements Validator {
    public void validate(String email, String password, String name) {
        if (!email.matches("^[a-z0-9]+\\@[a-z0-9]+\\.[a-z]{2,10}$")) {
            throw new IllegalArgumentException("invalid email: " + email);
        }
    }
}

@Component
public class PasswordValidator implements Validator {
    public void validate(String email, String password, String name) {
        if (!password.matches("^.{6,20}$")) {
            throw new IllegalArgumentException("invalid password");
        }
    }
}

@Component
public class NameValidator implements Validator {
    public void validate(String email, String password, String name) {
        if (name == null || name.isBlank() || name.length() > 20) {
            throw new IllegalArgumentException("invalid name: " + name);
        }
    }
}
代码:
@Component
public class Validators {
    @Autowired
    List<Validator> validators;

    public void validate(String email, String password, String name) {
        for (var validator : this.validators) {
            validator.validate(email, password, name);
        }
    }
}
如果要给List中的Bean指定顺序,可以加上@Order注解
代码:
@Component
@Order(1)
public class EmailValidator implements Validator {
    ...
}

@Component
@Order(2)
public class PasswordValidator implements Validator {
    ...
}

@Component
@Order(3)
public class NameValidator implements Validator {
    ...
}
可选注入
标记@Autowired后,如果Spring容器没有找到对应类型的Bean,会报NoSuchBeanDefinitionException。但如果给@Autowired加上required=false参数,则会忽略
代码:
@Component
public class MailService {
    @Autowired(required = false)
    ZoneId zoneId = ZoneId.systemDefault();
    ...
}
创建第三方Bean
如果一个Bean不在自己的package管理内,可以在@Configuration类中编写一个Java方法创建并返回它(给方法标记一个@Bean注解)
代码:
@Configuration
@ComponentScan
public class AppConfig {
    // 创建一个Bean(只调用一次该方法,因此仍然时单例)
    @Bean
    ZoneId createZoneId() {
        return ZoneId.of("Z");
    }
}
初始化和销毁
如果需要对Bean注入依赖后进行初始化(例如监听消息),或者在容器关闭时需要清理(例如关闭连接池),需要

  • 编写初始化方法init()并标记@PostConstruct注解
    代码:
    @PostConstruct
    public void init() {
        System.out.println("Init mail service with zoneId = " + this.zoneId);
    }
  • 编写结束方法shutdown()并标记@PreDestroy注解
    代码:
    @PreDestroy
    public void shutdown() {
        System.out.println("Shutdown mail service");
    }
  • 引入JSR-250定义的Annotation
    代码:
    <dependency>
        <groupId>javax.annotation</groupId>
        <artifactId>javax.annotation-api</artifactId>
        <version>1.3.2</version>
    </dependency>

使用别名
对一种类型的Bean如果需要创建多个实例用于不同服务(比如同时连接多个数据库),需要在@Configuration类中创建多个同类型Bean,然后给每个Bean添加不同的名字
代码:
@Configuration
@ComponentScan
public class AppConfig {
    @Bean("z")
    ZoneId createZoneOfZ() {
        return ZoneId.of("Z");
    }

    @Bean
    @Qualifier("utc8")
    ZoneId createZoneOfUTC8() {
        return ZoneId.of("UTC+08:00");
    }
}
用@Bean("name")和@Bean+@Qualifier("name")都可以指定别名

一旦定义了多个同类型的Bean,注入时需要指定Bean名称
代码:
@Component
public class MailService {
        @Autowired(required = false)
        @Qualifier("z") // 指定注入名称为"z"的ZoneId
        ZoneId zoneId = ZoneId.systemDefault();
    ...
}
还有一种方法时指定某个Bean为@Primay,例如主从数据库的使用
代码:
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @Primary
    DataSource createMasterDataSource() {
        ...
    }

    @Bean
    @Qualifier("slave")
    DataSource createSlaveDataSource() {
        ...
    }
}
使用FactoryBean
Spring可以定义一个工厂,然后由工厂创建Bean。
用工厂模式创建Bean需要实现FactoryBean接口
代码:
@Component
public class ZoneIdFactoryBean implements FactoryBean<ZoneId> {

    String zone = "Z";

    @Override
    public ZoneId getObject() throws Exception {
        return ZoneId.of(zone);
    }

    @Override
    public Class<?> getObjectType() {
        return ZoneId.class;
    }
}
Spring会先实例化工厂,然后通过这个工厂的getObject()方法创建真正的Bean
getObjectType()可以指定创建的Bean的类型,因为指定的类型不一定于实际类型一致,可以是接口或抽象类
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-IoC容器-使用Resource

使用Spring容器时,可以通过使用org.springframework.core.io.Resource把文件作为变量注入,使用@Value注解
代码:
@Value("classpath:/logo.txt")
private Resource resource1;

@Value("file:/path/to/logo.txt")
private Resource resource2;
注入后,直接调用Resource.getInputStream()就可以获取到输入流
代码:
@Component
public class AppService {
    @Value("classpath:/logo.txt")
    private Resource resource;

    private String logo;

    @PostConstruct
    public void init() throws IOException {
        try (var reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
            this.logo = reader.lines().collect(Collectors.joining("\n"));
        }
    }
}
Maven的标准目录结构下,推荐把资源文件放入src/main/resources
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-IoC容器-注入配置

对于配置文件.properties,只需要在Configuration配置类上再添加@PropertySource就可以自动读取配置文件
代码:
@Configuration
@ComponentScan
@PropertySource("app.properties") // 表示读取classpath的app.properties
public class AppConfig {
    @Value("${app.zone:Z}")
    String zoneId;

    @Bean
    ZoneId createZoneId() {
        return ZoneId.of(zoneId);
    }
}
使用@Value可以正常注入
代码:
@Value("${app.zone:Z}")
String zoneId;

  • ${app.zone} - 表示读取key为app.zone的value,如果key不存在,启动将报错;
  • ${app.zone:Z} - 表示读取key为app.zone的value,但如果key不存在,就使用默认值Z。

注解也可以写入参数中
代码:
@Bean
ZoneId createZoneId(@Value("${app.zone:Z}") String zoneId) {
    return ZoneId.of(zoneId);
}
另一种方法是先通过一个Bean持有所有配置,例如smtpConfig:
代码:
@Component
public class SmtpConfig {
    @Value("${smtp.host}")
    private String host;

    @Value("${smtp.port:25}")
    private int port;

    public String getHost() {
        return host;
    }

    public int getPort() {
        return port;
    }
}
然后再其他地方使用#{smtpConfig.host}注入:
代码:
@Component
public class MailService {
    @Value("#{smtpConfig.host}")
    private String smtpHost;

    @Value("#{smtpConfig.port}")
    private int smtpPort;
}
#{smtpConfig.host}的意思是:从名称为smtpConfig的Bean中调用getHost()方法,读取host属性
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-IoC容器-条件装配

Spring提供了Profile,用来区分不同的运行环境(例如native, test, production等)

创建某个Bean时,Spring容器可以根据注解@Profile来决定是否创建
代码:
@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @Profile("!test") //表示非test环境。
    ZoneId createZoneId() {
        return ZoneId.systemDefault();
    }

    @Bean
    @Profile("test")
    ZoneId createZoneIdForTest() {
        return ZoneId.of("America/New_York");
    }
}
如果当前的Profile设置为test,则Spring容器会调用createZoneIdForTest()创建ZoneId,否则,调用createZoneId()创建ZoneId。

运行程序时,加上JVM参数指定以test环境启动
代码:
-Dspring.profiles.active=test
也可以指定多个Profile
代码:
-Dspring.profiles.active=test,master
注解这样写
代码:
@Bean
@Profile({ "test", "master" }) // 同时满足test和master
ZoneId createZoneId() {
    ...
}
使用Conditional
也可以根据@Conditional决定是否创建Bean
代码:
@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
    ...
}
但逻辑需要自己写
代码:
public class OnSmtpEnvCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return "true".equalsIgnoreCase(System.getenv("smtp"));
    }
}
如果不想写复杂的判断逻辑,可以用Spring Boot提供的简单的条件注解

  • 如果配置文件中存在app.smtp=true,则创建MailService
    代码:
    @Component
    @ConditionalOnProperty(name="app.smtp", havingValue="true")
    public class MailService {
        ...
    }
  • 如果当前classpath中存在类javax.mail.Transport,则创建MailService
    代码:
    @Component
    @ConditionalOnClass(name = "javax.mail.Transport")
    public class MailService {
        ...
    }
  • 如果配置文件中的值符合,则创建Bean,例如开发用户头像上传功能:

    • 存储到本地文件(例如本地开发时)
      代码:
      @Component
      @ConditionalOnProperty(name = "app.storage", havingValue = "file", matchIfMissing = true)
      public class FileUploader implements Uploader {
          ...
      }
    • 存储到AWS S3(例如生产环境运行时)
      代码:
      @Component
      @ConditionalOnProperty(name = "app.storage", havingValue = "s3")
      public class S3Uploader implements Uploader {
          ...
      }
    • 存储到其他服务
      代码:
      @Component
      public class UserImageService {
          @Autowired
          Uploader uploader;
      }
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-使用AOP-装配AOP

AOP是Aspect Oriented Programming,面向切面编程。跟面向对象编程不同,AOP把系统做多个不同的关注点(切面)

AOP解决的问题是:如何把切面织入到核心逻辑中。比如调用某个方法时,如何对该方法进行拦截,并在拦截前后进行安全检查,日志,事务等处理。

AOP织入有三种方式:
  • 编译期:在编译时,由编译器把切面调用编译进字节码,这种方式需要定义新的关键字并扩展编译器,AspectJ就扩展了Java编译器,使用关键字aspect来实现织入;
  • 类加载器:在目标类被装载到JVM时,通过一个特殊的类加载器,对目标类的字节码重新“增强”;
  • 运行期:目标对象和切面都是普通Java类,通过JVM的动态【袋里】功能或者第三方库实现运行期动态织入。
Spring的AOP实现时基于JVM的动态【袋里】。由于动态【袋里】要求必须实现接口,如果一个普通类没有业务接口,就需要通过CGLIB或者javassist等第三方库实现。

一些常见的AOP术语:
  • Aspect:切面,即一个横跨多个核心逻辑的功能,或者称之为系统关注点;
  • Joinpoint:连接点,即定义在应用程序流程的何处插入切面的执行;
  • Pointcut:切入点,即一组连接点的集合;
  • Advice:增强,指特定连接点上执行的动作;
  • Introduction:引介,指为一个已有的Java对象动态地增加新的接口;
  • Weaving:织入,指将切面整合到程序的执行流程中;
  • Interceptor:拦截器,是一种实现增强的方式;
  • Target Object:目标对象,即真正执行业务的核心逻辑对象;
  • AOP Proxy:AOP【袋里】,是客户端持有的增强后的对象引用。


例如我们准备给UserService业务方法执行前添加日志,给MailService业务方法后添加日志:

  • 首先通过Maven引入Spring对AOP的支持(会自动引入AspectJ,使用AspectJ实现AOP比较方便)
    代码:
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring.version}</version>
    </dependency>
  • 然后定义LoggingAspect
    代码:
    @Aspect
    @Component
    public class LoggingAspect {
        // 在执行UserService的每个方法前执行:
        @Before("execution(public * com.itranswarp.learnjava.service.UserService.*(..))")
        public void doAccessCheck() {
            System.err.println("[Before] do access check...");
        }

        // 在执行MailService的每个方法前后执行:
        @Around("execution(public * com.itranswarp.learnjava.service.MailService.*(..))")
        public Object doLogging(ProceedingJoinPoint pjp) throws Throwable {
            System.err.println("[Around] start " + pjp.getSignature());
            Object retVal = pjp.proceed();
            System.err.println("[Around] done " + pjp.getSignature());
            return retVal;
        }
    }
    • 观察doAccessCheck()方法:@Before注解后面的字符串时告诉AspectJ应该在何处执行该方法(这里的意思时:执行UserService的每个public方法前执行doAccessCheck()代码)
    • 观察doLogging()方法:@Around注解可以决定是否执行目标方法
    • 观察LoggingAspect类声明:@Aspect注解,表示它的@Before标注的方法需要注入到UserService的每个public方法执行前,@Around标注的方法需要注入到MailService的每个public方法执行前后

  • 接着给@Configuration类加上一个@EnableAspectJAutoProxy注解
    代码:
    @Configuration
    @ComponentScan
    @EnableAspectJAutoProxy
    public class AppConfig {
        ...
    }
    Spring的IoC容器看到@EnableAspectJAutoProxy,就会自动查找带有@Aspect的Bean,然后根据每个方法的@Before、@Around等注解,把AOP注入到特定的Bean中


LoggingAspect的方法注入原理
编写一个子类,并持有原始实例的引用
代码:
public UserServiceAopProxy extends UserService {
    private UserService target;
    private LoggingAspect aspect;

    public UserServiceAopProxy(UserService target, LoggingAspect aspect) {
        this.target = target;
        this.aspect = aspect;
    }

    public User login(String email, String password) {
        // 先执行Aspect的代码:
        aspect.doAccessCheck();
        // 再执行UserService的逻辑:
        return target.login(email, password);
    }

    public User register(String email, String password, String name) {
        aspect.doAccessCheck();
        return target.register(email, password, name);
    }

    ...
}
上面是Spring容器启动时,自动创建的注入Aspect的子类,取代了原始的UserService(原始UserService在userServiceAopProxy变量Target中)。
如果打印从Spring容器中获取的userService实例类型,类似于
代码:
UserService$EnhancerBySpringCGLIB$1f44e01c
实际上是Spring使用CGLIB动态创建的子类。

总结使用AOP的三步:

  • 定义执行方法,并在方法上通过AspectJ的注解,告诉Spring应该在何处调用
  • 标记@Component和@Aspect
  • 在@Configuration类上标注@EnableAspectJAutoProxy


拦截器类型

  • @Before:这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了;
  • @After:这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行;
  • @AfterReturning:和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码;
  • @AfterThrowing:和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
  • @Around:能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

Spring开发-使用AOP-使用注解装配AOP

实际使用时,由于使用AspectJ的注解方法基于语法匹配,可能会造成误伤(即不需要覆盖AOP方法的地方也会被覆盖到),因此推荐使用注解装配AOP,保证被装配的Bean自己清楚知道被装配了什么AOP

举例使用AOP装配,用来监控程序的性能:

  • 定义一个性能监控的注解
    代码:
    @Target(METHOD)
    @Retention(RUNTIME)
    public @interface MetricTime {
        String value();
    }
  • 在需要被监控的关键方法上标注该注解
    代码:
    @Component
    public class UserService {
        // 监控register()方法性能:
        @MetricTime("register")
        public User register(String email, String password, String name) {
            ...
        }
        ...
    }
  • 定义AOP组件MetricAspect
    代码:
    @Aspect
    @Component
    public class MetricAspect {
        @Around("@annotation(metricTime)")
        public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) throws Throwable {
            String name = metricTime.value();
            long start = System.currentTimeMillis();
            try {
                return joinPoint.proceed();
            } finally {
                long t = System.currentTimeMillis() - start;
                // 写入日志或发送至JMX:
                System.err.println("[Metrics] " + name + ": " + t + "ms");
            }
        }
    }
    metric()方法标注了@Around("@annotation(metricTime)"),意思是,符合条件的目标方法是带有@MetricTime注解的方法,因为metric()方法的参数类型是MetricTime(注意参数名是metricTime,不是MetricTime)
流浪了那么多年,终于发现,这里才是我唯一的家。我只想回到这个对自己是那样熟悉和那样亲切的环境里,在和自己极为相似的人群里停留下来,才能够安心地去生活,安心地去爱与被爱。

TOP

发新话题