全網最詳細的Spring入門教程

seven97_top發表於2024-12-09

為什麼用Spring

什麼是Spring

Spring 是一款開源的輕量級 Java 開發框架,旨在提高開發人員的開發效率以及系統的可維護性。

Spring的一個最大的目的就是使JAVA EE開發更加容易。同時,Spring之所以與Struts、Hibernate等單層框架不同,是因為Spring致力於提供一個以統一的、高效的方式構造整個應用,並且可以將單層框架以最佳的組合揉和在一起建立一個連貫的體系。可以說Spring是一個提供了更完善開發環境的一個框架,可以為POJO(Plain Ordinary Java Object)物件提供企業級的服務。

Spring的特性和優勢

從Spring 框架的特性來看:

  • 非侵入式:基於Spring開發的應用中的物件可以不依賴於Spring的API
  • 控制反轉:Inversion of Control(IOC),指的是將物件的建立權交給 Spring 去建立,是一個輕量級的IOC容器。使用 Spring 之前,物件的建立都是由我們自己在程式碼中new建立。而使用 Spring 之後。物件的建立都是給了 Spring 框架,實現松耦合
  • 依賴注入:Dependency Injection(DI),是指依賴的物件不需要手動呼叫 setXX 方法去設定,而是透過配置賦值。
  • 面向切面程式設計:Aspect Oriented Programming(AOP),把應用業務邏輯和系統服務分開,透過切面和模板減少樣板式程式碼。
  • 容器:Spring 是一個容器,它包含並且管理應用物件的生命週期
  • 元件化:Spring 實現了使用簡單的元件配置組合成一個複雜的應用。在 Spring 中可以使用XML和Java註解組合這些物件。
  • 宣告式事務的支援:可以從單調繁冗的事務管理程式碼中解脫出來,透過宣告式方式靈活地進行事務的管理,可向下擴充套件到(例如使用一個單一的資料庫)本地事務並擴充套件到全域性事務(例如,使用 JTA),提高開發效率和質量。
  • 一站式:在 IOC 和 AOP 的基礎上可以整合各種企業應用的開源框架和優秀的第三方類庫(實際上 Spring 自身也提供了表現層的 SpringMVC 和持久層的 Spring JDBC)

從使用Spring 框架的優勢看:

  • Spring 可以使開發人員使用 POJOs 開發企業級的應用程式。只使用 POJOs 的好處是你不需要一個應用程式伺服器,但是你可以選擇使用一個健壯的 servlet 容器,比如 Tomcat 或者一些商業產品。
  • Spring 在一個單元模式中是有組織的。即使包和類的數量非常大,你只要擔心你需要的,而其它的就可以忽略了。
  • Spring 不會讓你白費力氣做重複工作,它可以整合一些現有的技術,像 ORM 框架、日誌框架、JEE、Quartz 和 JDK 計時器,其他檢視技術。
  • 測試一個用 Spring 編寫的應用程式很容易,因為環境相關的程式碼被移動到這個框架中。此外,透過使用 JavaBean-style POJOs,它在使用依賴注入測試資料時變得更容易。
  • Spring 的 web 框架是一個設計良好的 web MVC 框架,它為比如 Structs 或者其他工程上的或者不怎麼受歡迎的 web 框架提供了一個很好的供替代的選擇。MVC 模式導致應用程式的不同方面(輸入邏輯,業務邏輯和UI邏輯)分離,同時提供這些元素之間的鬆散耦合。模型(Model)封裝了應用程式資料,通常它們將由 POJO 類組成。檢視(View)負責渲染模型資料,一般來說它生成客戶端瀏覽器可以解釋 HTML 輸出。控制器(Controller)負責處理使用者請求並構建適當的模型,並將其傳遞給檢視進行渲染。
  • Spring 對 JavaEE 開發中非常難用的一些 API(JDBC、JavaMail、遠端呼叫等),都提供了封裝,使這些API應用難度大大降低。

相關資料

  • 官網
  • 歸檔文件
  • 官方github原始碼

Spring的元件

Spring5.x主要模組

Spring5.x 版本中 Web 模組的 Portlet 元件已經被廢棄掉,同時增加了用於非同步響應式處理的 WebFlux 元件。

Spring 各個模組的依賴關係

從最下層往上介紹

Spring Test

Spring 團隊提倡測試驅動開發(TDD)。有了控制反轉 (IoC)的幫助,單元測試和整合測試變得更簡單。

Spring 的測試模組對 JUnit(單元測試框架)、TestNG(類似 JUnit)、Mockito(主要用來 Mock 物件)、PowerMock(解決 Mockito 的問題比如無法模擬 final, static, private 方法)等等常用的測試框架支援的都比較好。而且還額外提供了一些基於 Spring 的測試功能,比如在測試 Web 框架時,模擬 Http 請求的功能。

包含Mock Objects, TestContext Framework, Spring MVC Test, WebTestClient。

原始碼對應模組如下:

Core Container

Spring 框架的核心模組,也可以說是基礎模組,主要提供 IoC 依賴注入功能的支援。由 Beans 模組、Core 核心模組、Context 上下文模組和 SpEL 表示式語言模組組成,沒有這些核心容器,也不可能有 AOP、Web 等上層的功能。

  • spring-core:封裝了 Spring 框架的底層部分,包括資源訪問、型別轉換及一些常用工具類。
  • spring-beans:提供對 bean 的建立、配置和管理等功能的支援,包括控制反轉和依賴注入。
  • spring-context:建立在 Core 和 Beans 模組的基礎之上,整合 Beans 模組功能並新增資源繫結、資料驗證、國際化、Java EE 支援、容器生命週期、事件傳播等。ApplicationContext 介面是上下文模組的焦點。
  • spring-expression:提供對錶達式語言(Spring Expression Language) SpEL 的支援,只依賴於 core 模組,不依賴於其他模組,可以單獨使用。支援訪問和修改屬性值,方法呼叫,支援訪問及修改陣列、容器和索引器,命名變數,支援算數和邏輯運算,支援從 Spring 容器獲取 Bean,它也支援列表投影、選擇和一般的列表聚合等。

對應原始碼模組如下:

AOP、Aspects、Instrumentation和Messaging

  • spring-aspects:該模組為與 AspectJ 的整合提供支援,是一個功能強大且成熟的面向切面程式設計(AOP)框架。
  • spring-aop:提供了面向切面的程式設計實現。提供比如日誌記錄、許可權控制、效能統計等通用功能和業務邏輯分離的技術,並且能動態的把這些功能新增到需要的程式碼中,這樣各司其職,降低業務邏輯和通用功能的耦合。
  • spring-instrument:提供了為 JVM 新增代理(agent)的功能。 具體來講,它為 Tomcat 提供了一個織入代理,能夠為 Tomcat 傳遞類文 件,就像這些檔案是被類載入器載入的一樣。沒有理解也沒關係,這個模組的使用場景非常有限。
  • spring-messaging:是從 Spring4.0 開始新加入的一個模組,主要職責是為 Spring 框架整合一些基礎的報文傳送應用。
  • spring-jcl 模組: Spring 5.x中新增了日誌框架整合的模組。

對應原始碼模組如下:

Data Access/Integration

  • spring-jdbc:提供了對資料庫訪問的抽象 JDBC。不同的資料庫都有自己獨立的 API 用於運算元據庫,而 Java 程式只需要和 JDBC API 互動,這樣就遮蔽了資料庫的影響。
  • spring-tx:支援程式設計和宣告式事務管理。
  • spring-orm:提供對 Hibernate、JPA、iBatis 和 MyBatis 等 ORM 框架的支援。而且還可以使用 Spring 事務管理,無需額外控制事務。
  • spring-oxm:提供一個抽象層支撐 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。將 Java 物件對映成 XML 資料,或者將XML 資料對映成 Java 物件。
  • spring-jms : 指 Java 訊息服務,提供一套 “訊息生產者、訊息消費者”模板用於更加簡單的使用 JMS,JMS 用於用於在兩個應用程式之間,或分散式系統中傳送訊息,進行非同步通訊。自 Spring Framework 4.1 以後,它還提供了對 spring-messaging 模組的繼承。

對應原始碼模組:

Spring Web

  • spring-web:提供了基本的 Web 開發整合特性,例如多檔案上傳功能、使用的 Servlet 監聽器的 IOC 容器初始化以及 Web 應用上下文。
  • spring-webmvc:提供了一個 Spring MVC Web 框架實現。Spring MVC 框架提供了基於註解的請求資源注入、更簡單的資料繫結、資料驗證等及一套非常易用的 JSP 標籤,完全無縫與 Spring 其他技術協作。
  • spring-websocket:提供了對 WebSocket 的支援,WebSocket 可以讓客戶端和服務端進行雙向通訊。
  • spring-webflux:提供對 WebFlux 的支援。WebFlux 是 Spring Framework 5.0 中引入的新的響應式框架。與 Spring MVC 不同,它不需要 Servlet API,是完全非同步,並且透過Reactor專案實現了Reactive Streams規範。Spring WebFlux 用於建立基於事件迴圈執行模型的完全非同步且非阻塞的應用程式。

對應原始碼模組如下:

Spring、SpringMVC、SpringBoot之間的關係

Spring 包含了多個功能模組(上面剛剛提到過),其中最重要的是 Spring-Core(主要提供 IoC 依賴注入功能的支援) 模組, Spring 中的其他模組(比如 Spring MVC)的功能實現基本都需要依賴於該模組。

Spring MVC 是 Spring 中的一個很重要的模組,主要賦予 Spring 快速構建 MVC 架構的 Web 程式的能力。MVC 是模型(Model)、檢視(View)、控制器(Controller)的簡寫,其核心思想是透過將業務邏輯、資料、顯示分離來組織程式碼。

使用 Spring 進行開發各種配置過於麻煩比如開啟某些 Spring 特性時,需要用 XML 或 Java 進行顯式配置。於是,Spring Boot 誕生了!

Spring 旨在簡化 J2EE 企業應用程式開發。Spring Boot 旨在簡化 Spring 開發(減少配置檔案,開箱即用!)。

Spring Boot 只是簡化了配置,如果你需要構建 MVC 架構的 Web 程式,你還是需要使用 Spring MVC 作為 MVC 框架,只是說 Spring Boot 幫你簡化了 Spring MVC 的很多配置,真正做到開箱即用!

HelloWorld-xml

這裡只是表示這是Spring第一個專案,以HelloWorld作為標註。實際需求是獲取 使用者列表資訊,並列印執行日誌

案例

案例原始碼點選這裡

  • 引入依賴
<properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring.version>5.3.37</spring.version>
        <aspectjweaver.version>1.9.6</aspectjweaver.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectjweaver.version}</version>
        </dependency>
    </dependencies>
  • POJO - User
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;
    }
}
  • DAO 獲取 POJO, UserDaoServiceImpl (mock 資料)
public class UserDaoImpl{

    public List<User> findUserList() {
        return Collections.singletonList(new User("seven", 18));
    }
}
  • 業務層 UserServiceImpl(呼叫DAO層)
public class UserServiceImpl {
    private UserDaoImpl userDao;

    public void setUserDao(UserDaoImpl userDao) {
        this.userDao = userDao;
    }

    public List<User> findUserList() {
        return userDao.findUserList();
    }
}
  • 攔截所有service中的方法,並輸出記錄
@Aspect
public class LogAspect {

    @Around("execution(* com.seven.springhelloworldxml.service.*.*(..))")
    public Object businessService(ProceedingJoinPoint pjp) throws Throwable {
        // get attribute through annotation
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        System.out.println("execute method: " + method.getName());

        // continue to process
        return pjp.proceed();
    }
}
  • 新增並增加spring.xml和aspects.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="userDao" class="com.seven.springhelloworldxml.dao.UserDaoImpl">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <bean id="userService" class="com.seven.springhelloworldxml.service.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>
    
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/aop
 http://www.springframework.org/schema/aop/spring-aop.xsd
 http://www.springframework.org/schema/context
 http://www.springframework.org/schema/context/spring-context.xsd
">

    <context:component-scan base-package="com.seven.springhelloworldxml" />

    <aop:aspectj-autoproxy/>

    <bean id="logAspect" class="com.seven.springhelloworldxml.aspects.LogAspect">
        <!-- configure properties of aspect here as normal -->
    </bean>
    <!-- more bean definitions for data access objects go here -->
</beans>
  • APP中設定xml檔案
public class APP {

    public static void main(String[] args) {
        // create and configure beans
        ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml", "spring.xml");

        // retrieve configured instance
        UserServiceImpl service = context.getBean("userService", UserServiceImpl.class);

        // use configured instance
        List<User> userList = service.findUserList();

        // print info from beans
        userList.forEach(a -> System.out.println(a.getName() + "," + a.getAge()));
    }
}

執行結果:

如何體現的Spring優勢

控制反轉 - IOC

查詢使用者(service透過呼叫dao查詢pojo),本質上就是如何建立User/Dao/Service?

  • 如果沒有Spring框架,需要自己建立User/Dao/Service等,比如:
UserDaoImpl userDao = new UserDaoImpl();
UserSericeImpl userService = new UserServiceImpl();
userService.setUserDao(userDao);
List<User> userList = userService.findUserList();
  • 有了Spring框架,可以將原有Bean的建立工作轉給框架, 需要用時從Bean的容器中獲取即可,這樣便簡化了開發工作

Bean的建立和使用分離了。

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml", "spring.xml");

// retrieve configured instance
UserServiceImpl service = context.getBean("userService", UserServiceImpl.class);

// use configured instance
List<User> userList = service.findUserList();

更進一步,便能理解為何會有如下的知識點了

  1. Spring框架管理這些Bean的建立工作,即由使用者管理Bean轉變為框架管理Bean,這個就叫控制反轉 - Inversion of Control (IoC)
  2. Spring 框架託管建立的Bean放在哪裡呢? 這便是IoC Container;
  3. Spring 框架為了更好讓使用者配置Bean,必然會引入不同方式來配置Bean? 這便是xml配置,Java配置,註解配置等支援
  4. Spring 框架既然接管了Bean的生成,必然需要管理整個Bean的生命週期等;
  5. 應用程式程式碼從Ioc Container中獲取依賴的Bean,注入到應用程式中,這個過程叫 依賴注入(Dependency Injection,DI) ; 所以說控制反轉是透過依賴注入實現的,其實它們是同一個概念的不同角度描述。通俗來說就是IoC是設計思想,DI是實現方式
  6. 在依賴注入時,有哪些方式呢?這就是構造器方式,@Autowired, @Resource, @Qualifier... 同時Bean之間存在依賴(可能存在先後順序問題,以及迴圈依賴問題等)

面向切面 - AOP

第二個需求:給Service所有方法呼叫新增日誌(呼叫方法時),本質上是解耦問題;

  • 如果沒有Spring框架,需要在每個service的方法中都新增記錄日誌的方法,比如:
public List<User> findUserList() {
    System.out.println("execute method findUserList");
    return this.userDao.findUserList();
}
  • 有了Spring框架,透過@Aspect註解 定義了切面,這個切面中定義了攔截所有service中的方法,並記錄日誌; 可以明顯看到,框架將日誌記錄和業務需求的程式碼解耦了,不再是侵入式的了
/**
* aspect for every methods under service package.
*/
@Around("execution(* com.seven.springhelloworldxml.service.*.*(..))")
public Object businessService(ProceedingJoinPoint pjp) throws Throwable {
    // get attribute through annotation
    Method method = ((MethodSignature) pjp.getSignature()).getMethod();
    System.out.println("execute method: " + method.getName());

    // continue to process
    return pjp.proceed();
}

更進一步,便能理解為何會有如下的知識點了

  1. Spring 框架透過定義切面, 透過攔截切點實現了不同業務模組的解耦,這個就叫面向切面程式設計 - Aspect Oriented Programming (AOP)
  2. 為什麼@Aspect註解使用的是aspectj的jar包呢?這就引出了Aspect4J和Spring AOP的歷史淵源,只有理解了Aspect4J和Spring的淵源才能理解有些註解上的相容設計
  3. 如何支援更多攔截方式來實現解耦, 以滿足更多場景需求呢? 這就是@Around, @Pointcut... 等的設計
  4. 那麼Spring框架又是如何實現AOP的呢? 這就引入代理技術,分靜態代理和動態代理,動態代理又包含JDK代理和CGLIB代理

Spring框架逐步簡化開發

Java 配置方式改造

案例原始碼點選這裡

在前文的例子中, 透過xml配置方式實現的,這種方式實際上比較麻煩; 我透過Java配置進行改造:

  • User,UserDaoImpl, UserServiceImpl,LogAspect不用改
  • 將原透過.xml配置轉換為Java配置
@EnableAspectJAutoProxy
@Configuration
public class BeansConfig {

    /**
     * @return user dao
     */
    @Bean("userDao")
    public UserDaoImpl userDao() {
        return new UserDaoImpl();
    }

    /**
     * @return user service
     */
    @Bean("userService")
    public UserServiceImpl userService() {
        UserServiceImpl userService = new UserServiceImpl();
        userService.setUserDao(userDao());
        return userService;
    }

    /**
     * @return log aspect
     */
    @Bean("logAspect")
    public LogAspect logAspect() {
        return new LogAspect();
    }
}
  • 在App中載入BeansConfig的配置
public class APP {

    public static void main(String[] args) {
        // create and configure beans
        ApplicationContext context = new AnnotationConfigApplicationContext(BeansConfig.class);

        // retrieve configured instance
        UserServiceImpl service = context.getBean("userService", UserServiceImpl.class);

        // use configured instance
        List<User> userList = service.findUserList();

        // print info from beans
        userList.forEach(a -> System.out.println(a.getName() + "," + a.getAge()));
    }

}

這裡簡單提一下實現原理:

  1. 當應用啟動時,Spring框架會使用Java的反射API來檢查所有帶有@Configuration 註解的類(這裡不懂的可以看下註解實現的原理)。Spring框架內建了一個註解處理器ConfigurationClassPostProcessor,它是BeanFactoryPostProcessor 的一個實現。
  2. 接著ConfigurationClassPostProcessor 會在容器初始化時被呼叫,它會查詢所有帶有@Configuration 註解的類,並解析這些類中定義的@Bean 方法。

    • BeanDefinition:對於每個@Configuration 類,Spring會為其中的每個@Bean 方法生成一個BeanDefinition 物件。這些BeanDefinition 物件會包含建立和配置Bean所需的所有資訊。
    • 處理巢狀配置:如果一個@Configuration 類中包含了另一個@Configuration 類的引用,ConfigurationClassPostProcessor 會遞迴地處理這些巢狀的配置類。
  3. 註冊BeanDefinition: 一旦所有的BeanDefinition 被建立,它們會被註冊到Spring容器的BeanFactory 中。這樣,Spring容器就可以在需要時建立和注入這些Bean。
  4. 代理配置類: 為了支援巢狀配置類和迴圈依賴等特性,Spring會為每個@Configuration 類建立一個代理,這個代理會在執行時處理相關的邏輯。

註解配置方式改造

案例原始碼點選這裡

更進一步,Java 5開始提供註解支援,Spring 2.5 開始完全支援基於註解的配置並且也支援JSR250 註解。在Spring後續的版本發展傾向於透過註解和Java配置結合使用.

  • BeanConfig 不再需要Java配置
@EnableAspectJAutoProxy
@Configuration
public class BeansConfig {

}
  • UserDaoImpl 增加了 @Repository註解
@Repository
public class UserDaoImpl{

    public List<User> findUserList() {
        return Collections.singletonList(new User("seven", 18));
    }
}
  • UserServiceImpl 增加了@Service 註解,並透過@Autowired注入userDao
@Service
public class UserServiceImpl {
    @Autowired
    private UserDaoImpl userDao;

    public List<User> findUserList() {
        return userDao.findUserList();
    }
}
  • 日誌類新增@Component註解
@Component
@Aspect
public class LogAspect {

    @Around("execution(* com.seven.springhelloworldanno.service.*.*(..))")
    public Object businessService(ProceedingJoinPoint pjp) throws Throwable {
        // get attribute through annotation
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        System.out.println("execute method: " + method.getName());

        // continue to process
        return pjp.proceed();
    }

}
  • 在App中掃描com.seven.springhelloworldanno包
public static void main(String[] args) {
        // create and configure beans
        ApplicationContext context = new AnnotationConfigApplicationContext("com.seven.springhelloworldanno");

        // retrieve configured instance
        UserServiceImpl service = context.getBean(UserServiceImpl.class);

        // use configured instance
        List<User> userList = service.findUserList();

        // print info from beans
        userList.forEach(a -> System.out.println(a.getName() + "," + a.getAge()));
    }
這裡要提一嘴的是,現在大多數的Spring專案,基本都是這種方式。主要步驟就是:
1、對類新增@Component相關的註解,比如@Controller,@Service,@Repository
2、設定ComponentScan的basePackage, 比如在xml檔案裡設定<context:component-scan base-package='com.seven.springframework'>, 或者在配置類中設定@ComponentScan("com.seven.springframework")註解,或者 直接在APP類中 new AnnotationConfigApplicationContext("com.seven.springframework")指定掃描的basePackage.

SpringBoot託管配置

Springboot實際上透過約定大於配置的方式,使用xx-starter統一的對Bean進行預設初始化,使用者只需要很少的配置就可以進行開發了。

面試題專欄

Java面試題專欄已上線,歡迎訪問。

  • 如果你不知道簡歷怎麼寫,簡歷專案不知道怎麼包裝;
  • 如果簡歷中有些內容你不知道該不該寫上去;
  • 如果有些綜合性問題你不知道怎麼答;

那麼可以私信我,我會盡我所能幫助你。

相關文章