SpringBoot零配置原理详解

序言

在正式开始讲解原理之前,着重说明一下如何去学习一门技术。

学习一门新技术,最好的方式就是阅读官网文档、跟着官方文档例子一步一步走。当然如果是为了先感受一下技术的效果,建议先找个例子跑起来,感受感受。

Spring官网

Spring、SpringMVC、SpringBoot三者之间的关系

  • Spring:Spring 是一个开源框架,起初是为解决企业应用开发的复杂性而创建,但是现在已经不止于企业应用。Spring的核心就是提供一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架。
  • Spring MVC:Spring MVC可以理解为Spring和MVC结合的一种开发架构,Spring的概念可以参阅上面所述。MVC是一种将数据业务、显示逻辑和控制逻辑进行分层的设计模式。SpringMVC是用于Web程序的通用框架。
  • SpringBoot
    SpringBoot是用来简化Spring框架应用初始化搭建以及开发的全新框架,比如通过自动化配置省去了大量繁琐的配置文件,因此SpringBoot可以理解成,一个通过简化配置文件来服务于框架的框架。

Spring是最为基础的部分,,提供容器;SpringMVC基于Spring,只是在Spring上面加上了MVC的思想;SpringBoot则是对SpringMVC的简化。

从SpringMVC谈起

上古时代使用SpringMVC的时候都是基于web.xml那种开发方式。常见的几个开发配置文件有

  1. web.xml
  2. applicationContext.xml
  3. spingMVC.xml

下面分别来说说它们的作用,接下来再尝试一个一个去删掉,最终达到SpringBoot零配置的目的。

web.xml

当需要载入多个spring相关的配置文件时,首先加载ContextLoaderListener类,再指定context-param中指定多个spring配置文件,使用逗号分别隔开各个文件。为了使用方便可以将配置文件进行MVC式的分解,配置控制器Bean的配置文件放置在一个xml文件中,server的Bean放在service.xml文件中。

Spring分为多个文件进行分别的配置,其中在servlet-name中如果没有指定init-param属性,那么系统自动寻找的spring配置文件为[servlet-name]-servlet.xml

<servlet-mapping>指定的该servlet接管的url的行为,此处为了简便起见使用.,则表示在URL只要是在本机使用的任何request都是由该dispatchServlet来处理。

加载顺序如下:

  1. 配置<listener>,当web容器(tomcat)启动之后,这里监听就会得到触发。然后初始化Spring环境。而初始化环境就需要确定Spring需要加载哪些东西,通过context-param指定加载的东西。——>什么是初始化环境,就是初始化一个对象。

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/applicationContext.xml</param-value>
      </context-param>
    
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
  2. 配置servlet,也就是springmvc前端控制器。

  <!-- springmvc前端控制器 -->
  <servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- contextConfigLocation配置springmvc加载的配置文件(配置处理器映射器、适配器等等) 如果不配置contextConfigLocation,默认加载的是/WEB-INF/servlet名称-serlvet.xml(springmvc-servlet.xml) -->
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:spring/springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

由于web.xml中要初始化Spring容器,初始化容器里面需要有applicationcontxt.xml所以我们要配置applicationcontxt.xml,其实整个web.xml有很多配置的我只要了这两段,因为剩下的是没必要。

applicationContext.xml

在appcliationContext.xml中,其实只有一行就可以了(不需要连接数据库,仅仅用于测试SpringMVC)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    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-4.0.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">

    <!-- 配置Service扫描 -->
    <context:component-scan base-package="com.xxx.service" />
</beans>

这个配置的作用是扫描你的项目的业务类,比如说这里的servce。

这个配置的作用是扫描你的项目的业务类,比如说这里的servce。

springMVC.xml

因为Web.xml要初始化MVC的前端控制器所以我们还需要配置一个springMVC.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" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <!-- 配置Controller扫描 -->
    <context:component-scan base-package="com.xxx.controller" />

    <!-- 配置注解驱动 -->
    <mvc:annotation-driven />

     <!-- 对静态资源放行  -->
    <mvc:resources location="/assets/" mapping="/assets/**"/>

    <!-- 视图解释器 -->
    <bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

这个配置文件中其实只有配置Controller扫描、配置注解驱动有用,它的作用是用来扫描controller的,下面这一大段东西其实也是可要可不要的,为什么呢,因为下面这一段是视图解析器,假设来写一个不用返回视图的controller:

    @RequestMapping("/test")
    public void testIndex() {
        System.out.println("test Index");
    }

这样不需要返回JSP,假设我的项目中都没有其他方法了,如果我的整个controller里就这一个方法的话,那就不需要这个视图解析器了

所以换言之,配置文件里的配置视图解析器这一段代码就可以省去了。

于是得出结论整个SpringMVC.xml中他主要做了一件事情就是扫描controller。

Tomcat与web.xml的关系

我们知道web应用程序中,我们没有写任何main方法,只把对应的war包放到web容器中就可以运行。而在非web应用程序中,要运行程序必须定义一个main方法入口。那么web程序又是怎么调用的呢?

这里用tomcat举例。在使用tomcat的时候,我们需要定义一个web.xml文件,就如上面使用springmvc一样。前面在讲web.xml的时候提到Listener的作用就是用来加载Spring环境的。

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

Tomcat是由java开发的,那么它必然就有一个main方法。举个例子,Tomcat的启动方式是不是点击Tomcat.exe(java开发也同样可以编译成exe,以jar为例都是一个意思)或者点击编译器上面的启动按钮?你点击Tomcat.exe或者编译器上的启动按钮时,就会运行Tomcat的main方法。

要初始化Spring容器是不是要放在Tomcat的main方法里启动?

因为Tomcat的main方法里会去读取或者parse(解析)web.xml(读取你的那段初始化spring环境的那段配置——listener),这个就无需开发者担心了因为这个是标准的模式,他会找到并解析你项目里的web.xml,而且会找到xml里的一个标签叫 listener,它找到一个类叫ContextLoaderListener的某个方法里面执行下面这行代码

//init spring context new bean(初始化Spring容器)
AnnotationConfigApplicationContext ac=
    new AnnotationConfigApplicationContext(Appconfig.class);

我们可以想象为,当tomcat启动的时候,就会自动的调用这里的ContextLoaderListener,进而实现Spring 环境的初始化。

下面介绍tomcat启动流程

Tomcat启动流程

tomcat启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取<listener><context-param>两个结点。

紧接着,容器创建一个ServletContext(servlet上下文,全局的),这个web项目的所有部分都将共享这个上下文。可以把ServletContext看成是一个Web应用的服务器端组件的共享内存,在ServletContext中可以存放共享数据。ServletContext对象是真正的一个全局对象,凡是web容器中的Servlet都可以访问

容器将<context-param>转换为键值对,并交给servletContext。

容器创建<listener>中的类实例,创建监听器。 listener中ContextLoaderListener监听器的作用就是启动Web容器时,监听servletContext对象的变化,获取servletContext对象的<context-param>,来自动装配ApplicationContext的配置信息。(即加载applicationContext.xml文件)

SpringMVC官网的写法

讲了这么久,我们去看看官网的写法,因为非常精简。接下来我们也是跟着官网的写法将xml文件一个一个去掉。

对应的解释:The following example of the Java configuration registers and initializes the DispatcherServlet, which is auto-detected by the Servlet container (see Servlet Config):

public class MyWebApplicationInitializer  implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

官网的代码只需要上面这点代码就能实现web.xml、applicationContext.xml、springMVC.xml的功能。

下面一步一步来分析

去掉web.xml

去掉listenser

我们知道web.xml的作用就是通过listener在tomcat启动的时候,初始化spring环境。如果可以不通过web.xml来初始化环境,那么就可以不用web.xml。

官网的推荐写法中直接通过AnnotationConfigWebApplicationContext来初始化Spring环境,所以这里的web.xml我们是可以去掉的。

去掉dispathservlet

DispatcherServlet我们可以通过注解来取代xml的方式。

@WebServlet("index")
public class TestServlet extends HttpServlet {

}

所以说这个web.xml这个servlet的配置也是可以去掉的。

但其实Spring的源码做法不是如此,我们要把这个Servlet注册给容器(Tomcat)。——>关于内嵌tomcat,后面会介绍

假设某一天我能得到这个Tomcat对象(SpringBoot就是这样做的),我就调用Tomcat的一个方法比如说是.addServlet(new DispatcherServlet)不就也等于完成了注册这件事情吗?

根据官网的代码

// Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");

registration.setLoadOnStartup(1)的意思就相当于web.xml中<load-on-startup>1</load-on-startup>

去掉applicationContext.xml

按照我们刚刚的分析,这个applicationContext.xml就做了一个事情:扫描业务类。

那我们看看官方文档是怎么去扫描的:

 // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();

核心的就是这句ac.register(AppConfig.class);

假设这类的Appconfig内容如下:


@ComponentScan({"com.test"})
@Configuration
public class AppConfig {

}

官方文档中的例子是:把AppConfig注册给我们的ApplicationContext对象,他就可以在appconfig里面配置信息拿到包名,然后去扫描,于是我们的applicationcontxt.xml也可以删掉了。

去掉springMVCxml

前面我们讲过spring.mvc只有配置Controller扫描、配置注解驱动有用,它的作用是用来扫描controller的,下面这一大段东西其实也是可要可不要的。

所以我们同样可以在AppConfig类中实现扫描注解。

那么和applicationContext.xml完全可以配在一起,为什么不配在一起要分开配置呢?

因为都放在一起的话,老版本会导致事务失效,其实也有办法去解决让他们放在一起不会事物失效,那为什么现在大家还是在分在两个配置文件用呢?因为很多这种网上所讲的配置都是很老的文章了,大概是Spring2.5左右的版本了(那个时候是没办法解决这个问题的)现在是Spring5.X这个问题早就解决了,我们现在如果要配置的话无需分开配置不会导致事物失效。

下面顺便提一下如何解决

老版spring事务失效原因及解决办法

现在的版本已经不会有这个问题了。

其实Spring和SpringMVC俩个容器为父子关系,Spring为父容器,而SpringMVC为子容器。

也就是说application.xml中应该负责扫描除@Controller的注解如@Service,而SpringMVC的配置文件应该只负责扫描@Controller,否则会产生重复扫描导致Spring容器中配置的事务失效。

也就是说spring先扫描要添加事务的包或者类然后再由springMVC扫描的话加上的事务就会失效。

解决方法就是在扫描配置中加上过滤。

Spring的配置文件:application.xml

<context:component-scan base-package="org.xxx.redis" use-default-filters="true">
  <!-- 排除含@Controller注解的类 -->
  <context:exclude-filter type="annotation" expression="org.xxxx.controller.UserController"/>
</context:component-scan>

SpringMVC的配置文件:springmvc.xml

<!-- 只扫描含@Controller注解的包,避免重复扫描 -->
<context:component-scan base-package="org.xx.controller" use-default-filters="true">
</context:component-scan>

在spring配置文件中用过滤器排除扫描加过@Controller的类然后springMVC配置文件中只扫描带@Controller的类排除@Service注解的类.

配置都去掉

综上这三个xml都可以去掉,于是我们再次证明了官方文档上面的那一段代码确实可以完成SpringMVC。

再来回顾一下官网启动SpringMVC的代码

public class MyWebApplicationInitializer  implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();//去掉web.xml中的listener节点
        ac.register(AppConfig.class); //去掉包扫描的applicationContext.xml,sprngmvc.xml
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);//去掉web.xml中的servlet几点
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

目前还剩下一个问题:内嵌的Tomcat如何启动?

内嵌Tomcat

Tomcat可以利用依赖直接new tomcat start,并且版本没啥要求。

 <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>9.0.14</version>
</dependency>

并且要初始化Spring环境

public class WeslyxlApplication implements WebApplicationInitializer{

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //测试Spring环境初始化是否成功
        System.out.println("tomcat-----init-------");
        //初始化 spring context环境(Spring环境)
        AnnotationConfigWebApplicationContext acweb 
                = new AnnotationConfigWebApplicationContext();

        //当你这段代码执行完成之后他就会去完成扫描,你整个项目当中的bean都有了
        //就相当于你的Spring已经准备好了,它帮你那些类new好,把bean扫描出来,这就是初始化Spring环境。
        acweb.register(AppConfig.class);

        // 创建和注册DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(acweb);
        ServletRegistration.Dynamic ds = servletContext.addServlet("app", servlet);
        ds.setLoadOnStartup(1);
        ds.addMapping("/app/*");
    }
}

上面有Tomcat依赖,接下来就是初始化Tomcat

首先写一个run方法,因为Springboot就是run方法启动的:

public class SpringApplication {
         //实例化tomcat对象
        Tomcat tomcat = new Tomcat();
        //端口号设置
        tomcat.setPort(9090);
        tomcat.addWebapp(contextPath:"/",docBase:"d:\\test\\");
        try {
            tomcat.start();//Tomcat启动方法
            tomcat.getServer().await();//Tomcat阻塞方法,保证不退出
        } catch (LifecycleException e) {
            e.printStackTrace();
        }
}

这个Tomcat就是前面引入的依赖来的,底层操作就是Tomcat去new一个tomcat

  • contextPath是设置你的访问路径,如:localhost:9090/ 这个/就是设置的访问根路径。

  • docBase:则是你的工作空间地址。

Tomcat需要阻塞的原因是如果不加这个方法的话,Tomcat整个主线程完成结束之后Tomcat也会跟着结束,调用这个await()方法就是把Tomcat阻塞在这里,等待用户请求,所以在启动Tomcat的时候你会发现Tomcat并没有结束就是这个原因。

接下来写测试验证一下

controller:

@Controller
public class SpringController {

    @RequestMapping("/index")
    public void index() {
        System.out.println("xxxxx");

    }
}

写一个测试类Test去测试你的这些代码能否成功:

public class Test {
    public static void main(String[] args) {
        SpringApplication.run();
    }
}

最终就会在终端打印——System.out.println(“tomcat—–init——-“);

Servlet标准

这个跟Java的一种标准有关,Tomcat可以作为java的web容器,Nginx不行,Nginx只能做静态服务器,只有Jetty和Tomcat可以,是因为Tomcat和Jetty都实现了一种Java标准就是Servlet标准,同理只要实现了Servlet标准,才能作为Java的Web容器

我们会觉得很奇怪,为什么onStartup就会自动调用。

Servlet的标准有两个很重要的两个版本一个是2.5一个是3.1,一般的培训机构和论坛教程都是使用的Servlet2.5,如果你的Tomcat是6的版本,Tomcat6对应的Servlet版本就是2.5的版本,那么2.5的版本的Servlet是没有@WebServlet的这个注解

这个注解也其实同样是个类,我们装好Tomcat6的时候,它的lib里有很多jar包其中有一个jar包叫Servlet-api,如果版本是Tomcat6那么Servlet的版本可能是2.5甚至之前的版,2.5以前的版本根本就没有这个类,即使在项目编译时期能够编译过去,但是一旦放到Tomcat6上面运行就会不识别这个注解就会报错。我印象当中从Tomcat7或者8以后就开始支持3.1的Servlet标准了。

Servlet标准3.1

Spring-web是相当于一个jar包放在我们的项目中来的,那么这个jar包里的东西就相当我们项目文件中的一部分,会把jar包里面的项目复制到我们的项目当中去。

2019-11-25_164059.png

里面的内容就是:org.springframework.web.SpringServletContainerInitializer

Servlet3.1标准规定在你的meta-inf文件下面如果提供了一个service文件夹并且在这个文件夹里提供了一个文件名叫javax-servlet-ServletContainerlnitializer的文件(注意:Spring-web到meta-inf到service到这个文件都是不可以变的Java规定要这样写),这个文件里面又提供了一个类``SpringServletContainerInitializer`,而这个类又实现了ServletainerIitializer这个接口,那么任何容器启动时都要调用这个onStartup()方法

也就是说tomcat7或者8启动的时候,如果用户真的去写了javax-servlet-ServletContainerlnitializer并在里面配了一个类的话Tomcat7-8必须要去无条件的调用这个类的onStartup();方法。这就是Servlet3.1的标准。也就是所谓的SPI扩容技术,所以Spring利用了Servlet3.1这个标准来完成了他的一个0配置原理

零配置原理

2个方面来说Spring的0配置原理:

Spring注解

我们为什么能把Xml文件去掉还可以一样的达到Xml的效果是因为我们使用了注解,所有的Spring注解统称Java config

官网的那句话:

The following example of the Java configuration registers and initializes the DispatcherServlet, which is auto-detected by the Servlet container (see Servlet Config):

所以是Java config+Servlet3.1的新特性完成的我们零配置原理(0 XML文件)

注意零配置并不代表自动装配。自动装配由注解实现的,换句话说就是自动装配是针对于去掉xml而言。关于自动装配的原理,后面会单独介绍,核心也是用注解。

Servlet3.1标准

上面介绍了tomcat启动的时候就必须无条件的调用ServletContainerlnitializer的onstartup方法。但会发现文中说的Tomcat启动要找一个类实现ServletainerIitializer这个接口,但是示例代码里却又是实现的WebApplicationInitializer它不一样。

public class WeslyxlApplication implements WebApplicationInitializer{

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        //测试Spring环境初始化是否成功
        System.out.println("tomcat-----init-------");
        //初始化 spring context环境(Spring环境)
        AnnotationConfigWebApplicationContext acweb 
                = new AnnotationConfigWebApplicationContext();

        //当你这段代码执行完成之后他就会去完成扫描,你整个项目当中的bean都有了
        //就相当于你的Spring已经准备好了,它帮你那些类new好,把bean扫描出来,这就是初始化Spring环境。
        acweb.register(AppConfig.class);

            // 创建和注册DispatcherServlet
            DispatcherServlet servlet = new DispatcherServlet(acweb);
            ServletRegistration.Dynamic ds = servletContext.addServlet("app", servlet);
            ds.setLoadOnStartup(1);
            ds.addMapping("/app/*");
    }

}

第二个标准

除了上面提到的这个标准,还有第二个标准:如果是在第一个标准成立的前提下,在service文件夹里的那个javax文件里提供的那个实现了ServletainerIitializer接口的类上方加一个@HandlesTpes这个注解,在容器回调这个onStartup这个方法的时候,它会传一个Set集合给,这个set集合表示加在这个注解里面的所有的接口的实现类。

2019-11-25_184626.png

在这里断点断住可以看到这是springframework第一次被执行的地方。


@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
    public SpringServletContainerInitializer() {
    }

    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
        List<WebApplicationInitializer> initializers = new LinkedList();
        Iterator var4;
        if (webAppInitializerClasses != null) {
            var4 = webAppInitializerClasses.iterator();

            while(var4.hasNext()) {
                Class<?> waiClass = (Class)var4.next();
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)waiClass.newInstance());
                    } catch (Throwable var7) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
        } else {
            AnnotationAwareOrderComparator.sort(initializers);
            servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);
            var4 = initializers.iterator();

            while(var4.hasNext()) {
                WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
                initializer.onStartup(servletContext);
            }

        }
    }
}

所以会得到这个WebApplicationInitializer,这个WebApplicationInitializer有个实现类就是我们的WeslyxlApplication这个类。Spring5的操作方法是拿到这个集合之后会做一次for循环,把集合里面的实现类给实例化出来,实例化出来之后再放到:

List<WebApplicationInitializer> initializers = new LinkedList<>();

当中,然后循环这个list依次调用onStartup方法:

for(WebApplicationInitializer initializer :initializers){
       initializer.onStartup(servletContext);
}

 上一篇
SpringBoot自动装配原理详解 SpringBoot自动装配原理详解
序言Spring自动装配主要就是基于注解。我们只需要使用@SpringBootApplication这样一个注解就就能够完全启动一个项目了。它是由下面三个注解完成 SpringBootApplication 注解详情 @SpringBoo
下一篇 
java基础中易忽略的几点 java基础中易忽略的几点
1. 基础1.1. 正确使用 equals 方法Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。 举个例子: // 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常 St
  目录