小心,99%的面試者,都倒在了這裏。一文帶你瞭解spring全家桶

 

 

Spring中有個非常重要的知識點,AOP,即面相切面編程,spring中提供的一些非常牛逼的功能都是通過aop實現的,比如下面這些大家比較熟悉的功能

  1. spring事務管理:@Transactional
  2. spring異步處理:@EnableAsync
  3. spring緩存技術的使用:@EnableCaching
  4. spring中各種攔截器:@EnableAspectJAutoProxy

大家想玩轉spring,成爲一名spring高手,aop是必須要掌握的,aop這塊東西比較多,我們將通過三四篇文章來詳解介紹這塊的內容,由淺入深,讓大家全面掌握這塊知識。

說的簡單點,spring中的aop就是依靠代理實現的各種功能,通過代理來對bean進行增強。

spring中的aop功能主要是通過2種代理來實現的

  1. jdk動態代理
  2. cglib代理

繼續向下之前,必須先看一下這篇文章:Spring系列第15篇:代理詳解(Java動態代理&cglib代理)?

spring aop中用到了更多的一些特性,上面這邊文章中沒有介紹到,所以通過本文來做一個補充,這2篇文章看過之後,再去看spring aop的源碼,理解起來會容易一些,這2篇算是最基礎的知識,所以一定要消化理解,不然aop那塊的原理你很難了解,會暈車,

jdk動態代理

特徵

  1. 只能爲接口創建代理對象
  2. 創建出來的代理都是java.lang.reflect.Proxy的子類

案例

案例源碼位置:


 
com.javacode2018.aop.demo1.JdkAopTest1

有2個接口


 
interface IService1 {     void m1(); }  interface IService2 {     void m2(); }

下面的類實現了上面2個接口


 
public static class Service implements IService1, IService2 {     @Override     public void m1() {         System.out.println("我是m1");     }     @Override     public void m2() {         System.out.println("我是m2");     } }

下面通過jdk動態代理創建一個代理對象,實現上面定義的2個接口,將代理對象所有的請求轉發給Service去處理,需要在代理中統計2個接口中所有方法的耗時。

比較簡單,自定義一個InvocationHandler


 
public static class CostTimeInvocationHandler implements InvocationHandler {     private Object target;     public CostTimeInvocationHandler(Object target) {         this.target = target;     }     @Override     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {         long startime = System.nanoTime();         Object result = method.invoke(this.target, args); //將請求轉發給target去處理         System.out.println(method + ",耗時(納秒):" + (System.nanoTime() - startime));         return result;     } }

測試方法


 
{     Service target = new Service();     CostTimeInvocationHandler costTimeInvocationHandler = new CostTimeInvocationHandler(target);     //創建代理對象     Object proxyObject = Proxy.newProxyInstance(             target.getClass().getClassLoader(),             new Class[]{IService1.class, IService2.class}, //創建的代理對象實現了2個接口             costTimeInvocationHandler);     //判斷代理對象是否是Service類型的,肯定是false咯     System.out.println(String.format("proxyObject instanceof Service = %s", proxyObject instanceof Service));     //判斷代理對象是否是IService1類型的,肯定是true     System.out.println(String.format("proxyObject instanceof IService1 = %s", proxyObject instanceof IService1));     //判斷代理對象是否是IService2類型的,肯定是true     System.out.println(String.format("proxyObject instanceof IService2 = %s", proxyObject instanceof IService2));     //將代理轉換爲IService1類型     IService1 service1 = (IService1) proxyObject;     //調用IService2的m1方法     service1.m1();     //將代理轉換爲IService2類型     IService2 service2 = (IService2) proxyObject;     //調用IService2的m2方法     service2.m2();     //輸出代理的類型     System.out.println("代理對象的類型:" + proxyObject.getClass()); }

運行輸出


 
proxyObject instanceof Service = false proxyObject instanceof IService1 = true proxyObject instanceof IService2 = true 我是m1 public abstract void com.javacode2018.aop.demo1.JdkAopTest1$IService1.m1(),耗時(納秒):225600 我是m2 public abstract void com.javacode2018.aop.demo1.JdkAopTest1$IService2.m2(),耗時(納秒):36000 代理對象的類型:class com.javacode2018.aop.demo1.$Proxy0

m1方法和m2方法被CostTimeInvocationHandler#invoke給增強了,調用目標方法的過程中統計了耗時。

最後一行輸出可以看出代理對象的類型,類名中包含了$Proxy的字樣,所以以後注意,看到這種字樣的,基本上都是通過jdk動態代理創建的代理對象。

下面來說cglib代理的一些特殊案例。

cglib代理

cglib的特點

  1. cglib彌補了jdk動態代理的不足,jdk動態代理只能爲接口創建代理,而cglib非常強大,不管是接口還是類,都可以使用cglib來創建代理
  2. cglib創建代理的過程,相當於創建了一個新的類,可以通過cglib來配置這個新的類需要實現的接口,以及需要繼承的父類
  3. cglib可以爲類創建代理,但是這個類不能是final類型的,cglib爲類創建代理的過程,實際上爲通過繼承來實現的,相當於給需要被代理的類創建了一個子類,然後會重寫父類中的方法,來進行增強,繼承的特性大家應該都知道,final修飾的類是不能被繼承的,final修飾的方法不能被重寫,static修飾的方法也不能被重寫,private修飾的方法也不能被子類重寫,而其他類型的方法都可以被子類重寫,被重寫的這些方法可以通過cglib進行攔截增強

cglib整個過程如下

  1. Cglib根據父類,Callback, Filter 及一些相關信息生成key
  2. 然後根據key 生成對應的子類的二進制表現形式
  3. 使用ClassLoader裝載對應的二進制,生成Class對象,並緩存
  4. 最後實例化Class對象,並緩存

案例1:爲多個接口創建代理

代碼比較簡單,定義了2個接口,然後通過cglib來創建一個代理類,代理類會實現這2個接口,通過setCallback來對2個接口的方法進行增強。


 
public class CglibTest1 {     interface IService1 {         void m1();     }     interface IService2 {         void m2();     }     public static void main(String[] args) {         Enhancer enhancer = new Enhancer();         //設置代理對象需要實現的接口         enhancer.setInterfaces(new Class[]{IService1.class, IService2.class});         //通過Callback來對被代理方法進行增強         enhancer.setCallback(new MethodInterceptor() {             @Override             public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {                 System.out.println("方法:" + method.getName());                 return null;             }         });         Object proxy = enhancer.create();         if (proxy instanceof IService1) {             ((IService1) proxy).m1();         }         if (proxy instanceof IService2) {             ((IService2) proxy).m2();         }         //看一下代理對象的類型         System.out.println(proxy.getClass());         //看一下代理類實現的接口         System.out.println("創建代理類實現的接口如下:");         for (Class<?> cs : proxy.getClass().getInterfaces()) {             System.out.println(cs);         }     } }

運行輸出


 
方法:m1 方法:m2 class com.javacode2018.aop.demo2.CglibTest1$IService1$$EnhancerByCGLIB$$1d32a82 創建代理類實現的接口如下: interface com.javacode2018.aop.demo2.CglibTest1$IService1 interface com.javacode2018.aop.demo2.CglibTest1$IService2 interface org.springframework.cglib.proxy.Factory

上面創建的代理類相當於下面代碼


 
public class CglibTest1$IService1$$EnhancerByCGLIB$$1d32a82 implements IService1, IService2 {     @Override     public void m1() {         System.out.println("方法:m1");     }     @Override     public void m2() {         System.out.println("方法:m2");     } }

案例2:爲類和接口同時創建代理

下面定義了2個接口:IService1和IService2,2個接口有個實現類:Service,然後通過cglib創建了個代理類,實現了這2個接口,並且將Service類作爲代理類的父類。


 
public class CglibTest2 {     interface IService1 {         void m1();     }     interface IService2 {         void m2();     }     public static class Service implements IService1, IService2 {         @Override         public void m1() {             System.out.println("m1");         }         @Override         public void m2() {             System.out.println("m2");         }     }     public static void main(String[] args) {         Enhancer enhancer = new Enhancer();         //設置代理類的父類         enhancer.setSuperclass(Service.class);         //設置代理對象需要實現的接口         enhancer.setInterfaces(new Class[]{IService1.class, IService2.class});         //通過Callback來對被代理方法進行增強         enhancer.setCallback(new MethodInterceptor() {             @Override             public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {                 long startime = System.nanoTime();                 Object result = methodProxy.invokeSuper(o, objects); //調用父類中的方法                 System.out.println(method + ",耗時(納秒):" + (System.nanoTime() - startime));                 return result;             }         });         //創建代理對象         Object proxy = enhancer.create();         //判斷代理對象是否是Service類型的         System.out.println("proxy instanceof Service" + (proxy instanceof Service));         if (proxy instanceof Service) {             Service service = (Service) proxy;             service.m1();             service.m2();         }         //看一下代理對象的類型         System.out.println(proxy.getClass());         //輸出代理對象的父類         System.out.println("代理類的父類:" + proxy.getClass().getSuperclass());         //看一下代理類實現的接口         System.out.println("創建代理類實現的接口如下:");         for (Class<?> cs : proxy.getClass().getInterfaces()) {             System.out.println(cs);         }     } }

運行輸出


 
proxy instanceof Servicetrue m1 public void com.javacode2018.aop.demo2.CglibTest2$Service.m1(),耗時(納秒):14219700 m2 public void com.javacode2018.aop.demo2.CglibTest2$Service.m2(),耗時(納秒):62800 class com.javacode2018.aop.demo2.CglibTest2$Service$$EnhancerByCGLIB$$80494536 代理類的父類:class com.javacode2018.aop.demo2.CglibTest2$Service 創建代理類實現的接口如下: interface com.javacode2018.aop.demo2.CglibTest2$IService1 interface com.javacode2018.aop.demo2.CglibTest2$IService2 interface org.springframework.cglib.proxy.Factory

輸出中可以代理對象的類型是:


 
class com.javacode2018.aop.demo2.CglibTest2$Service$$EnhancerByCGLIB$$80494536

帶有$$EnhancerByCGLIB$$字樣的,在調試spring的過程中,發現有這樣字樣的,基本上都是cglib創建的代理對象。

上面創建的代理類相當於下面代碼


 
public class CglibTest2$Service$$EnhancerByCGLIB$$80494536 extends Service implements IService1, IService2 {     @Override     public void m1() {         long starttime = System.nanoTime();         super.m1();         System.out.println("方法m1,耗時(納秒):" + (System.nanoTime() - starttime));     }     @Override     public void m2() {         long starttime = System.nanoTime();         super.m1();         System.out.println("方法m1,耗時(納秒):" + (System.nanoTime() - starttime));     } }

案例3:LazyLoader的使用

LazyLoader是cglib用於實現懶加載的callback。當被增強bean的方法初次被調用時,會觸發回調,之後每次再進行方法調用都是對LazyLoader第一次返回的bean調用,hibernate延遲加載有用到過這個。

看案例吧,通過案例理解容易一些。


 
public class LazyLoaderTest1 {     public static class UserModel {         private String name;         public UserModel() {         }         public UserModel(String name) {             this.name = name;         }         public void say() {             System.out.println("你好:" + name);         }     }     public static void main(String[] args) {         Enhancer enhancer = new Enhancer();         enhancer.setSuperclass(UserModel.class);         //創建一個LazyLoader對象         LazyLoader lazyLoader = new LazyLoader() {             @Override             public Object loadObject() throws Exception {                 System.out.println("調用LazyLoader.loadObject()方法");                 return new UserModel("路人甲java");             }         };         enhancer.setCallback(lazyLoader);         Object proxy = enhancer.create();         UserModel userModel = (UserModel) proxy;         System.out.println("第1次調用say方法");         userModel.say();         System.out.println("第1次調用say方法");         userModel.say();     } }

運行輸出


 
第1次調用say方法 調用LazyLoader.loadObject()方法 你好:路人甲java 第1次調用say方法 你好:路人甲java

當第1次調用say方法的時候,會被cglib攔截,進入lazyLoader的loadObject內部,將這個方法的返回值作爲say方法的調用者,loadObject中返回了一個路人甲Java的UserModel,cglib內部會將loadObject方法的返回值和say方法關聯起來,然後緩存起來,而第2次調用say方法的時候,通過方法名去緩存中找,會直接拿到第1次返回的UserModel,所以第2次不會進入到loadObject方法中了。

將下代碼拆分開來


 
System.out.println("第1次調用say方法"); userModel.say(); System.out.println("第1次調用say方法"); userModel.say();

相當於下面的代碼


 
System.out.println("第1次調用say方法"); System.out.println("調用LazyLoader.loadObject()方法"); userModel = new UserModel("路人甲java"); userModel.say(); System.out.println("第1次調用say方法"); userModel.say();

下面通過LazyLoader實現延遲加載的效果。

案例4:LazyLoader實現延遲加載

博客的內容一般比較多,需要用到內容的時候,我們再去加載,下面來模擬博客內容延遲加載的效果。


 
public class LazyLoaderTest2 {     //博客信息     public static class BlogModel {         private String title;         //博客內容信息比較多,需要的時候再去獲取         private BlogContentModel blogContentModel;         public BlogModel() {             this.title = "spring aop詳解!";             this.blogContentModel = this.getBlogContentModel();         }         private BlogContentModel getBlogContentModel() {             Enhancer enhancer = new Enhancer();             enhancer.setSuperclass(BlogContentModel.class);             enhancer.setCallback(new LazyLoader() {                 @Override                 public Object loadObject() throws Exception {                     //此處模擬從數據庫中獲取博客內容                     System.out.println("開始從數據庫中獲取博客內容.....");                     BlogContentModel result = new BlogContentModel();                     result.setContent("歡迎大家和我一起學些spring,我們一起成爲spring高手!");                     return result;                 }             });             return (BlogContentModel) enhancer.create();         }     }     //表示博客內容信息     public static class BlogContentModel {         //博客內容         private String content;         public String getContent() {             return content;         }         public void setContent(String content) {             this.content = content;         }     }     public static void main(String[] args) {         //創建博客對象         BlogModel blogModel = new BlogModel();         System.out.println(blogModel.title);         System.out.println("博客內容");         System.out.println(blogModel.blogContentModel.getContent()); //@1     } }

@1:調用blogContentModel.getContent()方法的時候,纔會通過LazyLoader#loadObject方法從db中獲取到博客內容信息

運行輸出


 
spring aop詳解! 博客內容 開始從數據庫中獲取博客內容..... 歡迎大家和我一起學些spring,我們一起成爲spring高手!

案例5:Dispatcher

Dispatcher和LazyLoader作用很相似,區別是用Dispatcher的話每次對增強bean進行方法調用都會觸發回調。

看案例代碼


 
public class DispatcherTest1 {     public static class UserModel {         private String name;         public UserModel() {         }         public UserModel(String name) {             this.name = name;         }         public void say() {             System.out.println("你好:" + name);         }     }     public static void main(String[] args) {         Enhancer enhancer = new Enhancer();         enhancer.setSuperclass(LazyLoaderTest1.UserModel.class);         //創建一個Dispatcher對象         Dispatcher dispatcher = new Dispatcher() {             @Override             public Object loadObject() throws Exception {                 System.out.println("調用Dispatcher.loadObject()方法");                 return new LazyLoaderTest1.UserModel("路人甲java," + UUID.randomUUID().toString());             }         };         enhancer.setCallback(dispatcher);         Object proxy = enhancer.create();         LazyLoaderTest1.UserModel userModel = (LazyLoaderTest1.UserModel) proxy;         System.out.println("第1次調用say方法");         userModel.say();         System.out.println("第1次調用say方法");         userModel.say();     } }

運行輸出


 
第1次調用say方法 調用Dispatcher.loadObject()方法 你好:路人甲java,514f911e-06ac-4e3b-aee4-595f82c16a5f 第1次調用say方法 調用Dispatcher.loadObject()方法 你好:路人甲java,bc062990-bc16-4226-97e3-b1b321a03468

案例6:通過Dispathcer對類擴展一些接口

下面有個UserService類,我們需要對這個類創建一個代理。

代碼中還定義了一個接口:IMethodInfo,用來統計被代理類的一些方法信息,有個實現類:DefaultMethodInfo。

通過cglib創建一個代理類,父類爲UserService,並且實現IMethodInfo接口,將接口IMethodInfo所有方法的轉發給DefaultMethodInfo處理,代理類中的其他方法,轉發給其父類UserService處理。

這個代碼相當於對UserService這個類進行了增強,使其具有了IMethodInfo接口中的功能。


 
public class DispatcherTest2 {     public static class UserService {         public void add() {             System.out.println("新增用戶");         }         public void update() {             System.out.println("更新用戶信息");         }     }     //用來獲取方法信息的接口     public interface IMethodInfo {         //獲取方法數量         int methodCount();         //獲取被代理的對象中方法名稱列表         List<String> methodNames();     }     //IMethodInfo的默認實現     public static class DefaultMethodInfo implements IMethodInfo {         private Class<?> targetClass;         public DefaultMethodInfo(Class<?> targetClass) {             this.targetClass = targetClass;         }         @Override         public int methodCount() {             return targetClass.getDeclaredMethods().length;         }         @Override         public List<String> methodNames() {             return Arrays.stream(targetClass.getDeclaredMethods()).                     map(Method::getName).                     collect(Collectors.toList());         }     }     public static void main(String[] args) {         Class<?> targetClass = UserService.class;         Enhancer enhancer = new Enhancer();         //設置代理的父類         enhancer.setSuperclass(targetClass);         //設置代理需要實現的接口列表         enhancer.setInterfaces(new Class[]{IMethodInfo.class});         //創建一個方法統計器         IMethodInfo methodInfo = new DefaultMethodInfo(targetClass);         //創建會調用器列表,此處定義了2個,第1個用於處理UserService中所有的方法,第2個用來處理IMethodInfo接口中的方法         Callback[] callbacks = {                 new MethodInterceptor() {                     @Override                     public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {                         return methodProxy.invokeSuper(o, objects);                     }                 },                 new Dispatcher() {                     @Override                     public Object loadObject() throws Exception {                         /**                          * 用來處理代理對象中IMethodInfo接口中的所有方法                          * 所以此處返回的爲IMethodInfo類型的對象,                          * 將由這個對象來處理代理對象中IMethodInfo接口中的所有方法                          */                         return methodInfo;                     }                 }         };         enhancer.setCallbacks(callbacks);         enhancer.setCallbackFilter(new CallbackFilter() {             @Override             public int accept(Method method) {                 //當方法在IMethodInfo中定義的時候,返回callbacks中的第二個元素                 return method.getDeclaringClass() == IMethodInfo.class ? 1 : 0;             }         });         Object proxy = enhancer.create();         //代理的父類是UserService         UserService userService = (UserService) proxy;         userService.add();         //代理實現了IMethodInfo接口         IMethodInfo mf = (IMethodInfo) proxy;         System.out.println(mf.methodCount());         System.out.println(mf.methodNames());     } }

運行輸出


 
新增用戶 2 [add, update]

案例7:cglib中的NamingPolicy接口

接口NamingPolicy表示生成代理類的名字的策略,通過Enhancer.setNamingPolicy方法設置命名策略。

默認的實現類:DefaultNamingPolicy, 具體cglib動態生成類的命名控制。

DefaultNamingPolicy中有個getTag方法。

DefaultNamingPolicy生成的代理類的類名命名規則:


 
被代理class name + "$$" + 使用cglib處理的class name + "ByCGLIB" + "$$" + key的hashcode

如:


 
com.javacode2018.aop.demo2.DispatcherTest2$UserService$$EnhancerByCGLIB$$e7ec0be5@17d10166

自定義NamingPolicy,通常會繼承DefaultNamingPolicy來實現,spring中默認就提供了一個,如下


 
public class SpringNamingPolicy extends DefaultNamingPolicy {     public static final SpringNamingPolicy INSTANCE = new SpringNamingPolicy();     @Override     protected String getTag() {         return "BySpringCGLIB";     } }

案例代碼


 
public class NamingPolicyTest {     public static void main(String[] args) {         Enhancer enhancer = new Enhancer();         enhancer.setSuperclass(NamingPolicyTest.class);         enhancer.setCallback(NoOp.INSTANCE);         //通過Enhancer.setNamingPolicy來設置代理類的命名策略         enhancer.setNamingPolicy(new DefaultNamingPolicy() {             @Override             protected String getTag() {                 return "-test-";             }         });         Object proxy = enhancer.create();         System.out.println(proxy.getClass());     } }

輸出


 
class com.javacode2018.aop.demo2.NamingPolicyTest$$Enhancer-test-$$5946713

Objenesis:實例化對象的一種方式

先來看一段代碼,有一個有參構造函數:


 
public static class User {     private String name;     public User(String name) {         this.name = name;     }     @Override     public String toString() {         return "User{" +                 "name='" + name + '\'' +                 '}';     } }

大家來思考一個問題:如果不使用這個有參構造函數的情況下,如何創建這個對象?

通過反射?大家可以試試,如果不使用有參構造函數,是無法創建對象的。

cglib中提供了一個接口:Objenesis,通過這個接口可以解決上面這種問題,它專門用來創建對象,即使你沒有空的構造函數,都木有問題,它不使用構造方法創建Java對象,所以即使你有空的構造方法,也是不會執行的。

用法比較簡單:


 
@Test public void test1() {     Objenesis objenesis = new ObjenesisStd();     User user = objenesis.newInstance(User.class);     System.out.println(user); }

輸出


 
User{name='null'}

大家可以在User類中加一個默認構造函數,來驗證一下上面的代碼會不會調用默認構造函數?


 
public User() {     System.out.println("默認構造函數"); }

再次運行會發現,並不會調用默認構造函數。

如果需要多次創建User對象,可以寫成下面方式重複利用


 
@Test public void test2() {     Objenesis objenesis = new ObjenesisStd();     ObjectInstantiator<User> userObjectInstantiator = objenesis.getInstantiatorOf(User.class);     User user1 = userObjectInstantiator.newInstance();     System.out.println(user1);     User user2 = userObjectInstantiator.newInstance();     System.out.println(user2);     System.out.println(user1 == user2); }

運行輸出


 
User{name='null'} User{name='null'} false

代碼位置


 
com.javacode2018.aop.demo2.CreateObjectTest

總結

  1. 代理這2篇文章是spring aop的基礎,基礎牢靠了,才能走的更遠,大家一定要將這2篇文章中的內容吃透,全面掌握jdk動態代理和cglib代理的使用
  2. 這些知識點spring aop中全部都用到了,大家消化一下,下一篇講解spring aop具體是如何玩的

Spring作爲現在最流行的java 開發技術,其內部源碼設計非常優秀。如果你不會Spring,那麼很可能面試官會讓你回家等通知。

Spring是什麼?

有一個工地,幾百號人在用鐵鍬鏟子挖坑。

如果開一輛挖掘機來,用一天時間乾的活就相當於一個工人一個月的工作量。而且這個挖掘機是免費開源的,不用花錢買,僅僅需要學習掌握如何操作。

你會如何選擇?

這幾百號人的工地就是企業應用項目實施團隊,而挖掘機就是Spring。

Spring框架爲開發Java應用程序提供了全面的基礎架構支持。Spring包含了一些很好的功能,如依賴注入和開箱即用的模塊:

Spring JDBC

Spring MVC

Spring Security

Spring AOP

Spring ORM

Spring Test

這些模塊能極大縮短應用程序的開發時間,提高我們的工作效率。

Spring底層到底要看什麼?以下是大神整理的學習筆記,給大家分享一下,希望可以對你掌握Spring有所幫助。(xmind格式可在文末獲取)

 

Spring學習筆記(完整內容在xmind文件中)

但是現在很多程序員對於Spring的理解只停留在很淺的層面。很多人只關注自己用的那部分代碼的邏輯,而並不真正去理解框架。

如果你不懂Spring,那麼大廠面試官也不會懂你爲什麼敢來面試?

看騰訊技術大牛帶你玩轉Spring全家桶,贈三本Spring實戰篇電子文檔

背景介紹

毋庸置疑,Spring 早已成爲 Java 後端開發事實上的行業標準,無數的公司選擇 Spring 作爲基礎的開發框架,大部分 Java 後端程序員在日常工作中也會接觸到Spring ,因此,如何用好 Spring ,也就成爲 Java程序員的必修課之一。

同時,Spring Boot 和 Spring Cloud的出現,可以幫助工程師更好地基於 Spring 及各種基礎設施來快速搭建系統,可以說,它們的誕生又一次解放了大家的生產力。

因此,Spring Boot 和 Spring Cloud已成爲 Spring 生態中不可或缺的一環。想成爲一名合格的Java 後端工程師,Spring Framework、Spring Boot、Spring Cloud 這三者必須都牢牢掌握。

今天樓主就給大家分享Spring,Spring Boot,Spring Cloud的電子書學習資料!

轉發文章並關注樓主,然後私信我回復【架構書籍】領取Spring全家桶實戰文檔

內容目錄

【Spring實戰】

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

【深入實踐Spring Boot2.x】

 

 

 

 

 

【Spring Cloud微服務實戰】

 

 

 

 

 

 

轉發文章並關注樓主,然後私信我【架構書籍】即可免費領取Spring全家桶實戰文檔

資料真實有效,絕不弄虛作假!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章