假装是小白之重学Spring MVC(二)

360 阅读12分钟

在假装是小白之重学Spring MVC(一)中已经介绍了Spring MVC比较常用的知识点,但是还是有些遗漏,比如将请求放入Session,在配置文件中配置太烦了,我们能否将配置文件移动至配置类中,又比如SSM(Spring Spring MVC MyBatis)整合。 本篇的主要内容就是介绍这些,将配置文件转换成配置类需要懂一些Spring的核心注解,比如@Import,这个注解在欢迎光临Spring时代(一) 上柱国IOC列传已经介绍过了,不懂的可以再翻一下这篇文章。

放入session中

我们如何将数据放入session中呢? 在原生Servlet时代,我们从HttpServletRequest中获取HttpSession对象就好,像下面这样: 当然你在Spring MVC框架还可以接着用,Spring MVC又提供了@SessionAttributes注解用于将数据放入Session中,@SessionAttribute用于将session中的值,通过翻阅源码,我们可以发现@SessionAttributes只能作用于类上,在翻阅源码的过程中发现了@SessionAttribute注解,本来以为和@SessionAttributes是一样的用处 ,后来翻源码才发现,这个是用来取出Session中的值放到方法参数中,只能作用方法参数上。 又通过看源码的注释,我们可以看到SessionAttributes的两个value和name是同义语,假设有请求域(请求域中的数据都是呈K、V对形式,我门现在谈的就是key)中有和value值相同的数据,那么就会将该数据也会放到Session中。而Type属性则是,只要是请求域的数据是该类型的,就放入到Session中。 我们现在来测试一下@SessionAttributes:

Controller
SessionAttributes(value = "password")
public class SessionDemoController {

    @RequestMapping(value = "/session", method = RequestMethod.GET)
    public String testGet(@RequestParam(value = "name") String userName, @RequestParam(value = "password") String password ,Model model) {
        System.out.println("username:" + userName);
        System.out.println("password:" + password);
        model.addAttribute("password",password);
        return "success";
    }
}

success.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1> success </h1>
<h1> username:  ${sessionScope.userName}</h1>
<h1> password: ${sessionScope.password}</h1>
</body>
</html>

url: http://localhost:8080/studySpringFrameWork_war_exploded/session?name=aa&&password=aaa

测试结果: SessionAttribute:

RequestMapping(value = "/servlet", method = RequestMethod.GET)
    public String testGet(@RequestParam(value = "name") String userName,  @SessionAttribute(value = "password")String password) {
        System.out.println("username:" + userName);
        System.out.println("password:" + password);
        return "success";
    }

url: http://localhost:8080/studySpringFrameWork_war_exploded/servlet?name=aaa 测试结果: 可以看到url中没有password,password依然打印出来有值,这个就是session中的值。

配置文件的配置移动至配置类

简单的说配置文件的本质就是,在读取配置文件的时候,根据标签将对应类的对象注册进IOC容器中。但是配置文件配置了太多也是很烦的,可不可以转换成配置类的形式呢? Spring MVC: 当然可以。 那首先第一个问题就是以前是配置文件中让Tomcat启动的时候读取配置文件,注册DispacherServlet怎么移植? 通过org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer来移植,上面的注释很清晰,有的时候碰见英文不要抵触,读一读多累积几个单词,语法大多都不难: 如果觉得这个格式不是很喜欢,可以去看对应的Java Doc,下面是地址:

Interface to be implemented in Servlet 3.0+ environments in order to configure the ServletContext programmatically -- as opposed to (or possibly in conjunction with) the traditional web.xml-based approach. 该接口在在Servlet 3.0以上的版本才被实现能够实现以配置类的形式配置ServletContext ,和传统的基于web.xml的配置方式相对。

Implementations of this SPI will be detected automatically by SpringServletContainerInitializer, which itself is bootstrapped automatically by any Servlet 3.0 container. See its Javadoc for details on this bootstrapping mechanism.

SpringServletContainerInitializer会自动监测该接口的实现类,然后被任何支持Servlet 3.0版本以上的容器自动加载。

感觉是不是有点拗口,有点不明白什么意思的感觉,我的理解就是在支持Servlet 3.0的Servelt容器才能使用Spring MVC配置类的形式。下面这段注释说的是该接口的实现类会被Servlet容器自动加载。 但是我们这次并不是选择实现WebApplicationInitializer,这样做有些麻烦了,Spring MVC提供了一个简单点的Web初始化器: org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer,供我们使用。 AbstractAnnotationConfigDispatcherServletInitializer是一个抽象类,我们继承它,然后继承一下它看看:

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {

        return new Class[0];
    }	
    /**
     * 用于设置拦截的URL
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[0];
    }
} 

getRootConfigClasses和getServletConfigClasses这两个方法,我们就需要专门拿出来讲讲,从名字上可以推断都是加载配置类,那这两个方法有什么区别呢? 我们还是翻翻官方文档,提供了Java Config 和 基于xml的等价写法: 我们看下对比哈: 也就是说getServletConfigClasses作用就是等同于加载Spring MVC的配置文件,getServletMappings就是设定要拦截的URL。getRootConfigClasses。我们本次学习的Spring MVC都是只注册了一个DispatcherServlet,Spring MVC允许我们注册多个DispacherServlet,那么多个Spring MVC的配置文件就是隔开的,但是呢,在这些不同的DispatcherServlet之间如果有一些bean需要共享怎么办呢?getRootConfigClasses就加载顶层的配置类,被各个DispatcherServlet共享。

当前我们就只有一个DispatcherServlet,所以我们就只建一个空类。所以AppInitializer 就被改造成了下面这样:

public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
	
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {

        return new Class[]{WebConfig.class};
    }

    /**
     * 用于设置拦截的URL
     *
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}
@Configuration
public class RootConfig {
}
@EnableWebMvc
@ComponentScan(basePackages = {"org.example.mvc"})
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyHandlerInterceptor());
    }
}

@EnableWebMvc是新面孔,我们这里需要大致讲一下。

@EnableWebMvc简介

要完全讲清楚@EnableWebMvc,我们完全可以再开一篇文章去讲,因为在这个注解身上,Spring做了大量的工作,要完全讲清楚不是一件简单的事情,这里我做的介绍只是让各位同学明白,@EnableWebMvc相当于配置文件中做了什么?

能翻官方文档,尽量还是翻官方文档,官方文档有这部分的介绍: 这个截图似乎还不是很清晰,我们放在IDE里截图看一下:

   <mvc:annotation-driven/>  这行不少视频都说,这个是把加上了@Controller的类变成处理请求的类,但是也不知道是不是跟我用的Spring MVC版本有关系,我没加这个也行。

但是建议你还是加,解释清楚不加也行(可能会出一些莫名的问题),但是这也不是一件简单的事情,因为这要涉及很多源码的解读,如果有兴致了解可以参看:

拦截器等标签的移植

各位仔细看上面的WebConfig这个类,这个类继承了WebMvcConfigurer ,我在里面重写了addInterceptors方法,从方法名字上可以推断出来,这个方法是加拦截器的。然后我们将完整的写一下:

@EnableWebMvc
@ComponentScan(basePackages = {"org.example.mvc"})
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyHandlerInterceptor()).addPathPatterns("/**").excludePathPatterns("login");
    }
}

我们再度对比一下:

关于bean标签我们不讲怎么移植,这在欢迎光临Spring时代(一) 上柱国IOC列传已经讲过了,这里不再讲一遍。WebMvcConfigurer 的方法这里就不一一拿出来细讲了,可以自己看下注释。我们这里只讲大致的思想。

测试一下

注意要先注释掉web.xml预先加载的那个配置。基本上测试是成功的。这里我就不再贴代码了。 我把代码放在码云上,各位有兴致的同学可以翻翻看看。基本上到这里,Spring MVC就告一段落了。 地址如下:

SSM整合

SSM 整合前言

到这里我们就可以开始讲一下,SSM整合了,当时跟同学一块学SSM整合的时候,有的同学不理解思想,就是背,有的时候自己尝试做整合,背错了就整合失败了。这里我希望讲思想,有了这个思想,我想再去做整合的时候,就会知道自己错在哪里。 在看到这里的时候,我希望你对Spring的思想有一点了解,建议参看我Spring系列的文章:

注意这里是SSM整合,那MyBatis也要求会,如果不会请参看我MyBatis系列的文章。

如果不会Spring MVC,请参看我Spring MVC系列的文章:

SSM整合的思想-天下归Spring统

Spring Framework 是一个成功的框架,解决了对象之间有复杂的依赖关系的时候,在不希望是硬编码的情况下,取对象优雅和可配置化,那么其他框架也要纳入到Spring Framework中,让我们取该框架的对象时候优雅一点。这也就是整合思想,将该框架的核心对象放入IOC容器中,然后取出来。再讲一遍,假设是MyBatis需要数据源、扫描xml,我们就统一在IOC容器中配置。 然后取的时候用Spring 提供的取对象方式,取出来就行了。SSM整合的核心思想就是将Spring MVC、MyBatis的核心对象放进IOC容器中,然后我们取。

那么我们对MyBatis 整合的愿景是什么呢? 我们再看下我们学习MyBatis的代码: 那怎么整合将MyBatis整合进入Spring才能实现这样的效果呢? 仅仅看目前的代码,我们是将SqlSessionFactory放入IOC容器中,然后从IOC容器获取session,再调用getMapper方法。这样是不是有点麻烦呢? 有没有更简洁的方案呢? 这个解决方案叫mybatis-spring,依赖如下:

  <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.6</version>
 </dependency>

这个解决方案能够做到将Mapper接口(即Xml对应的接口,下文会统称为Mapper对象)对象放入到IOC容器中,我们只用在需要对应Mapper的地方,用@Autowired注解注入即可

整合MyBatis

我们这里只介绍基于xml形式的,基于配置类的,将bean标签移动至配置类即可。

基于xml形式的

首先我们用Druid连接池管理数据源,那当然是在配置文件中配置一个即可:

 <!--加载配置文件-->
<bean id = "placeholderConfigurer" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:jdbc.properties</value>
            </list>
        </property>
    </bean>
   <bean id = "dataSource" class = "com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.dev.driver}">
        </property>
        <property name = "url" value="${jdbc.dev.driver}">
        </property>
        <property name = "username" value = "${jdbc.dev.username}">
        </property>
        <property name = "password" value = "${jdbc.dev.password}">
        </property>
    </bean>

要实现将Mapper接口的对象放入到IOC容器中,核心是两个类:

  • org.mybatis.spring.SqlSessionFactoryBean
  • org.mybatis.spring.mapper.MapperScannerConfigurer

SqlSessionFactoryBean概览: MapperScannerConfigurer概览:

其实上面也有官方文档: 地址是: mybatis.org/spring/zh/g… 这上面讲了mybatis-spring的起源和如何整合,我们照着做就行。

  <bean  id = "sqlSessionFactory"  class = "org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref = "dataSource"></property>
        <property name = "mapperLocations" value="org\example\mybatis\*.xml">
        </property>
    </bean>

    <bean  id = "mapperScannerConfigurer" class = "org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value = "org.example.mybatis" />
        <property name="sqlSessionFactoryBeanName" value = "sqlSessionFactory"></property>
     </bean> 

可能有同学会问,那之前的配置文件咋弄? 切换环境怎么办? SqlSessionFactoryBean中有一个configuration字段是Configuration类型的,我们看下Configuration类: 然后再配置就行了。如果你不想配置bean,也可以指定路径,通过SqlSessionFactoryBean的configLocation指定MyBatis的配置文件位置就可以了。

整合Spring MVC

其实在上面介绍Spring MVC的时候已经在用Spring,大家可能感受不到整合的这个过程,这就叫无缝集成。 接下来讲的就是,如果你有多个DisPatcherServlet,但是有一批bean是共享的,又不想在在每个配置文件中都配置一份。怎么办。在用配置类的情况下,我们是通过getRootConfigClasses来配置的,还记得它的xml写法吗?

<web-app>
	
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
	<!-- 这里用来加载多有DispatcherServlet共享的bean-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>

小插曲

我以为这里整合的顺风顺水一点毛病都没有,就打算收尾了,然后就启动了项目,然后就报少jar包,少的是这两个:

<dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.8.RELEASE</version>
    </dependency>

然后发现补上了之后,还是注入失败,此时我陷入了深思,最大的痛苦还是,我不知道我错在了哪里? 因为控制台也不报错,比报错还可怕的就是,程序不报错。于是我开始排查,在main函数里加载配置文件试了试看:

public static void main(String[] args) throws IOException {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-MVC.xml");
    BlogMapper blogMapper = applicationContext.getBean(BlogMapper.class);
    System.out.println(blogMapper.selectAllReturnMap());
}

发现成功获取,于是我就怀疑我的配置文件没加载,下面是我的web.xml中的配置: 然后是Spring MVC的配置文件: 于是我猜想首先加载的是context-param标签中的配置文件,在这个过程中完成对各个bean的初始化。 然后再加载Spring MVC的配置文件,对Spring MVC的一些bean进行初始化。 然后我把整合MyBatis的代码就移入了applicationContext.xml中,然后就成功了。

排错思路

基本上我也是在引入日志之后,才推断出来我的Spring-MVC配置文件没有成功加载的,日志可以把Spring IOC容器创建的bean打印出来: 通过日志我发现我单独在main函数中加载Spring MVC的配置文件的时候,有sqlSessionFactory创建成功的日志,但是我启动Tomcat就没有。所以我才推断Spring-MVC的配置文件加载的是不是有些晚了。然后验证一下果然是。日志配置也有一些坑,比如说Log 4j 2不支持properties的配置文件,名字也必须交log4j2.properties。然后日志怎么配,跟着GitHub上的配就行了。GitHub上对应项目的地址如下:

源码选讲之Spring MVC的执行流程

接下来我们研究一下Spring MVC的执行流程,这也是高频面试题,研究这个是为了让我们对Spring MVC的理解更进一步。 首先请求总是先进入到DispatcherServlet中,然后我们还是大致看一下DispatcherServlet这个源码,源码太庞大了,不便于展示,这里就直接看两个核心方法了:

  • HandlerExecutionChain (处理器执行链)
  • doDispatch (请求分发) 我们主要看doDispath方法: 基于此我们可以大致画出Spring MVC的执行流程: 其实这个Spring MVC执行流程已经人尽皆知了,毕竟是高频面试题,但是我是想从源码的角度去看。基本上整个流程都在DispatcherServlet的dispatch方法中,有兴致的同学可以也看一下。

总结一下

每次重学框架都会有新的认知,这次也算是小有收货,把心里以前的疑问都扫清了。希望对各位同学而学习Spring MVC会有所帮助,但是到这里就结束了吗? 远远没有,不觉得这些配置有些烦吗? 还有依赖管理,其实我做的时候都是小心翼翼的,生怕起了依赖冲突,能不能简化一下呢? 当然可以,这些都将由Spring 家族的一个明星成员Spring Boot给出答案。

参考资料