推薦學習
- 1:肝了十天半月,獻上純手繪「Spring/Cloud/Boot/MVC」全家桶腦圖
- 2:全網首發!馬士兵內部共享—1658頁《Java面試突擊核心講》
1. 為啥要用 Spring
張三是一個編程小白,他每次在 service 層寫代碼都要自己 new 一堆 Dao 接口的實現類。
public class ProjectServiceImpl implements ProjectService {
UserDao userDao = new UserDaoImpl();
ProjectSectionDao projectSessionDao = new ProjectSessionDaoImpl();
ProjectDao projectDao = new ProjectDaoImpl();
SupplyDao supplyDao = new SupplyDaoImpl();
.......
}
有一天正 new 著對象,張三心想:"我這一個 service 都需要 new 好多 Dao ,那如果有一堆 service ,那我不得花費好長時間?"
"有沒有一個工具類或者什麼框架能幫我管理這些對象?我只需要配置一下,需要的時候它就能自動幫我 new 個對象出來?"
張三陷入了深深的沉思之中。
張三的室友李四也是一個編程小白。
李四呢想給自己的小項目增加一個功能:記錄方法執行的時間。結果他腦子一熱竟然給所有的方法都增加了一堆列印方法:
System.out.println("項目開始執行");
// 開始時間
long start = System.currentTimeMillis();
// 業務代碼
// 結束時間
long end = System.currentTimeMillis();
// 計算執行時間
System.out.printf("執行時間:%d 毫秒.", (end - start));
過了半個小時,李四終於給項目中所有的方法都複製粘貼上了列印語句。他長舒一口氣:"我真是個大聰明!"
張三看了一眼李四的代碼,連連鼓掌:"妙啊!咱們宿舍的技術大神!"
旁邊的王五實在忍不住了,對張三說:"妙個屁!最近的 Spring 框架課你倆是不是都沒去?光顧著打遊戲了?我都替你倆答了三次到了!"
李四問王五:"這個Spring 框架學了有用嗎?"
王五:"不僅能解決張三說的管理對象的問題,還能幫你解決記錄日誌的問題。配置完 Spring ,你只需要定義一個切面類,根本不需要在一堆類上面複製粘貼一堆代碼。"
張三摸摸後腦勺笑著說:"原來 Spring 框架那麼好用,我以後再也不逃課了。我這就去翻課本學習 Spring 框架去。"
2. Spring 簡介
Spring 是一個輕量級的 Java 開發框架。Spring 的核心是控制反轉(IOC)和面向切面編程(AOP)。
Spring 主要有如下優點:
1.解耦
2.支持面向切面編程
3.便於集成其他框架
3. 環境搭建
1.創建 Maven 項目
File -> New -> Project -> Maven
2.引入依賴
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
</dependencies>
3.創建接口和實現類
UserService
public interface UserService {
void print();
}
UserServiceImpl
public class UserServiceImpl implements UserService{
@Override
public void print() {
System.out.println("hello world");
}
}
4.創建配置文件
applicationContext.xml
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userService" class="com.xxl.service.impl.UserServiceImpl"/>
</Beans>
5.測試
@Test
public void testSpring(){
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/ApplicationContext.xml");
// 2、通過工廠類獲得對象
UserService userService = (UserService)act.getbean("userService");
// 3.調用方法
userService.print();
}
測試結果:
4. IOC
4.1 IOC 簡介
IOC,全稱 Inversion of Control,意思是控制反轉。它是 Spring 框架中的一種思想。
控制反轉就是將對象的控制權從程序中的代碼轉移到了 Spring 的工廠,通過 Spring 的工廠完成對象的創建以及賦值。
也就是說之前是我們自己 new 對象、給對象中的成員變量賦值。現在是讓 Spring 來幫助我們創建對象、給成員變量賦值。
4.2 Spring 核心內容描述
1.配置文件
Spring 的配置文件可以放到項目中的任意一個地方,也可以隨意命名,但是建議使用:applicationContext.xml。
你可以將這個配置文件看成一個裝有一堆 bean 標籤的容器。
2.bean 標籤
Spring 工廠創建的對象,叫做 bean,所以一個 bean 標籤代表一個對象。
<bean id="userService" class="com.xxl.service.impl.UserServiceImpl"/>
bean 標籤中必須要有 class 屬性,它的值是一個類的全限定名(包名+類名)。
除了 class 屬性,bean 標籤還可以設置 id 、name 、scope屬性。
id:
id 必須以字母開頭,相當於這個 bean 的身份證號,是唯一的。
如果這個 bean 只使用一次,id 可以省略不寫。
如果這個 bean 需要被其他 bean 引用,或者這個 bean 要使用很多次,則必須要有 id 屬性。
如果只配置 class 屬性,Spring 框架會給每一個 bean 配置一個默認的 id:"全限定名#1"。
例如:
com.xxl.service.impl.UserServiceImpl#1
name:
name 相當於這個 bean 的別名,它可以配置多個,例如:
<bean id="user" name="aa,bb,cc" class="com.xxl.model.User"/>
scope:
scope 屬性可以控制簡單對象的創建次數,它有兩個值:
1.singleton:每次只會創建唯一⼀個簡單對象,默認值。
2.prototype:每⼀次都會創建新的對象。
例如:
<bean id="user" class="com.xxl.model.User" scope="singleton"/>
3.ApplicationContext
ApplicationContext 是 Spring 的工廠,主要用來創建對象。
Spring 通過讀取配置文件創建工廠。
因為 Spring 的工廠會占用大量內存,所以一個程序一般只會創建一個工廠對象。
4.工廠常用方法
1.根據 id 獲取對象
UserService userService = (UserService)act.getBean("userService");
2.根據 id 和類名獲取對象
UserService userService = (UserService)act.getBean("userService",UserService.class);
3.只根據類名獲取對象
UserService userService = (UserService)act.getBean(UserService.class);
4.獲取配置文件中所有 bean 標籤的 id 值
String[] beanDefinitionNames = act.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
System.out.println(beanDefinitionName);
}
結果:
5.判斷是否存在指定 id 或者 name 的 bean
act.containsBean("userService")
6.判斷是否存在指定 id 的 bean,只能用來判斷 id
act.containsBeanDefinition("userService")
5.創建對象
Spring 是如何創建對象的呢?
工廠和反射。
首先說下反射,我們可以通過一個類的全限定名獲取 Class 對象,然後再通過 Class 實例化一個對象:
Class serviceClass = Class.forName("com.xxl.service.impl.UserServiceImpl");
UserService userService = (UserService)serviceClass.newInstance();
Spring 配置文件中 bean 標籤的 id 和類的全限定名一一對應,所以 Spring 工廠的 getBean 方法其實就是先根據 bean 的 id 獲取該類的全限定名,然後再利用反射根據類的全限定名創建對象並返回。
4.3 IOC 優點
解耦。
說起解耦之前先說下耦合:耦合是指代碼之間的關聯性太強,我如果改了這一段代碼,可能會影響到一堆代碼。
那創建對象哪裡有耦合了?其實就是new關鍵字帶來的耦合。
如果你發現一個接口的實現類需要修改,你需要手動改動程序中的代碼,比如修改 new 關鍵字後面的實現類,這樣可能會影響到其他的代碼。
但是使用了 Spring 之後,我們只需要修改配置文件中 bean 標籤的 class 屬性對應的類的全限定名,不用修改程序中的代碼,這樣就做到了解耦。
解耦就是解除不同代碼之間的關聯性、依賴性。
5. DI
DI 全稱 Dependency Injection,意思是依賴注入,它是 IOC 的具體實現。
依賴就是說我需要你,比如 Service 層依賴 Dao 層,注入就是賦值。
依賴注入:使用 Spring 的工廠和配置文件為一個類的成員變量賦值。
沒有使用 Spring 的依賴注入我們是這樣賦值的:
User user = new User();
user.setName("張三");
如果設置有誤,就需要手動修改代碼,代碼耦合度較高,而依賴注入的出現就是為了解耦。
Spring 的依賴注入包含兩種方式:
5.1 set 注入
set 注入:Spring 調用 Set 方法通過配置文件為成員變量賦值。
1.創建對象,為屬性添加 set/get 方法
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
2.修改配置文件
<bean id="user" class="com.xxl.model.User">
<property name="name" value="知否君" />
<property name="age" value="18" />
</bean>
3.測試
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
User user = (User)act.getBean("user");
System.out.println("姓名:"+user.getName());
System.out.println("性別:"+user.getAge());
測試結果:
從上面的例子可以看出 Set 注入就是在 property 標籤中為屬性賦值。spring 可以為 JDK 內置的數據類型進行賦值,也可以為用戶自定義的數據類型進行賦值。
5.1.1 JDK 內置數據類型
1.基本類型
<property name="name" value="知否君" />
<property name="age" value="18" />
2.List 集合
<property name="phones">
<list>
<value>15799999918</value>
<value>15788888819</value>
<value>15766666620</value>
</list>
</property>
3.Set 集合
<property name="phones">
<set>
<value>15799999918</value>
<value>15788888819</value>
<value>15766666620</value>
</set>
</property>
4.Map 集合
<property name="mapInfo">
<map>
<entry>
<key><value>name</value></key>
<value>知否君</value>
</entry>
<entry>
<key><value>age</value></key>
<value>23</value>
</entry>
</map>
</property>
5.數組
<property name="phones">
<list>
<value>15799999918</value>
<value>15788888819</value>
<value>15766666620</value>
</list>
</property>
6.Properites
<property name="prop">
<props>
<prop key="key1">value1</prop>
<prop key="key2">value2</prop>
</props>
</property>
5.1.2 用戶自定義數據類型
1.為成員變量添加 set/get 方法
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserDao getUserDao() {
return userDao;
}
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void print() {
userDao.print();
}
}
2.bean 標籤使用 ref 屬性
<bean id="userDao" class="com.xxl.dao.impl.UserDaoImpl" />
<bean id="userService" class="com.xxl.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
3.測試
@Test
public void testSpring(){
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
UserService userService = (UserService)act.getBean("userService");
// 3.調用方法
userService.print();
}
測試結果:
解釋:
上面的例子中,因為 userDao 是 userService 的一個成員變量,所以在配置文件中需要使用 property 標籤,ref 指向了 userDao 這個對象,然後調用 userDao 的 set 方法為 userDao 賦值。
4.自動注入
我們還可以使用 bean 標籤的 autowire 屬性為自定義變量自動賦值。當類中引用類型的屬性名和 bean 標籤的 id 值相同時,我們可以使用 byName。例如:
<bean id="userDao" class="com.xxl.dao.impl.UserDaoImpl" />
<bean id="userService" autowire="byName" class="com.xxl.service.impl.UserServiceImpl" />
當類中引用類型的全限定名和 bean 標籤的 class 屬性的值相同,或者是子類、實現類,我們可以使用 byType。例如:
<bean id="userDao" class="com.xxl.dao.impl.UserDaoImpl" />
<bean id="userService" autowire="byType" class="com.xxl.service.impl.UserServiceImpl" />
5.2 構造注入
構造注入:Spring 調用構造方法通過配置文件為成員變量賦值。
1.為類添加構造方法
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
2.修改配置文件
在 bean 標籤中使用 constructor-arg 標籤。
<bean class="com.xxl.model.User">
<constructor-arg value="張三"/>
<constructor-arg value="18"/>
</bean>
3.測試
@Test
public void testSpring(){
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
User user= (User)act.getBean(User.class);
System.out.println("姓名:"+user.getName());
System.out.println("年齡:"+user.getAge());
}
測試結果:
5.3 注入總結
注入就是通過 Spring 的配置文件為類的成員變量賦值。在實際開發中,我們一般採用 Set 方式為成員變量賦值。
6. Bean 的生命周期
Bean 生命周期指的就是由 Spring 管理的對象從創建到銷毀的過程,和人生老病死的過程一樣。
它主要分為三個階段: 創建 --> 初始化 --> 銷毀
6.1 創建階段
Spring 工廠創建對象的方式分兩類:
1. singleton 模式
當 scope 屬性為 singleton ,創建 Spring 工廠的同時創建所有單例對象。
例如:
新建 User 類:
public class User {
String name;
int age;
public User() {
System.out.println("調用User的構造方法");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
spring 配置文件註冊 bean :
<bean id="user" class="com.xxl.model.User">
<property name="name" value="知否君"/>
<property name="age" value="23"/>
</bean>
測試:
@Test
public void testSpring(){
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
}
執行結果:
我們發現當創建 Spring 工廠的同時就會調用對象的構造方法。因為 spring 中 bean 默認的 scope 就是 singleton ,所以創建工廠的同時默認就會創建多個單例對象。
如果想修改創建單例對象的方式為獲取的時候才創建,只需要在 bean 標籤上面添加如下屬性:
lazy-init="true"
例如:
<bean id="user" class="com.xxl.model.User" lazy-init="true">
<property name="name" value="知否君"/>
<property name="age" value="23"/>
</bean>
2. prototype 模式
只有獲取對象的時候才會創建對象。
修改 bean 標籤的 scope 屬性:
<bean id="user" class="com.xxl.model.User" scope="prototype">
<property name="name" value="知否君"/>
<property name="age" value="23"/>
</bean>
測試:
@Test
public void testSpring(){
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
Object user = act.getBean("user");
System.out.println(user);
}
執行結果:
通過上面的例子我們發現只有當執行 getBean() 方法的時候才會調用該類的構造方法。
6.2 初始化階段
spring 中 bean 的初始化操作指的是在創建對象的時候完成一些附加的功能。bean 的初始化操作有兩種實現方式:
1.實現 InitializingBean 接口
public class 類名 implements InitializingBean {
public void afterPropertiesSet(){
// 初始化方法操作
}
}
例如:
public class User implements InitializingBean {
String name;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
// 初始化操作
@Override
public void afterPropertiesSet(){
this.name = "張無忌";
this.age = 30;
}
}
測試:
@Test
public void testSpring(){
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
Object user = act.getBean("user");
System.out.println(user);
}
執行結果:
2.通過創建普通方法完成初始化
在 User 類中創建一個方法
// 初始化方法
public void initMethod() {
this.name = "張無忌";
}
在配置文件中配置 init-method 屬性
<bean id="user" class="com.xxl.model.User" init-method="initMethod" >
<property name="name" value="知否君"/>
<property name="age" value="23"/>
</bean>
測試:
@Test
public void testSpring(){
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
Object user = act.getBean("user");
System.out.println(user);
}
執行結果:
我們發現該初始化方法在創建對象之後修改了 user 對象的名字。
總結:
初始化方法修改了注入的值,所以初始化方法一定在注入之後執行。
6.3 銷毀階段
Spring 銷毀對象前,會調用對象的銷毀方法,完成銷毀操作。
Spring 什麼時候銷毀所創建的對象?當 Spring 工廠關閉時,Spring 工廠會調用我們自定義的銷毀方法。
銷毀方法的定義有兩種方式:
1.實現DisposableBean接口
public class 類名 implements DisposableBean {
// 銷毀操作
@Override
public void destroy(){
// 銷毀操作業務
}
}
2.創建普通方法
在 User 類中創建一個方法
// 銷毀方法
public void destroyMethod() {
// 銷毀操作業務
}
在配置文件中配置 destroy-method 屬性
<bean id="user" class="com.xxl.model.User" destroy-method="destroyMethod">
<property name="name" value="知否君"/>
<property name="age" value="23"/>
</bean>
7. Bean 的後置處理
Spring 工廠創建完對象後如果還想對對象干點別的事情,除了初始化階段,還可以採用Bean的後置處理。
Bean 的後置處理:對 Spring 工廠創建的對象進行二次加工處理,就是創建完對象後再干點別的事。
Bean 後置處理的流程:
1.實現 BeanPostProcessor 接口
public class BeanProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("後置bean: before 方法");
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("後置bean: after 方法");
if (bean instanceof User) {
User user = (User) bean;
user.setName("亞里士多德");
return user;
}
return bean;
}
}
2.配置文件添加 bean
<bean id="beanProcessor" class="com.xxl.config.BeanProcessor"/>
3.測試
@Test
public void testSpring(){
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
Object user = act.getBean("user");
System.out.println(user);
}
執行結果:
前面我們學習了對象的初始化方法,那麼初始化方法和 Bean 的後置處理的執行順序是什麼?
我們來修改一下 User 類,測試一下:
public class User implements InitializingBean {
String name;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public void afterPropertiesSet(){
System.out.println("初始化方法");
}
}
測試執行順序:
其實在實際開發中,我們很少對 Spring 工廠創建的對象進行初始化操作,一般是採用 Bean 的後置處理的方式來加工對象。
BeanPostProcessor 接口有兩個方法,這裡簡稱 before 和 after 方法。
這兩個方法都是先獲取 Spring 創建的對象,然後再對其加工,加工完成後再交給 Spring。
因為這兩個方法的作用一樣,所以我們一般採用其中的一個方法,這裡建議採用 after 方法。
從上面的例子中我們得到了Spring 操作 bean 的順序:
8. 代理設計模式
8.1 為啥要用代理設計模式?
咱們先來看一個需求:在所有方法的執行前後輸出一段日誌。
程序小白可能會這樣寫:
接口:
public interface CalculateService {
// 加法
int add(int a,int b);
// 減法
int sub(int a,int b);
}
實現類:
public class CalculateServiceImpl implements CalculateService {
@Override
public int add(int a, int b) {
System.out.println("方法執行前列印");
int result = a + b;
System.out.println("方法執行後列印");
return result;
}
@Override
public int sub(int a, int b) {
System.out.println("方法執行前列印");
int result = a - b;
System.out.println("方法執行後列印");
return result;
}
}
但是這樣寫有 3 個問題:
1.代碼混亂:業務代碼和非業務代碼混在一起,看著太亂了
2.代碼重複:如果有多個方法,那就要寫一堆輸出日誌的代碼片段,吃力不討好。
3.代碼耦合:如果非業務代碼(日誌列印)要做修改,那所有相關的業務方法都要改一遍,代碼耦合度太高。
那有什麼解決辦法呢?使用代理。
生活中有關代理的例子無處不在,例如:一些大學可以面向全球招生,所以會衍生很多留學中介,這些中介可以幫學校招生。
所以中介的作用就是幫助僱主做事,有了中介,僱主變得很輕鬆。而在 java 開發中,也存在這樣的代理關係,它的專業術語是代理設計模式。
代理設計模式可以很好解決上面開發中遇到的三個問題,幫助我們簡化代碼、提高工作效率。
8.2 代理設計模式
代理設計模式:通過代理類為目標類做一些額外(非業務)的功能。
專業名詞解釋:
1.目標類(原始類):指的是完成業務的核心類,一般指的是 service 層的各種實現類。
2.目標方法(原始方法):目標類中的方法是目標方法(原始方法)。
3.額外功能(附加功能):列印日誌等非業務功能。
代理設計模式開發步驟:
(1)代理類和目標類實現相同的接口
(2)代理類中除了要調用目標類的方法實現業務功能,還要實現額外功能。
例如:
// 接口
public interface CalculateService {
業務方法
}
// 目標類
public CalculateServiceImpl implements CalculateService {
業務方法
}
// 代理類:要實現目標類相同的接口
public CalculateServiceProxy implements CalculateService {
// 業務方法
// 額外功能
}
8.3 靜態代理
靜態代理:給每一個目標類手動開發一個代理類。
例如:
public interface CalculateService {
// 加法
int add(int a,int b);
// 減法
int sub(int a,int b);
}
// 目標類
public CalculateServiceImpl implements CalculateService {
@Override
public int add(int a, int b) {
int result = a + b;
return result;
}
@Override
public int sub(int a, int b) {
int result = a - b;
return result;
}
}
// 代理類:要實現目標類相同的接口
public CalculateServiceProxy implements CalculateService {
private CalculateService calculateService = new CalculateServiceImpl();
@Override
public int add(int a, int b) {
System.out.println("方法執行前列印");
int result = calculateService.add(a,b);
System.out.println("方法執行後列印");
return result;
}
@Override
public int sub(int a, int b) {
System.out.println("方法執行前列印");
int result = calculateService.sub(a,b);
System.out.println("方法執行後列印");
return result;
}
}
通過上面的例子我們發現靜態代理也存在很多問題:
1.如果存在很多目標類,我們就要手動創建一堆代理類,太繁瑣。
2.代理類中混雜著目標類方法和額外功能,代碼耦合度高。
那有沒有這樣一種代理模式?
- 1.目標類和代理類互不干擾
- 2.代碼耦合度低,便於維護
有的,動態代理閃亮登場!
8.4 動態代理
動態代理:也是通過代理類為目標類做一些額外的功能,但是不用手動寫一堆代理類,而是動態地為目標類創建代理類。
開發流程:
- 引入依賴
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.16.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
這裡我們主要是引入了 aspectj 這個技術,aspectj 是 spring 社區中非常流行的基於動態代理技術的框架。
- 創建目標類和目標方法
接口:
public interface CalculateService {
// 加法
int add(int a,int b);
// 減法
int sub(int a,int b);
}
實現類(目標類):
public class CalculateServiceImpl implements CalculateService {
@Override
public int add(int a, int b) {
int result = a + b;
System.out.println("加法操作。。。");
return result;
}
@Override
public int sub(int a, int b) {
int result = a - b;
System.out.println("減法操作。。。");
return result;
}
}
3.在 spring 配置文件中註冊 bean
<bean id="calculateService" class="com.xxl.service.impl.CalculateServiceImpl" />
4.實現額外功能
這裡我們需要創建一個類實現 MethodInterceptor 接口:
/**
* @Desc: 動態代理完成非業務功能
* @Author: 知否技術
* @date: 下午8:49 2022/5/4
*/
public class PrintLog implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("在目標方法執行之前列印。。。。。");
// 執行目標方法
Object object = methodInvocation.proceed();
System.out.println("在目標方法執行之後列印。。。。。");
return object;
}
}
5.註冊完成額外功能的 bean
<bean id="printLog" class="com.xxl.aop.PrintLog" />
- 定義切入點
<!--切入點:給哪些方法加入額外功能-->
<aop:config>
<aop:pointcut id="pc" expression="execution(* * (..))"/>
</aop:config>
- 組裝切入點和額外功能
<!--切入點:給哪些方法加入額外功能-->
<aop:config>
<aop:pointcut id="pc" expression="execution(* * (..))"/>
<aop:advisor advice-ref="printLog" pointcut-ref="pc"/>
</aop:config>
8.測試
@Test
public void testSpring() {
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
CalculateService calculateService = (CalculateService) act.getBean("calculateService");
// 3.調用方法
int result = calculateService.add(1, 2);
System.out.println("result:" + result);
}
講解:
1.上面的例子中我們定義了一個 PrintLog 列印日誌的類,並實現了 MethodInterceptor 接口的 invoke 方法。invoke 方法裡面實現了在目標方法執行前後列印日誌的功能。
2.invoke 方法的返回值就是原始方法的返回值,上個例子中的原始方法就是 add 方法。
3.aop:config 這個標籤用來配置切入點和額外功能。 上面例子中額外功能就是在要執行的方法前後列印日誌,而切入點就是額外功能要作用的位置:比如某些類上或者某些方法上。
4.execution(* * (..)) 是切入點表達式,表示作用在所有類的所有方法上,這個後面會講。
5.上面的例子表示:你無論執行哪個方法,這個方法的前面和後面都會列印一段日誌。
8.5 動態代理實現原理
我們通過 spring 的工廠獲取的對象,其實是通過動態代理技術創建的代理類。那這個代理類在哪裡?
當程序運行的時候,spring 框架通過動態字節碼技術在 JVM 內存中為目標類創建代理類。當程序運行結束的時候,這個代理類就會隨之消亡。
所以使用動態代理不需要手動創建多個代理類。
9. AOP
9.1 AOP 概念
AOP: 全稱 Producer Oriented Programing,即面向切面編程。
那啥是面向切面編程?其實說白了還是 Spring 的動態代理,通過代理類為原始類增加一些 額外功能(例如列印等)。
那啥是切面?
切面 = 切入點 + 額外功能。
切入點:額外功能作用的位置,在哪些類哪些方法上。
額外功能作用在不同的類上面,我們都知道點連接起來構成面,所以不同的切入點連接起來構成了切面,這個切面就像刀切西瓜一樣切在不同的類上面,所以額外功能就對這些類中的方法起了作用。
9.2 AOP 底層實現原理
AOP 的底層還是使用 Spring 的動態代理技術創建代理類對象。
動態代理的方式分為兩種:
- 基於接口實現動態代理: JDK 動態代理
- 基於繼承實現動態代理:Cglib 動態代理
9.2.1 JDK 動態代理
創建代理對象的三個元素:
- 1.原始對象
- 2.額外功能
- 3.原始對象實現的接口
代碼格式:
Proxy.newPorxyInstance(classloader,interfaces,invocationHandler)
講解:
(1)classloader:叫做類加載器,它可以用來創建代理對象。
創建方式:
類.class.getClassLOader()
(2)interfaces:原始對象實現的接口
創建方式
接口.getClass().getInterfaces()
(3)invocationHandler:額外功能
創建方式:
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("---- 方法執行前列印 ----");
// 執行原始方法
Object ret = method.invoke(caculateService, args);
System.out.println("---- 方法執行後列印 ----");
return ret;
}
};
完整代碼:
@Test
public void testJDKProxy() {
// 1. 原始對象
CalculateService calculateService = new CalculateServiceImpl();
// 2. JDK 動態代理:包含額外功能
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("---- 方法執行前列印 ----");
// 執行原始方法
Object result = method.invoke(calculateService, args);
System.out.println("---- 方法執行後列印 ----");
return result;
}
};
// 3. 代理類
CalculateService calService = (CalculateService) Proxy.
newProxyInstance(CalculateService.class.getClassLoader(),
calculateService.getClass().getInterfaces(),
handler);
// 4. 執行方法
int result = calService.add(1, 2);
System.out.println("result:" + result);
}
測試結果:
9.2.2 Cglib 動態代理
CGlib 創建動態代理的原理:原始類作為父類,代理類作為子類,通過繼承關係創建代理類。
代碼格式:
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(classLoader);
enhancer.setSuperclass(calculateService);
enhancer.setCallback(interceptor);
講解:
(1)classLoader:類加載器(了解即可)
(2)Superclass:父類,就是原始類
(3)interceptor:額外功能
MethodInterceptor interceptor = new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("---- 方法執行前列印 ----");
// 執行原始方法
Object result = method.invoke(calculateService, args);
System.out.println("---- 方法執行後列印 ----");
return result;
}
};
完整代碼:
@Test
public void testCglibProxy() {
// 1. 原始對象
CalculateService calculateService = new CalculateServiceImpl();
// 2. Cglib 動態代理:包含額外功能
MethodInterceptor interceptor = new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("---- 方法執行前列印 ----");
// 執行原始方法
Object result = method.invoke(calculateService, args);
System.out.println("---- 方法執行後列印 ----");
return result;
}
};
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(CalculateService.class.getClassLoader());
enhancer.setSuperclass(calculateService.getClass());
enhancer.setCallback(interceptor);
// 3. 創建代理類
CalculateService calService = (CalculateService)enhancer.create();
// 4. 執行方法
int result = calService.add(3, 4);
System.out.println("result:" + result);
}
執行結果:
9.2.3 Spring 如何創建代理對象?
Spring 是如何為原始對象創建目標對象的呢?是通過 BeanPostProcessor。
前面我們講過 BeanPostProcessor 可以對對象進行二次加工,所以可以用來創建代理對象。
Spring 創建代理對象的流程:
- 實現 BeanPostProcessor 接口
/**
* @Desc: 後置bean創建代理對象
* @Author: 知否技術
* @date: 上午11:59 2022/5/5
*/
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
InvocationHandler handler = (proxy, method, args) -> {
System.out.println("--- 方法執行前列印6666666---");
Object ret = method.invoke(bean, args);
System.out.println("--- 方法執行後列印7777777---");
return ret;
};
return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), handler);
}
}
- 註冊 bean
<bean id="calculateService" class="com.xxl.service.impl.CalculateServiceImpl" />
<bean id="proxyBeanPostProcessor" class="com.xxl.aop.ProxyBeanPostProcessor"/>
- 測試
@Test
public void testSpring() {
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
CalculateService calculateService = (CalculateService) act.getBean("calculateService");
// 3.調用方法
int result = calculateService.sub(7, 2);
System.out.println("result:" + result);
}
9.3 基於註解開發 AOP
開發流程:
- 開發切面類
@Aspect
public class TestAspect {
// 前置通知:方法執行前添加額外功能
@Before("execution(* *(..))")
public void beforePrint(){
System.out.println("------before: 方法執行前列印~");
}
//後置通知: 方法執行後添加額外功能
@After("execution(* *(..))")
public void afterPrint(){
System.out.println("------after: 方法執行前列印~");
}
// 環繞通知:方法執行前後添加額外功能
@Around("execution(* *(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法執行前列印~");
Object result = joinPoint.proceed();
System.out.println("方法執行後列印~");
return result;
}
}
- 配置切面類和掃描註解
<bean id="testMyAspect" class="com.xxl.aop.TestAspect" />
<!-- 掃描 aop 相關註解-->
<aop:aspectj-autoproxy/>
- 測試
@Test
public void testSpring() {
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
calculateService calculateService = (CalculateService) act.getBean("calculateService");
// 3.調用方法
int result = calculateService.add(100, 1);
System.out.println("result:" + result);
}
講解:
1.我們新建了一個 TestMyAspect 類,然後添加 @Aspect 註解,表示這是一個切面類,專門用來完成非業務功能的。
2.在這個類中,我們創建了三個方法,其中 @Before 註解標註的方法表示在目標方法操作前執行。@After 註解標註的方法表示在目標方法操作後執行。@Around 註解標註的方法表示在目標方法操作前後執行。
3.在實際開發中一般使用 @Around 註解標註的方法完成非業務功能。
4.我們新建了這個切面類,但是 spring 不知道啊,所以需要在 Spring 的配置文件中註冊一下 bean。
5.現在 Spring 工廠能夠管理這個類了,但是 Spring 不知道他是切面類啊!所以需要配置一下掃描註解的標籤。
6.然後通過 Spring 獲取創建的類,我們獲取的其實是 Spring 通過後置 Bean 加工後的代理類。
切入點復用
我們可以在切面類中定義⼀個方法,方法上面標註 @Pointcut 註解。 然後就可以重複使用切入點表達式了:
@Aspect
public class TestAspect {
@Pointcut("execution(* *(..))")
public void myPointcut() {}
// 環繞通知:方法執行前後添加額外功能
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法執行前列印~");
Object result = joinPoint.proceed();
System.out.println("方法執行後列印~");
return result;
}
}
9.4 切入點表達式
切入點:額外功能加入的位置。
<aop:pointcut id="pc" expression="execution(* * (..))"/>
複製代碼
- execution():切入點函數
- (* * (..)):切入點表達式
public int add(int a, int b)
* * (..)
第一個 * 表示方法的修飾符和返回值 第二個 * 是方法名 .. 表示方法中的參數
1.(包.類.方法)切入點:
修飾符-返回值 包.類.方法(參數)
expression="execution(* com.xxl.service.caculateServiceImpl.add(..))"
2.指定切入點為某個包下的所有類中的所有方法:
修飾符-返回值 包.類.方法(參數)
expression="execution(* com.xxl.service.*.*(..))"
3.@annotation
作用:用於匹配當前執行方法持有指定註解的方法,並為之加入額外的功能。
例如我們自定義了一個註解:NoToken
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoToken {
}
方法中添加自定義註解:
@Override
@NoToken
public int add(int a, int b) {
int result = a + b;
System.out.println("加法操作。。。");
return result;
}
然後我們要為包含 NoToken 註解的方法加入額外功能:
@Aspect
public class TestAspect {
// 環繞通知:方法執行前後添加額外功能
@Around("@annotation(com.xxl.annotion.NoToken)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("包含 NoToken 註解--------------");
Object result = joinPoint.proceed();
return result;
}
}
測試:
@Test
public void testSpring() {
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
CalculateService calculateService = (CalculateService) act.getBean("calculateService");
// 3.調用方法
int result1 = calculateService.add(99, 1);
System.out.println("-----------------------");
int result2 = calculateService.sub(99, 1);
System.out.println("result1:" + result1);
System.out.println("result2:" + result2);
}
10. Spring 相關註解
在講註解之前我們先看一下啥是註解。
代碼格式:@英文單詞,例如:
作用位置:常用在類上或者方法上
用處:簡化代碼、完成某些功能
所以 Spring 引入註解也是為了簡化我們的代碼,通過使用簡單的註解完成一些功能。
10.1 創建對象相關註解
我們前面在學 IOC 的時候知道如果想讓 Spring 創建對象,必須要在配置文件中寫 bean 標籤。
<bean id="calculateService" class="com.xxl.service.impl.CalculateServiceImpl" />
<bean id="proxyBeanPostProcessor" class="com.xxl.aop.ProxyBeanPostProcessor"/>
<bean id="testMyAspect" class="com.xxl.aop.TestAspect" />
......
可是如果想讓 Spring 管理一堆對象,我們就要寫一堆 bean 標籤。所以 Spring 為了簡化代碼,提供了一些與創建對象相關的註解。
10.1.1 @Component
作用:替換 bean 標籤,用來創建對象。就是在類上面加了這個註解,就不用在配置文件上寫 bean 標籤了。
位置:類上面
id 屬性:默認首單詞首字母小寫。
// id 屬性:默認首單詞首字母小寫。
@Component("user")
public class User{
}
10.1.2 @Component 衍生註解
我們在開發程序的時候一般會將程序分層,例如分為控制層(controller),業務層(service),持久層(dao)。
但是 @Component 註解並不能區分這些類屬於那些層,所以 Spring 提供了以下衍生註解:
1.@Controller:表示創建控制器對象
@Controller
public class UserController {
}
2.@Service:表示創建業務層對象
@Service
public class UserServiceImpl implements UserService {
}
3.@Repository:表示創建持久層對象
@Repository
public class UserDaoImpl implements UserDao {
}
這三個註解的作用和 @Component 的作用一樣,都是用來創建對象。
10.1.3 @Scope
我們知道 Spring 工廠創建的對象默認都是單例的,也就是 bean 標籤中 scope 屬性默認是 singleton。
@Scope 註解可以用來修改創建對象的 scope 屬性。
默認:也就是說你不寫 @Scope 註解,默認就是 singleton,所以可以省略。
@Component
// 可以省略不寫
@Scope("singleton")
public class User {
}
修改多例:
@Component
@Scope("prototype")
public class User {
}
10.1.4 生命周期相關註解
1.@PostConstruct
初始化方法註解,作用在方法上。用來替換 bean 標籤的 init-method 屬性。
例如:
@Component
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
// 初始化方法
@PostConstruct
public void init(){
this.name = "王小波";
}
}
測試:
@Test
public void testSpring() {
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
User user = (User) act.getBean("user");
System.out.println(user);
}
報錯了!這是為什麼?
我們先看報錯內容:
No bean named 'user'
也就是說找不到這個對象。我們雖然加了 @Component 註解,但是 Spring 不知道啊,所以需要在 Spring 的配置文件中配置註解掃描:
<context:component-scan base-package="com.xxl"/>
base-package: 添加註解的類所在的包位置。
配置了註解掃描,當程序啟動的時候 Spring 會先掃描一下相關的註解,這些註解才會生效。
就比如你去快遞驛站拿快遞,你看到了自己的快遞然後對快遞小哥說:"這我的快遞我拿走了啊。"但是人家小哥無法確認是你的啊!所以他需要用掃碼槍掃一下才能出貨!
我們配置註解掃描之後再次測試:
2.@PreDestory(了解即可)
銷毀方法註解,作用在方法上。用來替換 bean 標籤的 destory-method 屬性。
10.2 注入相關註解
10.2.1 @Autowired
我們之前學 DI 的時候知道:注入就是賦值。
@Autowired 主要是為自定義的類型賦值,例如 service、dao 層的各種類。
@Controller
public class UserController {
@Autowired
private UserService userService;
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
}
@Autowired 是基於類型進行注入,所注入對象的類型必須和目標 變量類型相同或者是他的子類、實現類。
如果想基於名字注入,可以和 @Qualifier 註解連用:
@Autowired
@Qualifier("orderDAOImpl")
private OrderDAO orderDAO;
10.2.2 @Resource
@Resource 註解是 JAVAEE 規範中提供的註解,他和 @Autowired 註解的作用一樣, 但是他是基於名字進行注入:
@Resource("orderDAOImpl")
private OrderDAO orderDAO;
在實際開發中,用 @Autowired 註解比較多一點。
10.2.3 案例
Product 類
@Component
public class Product {
private String productName;
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
@Override
public String toString() {
return "Product{" +
"productName='" + productName + '\'' +
'}';
}
@PostConstruct
public void init(){
this.productName = "西瓜";
}
}
User 類:
@Component
public class User {
private String name;
@Autowired
private Product product;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
@PostConstruct
public void init(){
this.name = "小明";
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", product=" + product +
'}';
}
}
測試:
@Test
public void testSpring() {
// 1、獲取工廠
ApplicationContext act = new ClassPathXmlApplicationContext("/applicationContext.xml");
// 2、通過工廠類獲得對象
User user = (User) act.getBean("user");
System.out.println(user);
}
10.3 Spring 配置文件相關註解
10.3.1 @Configuration
@Configuration 註解用於替換 xml 配置文件。
@Configuration
public class SpringConfig {
}
意思就是說你在一個類上面加一個 @Configuration 註解,這個類就可以看成 Spring 的配置類,你就不用再寫 xml 文件了。
我們之前是根據 xml 文件創建 Spring 的工廠,那怎樣根據配置類創建工廠呢?
有兩種方式:
方式一:根據類.class
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
方式二:根據配置類所在的路徑
ApplicationContext ctx = new AnnotationConfigApplicationContext("com.xxl");
10.3.2 @Bean
@Bean 註解也是用來創建對象的,相當於spring 配置文件中的 bean 標籤。
@Configuration
public class SpringConfig {
@Bean
public Product getProduct(){
return new Product();
}
}
自定義 id 值:
@Configuration
public class SpringConfig {
@Bean("product")
public Product getProduct(){
return new Product();
}
}
不過在實際開發中我們一般會用 @Bean 註解創建一些複雜的對象,例如 Redis、MQ 等一些組件對象。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
10.3.3 @ComponentScan
@ComponentScan 註解相當於 xml 配置文件中的註解掃描標籤:
<context:component-scan base-package="com.xxl"/>
作用:用來掃描@Component 等相關註解
屬性:
basePackages:註解所在的包路徑
例如:
@Configuration
@ComponentScan(basePackages = "com.xxl")
public class SpringConfig {
}
11. 註解小案例
1.User 類
@Component
public class User {
private String name;
@Autowired
private Product product;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
@PostConstruct
public void init(){
this.name = "渣渣輝";
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", product=" + product +
'}';
}
}
2.Product 類
@Component
public class Product {
private String productName;
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
@Override
public String toString() {
return "Product{" +
"productName='" + productName + '\'' +
'}';
}
}
3.配置類:
@Configuration
@ComponentScan(basePackages = "com.xxl")
public class SpringConfig {
@Bean
public Product product() {
Product product = new Product();
product.setProductName("草莓味的番茄");
return product;
}
}
4.測試
@Test
public void testSpring() {
// 1、獲取工廠
ApplicationContext act = new AnnotationConfigApplicationContext(SpringConfig.class);
// 2、通過工廠類獲得對象
User user = (User) act.getBean("user");
System.out.println(user);
}
作者:知否技術
原文連結:https://juejin.cn/post/7095532056632885284