前言
隨着 Spring
的崛起以及其功能的完善,現在可能絕大部分項目的開發都是使用 Spring(全家桶)
來進行開發,Spring
也確實和其名字一樣,是開發者的春天,Spring
解放了程序員的雙手,而等到 SpringBoot
出來之后配置文件大大減少,更是進一步解放了程序員的雙手,但是也正是因為Spring
家族產品的強大,使得我們習慣了面向 Spring
開發,那么假如有一天沒有了 Spring
,是不是感覺心里一空,可能一下子連最基本的接口都不會寫了,尤其是沒有接觸過Servlet
編程的朋友。因為加入沒有了 Spring
等框架,那么我們就需要利用最原生的 Servlet
來自己實現接口路徑的映射,對象也需要自己進行管理。
Spring 能幫我們做什么
Spring
是為解決企業級應用開發的復雜性而設計的一款框架,Spring
的設計理念就是:簡化開發。
在 Spring
框架中,一切對象都是 bean
,所以其通過面向 bean
編程(BOP),結合其核心思想依賴注入(DI)和面向切面((AOP)編程,Spring
實現了其偉大的簡化開發的設計理念。
控制反轉(IOC)
IOC
全稱為:Inversion of Control。控制反轉的基本概念是:不用創建對象,但是需要描述創建對象的方式。
簡單的說我們本來在代碼中創建一個對象是通過 new
關鍵字,而使用了 Spring
之后,我們不在需要自己去 new
一個對象了,而是直接通過容器里面去取出來,再將其自動注入到我們需要的對象之中,即:依賴注入。
也就說創建對象的控制權不在我們程序員手上了,全部交由 Spring
進行管理,程序要只需要注入就可以了,所以才稱之為控制反轉。
依賴注入(DI)
依賴注入(Dependency Injection,DI)就是 Spring
為了實現控制反轉的一種實現方式,所有有時候我們也將控制反轉直接稱之為依賴注入。
面向切面編程(AOP)
AOP
全稱為:Aspect Oriented Programming。AOP
是一種編程思想,其核心構造是方面(切面),即將那些影響多個類的公共行為封裝到可重用的模塊中,而使原本的模塊內只需關注自身的個性化行為。
AOP
編程的常用場景有:Authentication(權限認證)、Auto Caching(自動緩存處理)、Error Handling(統一錯誤處理)、Debugging(調試信息輸出)、Logging(日志記錄)、Transactions(事務處理)等。
利用 Spring 來完成 Hello World
最原生的 Spring
需要較多的配置文件,而 SpringBoot
省略了許多配置,相比較於原始的 Spring
又簡化了不少,在這里我們就以 SpringBoot
為例來完成一個簡單的接口開發。
- 1、新建一個
maven
項目,pom
文件中引入依賴(省略了少部分屬性):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- 2、新建一個
HelloController
類:
package com.lonely.wolf.note.springboot.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/hello")
public class HelloController {
@GetMapping("/demo")
public String helloWorld(String name){
return "Hello:" + name;
}
}
- 3、最后新建一個
SpringBoot
啟動類:
package com.lonely.wolf.note.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.lonely.wolf.note.springboot")
class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}
- 4、現在就可以輸入測試路徑:
http://localhost:8080/hello/demo?name=雙子孤狼
進行測試,正常輸出:Hello:雙子孤狼
。
我們可以看到,利用 SpringBoot
來完成一個簡單的應用開發非常簡單,可以不需要任何配置完成一個簡單的應用,這是因為 SpringBoot
內部已經做好了約定(約定優於配置思想),包括容器 Tomcat
都被默認集成,所以我們不需要任何配置文件就可以完成一個簡單的 demo
應用。
假如沒有了 Spring
通過上面的例子我們可以發現,利用 Spring
來完成一個 Hello World
非常簡單,但是假如沒有了 Spring
,我們又該如何完成這樣的一個 Hello World
接口呢?
基於 Servlet 開發
在還沒有框架之前,編程式基於原始的 Servlet
進行開發,下面我們就基於原生的 Servlet
來完成一個簡單的接口調用。
- 1、
pom
文件引入依賴,需要注意的是,package
屬性要設置成war
包,為了節省篇幅,這里沒有列出pom
完整的信息:
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
</dependencies>
- 2、在
src/main
下面新建文件夾webapp/WEB-INF
,然后在WEB-INF
下面新建一個web.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Lonely Wolf Web Application</display-name>
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>com.lonely.wolf.mini.spring.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello/*</url-pattern>
</servlet-mapping>
</web-app>
這里面定義了 selvlet
和 servlet-mapping
兩個標簽,這兩個標簽必須一一對應,上面的標簽定義了 servlet
的位置,而下面的 servlet-mapping
文件定義了路徑的映射,這兩個標簽通過 servlet-name
標簽對應。
- 3、新建一個
HelloServlet
類繼承HttpServlet
:
package com.lonely.wolf.mini.spring.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 原始Servlet接口編寫,一般需要實現GET和POST方法,其他方法可以視具體情況選擇性繼承
*/
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("Hello:" + request.getParameter("name"));
}
}
- 4、執行
maven
打包命令,確認成功打包成war
包:
- 5、
RUN-->Edit Configurations
,然后點擊左上角的+
號,新建一個Tomcat Server
,如果是第一次配置,默認沒有Tomcat Server
選項,需要點擊底部的xx more items...
:
- 6、點擊右邊的
Deployment
,然后按照下圖依次點擊,最后在彈框內找到上面打包好的war
包文件:
- 7、選中之后,需要注意的是,下面
Application Context
默認會帶上war
包名,為了方便,我們需要把它刪掉,即不用上下文路徑,只保留一個根路徑/
(當然上下文也可以保留,但是每次請求都要帶上這一部分), 再選擇Apply
,點擊OK
,即可完成部署:
- 8、最后我們在瀏覽器輸入請求路徑
http://localhost:8080/hello?name=雙子孤狼
,即可得到返回:Hello:雙子孤狼
。
上面我們就完成了一個簡單的 基於Servlet
的接口開發,可以看到,配置非常麻煩,每增加一個 Servlet
都需要增加對應的配置,所以才會有許多框架的出現來幫我們簡化開發,比如原來很流行的 Struts2
框架,當然現在除了一些比較老的項目,一般我們都很少使用,而更多的是選擇 Spring
框架來進行開發。
模仿Spring
Spring
的源碼體系非常龐大,大部分人對其源碼都敬而遠之。確實,Spring
畢竟經過了這么多年的迭代,功能豐富,項目龐大,不是一下子就能看懂的。雖然 Spring
難以理解,但是其最核心的思想仍然是我們上面介紹的幾點,接下來就基於 Spring
最核心的部分來模擬,自己動手實現一個超級迷你版本的 Spring
(此版本並不包含 AOP
功能)。
- 1、
pom
依賴和上面保持不變,然后web.xml
作如下改變,這里會攔截所有的接口/*
,然后多配置了一個參數,這個參數其實也是為了更形象的模擬Spring
:
<servlet>
<servlet-name>myDispatcherServlet</servlet-name>
<servlet-class>com.lonely.wolf.mini.spring.v1.MyDispatcherServlet</servlet-class>
<init-param>
<param-name>defaultConfig</param-name>
<param-value>application.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>myDispatcherServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
- 2、在
respurces
下面新建一個配置文件application.properties
,用來定義掃描的基本路徑:
basePackages=com.lonely.wolf.mini.spring
- 3、創建一些相關的注解類:
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfAutowired {
String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfController {
String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfGetMapping {
String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfRequestParam {
String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfService {
String value() default "";
}
- 4、這個時候最核心的邏輯就是
MyDispatcherServlet
類了:
package com.lonely.wolf.mini.spring.v1;
import com.lonely.wolf.mini.spring.annotation.*;
import com.lonely.wolf.mini.spring.v1.config.MyConfig;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;
public class MyDispatcherServlet extends HttpServlet {
private MyConfig myConfig = new MyConfig();
private List<String> classNameList = new ArrayList<String>();
private Map<String,Object> iocContainerMap = new HashMap<>();
private Map<String,HandlerMapping> handlerMappingMap = new HashMap<>();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
this.doDispatch(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception{
String requestUrl = this.formatUrl(request.getRequestURI());
HandlerMapping handlerMapping = handlerMappingMap.get(requestUrl);
if (null == handlerMapping){
response.getWriter().write("404 Not Found");
return;
}
//獲取方法中的參數類型
Class<?>[] paramTypeArr = handlerMapping.getMethod().getParameterTypes();
Object[] paramArr = new Object[paramTypeArr.length];
for (int i=0;i<paramTypeArr.length;i++){
Class<?> clazz = paramTypeArr[i];
//參數只考慮三種類型,其他不考慮
if (clazz == HttpServletRequest.class){
paramArr[i] = request;
}else if (clazz == HttpServletResponse.class){
paramArr[i] = response;
} else if (clazz == String.class){
Map<Integer,String> methodParam = handlerMapping.getMethodParams();
paramArr[i] = request.getParameter(methodParam.get(i));
}else{
System.out.println("暫不支持的參數類型");
}
}
//反射調用controller方法
handlerMapping.getMethod().invoke(handlerMapping.getTarget(), paramArr);
}
private String formatUrl(String requestUrl) {
requestUrl = requestUrl.replaceAll("/+","/");
if (requestUrl.lastIndexOf("/") == requestUrl.length() -1){
requestUrl = requestUrl.substring(0,requestUrl.length() -1);
}
return requestUrl;
}
@Override
public void init(ServletConfig config) throws ServletException {
//1.加載配置文件
try {
doLoadConfig(config.getInitParameter("defaultConfig"));
} catch (Exception e) {
System.out.println("加載配置文件失敗");
return;
}
//2.根據獲取到的掃描路徑進行掃描
doScanPacakge(myConfig.getBasePackages());
//3.將掃描到的類進行初始化,並存放到IOC容器
doInitializedClass();
//4.依賴注入
doDependencyInjection();
System.out.println("DispatchServlet Init End..." );
}
private void doDependencyInjection() {
if (iocContainerMap.size() == 0){
return;
}
//循環IOC容器中的類
Iterator<Map.Entry<String,Object>> iterator = iocContainerMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String,Object> entry = iterator.next();
Class<?> clazz = entry.getValue().getClass();
Field[] fields = clazz.getDeclaredFields();
//屬性注入
for (Field field : fields){
//如果屬性有WolfAutowired注解則注入值(暫時不考慮其他注解)
if (field.isAnnotationPresent(WolfAutowired.class)){
String value = toLowerFirstLetterCase(field.getType().getSimpleName());//默認bean的value為類名首字母小寫
if (field.getType().isAnnotationPresent(WolfService.class)){
WolfService wolfService = field.getType().getAnnotation(WolfService.class);
value = wolfService.value();
}
field.setAccessible(true);
try {
Object target = iocContainerMap.get(beanName);
if (null == target){
System.out.println(clazz.getName() + "required bean:" + beanName + ",but we not found it");
}
field.set(entry.getValue(),iocContainerMap.get(beanName));//初始化對象,后面注入
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
//初始化HanderMapping
String requestUrl = "";
//獲取Controller類上的請求路徑
if (clazz.isAnnotationPresent(WolfController.class)){
requestUrl = clazz.getAnnotation(WolfController.class).value();
}
//循環類中的方法,獲取方法上的路徑
Method[] methods = clazz.getMethods();
for (Method method : methods){
//假設只有WolfGetMapping這一種注解
if(!method.isAnnotationPresent(WolfGetMapping.class)){
continue;
}
WolfGetMapping wolfGetMapping = method.getDeclaredAnnotation(WolfGetMapping.class);
requestUrl = requestUrl + "/" + wolfGetMapping.value();//拼成完成的請求路徑
//不考慮正則匹配路徑/xx/* 的情況,只考慮完全匹配的情況
if (handlerMappingMap.containsKey(requestUrl)){
System.out.println("重復路徑");
continue;
}
Annotation[][] annotationArr = method.getParameterAnnotations();//獲取方法中參數的注解
Map<Integer,String> methodParam = new HashMap<>();//存儲參數的順序和參數名
retryParam:
for (int i=0;i<annotationArr.length;i++){
for (Annotation annotation : annotationArr[i]){
if (annotation instanceof WolfRequestParam){
WolfRequestParam wolfRequestParam = (WolfRequestParam) annotation;
methodParam.put(i,wolfRequestParam.value());//存儲參數的位置和注解中定義的參數名
continue retryParam;
}
}
}
requestUrl = this.formatUrl(requestUrl);//主要是防止路徑多了/導致路徑匹配不上
HandlerMapping handlerMapping = new HandlerMapping();
handlerMapping.setRequestUrl(requestUrl);//請求路徑
handlerMapping.setMethod(method);//請求方法
handlerMapping.setTarget(entry.getValue());//請求方法所在controller對象
handlerMapping.setMethodParams(methodParam);//請求方法的參數信息
handlerMappingMap.put(requestUrl,handlerMapping);//存入hashmap
}
}
}
/**
* 初始化類,並放入容器iocContainerMap內
*/
private void doInitializedClass() {
if (classNameList.isEmpty()){
return;
}
for (String className : classNameList){
if (StringUtils.isEmpty(className)){
continue;
}
Class clazz;
try {
clazz = Class.forName(className);//反射獲取對象
if (clazz.isAnnotationPresent(WolfController.class)){
String value = ((WolfController)clazz.getAnnotation(WolfController.class)).value();
//如果直接指定了value則取value,否則取首字母小寫類名作為key值存儲類的實例對象
iocContainerMap.put(StringUtils.isBlank(value) ? toLowerFirstLetterCase(clazz.getSimpleName()) : value,clazz.newInstance());
}else if(clazz.isAnnotationPresent(WolfService.class)){
String value = ((WolfService)clazz.getAnnotation(WolfService.class)).value();
iocContainerMap.put(StringUtils.isBlank(value) ? toLowerFirstLetterCase(clazz.getSimpleName()) : value,clazz.newInstance());
}else{
System.out.println("不考慮其他注解的情況");
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("初始化類失敗,className為" + className);
}
}
}
/**
* 將首字母轉換為小寫
* @param className
* @return
*/
private String toLowerFirstLetterCase(String className) {
if (StringUtils.isBlank(className)){
return "";
}
String firstLetter = className.substring(0,1);
return firstLetter.toLowerCase() + className.substring(1);
}
/**
* 掃描包下所有文件獲取全限定類名
* @param basePackages
*/
private void doScanPacakge(String basePackages) {
if (StringUtils.isBlank(basePackages)){
return;
}
//把包名的.替換為/
String scanPath = "/" + basePackages.replaceAll("\\.","/");
URL url = this.getClass().getClassLoader().getResource(scanPath);//獲取到當前包所在磁盤的全路徑
File files = new File(url.getFile());//獲取當前路徑下所有文件
for (File file : files.listFiles()){//開始掃描路徑下的所有文件
if (file.isDirectory()){//如果是文件夾則遞歸
doScanPacakge(basePackages + "." + file.getName());
}else{//如果是文件則添加到集合。因為上面是通過類加載器獲取到的文件路徑,所以實際上是class文件所在路徑
classNameList.add(basePackages + "." + file.getName().replace(".class",""));
}
}
}
/**
* 加載配置文件
* @param configPath - 配置文件所在路徑
*/
private void doLoadConfig(String configPath) {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(configPath);
Properties properties = new Properties();
try {
properties.load(inputStream);
} catch (IOException e) {
e.printStackTrace();
System.out.println("加載配置文件失敗");
}
properties.forEach((k, v) -> {
try {
Field field = myConfig.getClass().getDeclaredField((String)k);
field.setAccessible(true);
field.set(myConfig,v);
} catch (Exception e) {
e.printStackTrace();
System.out.println("初始化配置類失敗");
return;
}
});
}
}
- 5、這個
Servlet
相比較於上面的HelloServlet
多了一個init
方法,這個方法中主要做了以下幾件事情:
(1)初始化配置文件,拿到配置文件中配置的參數信息(對應方法:doLoadConfig
)。
(2)拿到第 1
步加載出來的配置文件,獲取到需要掃描的包路徑,然后將包路徑進行轉換成實際的磁盤路徑,並開始遍歷磁盤路徑下的所有 class
文件,最終經過轉換之后得到掃描路徑下的所有類的全限定類型,存儲到全局變量 classNameList
中(對應方法:doScanPacakge
)。
(3)根據第 2
步中得到的全局變量 classNameList
中的類通過反射進行初始化(需要注意的是只會初始化加了指定注解的類)並將得到的對應關系存儲到全局變量 iocContainerMap
中(即傳說中的 IOC
容器),其中 key
值為注解中的 value
屬性,如 value
屬性為空,則默認取首字母小寫的類名作為 key
值進行存儲(對應方法:doInitializedClass
)。
(4)這一步比較關鍵,需要對 IOC
容器中的所有類的屬性進行賦值並且需要對 Controller
中的請求路徑進行映射存儲,為了確保最后能順利調用 Controller
中的方法,還需要將方法的參數進行存儲 。對屬性進行映射時只會對加了注解的屬性進行映射,映射時會從 IOC
容器中取出第 3
步中已經初始化的實例對象進行賦值,最后將請求路徑和 Controller
中方法的映射關系存入變量 handlerMappingMap
,key
值為請求路徑,value
為方法的相關信息 (對應方法:doDependencyInjection
)。
- 6、存儲請求路徑和方法的映射關系時,需要用到
HandlerMapping
類來進行存儲:
package com.lonely.wolf.mini.spring.v1;
import java.lang.reflect.Method;
import java.util.Map;
//省略了getter/setter方法
public class HandlerMapping {
private String requestUrl;
private Object target;//保存方法對應的實例
private Method method;//保存映射的方法
private Map<Integer,String> methodParams;//記錄方法參數
}
- 7、初始化完成之后,因為攔截了
/*
,所以調用任意接口都會進入MyDispatcherServlet
,而且最終都會執行方法doDispatch
,執行這個方法時會拿到請求的路徑,然后和全局變量handlerMappingMap
進行匹配,匹配不上則返回404
,匹配的上則取出必要的參數進行賦值,最后通過反射調用到Controller
中的相關方法。 - 8、新建一個
HelloController
和HelloService
來進行測試:
package com.lonely.wolf.mini.spring.controller;
import com.lonely.wolf.mini.spring.annotation.WolfAutowired;
import com.lonely.wolf.mini.spring.annotation.WolfController;
import com.lonely.wolf.mini.spring.annotation.WolfGetMapping;
import com.lonely.wolf.mini.spring.annotation.WolfRequestParam;
import com.lonely.wolf.mini.spring.service.HelloService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WolfController
public class HelloController {
@WolfAutowired
private HelloService helloService;
@WolfGetMapping("/hello")
public void query(HttpServletRequest request,HttpServletResponse response, @WolfRequestParam("name") String name) throws IOException {
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("Hello:" + name);
}
}
package com.lonely.wolf.mini.spring.service;
import com.lonely.wolf.mini.spring.annotation.WolfService;
@WolfService(value = "hello_service")//為了演示能否正常取value屬性
public class HelloService {
}
- 9、輸入測試路徑:
http://localhost:8080////hello?name=雙子孤狼
, 進行測試發現可以正常輸出:Hello:雙子孤狼
。
上面這個例子只是一個簡單的演示,通過這個例子只是希望在沒有任何框架的情況下,我們也能知道如何完成一個簡單的應用開發。例子中很多細節都沒有進行處理,僅僅只是為了體驗一下 Spring
的核心思想,並了解 Spring
到底幫助我們做了什么,實際上 Spring
能幫我們做的事情遠比這個例子中多得多,Spring
體系龐大,設計優雅,經過了多年的迭代優化,是一款非常值得研究的框架。
總結
本文從介紹 Spring
核心功能開始入手,從如何利用 Spring
完成一個應用開發,講述到假如沒有 Spring
我們該如何基於 Servlet
進行開發,最后再通過一個簡單的例子體驗了 Spring
的核心思想。