1

Spring

What is Spring ?

Spring是一个以IoC(Inversion of Control,控制反转)和AOP(Aspect OrientedProgramming)为内核的框架。IoC是Spring的基础。IoC实现的是一种控制,简单地说,就是以前调用new构造方法来创建对象,现在变成使用Spring来创建对象。

spring 容器

  • Spring 容器是 Spring 框架的核心。容器将创建对象,把它们连接在一起,配置它们,

并管理他们的整个生命周期从创建到销毁。Spring 容器使用依赖注入(DI)来管理组成一个应用程序的组件。这些对象被称为 Spring Beans

  • Spring IoC 容器利用 Java 的 POJO 类和配置元数据来生成完全配置和可执行的系统或应用程序
  • 通过阅读配置元数据提供的指令,容器知道对哪些对象进行实例化,配置和组装。配置元数据可以通过 XML,Java 注释或 Java 代码来表示
  • 通常new一个类的实例,控制权由码农控制,而"控制反转"是指new实例工作不由码农来做而是交给Spring容器来做。
  • Spring中IOC容器的 BeanFactory (简单实现) 容器和有ApplicationContext(包含了BeanFactory的功能) 容器
  • Spring IoC容器是一个管理Bean的容器,在Spring的定义中,它要求所有的IoC容器都需要实现接口BeanFactory,它是一个顶级容器接口,下边是源码,由于BeanFactory的功能还不够强大,因此在BeanFactory的基础上,还设计了一个更为高级的接口ApplicationContext
  • ApplicationContext接口通过继承上级接口,进而继承BeanFactory接口,但是在BeanFactory的基础上,扩展了消息国际化接口(MessageSource)、环境可配置接口(EnvironmentCapable)、应用事件发布接口(ApplicationEventPublisher)和资源模式解析接口(ResourcePatternResolver),所以它的功能会更为强大

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface BeanFactory {
  // 前缀
  String FACTORY_BEAN_PREFIX = "&";

  // 多个getbean方法
  Object getBean(String var1) throws BeansException;

  <T> T getBean(String var1, Class<T> var2) throws BeansException;

  Object getBean(String var1, Object... var2) throws BeansException;

  <T> T getBean(Class<T> var1) throws BeansException;

  <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

  <T> ObjectProvider<T> getBeanProvider(Class<T> var1);

  <T> ObjectProvider<T> getBeanProvider(ResolvableType var1);
  // 是否包含bean
  boolean containsBean(String var1);
  // bean是否是单例
  boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;
  // bean是否原型
  boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;

  // 是否类型匹配 
  boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

  boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;
  // 获取bean的类型
  @Nullable
  Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

  @Nullable
  Class<?> getType(String var1, boolean var2) throws NoSuchBeanDefinitionException;

  // 获取bean的别名
  String[] getAliases(String var1);
}

控制反转

  • IoC理论上是借助于“第三方”实现具有依赖关系对象之间的解耦
  • 即把各个对象类封装之后,通过IoC容器来关联这些对象类。这样对象之间就通过IoC容器进行联系,而对象之间没有直接联系

依赖注入

  • DI是Dependency Inject的缩写,译为“依赖注入”。
  • 所谓依赖注入,就是由IoC容器在运行期间动态地将某种依赖关系注入对象之中
// TextEditor 类依赖 SpellChecker 类,并实例化 SpellChecker 类
public class TextEditor {
   private SpellChecker spellChecker;  
   public TextEditor() {
      spellChecker = new SpellChecker();
   }
}

// 在spring中,可能是这样,构造函数注入
// TextEditor 不实例化 SpellChecker类,控制权交给Spring 
public class TextEditor {
   private SpellChecker spellChecker;
   public TextEditor(SpellChecker spellChecker) {
      this.spellChecker = spellChecker;
   }
}

spring bean

  • Spring如同一个工厂,用于生产和管理Spring容器中的Bean。要使用这个工厂,需要开发者对Spring的配置文件进行配置
  • bean 是一个被实例化,组装,并通过 Spring IoC 容器所管理的对象
  • bean是由用容器提供的配置元数据创建
<bean id="helloWorld"
      class="com.huahuadavids.wehcat.pojo.HelloWorld"
      lazy-init="true"         // lazy-init="true" 延迟初始化的 bean 告诉 IoC 容器在它第一次被请求时,而不是在启动时去创建一个 bean 实例
      init-method="initData"   // 在 bean 的所有必需的属性被容器设置之后,调用回调方法
      destroy-method="destroy" // 当包含该 bean 的容器被销毁时,使用回调方法
>
    <property name="message" value="Hello huahuadavids!"/>
</bean>

// java 源文件
// 第一步利用框架提供的 XmlBeanFactory() API 去生成工厂 bean 以及利用 ClassPathResource() API 去加载在路径 CLASSPATH 下可用的 bean 配置文件。
// XmlBeanFactory() API 负责创建并初始化所有的对象,即在配置文件中提到的 bean
XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));

// 第二步利用第一步生成的 bean 工厂对象的 getBean() 方法得到所需要的 bean。
// 这个方法通过配置文件中的 bean ID 来返回一个真正的对象,该对象最后可以用于实际的对象
HelloWorld obj = (HelloWorld) factory.getBean("helloWorld");
obj.getMessage();

// FileSystemXmlApplicationContext 是从文件系统找 bean的配置
// ApplicationContext context = new FileSystemXmlApplicationContext("/Users/davidzhang/Desktop/spring-starter/src/main/resources/beans.xml");
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
HelloWorld obj1 = (HelloWorld) context.getBean("helloWorld");
obj1.getMessage();

spring的单元测试 更方便的测试bean

这里需要一个包 spring test, spring boot 默认有了这个包


import org.junit.runner.RunWith;
import org.junit.Test;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

// 在测试文件中 这么使用 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml") 指定配置文件 
public class AOPTest {

  @Resource(name="productDao")
  private ProductDao productDao;

  @Test
  public void demo1(){
    productDao.save();
    System.out.println("???");
  }
}

spring bean的使用过程

  1. 读取配置信息
  2. 实例化bean
  3. 把bean实例放到spring容器中
  4. 使用bean,从bean缓存池中取

spring bean的scope

  • 对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域
  • singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,默认值

    // 两次输出的信息一样,证明 obj1 和 obj2是一样的,单例模式 
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    HelloWorld obj1 = (HelloWorld) context.getBean("helloWorld");
    obj1.setMessage("huhu");
    obj1.getMessage();
    HelloWorld obj2 = (HelloWorld) context.getBean("helloWorld");
    obj2.getMessage();
  • prototype 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()

bean的生命周期

  • 就是 Bean的定义——Bean的初始化——Bean的使用——Bean的销毁

bean初始化拦截器

// 1. 实现BeanPostProcessor 接口 
public class MyBeanPostProcessor implements BeanPostProcessor {

  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    Class beanClass = bean.getClass();
    if (beanClass == HelloWorld.class) {
      System.out.println("HelloWorld bean 对象初始化之前······");
    }
    return bean;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    System.out.println("HelloWorld bean 对象初始化之后······");
    return bean;
  }
}

// 2. xml中配置 这表示应用于所有的bean 
<bean class="com.huahuadavids.wehcat.pojo.MyBeanPostProcessor" />

bean的定义继承

  • 就是继承一个bean的一些配置信息,而不是java的继承
// 1 两个类 
public class HelloWorld implements InitializingBean {

  private String message1;

  public void  getMessage2() {
    System.out.println("Hello World Message2 : " + message2);
  }

  public void setMessage2(String message2) {
    this.message2 = message2;
  }

  private String message2;

  public void setMessage1(String message1) {
    this.message1 = message1;
  }
  
  public void getMessage() {
    System.out.println("Hello World Message : " + message1);
  }
}

public class HelloIndia {
  private String message1;
  private String message2;
  private String message3;

  public void setMessage1(String message){
    this.message1  = message;
  }

  public void setMessage2(String message){
    this.message2  = message;
  }

  public void setMessage3(String message){
    this.message3  = message;
  }

  public void getMessage1(){
    System.out.println("India Message1 : " + message1);
  }

  public void getMessage2(){
    System.out.println("India Message2 : " + message2);
  }

  public void getMessage3(){
    System.out.println("India Message3 : " + message3);
  }
}

 // 2 xml的bean配置
 <bean id="helloWorld" class="com.huahuadavids.wehcat.pojo.HelloWorld">
        <property name="message1" value="world-message1"/>
        <property name="message2" value="world-message2"/>
    </bean>
    <bean id="helloIndia" class="com.huahuadavids.wehcat.pojo.HelloIndia" parent="beanTeamplate">
        <property name="message1" value="HelloIndia-message1"/>
        <property name="message3" value="HelloIndia-message3"/>
    </bean>
    <bean class="com.huahuadavids.wehcat.pojo.MyBeanPostProcessor" />
  • 继承模板
<bean id="beanTeamplate" abstract="true">
    <property name="message1" value="Hello bbbb!"/>
    <property name="message2" value="Hello aaaa World!"/>
    <property name="message3" value="Namaste India!"/>
</bean>
<bean id="helloIndia" class="com.demo.HelloIndia" parent="beanTeamplate">
  <property name="message1" value="Hello India!"/>
  <property name="message3" value="Namaste India!"/>
</bean>

Spring 基于构造函数的依赖注入

// 1 两个互相依赖的类
public class TextEditor {
   private SpellChecker spellChecker;
   public TextEditor(SpellChecker spellChecker) {
      System.out.println("Inside TextEditor constructor." );
      this.spellChecker = spellChecker;
   }
   public void spellCheck() {
      spellChecker.checkSpelling();
   }
}
public class SpellChecker {
  public SpellChecker(){
    System.out.println("SpellChecker constructor" );
  }
  public void checkSpelling() {
    System.out.println("SpellChecker checkSpelling..." );
  }
}

// 2. bean的xml配置
<!-- Definition for textEditor bean -->
<bean id="textEditor" class="com.demo.TextEditor">
   <constructor-arg ref="spellChecker"/>
</bean>

<!-- Definition for spellChecker bean -->
<bean id="spellChecker" class="com.demo.SpellChecker">
</bean>

// 3. 使用
AbstractApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
TextEditor te = (TextEditor) context.getBean("textEditor");
te.spellCheck();

// 4 如果构造函数有多个参数,顺序问题,就bean中顺序一样即可 ,最好的方式是指定 index
<bean id="textEditor" class="com.demo.TextEditor">
   <constructor-arg ref="spellChecker1" index="0"/>
   <constructor-arg ref="spellChecker2" index="1"/>
</bean>

// 5. bean 配置可以指定参数的类型
<bean id="exampleBean" class="examples.ExampleBean">
   <constructor-arg type="int" value="2001"/>
   <constructor-arg type="java.lang.String" value="Zara"/>
</bean>

Spring 基于设值函数的依赖注入

当容器调用一个无参的构造函数或一个无参的静态 factory 方法来初始化你的 bean 后,通过容器在你的 bean 上调用设值函数

// 1 修改 TextEditor 类
public class TextEditor {
  public SpellChecker getSpellChecker() {
    return spellChecker;
  }

  public void setSpellChecker(SpellChecker spellChecker) {
    System.out.println("SpellChecker setter");
    this.spellChecker = spellChecker;
  }

  private SpellChecker spellChecker;

  public void spellCheck() {
    spellChecker.checkSpelling();
  }
}

// 2 修改bean xml的配置 关联类作为一个属性传入
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor">
 <property name="spellChecker" ref="spellChecker"/>
</bean>

p-namespace 配置XML

// 加入xml的命名空间
xmlns:p="http://www.springframework.org/schema/p"
       
// 上述配置可以写成  
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor" p:spellChecker-ref="spellChecker" />
    

内部Bean bean内部配置 Bean

<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor">
   <property name="spellChecker">
       <bean id="spellChecker" class="com.starbugs.wehcat.pojo.SpellChecker" />
   </property>
</bean>

注入集合
集合中还可以注入类

// 1 定义pojo 
public class JavaBeanCollection {
  private List addressList;
  private Set  addressSet;
  private Map  addressMap;
  private Properties addressProp;
  
  public void setAddressList(List addressList) {
    this.addressList = addressList;
  }
  
  public List getAddressList() {
    System.out.println("List Elements :"  + addressList);
    return addressList;
  }
  
  public void setAddressSet(Set addressSet) {
    this.addressSet = addressSet;
  }
  
  public Set getAddressSet() {
    System.out.println("Set Elements :"  + addressSet);
    return addressSet;
  }

  public void setAddressMap(Map addressMap) {
    this.addressMap = addressMap;
  }
  
  public Map getAddressMap() {
    System.out.println("Map Elements :"  + addressMap);
    return addressMap;
  }
  
  public void setAddressProp(Properties addressProp) {
    this.addressProp = addressProp;
  }
  
  public Properties getAddressProp() {
    System.out.println("Property Elements :"  + addressProp);
    return addressProp;
  }
}
// 2 bean的xml配置
 <bean id="javaBeanCollection" class="com.starbugs.wehcat.pojo.JavaBeanCollection">
        <property name="addressList">
            <list>
                <value>list1</value>
                <value>list2</value>
                <value>list3</value>
            </list>
        </property>
        <property name="addressSet">
            <set>
               <ref bean="address2"/>
                <value>set1</value>
                <value>set2</value>
                <value>set3</value>
            </set>
        </property>
        <property name="addressMap">
            <map>
                <entry key="11" value="value-11" />
                <entry key="22" value="value-22" />
                <entry key ="three" value-ref="address2"/>
            </map>
        </property>
        <property name="addressProp">
            <props>
                <prop key="one">INDIA</prop>
                <prop key="two">Pakistan</prop>
                <prop key="three">USA</prop>
                <prop key="four">USA</prop>
            </props>
        </property>
    </bean>

spring 自动装配

byName 、 byType

// 1 定义类 使用setter注入 
public class TextEditor {
  private SpellChecker spellChecker;
  private String name;
  public void setSpellChecker( SpellChecker spellChecker ){
    System.out.println("TextEditor setSpellChecker");
    this.spellChecker = spellChecker;
  }
  public SpellChecker getSpellChecker() {
    return spellChecker;
  }
  public void setName(String name) {
    System.out.println("TextEditor setName");
    this.name = name;
  }
  public String getName() {
    return name;
  }
  public void spellCheck() {
    spellChecker.checkSpelling();
  }
}
// 2 xml 的Bean 配置
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor" autowire="byName">
    <property name="name" value="huahua" />
</bean>

 <bean id="spellChecker" class="com.starbugs.wehcat.pojo.SpellChecker" />

由构造函数自动装配

// 1 类修改为构造方法注入

public TextEditor( SpellChecker spellChecker, String name ) {
  this.spellChecker = spellChecker;
  this.name = name;
}
// 2 修改xml配置
<bean id="textEditor" class="com.starbugs.wehcat.pojo.TextEditor" autowire="constructor">
    <constructor-arg ref="spellChecker"/>
    <constructor-arg value="huhuhu"/>
</bean>

基于注解的配置

注解来配置依赖注入,会被XML注入所覆盖。

@Required
注释应用于 bean 属性的 setter 方法,它表明受影响的 bean 属性在配置时必须放在 XML 配置文件中

public class Student {
  private Integer age;
  private String name;

  @Required
  public void setAge(Integer age) {
    this.age = age;
  }
  public Integer getAge() {
    return age;
  }

  @Required
  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return name;
  }
}

// 标注了 @required的,必须要出现在xml的property 配置中
<bean id="student" class="com.starbugs.wehcat.pojo.Student">
   <property name="name" value="huahua"/>
   <property name="age" value="22"/>
</bean>

@Autowired

// @Autowired 修饰 setter ,相当于 配置了 byType 方式自动连接 
public class TextEditor {
   private SpellChecker spellChecker;
   @Autowired
   public void setSpellChecker( SpellChecker spellChecker ){
      this.spellChecker = spellChecker;
   }
   public SpellChecker getSpellChecker( ) {
      return spellChecker;
   }
   public void spellCheck() {
      spellChecker.checkSpelling();
   }
}

// 2 @Autowired 修饰属性,消除了setter,但是并不是为类自动产生了一个 setter 
@Autowired
private SpellChecker spellChecker;
 
// 3 @Autowired默认必须有Integer 这个bean,否则就报错,加上required = false就不会报错了 
@Autowired(required = false)
public void setAge(Integer age) {
  this.age = age;
}

@Qualifier
当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配

// 1 xml配置 
<bean id="student" class="com.starbugs.wehcat.pojo.Student">
     <property name="name" value="huahua"/>
     <property name="age" value="22"/>
 </bean>

 <bean id="student1" class="com.starbugs.wehcat.pojo.Student">
     <property name="name" value="name1"/>
     <property name="age" value="111"/>
 </bean>
// 2 使用类
public class Profile {

  @Autowired
  @Qualifier("student1")
  private Student student;

  public Profile(){
    System.out.println("Inside Profile constructor." );
  }

  public void printAge() {
    System.out.println("Age : " + student.getAge() );
  }

  public void printName() {
    System.out.println("Name : " + student.getName() );
  }
}

@Component
标注一个类,表示声明为一个spring配置的bean

// 1 xml的bean配置 开启组件扫描 
<context:component-scan base-package="com.starbugs.wehcat.pojo" />

// 2 pojo类中声明bean的id 
@Component("profile")

// 3 具体使用
AbstractApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Profile profile = (Profile) context.getBean("profile");
profile.printAge();
profile.printName();

@ComponentScan 组件扫描注解
spring boot的SpringBootApplication注解,默认集成了这个注解


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
// 一个类中可重复定义
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
  // 定义扫描的包
  @AliasFor("basePackages")
  String[] value() default {};
  // 定义扫描的包
  @AliasFor("value")
  String[] basePackages() default {};
  
  // 定义扫描的类
  Class<?>[] basePackageClasses() default {};

   // bean name 生成器
  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
  // 作用域解析器
  Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
  // 作用域代理模式 
  ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

  // 资源匹配模式
  String resourcePattern() default "**/*.class";

  // 是否使用默认过滤器 
  boolean useDefaultFilters() default true;

  // 当满足过滤器条件是扫描
  ComponentScan.Filter[] includeFilters() default {};
  
  // 当不满足过滤器条件是扫描
  ComponentScan.Filter[] excludeFilters() default {};

  // 是否延迟初始化
  boolean lazyInit() default false;

  // 定义过滤器 
  @Retention(RetentionPolicy.RUNTIME)
  @Target({})
  public @interface Filter {
    FilterType type() default FilterType.ANNOTATION;

    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    String[] pattern() default {};
  }
}

@PostConstruct 、@PreDestroy

@PostConstruct ===  init-method
@PreDestroy === destroy-method

@Resources
提供一个bean的name字段注入,不可以用在构造函数上

 @Resource(name = "spellChecker")
 private SpellChecker spellChecker;

基于java的配置

  • 使用这种配置,可以不用xml,使用一个类,作为bean的来源
  • 带有 @Configuration 的注解类表示这个类可以使用 Spring IoC 容器作为 bean 定义的来源。
  • @Bean 注解告诉 Spring,一个带有 @Bean 的注解方法将返回一个对象,该对象应该被注册为在 Spring 应用程序上下文中的 bean
  • @Bean注解也可以配置初始化方法和销毁方法
// 1 创建一个配置类 

@Configuration
public class HelloWorldConfig {

  @Bean(initMethod = "init", destroyMethod = "cleanup" )
  public HelloWorld helloWorld(){
    return new HelloWorld();
  }
}

// 2 使用
public static void main(String[] args) {
   ApplicationContext ctx = 
   new AnnotationConfigApplicationContext(HelloWorldConfig.class); 
   HelloWorld helloWorld = ctx.getBean(HelloWorld.class);
   helloWorld.setMessage("Hello World!");
   helloWorld.getMessage();
}
// 3 还可以加载 refresh 多个配置类 
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(HelloWorldConfig.class);
ctx.refresh();
HelloWorld helloWorld = ctx.getBean(HelloWorld.class);

helloWorld.setMessage("Hello World!");
helloWorld.getMessage();

@Import 注解:
@import 注解允许从另一个配置类中加载 @Bean 定义

@Configuration
public class ConfigA {
   @Bean
   public A a() {
      return new A(); 
   }
}

// 这个配置可以使用上一个配置的信息 
@Configuration
@Import(ConfigA.class)
public class ConfigB {
   @Bean
   public B a() {
      return new A(); 
   }
}

spring事件处理

通过 ApplicationEvent 类和 ApplicationListener 接口来提供在 ApplicationContext 中处理事件。如果一个 bean 实现 ApplicationListener,那么每次 ApplicationEvent 被发布到 ApplicationContext 上,那个 bean 会被通知

// 1 实现 ApplicationListener 接口 
public class CStartEventHandler implements ApplicationListener<ContextStartedEvent> {
  @Override
  public void onApplicationEvent(ContextStartedEvent event) {
    System.out.println("ContextStartedEvent Received");
  }
}

// 2 xml中配置 
<bean id="cStartEventHandler" class="com.starbugs.wehcat.demos.CStartEventHandler" />
<bean id="cStopEventHandler" class="com.starbugs.wehcat.demos.CStopEventHandler" />

自定义事件

// 1 新建 事件类
public class CustomEvent extends ApplicationEvent {

  public CustomEvent(Object source) {
    super(source);
  }

  @Override
  public String toString() {
    return "CustomEvent{}";
  }
}

// 2 新建 handler类
public class CustomEventHandler implements ApplicationListener<CustomEvent> {
  @Override
  public void onApplicationEvent(CustomEvent event) {
    System.out.println(event.toString());
  }
}
// 3 新建publisher发布类
public class CustomEventPublisher implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher){
      this.publisher = publisher;
    }
    public void publish() {
      CustomEvent ce = new CustomEvent(this);
      publisher.publishEvent(ce);
    }
}
// 4 配置类到xml里
// 5. 使用 
CustomEventPublisher cvp = (CustomEventPublisher) context.getBean("customEventPublisher");
cvp.publish();
cvp.publish();

AOP

传送门

Spring mvc

传送门

参考资料
  • Spring+Spring MVC+MyBatis从零开始学(吴为胜 杨章伟)
  • 深入浅出Spring Boot 2.x-杨开振

huahuadavids
669 声望26 粉丝

nothing to say, but day day up