Spring

1. Spring简介

  1. spring的模块划分

    1. Test:spring的单元测试模块
    2. Core Container:核心容器(IOC)黑色代表这部分的功能由哪些jar包组成,要使用这个部分的完整功能,就要导入这些jar包
    3. AOP + Aspects:面向切面编程模块
    4. Data Access:访问数据库的模块
    5. Web:web应用模块
  2. spring的优势

    1. 降低了 2EE 的使用难度,并且方便集成各种框架
    2. 推荐及大量使用面向对象的设计思想,是学习 Java 源码的经典框架
    3. 面向接口编程,而不是面向类编程,不断地利用 ava 的多态特性及良好的面向对 象设计思想,来降低程序的复杂度及偶合度
    4. 提供了测试框架,并且支持集成其 测试框架,使测试更容易,对测试程序的编程更简单、高效
  3. Spring的优良特性
    [1]非侵入式:基于Spring开发的应用中的对象可以不依赖于Spring的API
    [2]依赖注入:DI——Dependency Injection,反转控制(IOC)最经典的实现。
    [3]面向切面编程:Aspect Oriented Programming——AOP
    [4]容器:Spring是一个容器,因为它包含并且管理应用对象的生命周期
    [5]组件化:Spring实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用XML和Java注解组合这些对象。
    [6]一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上Spring 自身也提供了表述层的SpringMVC和持久层的Spring JDBC)

2. IOC 控制反转

IOC和bean的配置

  1. /*
  2. 1. IOC(Inversion of Control): 反转控制
  3. 1. 在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下 开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率
  4. 2. 反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资 源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的 降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式
  5. 2. DI(Dependency Injection): 依赖注入
  6. IOC的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入,相对 于IOC而言,这种表述更直接
  7. 3. IOC容器在Spring中的实现
  8. 1. 在通过IOC容器读取Bean的实例之前,需要先将IOC容器本身实例化
  9. 2. Spring提供了IOC容器的两种实现方式
  10. 1. BeanFactory:IOC容器的基本实现,是Spring内部的基础设施,是面向Spring本身的,不是提供给 开发人员使用的
  11. 2. ApplicationContext:BeanFactory的子接口,提供了更多高级特性。面向Spring的使用者,几 乎所有场合都使用ApplicationContext而不是底层的BeanFactory
  12. 4. ApplicationContext的主要实现类
  13. 1. ClassPathXmlApplicationContext:对应类路径下的XML格式的配置文件
  14. 2. FileSystemXmlApplicationContext:对应文件系统中的XML格式的配置文件
  15. 3. 在初始化时就创建单例的bean,也可以通过配置的方式指定创建的Bean是多实例的
  16. 5. ConfigurableApplicationContext
  17. 1. 是ApplicationContext的子接口,包含一些扩展方法
  18. 2. refresh()和close()让ApplicationContext具有启动、关闭和刷新上下文的能力
  19. 6. WebApplicationContext
  20. 专门为WEB应用而准备的,它允许从相对于WEB根目录的路径中完成初始化工作
  21. 7. 通过类型获取bean
  22. 从IOC容器中获取bean时,除了通过id值获取,还可以通过bean的类型获取。但如果同一个类型的bean在 XML文件中配置了多个,则获取时会抛出异常,所以同一个类型的bean在容器中必须是唯一的
  23. 8. 给bean的属性赋值
  24. 1. 通过bean的set方法赋值
  25. 2. 通过bean的构造器赋值
  26. 3. 通过索引值指定参数位置
  27. 4. 通过类型不同区分重载的构造器
  28. 9. 集合属性: 在Spring中可以通过一组内置的XML标签来配置集合属性,例如:<list>,<set>或<map>
  29. 1. 数组和List: 配置java.util.List类型的属性,需要指定<list>标签,在标签里包含一些元素。这些标签 可以通过<value>指定简单的常量值,通过<ref>指定对其他Bean的引用,通过<bean>指定内置bean定义,通 过<null/>指定空元素。甚至可以内嵌其他集合
  30. 2. 数组的定义和List一样,都使用<list>元素,配置java.util.Set需要使用<set>标签,定义的方法与 List一样
  31. 3. Map: Java.util.Map通过<map>标签定义,<map>标签里可以使用多个<entry>作为子标签。每个条目包 含一个键和一个值,必须在<key>标签里定义键,因为键和值的类型没有限制,所以可以自由地为它们指定 <value>、<ref>、<bean>或<null/>元素,可以将Map的键和值作为<entry>的属性定义:简单常量使用key 和value来定义;bean引用通过key-ref和value-ref属性定义
  32. 4. Properties: 使用<props>定义java.util.Properties,该标签使用多个<prop>作为子标签。每个 <prop>标签必须定义key属性
  33. 5. 集合类型的bean: 如果只能将集合对象配置在某个bean内部,则这个集合的配置将不能重用。我们需要将集 合bean的配置拿到外面,供其他bean引用。配置集合类型的bean需要引入util名称空间
  34. */
  1. /*
  2. 10. 通过工厂创建bean
  3. 1. 静态工厂
  4. 调用静态工厂方法创建bean是将对象创建的过程封装到静态方法中。当客户端需要对象时,只需要简单地调 用静态方法,而不用关心创建对象的细节。声明通过静态方法创建的bean需要在bean的class属性里指定静 态工厂类的全类名,同时在factory-method属性里指定工厂方法的名称。最后使用<constrctor-arg> 元素为该方法传递方法参数
  5. 2. 实例工厂
  6. 实例工厂方法:将对象的创建过程封装到另外一个对象实例的方法里。当客户端需要请求对象时,只需要简单 的调用该实例方法而不需要关心对象的创建细节
  7. 3. 实现方式
  8. 1. 配置工厂类实例的bean
  9. 2. 在factory-method属性里指定该工厂方法的名称
  10. 3. 使用 construtor-arg 元素为工厂方法传递方法参数
  11. 4. FactoryBean
  12. Spring中有两种类型的bean,一种是普通bean,另一种是工厂bean,即FactoryBean
  13. 工厂bean跟普通bean不同,其返回的对象不是指定类的一个实例,其返回的是该工厂bean的getObject方法所返回的对象
  14. 工厂bean必须实现org.springframework.beans.factory.FactoryBean接口
  15. */
  1. /*
  2. bean的高级配值
  3. 1. 配置信息的继承
  4. 1. Spring允许继承bean的配置,被继承的bean称为父bean。继承这个父bean的bean称为子bean
  5. 2. 子bean从父bean中继承配置,包括bean的属性配置
  6. 3. 子bean也可以覆盖从父bean继承过来的配置
  7. 4. 补充说明: 父bean可以作为配置模板,也可以作为bean实例。若只想把父bean作为模板,可以设置<bean> 的abstract 属性为true,这样Spring将不会实例化这个bean如果一个bean的class属性没有指定,则必须 是抽象bean并不是<bean>元素里的所有属性都会被继承。比如:autowire,abstract等。也可以忽略父bean 的class属性,让子bean指定自己的类,而共享相同的属性配置。但此时abstract必须设为true
  8. 2. bean之间的依赖
  9. 有的时候创建一个bean的时候需要保证另外一个bean也被创建,这时我们称前面的bean对后面的bean有依赖。 例如:要求创建Employee对象的时候必须创建Department。这里需要注意的是依赖关系不等于引用关系, Employee即使依赖Department也可以不引用它
  10. 3. bean的作用域
  11. 1. 在Spring中,可以在<bean>元素的scope属性里设置bean的作用域,以决定这个bean是单实例的还是多实 例的。默认情况下,Spring只为每个在IOC容器里声明的bean创建唯一一个实例,整个IOC容器范围内都能共享 该实例:所有后续的getBean()调用和bean引用都将返回这个唯一的bean实例。该作用域被称为singleton, 它是所有bean的默认作用域
  12. 2. 注意: 当bean的作用域为单例时,Spring会在IOC容器对象创建时就创建bean的对象实例。而当bean的作 用域为prototype时,IOC容器在获取bean的实例时创建bean的实例对象
  13. */
  1. /*
  2. 4. bean的生命周期
  3. 1. Spring IOC容器可以管理bean的生命周期,Spring允许在bean生命周期内特定的时间点执行指定的任务
  4. 2. Spring IOC容器对bean的生命周期进行管理的过程:
  5. 1. 通过构造器或工厂方法创建bean实例
  6. 2. 为bean的属性设置值和对其他bean的引用
  7. 3. 调用bean的初始化方法
  8. 4. bean可以使用了
  9. 5. 当容器关闭时,调用bean的销毁方法
  10. 3. 在配置bean时,通过init-method和destroy-method 属性为bean指定初始化和销毁方法
  11. 5. bean的后置处理器
  12. 1. bean的后置处理器允许在调用初始化方法前后对bean进行额外的处理
  13. 2. bean后置处理器对IOC容器里的所有bean实例逐一处理,而非单一实例。其典型应用是:检查bean属性的正 确性或根据特定的标准更改bean的属性
  14. 3. bean后置处理器时需要实现接口: org.springframework.beans.factory.config.BeanPostProcessor。在初始化方法被调用前 后,Spring将把每个bean实例分别传递给上述接口的以下两个方法:
  15. postProcessBeforeInitialization(Object, String)
  16. postProcessAfterInitialization(Object, String)
  17. 4. 添加bean后置处理器后bean的生命周期
  18. 1. 通过构造器或工厂方法创建bean实例
  19. 2. 为bean的属性设置值和对其他bean的引用
  20. 3. 将bean实例传递给bean后置处理器的postProcessBeforeInitialization()方法
  21. 4. 调用bean的初始化方法
  22. 5. 将bean实例传递给bean后置处理器的postProcessAfterInitialization()方法
  23. 6. bean可以使用了
  24. 7. 当容器关闭时调用bean的销毁方法
  25. 6. 引用外部属性文件
  26. 当bean的配置信息逐渐增多时,查找和修改一些bean的配置信息就变得愈加困难。这时可以将一部分信息提取到 bean配置文件的外部,以properties格式的属性文件保存起来,同时在bean的配置文件中引用properties属 性文件中的内容,从而实现一部分属性值在发生变化时仅修改properties属性文件即可。这种技术多用于连接数 据库的基本信息的配置
  27. 7. 自动装配
  28. 1. 自动装配的概念
  29. 1. 手动装配:以value或ref的方式明确指定属性值都是手动装配
  30. 2. 自动装配:根据指定的装配规则,不需要明确指定,Spring自动将匹配的属性值注入bean中
  31. 2. 装配模式
  32. 1. 根据类型自动装配:将类型匹配的bean作为属性注入到另一个bean中。若IOC容器中有多个与目标bean 类型一致的bean,Spring将无法判定哪个bean最合适该属性,所以不能执行自动装配
  33. 2. 根据名称自动装配:必须将目标bean的名称和属性名设置的完全相同
  34. 3. 通过构造器自动装配:当bean中存在多个构造器时,此种自动装配方式将会很复杂。不推荐使用
  35. 3. 选用建议
  36. 相对于使用注解的方式实现的自动装配,在XML文档中进行的自动装配略显笨拙,在项目中更多的使用注解的 式实现
  37. */

实验1:通过IOC容器创建对象,并为属性赋值

  1. 导入依赖
    1. <dependencies>
    2. <!--spring的配置包-->
    3. <dependency>
    4. <groupId>org.springframework</groupId>
    5. <artifactId>spring-context</artifactId>
    6. <version>5.0.8.RELEASE</version>
    7. </dependency>
    8. <!--spring的日志依赖包-->
    9. <dependency>
    10. <groupId>commons-logging</groupId>
    11. <artifactId>commons-logging</artifactId>
    12. <version>1.1.1</version>
    13. </dependency>
    14. <!--spring的单元测试-->
    15. <dependency>
    16. <groupId>org.springframework</groupId>
    17. <artifactId>spring-test</artifactId>
    18. <version>5.0.8.RELEASE</version>
    19. </dependency>
    20. </dependencies>
  1. 写配置文件 ```xml <?xml version=”1.0” encoding=”UTF-8”?>



3. 
测试
```java
//person类
public class Person {

    private String name;

    private String password;

    private String loving;

    public Person(){
        System.out.println("person对象创建了...");
    }

    public Person(String name, String password, String loving) {
        this.name = name;
        this.password = password;
        this.loving = loving;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getLoving() {
        return loving;
    }

    public void setLoving(String loving) {
        this.loving = loving;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", loving='" + loving + '\'' +
                '}';
    }
}


//测试
@Test
public void test(){
    //ApplicationContext: 代表容器
    //ClassPathXmlApplicationContext: 当前spring的应用配置文件在类路径下
    //参数: 根据spring的配置文件得到IOC容器
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    //取出容器中的组件
    Object person1 = context.getBean("person");
    Object person2 = context.getBean("person");
    System.out.println(person1 == person2);    //true
    System.out.println(context.getBean("person1"));  //没有这个组件
}

/*
几个细节
    1. person对象是在容器创建完成的时候进行初始化的
    2. 同一个组件在IOC容器中是单实例的,而且在容器创建完成之前就已经创建好了
    3. 容器中如果没有这个组件,获取组件时会报异常
    org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'person1' available
    4. IOC容器在创建这个组件的时候,property属性会利用set和get方法为person对象的属性进行赋值
    5. JavaBean的属性名是由get和set方法决定的,所有的get和set方法千万不要乱改
*/

实验2:根据bean的类型从IOC容器中获取bean的实例

  1. 配置文件 ```xml
2. 测试java @Test public void test02(){ //如果IOC容器中这个类型的bean有多个,查找就会报错 //org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.siki.bean.Person’ available: expected single matching bean but found 2: person,person1 Person person = context.getBean(Person.class); System.out.println(person); //解决 Person person1 = context.getBean(“person1”, Person.class); System.out.println(person1); } <a name="a28d16f7"></a> ### 实验3:通过构造器为bean的属性赋值 1. 配置文件xml 2. 测试java @Test public void test03(){ Object person2 = context.getBean(“person2”); System.out.println(person2); Object person3 = context.getBean(“person3”); System.out.println(person3); Object person4 = context.getBean(“person4”); System.out.println(person4); } <a name="ef6e7c29"></a> ### 实验4:正确的为各种属性赋值 <a name="f6e214f3"></a> #### 1. 测试null值 1. 配置文件xml ```java //2. 测试 public class Person { //基本数据类型直接使用<property>属性 private String name; private String password; private String loving; //引用数据类型 private Car car; private List<Book> books; private Map<String,Object> map; private Properties properties; public Person(){ } public Person(String name, String password, String loving) { this.name = name; this.password = password; this.loving = loving; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getLoving() { return loving; } public void setLoving(String loving) { this.loving = loving; } public Car getCar() { return car; } public void setCar(Car car) { this.car = car; } public List<Book> getBooks() { return books; } public void setBooks(List<Book> books) { this.books = books; } public Map<String, Object> getMap() { return map; } public void setMap(Map<String, Object> map) { this.map = map; } public Properties getProperties() { return properties; } public void setProperties(Properties properties) { this.properties = properties; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", password='" + password + '\'' + ", loving='" + loving + '\'' + ", car=" + car + ", books=" + books + ", map=" + map + ", properties=" + properties + '}'; } } //1. 测试null值,引用类型默认为null @Test public void test01(){ Person person01 = (Person) context.getBean("person01"); System.out.println(person01); } #### 2. ref引用外部的值 1. 配置文件 ```xml 2. 测试java @Test public void test02(){ //引用外部bean Person person02 = (Person) context.getBean(“person02”); System.out.println(person02.getCar()); //引用内部bean Person person03 = (Person) context.getBean(“person03”); System.out.println(person03.getCar()); } <a name="aec9dc28"></a> #### 3. 集合类型赋值xml root 123456 ```java @Test public void test03(){ //list属性赋值 Person person04 = (Person) context.getBean("person04"); System.out.println(person04.getBooks()); //map属性赋值 Map<String, Object> map = person04.getMap(); System.out.println(map); //properties属性赋值 Properties properties = person04.getProperties(); System.out.println(properties); } #### 4.util名称空间创建集合类型的bean xml <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <!--导入util名称空间--> xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd"> <!--4. util名称空间创建集合类型的bean,方便别人引用--> <!--相当于new LinkedHashMap<>()--> <util:map id="map"> <!--添加元素--> <entry key="key01" value="xinxin"></entry> <entry key="key02" value="520"></entry> <entry key="key03" value-ref="book01"></entry> <entry key="key04"> <bean class="com.siki.bean.Car"> <property name="name" value="奥迪"/> <property name="price" value="100000"/> </bean> </entry> </util:map> <bean id="person05" class="com.siki.bean.Person"> <property name="map" ref="map"></property> </bean> java @Test public void test04(){ Person person05 = (Person) context.getBean("person05"); System.out.println(person05.getMap()); } #### 5. 级联属性赋值 xml <!--5. 级联属性(属性的属性)赋值 级联属性可以修改属性的属性,但是原来的bean中的值可能也会被修改--> <bean id="person06" class="com.siki.bean.Person"> <!--为car赋值的时候,修改car的price--> <property name="car" ref="car01"/> <property name="car.price" value="20000"/> </bean> java public void test05(){ Person person06 = (Person) context.getBean("person06"); Car car = (Car) context.getBean("car01"); System.out.println(car); System.out.println(person06.getCar()); } ### 实验5:配值通过静态工厂方法创建的bean、实例工厂方法创建的bean、FactoryBean ```xml ```java //实例工厂 public class InstanceFactory { public Book getBook(String name){ System.out.println("InstanceFactory..."); Book book = new Book(); book.setName(name); book.setPrice(30); return book; } } //静态工厂 public class StaticFactory { public static Book getBook(String name){ System.out.println("StaticFactory..."); Book book = new Book(); book.setName(name); book.setPrice(10); return book; } } //实现了FactoryBean接口的类都是spring认识的工厂类,spring会自动调用工厂方法创建实例 public class MyFactoryBean implements FactoryBean<Book> { //工厂方法 @Override public Book getObject() throws Exception { Book book = new Book(); book.setName("墨菲定律"); return book; } //返回创建对象的类型,spring会自动创建这个方法来确定对象是什么类型的 @Override public Class<?> getObjectType() { return Book.class; } //是否是单例模式 @Override public boolean isSingleton() { return false; } } @Test public void test02(){ System.out.println(context.getBean("book01")); System.out.println(context.getBean("book02")); System.out.println(context.getBean("myFactoryBean")); } ### 实验6:通过继承实现bean配值信息的重用 xml <!--6. 通过继承实现bean配置信息的重用--> <bean id="person07" class="com.siki.bean.Person"> <property name="name" value="昕昕"/> <property name="password" value="1314520"/> <property name="loving" value="xinxin"/> </bean> <!--通过parent属性指定当前bean的配值继承哪个bean--> <bean id="person08" class="com.siki.bean.Person" parent="person07"> <!-- 只有name属性不一样 --> <property name="name" value="宝贝"/> </bean> java @Test public void test06(){ Person person08 = (Person) context.getBean("person08"); System.out.println(person08); } ### 实验7:通过abstract属性创建一个模板bean xml <!--abstract="true" 表示这个bean的配值是抽象的,不能获取它的实例,只能被别人用来继承--> <bean id="person07" class="com.siki.bean.Person" abstract="true"> <property name="name" value="昕昕"/> <property name="password" value="1314520"/> <property name="loving" value="xinxin"/> </bean> ### 实验8:bean之间的依赖 xml <!--9. bean之间的依赖(只是改变bean的创建顺序)--> <!--一般情况下,按照bean的配值顺序创建bean--> <!--但是我们可以改变bean的创建顺序,depends-on: 表示先创建book,再创建person,最后创建car--> <bean id="car" class="com.siki.bean.Car" depends-on="book,person"></bean> <bean id="person" class="com.siki.bean.Person"></bean> <bean id="book" class="com.siki.bean.Book"></bean> ### 实验9:测试bean的作用域,分别创建单实例、多实例的bean xml <!--10. 测试bean的作用域,分别创建单实例和多实例的bean 默认情况下是单实例 1. prototype: 多实例 1. 容器启动默认不会创建多实例bean 2. 获取的时候才会创建bean实例 3. 每次获取都会创建一个新的实例 2. singleton: 单实例 1. 在容器启动完成之前就已经创建好对象了,保存在容器中 2. 任何时候获取都是获取到的之前创建好的那个对象 3. request: web环境下使用,同一次请求创建一个bean实例(基本上没用) 4. session: web环境下使用,同一次会话创建一个bean实例(基本上没用) --> <bean id="book" class="com.siki.bean.Book" scope="prototype"></bean> ### 实验10:创建带有生命周期方法的bean xml <!--10. 创建带有生命周期的bean 1. IOC容器中创建的bean: 1. 单实例bean: 容器启动的时候就会创建,容器关闭的时候也会销毁bean 2. 多实例bean: 获取的时候创建,但是关闭容器的时候不会销毁bean 2. 我们可以为bean自定义一些生命周期的方法,spring在创建或者销毁bean的时候就会调用这些指定的方法,完成指定的操作 3. 自定义初始化和销毁方法: 可以抛异常,但是不能有参数 --> <bean id="book01" class="com.siki.bean.Book" init-method="init" destroy-method="destory"></bean> java //初始化方法 public void init(){ System.out.println("init..."); } //销毁方法 public void destory(){ System.out.println("destory..."); } /* 单实例bean的生命周期 (容器启动)构造器--->初始化init()--->销毁destory() 多实例bean的生命周期 获取bean(构造器--->初始化方法)--->容器关闭不会调用bean的销毁方法 */ @Test public void test01(){ //关闭容器 注意: 只有ApplicationContext的子类才有close()方法 context.close(); //容器关闭的时候会调用destory方法 } ### 实验11:测试bean的后置处理器 xml <!--11. 测试bean的后置处理器: spring的一个接口,可以在bean的初始化前后调用指定方法 1. 编写bean的后置处理器的实现类 2. 在配置文件中注册 3. 注意: 无论bean是否有初始化方法,后置处理器都会工作 --> <bean id="myBeanPostProcessor" class="com.siki.processor.MyBeanPostProcessor"></bean> java //bean的后置处理器 public class MyBeanPostProcessor implements BeanPostProcessor { //初始化之前调用 @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println(bean + "-->" + beanName + " will postProcessBeforeInitialization..."); //返回传入的bean,这是初始化之后返回的bean,返回的是什么,容器中保存的就是什么 return bean; } //初始化之后调用 @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println(bean + "-->" + beanName + " will postProcessAfterInitialization..."); return bean; } } ### 实验12:引用外部属性文件 xml <!--导入数据库连接池--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency> <!--db.properties--> jdbc.username=root jdbc.password=123456 jdbc.jdbcUrl=jdbc:mysql://localhost:3306 jdbc.driverClass=com.mysql.jdbc.Driver <!--2. 通过外部属性文件db.properties进行赋值--> <!--加载外部属性文件 classpath: 表示引用类路径下的资源--> <context:property-placeholder location="classpath:db.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--因为username是spring中key的一个关键字,为了防止配置文件中的key和spring中的key命名一样,所以改为jdbc.username--> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/> <property name="driverClass" value="${jdbc.driverClass}"/> </bean> <!--我们可以看看spring中的关键字username是什么--> <bean id="book01" class="com.siki.bean.Book"> <property name="name" value="${username}"/> </bean> java @Test public void test01() throws SQLException { //从容器中拿到连接池 DataSource dataSource = (DataSource) context.getBean("dataSource"); System.out.println(dataSource.getConnection()); //查看spring中的username关键字 System.out.println(context.getBean("book01")); } ### 实验13:基于xml的自动装配 ```xml ```java //可以为car赋值的有参构造函数 public Person(Car car){ this.car = car; } @Test public void test02() { //没有赋值 System.out.println(context.getBean("person01")); //手动赋值 System.out.println(context.getBean("person02")); //自动装配 System.out.println(context.getBean("person03")); } ### 实验14:SpEL测试 xml <!--14. SpEL测试(spring表达式语言 Spring Expression Language) 1. 在SpEL中使用字面量 2. 引用其他bean 3. 引用其他bean的某个属性值 4. 调用非静态方法 5. 调用静态方法 6. 使用运算符 --> <bean id="person04" class="com.siki.bean.Person"> <!--字面量--> <!-- <property name="password" value="#{13*14}"/>--> <!--引用其他bean的某个属性值--> <property name="name" value="#{car.name}"/> <property name="car" value="#{car}"/> <!--调用静态方法 #{T(全类名).静态方法名(参数列表)}--> <property name="loving" value="#{T(java.util.UUID).randomUUID().toString()}"/> <!--调用非静态方法 对象.方法名--> <property name="password" value="#{car.getPrice()}"/> </bean> ### 实验15:通过注解分别创建Dao、Service、Controller xml <!--15. 通过注解分别创建Dao、Service、Controller 1. 通过给bean添加某些注解,可以快速的将bean加入到容器中 1. @Controller: 控制器 2. @Service: 业务逻辑 3. @Repository: dao层 4. @Component: 给不属于以上几层的组件添加这个注解 2. 注解可以随便加,spring底层不会去验证这个注解,推荐各自层添加各自注解,注解是给程序员看的 3. 组件的id就是默认类名首字母小写 4. 一定要导入aop包,支持注解模式 5. 使用注解添加到容器中的组件,和使用配置文件加入到容器中的组件行为都是默认一样的 1. 组件的id,默认都是类名首字母小写 2. 组件的作用域,默认都是单实例 --> <!--告诉spring,自动扫描添加了注解的组件 1. context:component-scan: 自动逐渐扫描 2. base-package: 指定扫描的基础包,把基础包及下面所有的子包中所有添加注解了的组件,全部扫描进IOC容器中 --> <context:component-scan base-package="com.siki"/> java @Controller public class BookServlet { } @Service public class BookService { } @Repository("book") @Scope(value = "prototype") //指定为多实例 public class BookDao { } @Test public void test01(){ Object bookDao1 = context.getBean("book"); Object bookDao2 = context.getBean("book"); System.out.println(bookDao1 == bookDao2); } ### 实验16:使用context:include-filter指定扫描包时要包含的类 xml <!--16. 使用context:include-filter指定扫描包时要包含的类,只扫描哪些组件--> <!--use-default-filters: 一定要禁用掉默认的过滤规则,默认全部扫描--> <context:component-scan base-package="com.siki" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> ### 实验17:使用context:exclude-filter指定扫描包时不包含的类 xml <!--17. 使用context:exclude-filter指定扫描包时不包含的类,扫描的时候可以排除一些不要的组件--> <context:component-scan base-package="com.siki"> <!--type="annotation": 指定排除规则,按照注解进行排除--> <!--type="assignable": 指定排除某个具体的类,按照类进行排除--> <!--type="custom": 自定义一个TypeFilter,自己写代码决定是否排除(了解即可,用不到)--> <!--type="regex": 正则表达式(了解即可,用不到)--> <!--expression: 注解的全类名/类的全类名--> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/> <context:exclude-filter type="assignable" expression="com.siki.service.BookService"/> </context:component-scan> ### 实验18:使用@Autowired注解实现根据类型实现自动装配 xml <!--18. 使用@Autowired注解实现根据类型实现自动装配--> <context:component-scan base-package="com.siki"/> java @Controller public class BookServlet { //spring会自动的为这个属性赋值,去容器中找到这个属性对应的组件 @Autowired private BookService service; public void sava(){ service.sava(); } } @Service public class BookService { @Autowired private BookDao dao; public void sava(){ dao.saveBook(); } } @Repository public class BookDao { public void saveBook(){ System.out.println("saveBook..."); } } @Test public void test02(){ BookServlet servlet = (BookServlet) context.getBean("bookServlet"); servlet.sava(); } ### 实验19:默认根据@Autowired注解标记的成员变量名作为id查找bean xml <!--18. 默认根据@Autowired注解标记的成员变量名作为id查找bean @Autowired原理: 1. 先按照类型去容器中找对应的组件 1. 如果找到一个,直接进行装配 2. 如果没有找到,就会报错 3. 如果找到多个,按照变量名作为默认id继续进行查找,找到就装配 --> ### 实验20:@Qualifier注解 xml <!--20. @Qualifier注解 根据@Autowired进行查找,如果找到多个,有两种可能 1. 匹配上 进行装配 2. 没有匹配上 报错 解决办法: @Qualifier: 指定一个名称作为id,让spring别使用变量名作为id 注意: @Autowired标注的自动装配的属性默认一定是会装配上的,如果找不到立马报错 --> java @Service public class BookService { @Qualifier("dao") @Autowired private BookDao daoExt; public void sava(){ daoExt.saveBook(); } } ### 实验21:在方法上位置使用@Qualifier注解 java /* 方法上写@Autowired 1. 这个方法也会在bean创建的时候自动运行 2. 这个方法的每一个参数都会自动装配 */ @Autowired public void hello(BookDao bookDao){ System.out.println("---->" + bookDao); } ### 实验22:@Autowired注解的required属性指定某个属性允许不被赋值 java @Service public class BookService { @Qualifier("dao") @Autowired(required = false) //表示找不到赋值为null,不是一定要装配成功 private BookDao daoExt; public void sava(){ //daoExt.saveBook(); } } ### @Resource注解和@Autowired注解的区别 java @Controller public class BookServlet { /* 区别: 1. @Autowired: 最强大、spring自己的注解 2. @Resource: Java自己的注解,JDK的标准 3. @Resource扩展性很强,如果切换成另外一个框架,还是可以使用的,但是@Autowired就不行了 */ //@Autowired @Resource private BookService service; public void sava(){ service.sava(); } } ### Spring的单元测试 java /* 1. 导包 2. @ContextConfiguration: 指定配置文件的路径 3. @RunWith: 指定使用哪种驱动进行单元测试,默认是junit 4. 好处: 我们不需要再使用context.getBean()获取组件了,直接使用@Autowired进行自动装配即可 */ @ContextConfiguration(locations = "classpath:applicationContext.xml") @RunWith(SpringJUnit4ClassRunner.class) public class SpringTest { @Autowired private BookDao dao; @Test public void test(){ System.out.println(dao); } } ### 实验23:测试泛型依赖注入 xml <!--23. 测试泛型依赖注入--> <context:component-scan base-package="com.siki"/> java //原始方式 public class Book { } public class User { } public abstract class BaseDao<T> { public abstract void save(); } @Repository public class BookDao extends BaseDao<Book>{ @Override public void save() { System.out.println("bookDao..."); } } @Repository public class UserDao extends BaseDao<User>{ @Override public void save() { System.out.println("userDao..."); } } @Service public class BookService { @Autowired private BookDao dao; public void save(){ dao.save(); } } @Service public class UserService { @Autowired private UserDao dao; public void save(){ dao.save(); } } @ContextConfiguration(locations = "classpath:applicationContext.xml") @RunWith(SpringJUnit4ClassRunner.class) public class SpringTest { @Autowired private BookService bookService; @Autowired private UserService userService; @Test public void test(){ bookService.save(); userService.save(); } } java //泛型依赖注入 public abstract class BaseDao<T> { public abstract void save(); } @Repository public class BookDao extends BaseDao<Book>{ @Override public void save() { System.out.println("bookDao..."); } } @Repository public class UserDao extends BaseDao<User>{ @Override public void save() { System.out.println("userDao..."); } } //这里不需要添加@Service注解,因为只要子类继承该类,就可以实现dao的自动装配 public class BaseService<T> { @Autowired private BaseDao<T> dao; public void save(){ System.out.println(dao); dao.save(); } } //子类只需要继承父类即可 @Service public class BookService extends BaseService<Book>{ } @Service public class UserService extends BaseService<User> { } @ContextConfiguration(locations = "classpath:applicationContext.xml") @RunWith(SpringJUnit4ClassRunner.class) public class SpringTest { @Autowired private BookService bookService; @Autowired private UserService userService; @Test public void test(){ bookService.save(); userService.save(); //带泛型的父类类型: com.siki.service.BaseService<com.siki.bean.Book> //spring中可以使用带泛型的父类类型来确定子类的类型 System.out.println(bookService.getClass().getGenericSuperclass()); } } ## 3. AOP 面向切面编程 ### 1. AOP概述 1. AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充 2. AOP编程操作的主要对象是切面(aspect),而切面模块化横切关注点 3. 在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面” 4. AOP的好处: 1. 每个事物逻辑位于一个位置,代码不分散,便于维护和升级 2. 业务模块更简洁,只包含核心业务代码 5. AOP术语 1. 横切关注点:从每个方法中抽取出来的同一类非核心业务 2. 切面(Aspect):封装横切关注点信息的类,每个关注点体现为一个通知方法 3. 通知(Advice):切面必须要完成的各个具体工作 4. 目标(Target):被通知的对象 5. 代理(Proxy):向目标对象应用通知之后创建的代理对象 6. 连接点(Joinpoint):横切关注点在程序代码中的具体体现, 对应程序执行的某个特定位置, 例如:类某个方法调用前、调用后、方法捕获到异常后等, 在应用程序中可以使用横纵两个坐标来定位一个具体的连接点 7. 切入点(pointcut):定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件 6. 面向切面编程:指在程序运行期间,将某段代码动态的切入到指定方法的指定位置进行运行的编程方式 ### 2. 计算机执行加减乘除计算 1. 动态代理
使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上 java /* 要求: 核心功能(+、-、*、/) + 日志模块,在核心功能运行期间,日志可以自己动态的加上 1. 动态代理很强大,日志记录可以做的很好,而且与业务逻辑解耦 2. 缺点 1. 代码写起来太难 2. JDK默认的动态代理,如果目标对象没有实现任何接口,是无法为它创建代理对象的(致命缺点) 3. 代理对象和被代理对象唯一能产生的关联就是实现了同一个接口 4. spring实现了AOP功能,底层就是动态代理,可以利用spring一句代码都不写就创建动态代理,实现简单,而且,没有强制要求目标对象必须实现接口 */ public interface Calculator { public int add(int i,int j); public int sub(int i,int j); public int mul(int i,int j); public int div(int i,int j); } public class MyCalculator implements Calculator { @Override public int add(int i, int j) { return i + j; } @Override public int sub(int i, int j) { return i - j; } @Override public int mul(int i, int j) { return i * j; } @Override public int div(int i, int j) { return i / j; } } //Calculator代理对象 public class CalculatorProxy { //为传入的参数对象创建一个代理对象 public static Calculator getProxy(Calculator calculator){ ClassLoader loader = calculator.getClass().getClassLoader(); //类加载器 Class<?>[] interfaces = calculator.getClass().getInterfaces(); //实现的接口 Object proxy = Proxy.newProxyInstance(loader, interfaces, new InvocationHandler() { //proxy: 代理对象,给JDK使用的,任何时候都不要动这个对象 //method: 当前将要执行的目标对象的方法 //args: @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //利用反射执行目标方法 System.out.println(method.getName() + " --> " + Arrays.asList(args)); Object invoke = method.invoke(calculator, args); //返回值必须返回出去,外界才能拿到真正执行后的返回值 return invoke; } }); return (Calculator) proxy; } } @Test public void test(){ Calculator calculator = new MyCalculator(); Calculator proxy = CalculatorProxy.getProxy(calculator); System.out.println(proxy.add(1, 2)); } ### 3. AOP测试 1. AOP简单配置 1. AspectJ:Java社区里最完整最流行的AOP框架
在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP 2. 在Spring中启用AspectJ注解支持 1. 导入jar包 2. 引入aop名称空间 3. 配置aop:aspectj-autoproxy
当Spring IOC容器侦测到bean配置文件中的aop:aspectj-autoproxy元素时,会自动为与AspectJ切面匹配的bean创建代理 3. 用AspectJ注解声明切面 1. 要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例 2. 当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理 3. 在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知 4. 通知是标注有某种注解的简单的Java方法 5. AspectJ支持5种类型的通知注解: 1. @Before:前置通知,在方法执行之前执行 2. @After:后置通知,在方法执行之后执行 3. @AfterRunning:返回通知,在方法返回结果之后执行 4. @AfterThrowing:异常通知,在方法抛出异常之后执行 5. @Around:环绕通知,围绕着方法执行 4. 导包 xml <!--AOP加强版jar包--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version> </dependency> <dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.1</version> </dependency> 5. 写配置文件 xml <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" <!--导入aop名称空间--> xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.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.xsd"> <!--1. 将目标类和切面类(封装了通知方法的类)加入到容器中--> <!--2. 还应该告诉spring到底哪个类是切面类(@Aspect注解标注的类)--> <!--3. 告诉spring,切面类里面的方法,什么时候执行--> <context:component-scan base-package="com.siki"/> <!--4. 开启基于注解的aop模式--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans> 6. 方式一:采用接口 java public interface Calculator { public int add(int i,int j); public int sub(int i,int j); public int mul(int i,int j); public int div(int i,int j); } @Service public class MyCalculator implements Calculator { @Override public int add(int i, int j) { LogUtils.logStart(); return i + j; } @Override public int sub(int i, int j) { LogUtils.logStart(); return i - j; } @Override public int mul(int i, int j) { LogUtils.logStart(); return i * j; } @Override public int div(int i, int j) { LogUtils.logStart(); return i / j; } } @Aspect //表示这是一个切面类 @Component public class LogUtils { //在目标方法执行之前运行 @Before("execution(public int com.siki.impl.MyCalculator.*(int,int))") //切入点表达式 public static void logStart(){ System.out.println("logAdd --> xxx"); } //在目标方法正常执行完成之后运行 @AfterReturning("execution(public int com.siki.impl.MyCalculator.*(int,int))") public static void logReturn(){ System.out.println("logSub --> xxx"); } //在目标方法出现异常的时候运行 @AfterThrowing("execution(public int com.siki.impl.MyCalculator.*(int,int))") public static void logException(){ System.out.println("logMul --> xxx"); } //在目标方法结束的时候运行 @After("execution(public int com.siki.impl.MyCalculator.*(int,int))") public static void logEnd(){ System.out.println("logDiv --> xxx"); } } @Test public void test02(){ //从容器中拿到目标对象 //注意: 如果想要用类型获取,一定要用接口类型,不要用本类 Calculator bean = context.getBean(Calculator.class); System.out.println(bean.add(1, 2)); //细节1: AOP底层用的就是动态代理,所以容器中保存的组件是它的代理对象,也就是接口类型 System.out.println(bean); //com.siki.impl.MyCalculator@399c4be1 System.out.println(bean.getClass()); //class com.sun.proxy.$Proxy17 } 7. 方式二:不要接口 java @Service public class MyCalculator { public int add(int i, int j) { LogUtils.logStart(); return i + j; } public int sub(int i, int j) { LogUtils.logStart(); return i - j; } public int mul(int i, int j) { LogUtils.logStart(); return i * j; } public int div(int i, int j) { LogUtils.logStart(); return i / j; } } //去掉接口 @Test public void test03(){ //没有接口,就是本类型 //细节2: cglib可以为没有接口的组件创建代理对象 MyCalculator bean = context.getBean(MyCalculator.class); System.out.println(bean.add(1, 2)); //cglib帮我们创建好的代理对象 System.out.println(bean.getClass());//class com.siki.impl.MyCalculator$$EnhancerBySpringCGLIB$$2763047e } 8. 切入点表达式的作用:通过表达式的方式定位一个或多个具体的连接点 9. 切入点表达式的语法格式:execution([权限修饰符] [返回值类型] [简单类名/全类名]方法名(参数列表) 1. 例如:execution(public int com.siki.impl.MyCalculator.(..)) 2. :通配符,可以匹配一个或多个字符(权限位置不能写,可以不写,表示任意权限) 3. .. :匹配任意多个参数,任意类型参数(还可以表示匹配任意多层路径) 4. 在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来 1. 例如:execution ( .add(int,..)) || execution( .sub(int,..)) java @Aspect //表示这是一个切面类 @Component public class LogUtils { /* 细节4: 我们可以在通知方法运行的时候,拿到目标方法的详细信息 JoinPoint point 封装了当前目标方法的详细信息 切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点 那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息 例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中 1. 只需要为通知方法的参数列表上写一个参数 2. 获取方法返回值时,告诉spring,使用哪个变量获取返回值 3. 获取异常信息时,告诉spring,使用哪个变量获取异常信息 细节5: spring对通知方法的要求一点都不严格,唯一的要求就是参数列表一定要正确,不能乱写 因为通知方法是spring利用反射调用的,每次方法调用都得确定这个方法的参数列表的值 spring必须明确知道每一个参数 细节6: 抽取可重用的切入点表达式 1. 随便声明一个没有实现的返回void的空方法 2. 给方法上标注@Pointcut注解 */ @Pointcut("execution(public int com.siki.impl.MyCalculator.*(int,int))") public void helloPointcut(){ } //在目标方法执行之前运行 @Before("helloPointcut()") //切入点表达式 public static void logStart(JoinPoint point){ System.out.println(point.getSignature().getName() + " --> " + Arrays.asList(point.getArgs())); } //在目标方法正常执行完成之后运行(returning: 用来接收返回值) @AfterReturning(value = "helloPointcut()",returning = "result") public static void logReturn(JoinPoint point,Object result){ System.out.println(point.getSignature().getName() + " --> xxx"); } //在目标方法出现异常的时候运行(throwing: 用来接收异常信息) @AfterThrowing(value = "helloPointcut()",throwing = "e") public static void logException(JoinPoint point,Exception e){ System.out.println(point.getSignature().getName() + " --> " + e.getMessage()); } //在目标方法结束的时候运行 @After("helloPointcut()") public static void logEnd(JoinPoint point){ System.out.println(point.getSignature().getName() + " --> xxx"); } /* 细节8: 环绕通知: 最强大的通知 1. 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。 2. 对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint 它是JoinPoint的子接口,允许控制何时执行,是否执行连接点 3. 在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。 如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。 注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用joinPoint.proceed()的返回值,否则会出现空指针异常 执行顺序: 环绕通知优先执行 环绕前置-->普通前置-->目标方法执行-->环绕正常返回/出现异常-->环绕后置-->普通后置-->普通返回/出现异常 */ @Around("helloPointcut()") public Object myAround(ProceedingJoinPoint point){ Object[] args = point.getArgs(); Object proceed = null; try { //其实就是利用反射调用目标方法,相当于动态代理的method.invoke(obj,args) System.out.println("before"); //前置通知 proceed = point.proceed(args); System.out.println("afterReturning"); } catch (Throwable throwable) { //注意: 如果使用try..catch捕捉异常,一旦环绕通知捕捉到了,那么异常通知将捕捉不到该异常 //解决办法: 可以将异常抛出 System.out.println("afterThrowing " + point.getSignature().getName()); }finally { System.out.println("after"); } //反射调用后的值也要返回出去 return proceed; } } @Test public void test04(){ /* 细节3: 通知方法的执行顺序 1. 正常执行 前置通知-->后置通知-->返回通知(正常返回才会有) 2. 异常执行 前置通知-->后置通知-->异常通知(返回时出现异常才会有) */ MyCalculator bean = context.getBean(MyCalculator.class); bean.mul(1,2); System.out.println("--------------------"); //bean.div(1,0); } java //多个切面执行顺序 @Aspect @Component @Order(1) //@Order可以改变切面的执行顺序,数值越小,优先级越高 public class ValidateUtils { @Before("com.siki.utils.LogUtils.helloPointcut()") public static void logStart(JoinPoint point){ System.out.println("Validate" + point.getSignature().getName() + " --> " + Arrays.asList(point.getArgs())); } @AfterReturning(value = "com.siki.utils.LogUtils.helloPointcut()",returning = "result") public static void logReturn(JoinPoint point,Object result){ System.out.println("Validate " + point.getSignature().getName() + " --> yyy"); } @AfterThrowing(value = "com.siki.utils.LogUtils.helloPointcut()",throwing = "e") public static void logException(JoinPoint point,Exception e){ System.out.println("Validate" + point.getSignature().getName() + " --> " + e.getMessage()); } @After("com.siki.utils.LogUtils.helloPointcut()") public static void logEnd(JoinPoint point){ System.out.println("Validate " + point.getSignature().getName() + " --> yyy"); } } @Test public void test05(){ MyCalculator bean = context.getBean(MyCalculator.class); bean.add(1,1); } /* 运行结果: Logadd --> [1, 1] Validateadd --> [1, 1] Validateadd --> yyy Validateadd --> yyy Logadd --> xxx Logadd --> xxx */ ### 4. AOP应用 1. AOP使用场景 1. 日志,保存到数据库中 2. 权限验证 3. 安全检查 4. 事务控制 2. 基于配置的AOP ```xml

<aop:aspect ref="validateUtils" order="1">
    <aop:before method="logStart" pointcut-ref="myPoint"/>
    <aop:after-returning method="logReturn" pointcut-ref="myPoint" returning="result"/>
    <aop:after-throwing method="logException" pointcut-ref="myPoint" throwing="e"/>
    <aop:after method="logEnd" pointcut-ref="myPoint"/>
</aop:aspect>




<a name="55f11aed"></a>
## 4. 声明式事务

<a name="1f25585c"></a>
### 1. 事务

1. 在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术
2. 事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行
3. 事务的四个关键属性(ACID)

   1. **原子性**(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行
   2. **一致性**(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都**必须保证事务执行之前数据是正确的**,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是**回滚**
   3. **隔离性**((isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求**多个事务在并发执行过程中不会互相干扰**
   4. **持久性**(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,**事务对数据的修改应该被写入到持久化存储器中**

<a name="9ef91d1e"></a>
### 2. Spring事务管理

1. 
编程式事务管理

   1. 
使用原生的JDBC API进行事务管理

      1. 获取数据库连接Connection对象
      2. 取消事务的自动提交
      3. 执行操作
      4. 正常完成操作时手动提交事务
      5. 执行失败时回滚事务
      6. 关闭相关资源
   2. 
评价
<br />使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余

2. 
声明式事务管理

   1. 大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理
   2. 事务管理代码的固定模式作为一种**横切关注点,可以通过AOP方法模块化**,进而借助Spring AOP框架实现声明式事务管理
   3. Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制
   4. Spring既支持编程式事务管理,也支持声明式的事务管理
3. 
Spring提供的事务管理器

   1. Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的
   2. Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的
   3. 事务管理器可以以普通的bean的形式声明在Spring IOC容器中
4. 
事务管理器的主要实现

   1. DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取
   2. JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理
   3. HibernateTransactionManager:用Hibernate框架存取数据库
5. 
事务的传播行为

   1. 
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行

   2. 
事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为
<br />

6. 
事务的隔离级别

   1. 
数据库事务并发问题:假设现在有两个事务Transaction01和Transaction02并发执行

      1. 脏读

         1. Transaction01将某条记录的AGE值从20修改为30
         2. Transaction02读取了Transaction01更新后的值:30
         3. Transaction01回滚,AGE值恢复到了20
         4. Transaction02读取到的30就是一个无效的值
      2. 不可重复读

         1. Transaction01读取了AGE值为20
         2. Transaction02将AGE值修改为30
         3. Transaction01再次读取AGE值为30,和第一次读取不一致
      3. 幻读

         1. Transaction01读取了STUDENT表中的一部分数据
         2. Transaction02向STUDENT表中插入了新的行
         3. Transaction01读取了STUDENT表时,多出了一些行
   2. 
隔离级别:数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱

      1. 读未提交(READ UNCOMMITTED):允许Transaction01读取Transaction02未提交的修改
      2. 读已提交(READ COMMITTED):要求Transaction01只能读取Transaction02已提交的修改
      3. 可重复读(REPEATABLE READ):确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新
      4. 串行化(SERIALIZABLE):确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下
   3. 
各个隔离级别解决并发问题的能力

   4. 
在Spring中指定事务隔离级别

      1. 用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别
   5. 
在Spring 2.x事务通知中,可以在tx:method元素中指定隔离级别


<a name="5156ed33"></a>
### 3. 事务测试

1. 未使用事务

```xml
<!--事务(注意: 使用事务时,aop的所有jar包也要导入)-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.0.8.RELEASE</version>
</dependency>
<!--db.properties-->
jdbc.username=root
jdbc.password=123456
jdbc.jdbcUrl=jdbc:mysql://localhost:3306/tx
jdbc.driverClass=com.mysql.jdbc.Driver

<!--1. 包扫描-->
<context:component-scan base-package="com.siki"/>

<!--2. 配置数据源-->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
    <property name="driverClass" value="${jdbc.driverClass}"/>
</bean>

<!--3. 配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>
@Repository
public class BookDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    //修改余额
    public void updateBalance(String username,int price){
        String sql = "update account set balance = balance - ? where username = ?";
        jdbcTemplate.update(sql,price,username);
    }

    //获取某本图书的价格
    public int getPrice(String isbn){
        String sql = "select price from book where isbn = ?";
        return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
    }

    //修改库存(为了简单起见,每次减1)
    public void updateStock(String isbn){
        String sql = "update book_stock set stock = stock - 1 where isbn = ?";
        jdbcTemplate.update(sql,isbn);
    }

}

@Service
public class BookService {

    @Autowired
    private BookDao dao;

    //结账
    public void checkout(String username,String isbn){
        dao.updateStock(isbn);  //减库存
        int price = dao.getPrice(isbn);
        dao.updateBalance(username,price);  //减余额
    }

}

@Test
public void test01(){
    BookService bean = context.getBean(BookService.class);
    bean.checkout("Tom","ISBN-001");
    System.out.println("sucess...");
}
  1. 使用事务
<!--1. 包扫描-->
<context:component-scan base-package="com.siki"/>

<!--2. 配置数据源-->
<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
    <property name="driverClass" value="${jdbc.driverClass}"/>
</bean>

<!--3. 配置jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!--4. 事务控制-->
<!--4.1 配置事务管理器,让其进行事务控制-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <!--控制数据源-->
    <property name="dataSource" ref="dataSource"/>
</bean>
<!--4.2 开启基于注解的事务控制模式(导入tx名称空间)-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<!--4.3 给事务方法加注解@Transactional-->
@Repository
public class BookDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    //修改余额
    public void updateBalance(String username,int price){
        String sql = "update account set balance = balance - ? where username = ?";
        jdbcTemplate.update(sql,price,username);
    }

    //获取某本图书的价格
    public int getPrice(String isbn){
        String sql = "select price from book where isbn = ?";
        return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
    }

    //修改库存(为了简单起见,每次减1)
    public void updateStock(String isbn){
        String sql = "update book_stock set stock = stock - 1 where isbn = ?";
        jdbcTemplate.update(sql,isbn);
    }

    //修改图书价格
    public void updatePrice(String isbn,int price){
        String sql = "update book set price = ? where isbn = ?";
        jdbcTemplate.update(sql,isbn,price);
    }

}

@Service
public class BookService {

    @Autowired
    private BookDao dao;

    //表示这是一个事务方法
    @Transactional
    public void checkout(String username,String isbn){
        dao.updateStock(isbn);  //减库存
        int price = dao.getPrice(isbn);
        dao.updateBalance(username,price);  //减余额
    }

}

4. 事务的属性

/*
    事务属性: 注意: 有事务逻辑,容器中保存的是这个事务的代理对象
        1. isolation-Isolation: 事务的隔离级别
            @Transactional(isolation = Isolation.READ_UNCOMMITTED)

        2. propagation-Propagation: 事务的传播行为
            当前事务是否需要和大事务使用同一事务

        3. noRollbackFor-Class[]: 哪些异常,事务可以不回滚
            1. 可以让原来默认回滚的事务,让其不回滚
            2. @Transactional(noRollbackFor = {ArithmeticException.class})
            注意: 运行时异常可以不用处理,默认回滚,编译时异常默认不回滚

        4. rollbackFor-Class[]: 哪些异常,事务需要回滚
            1. 可以让原来默认不回滚的事务,让其回滚
            2. @Transactional(rollbackFor = {FileNotFoundException.class})

        5. readOnly-boolean: 设置事务为只读事务  @Transactional(readOnly = true)
            readOnly = true 可以加快查询速度

        6. timeout-int: 超时,超出指定执行时长后自动终止并回滚  @Transactional(timeout = 2)
     */
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void checkout(String username,String isbn){
    //        try {
    //            Thread.sleep(3000);
    //        } catch (InterruptedException e) {
    //            e.printStackTrace();
    //        }
    dao.updateStock(isbn);  //减库存
    int price = dao.getPrice(isbn);
    dao.updateBalance(username,price);  //减余额
    int i = 1 / 0;
}

5. 事务传播

@Service
public class BookService {

    @Autowired
    private BookDao dao;

    @Transactional(propagation = Propagation.REQUIRED)
    public void checkout(String username,String isbn){
        dao.updateStock(isbn); 
        int price = dao.getPrice(isbn);
        dao.updateBalance(username,price); 
        int i = 1 / 0;
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void updatePrice(String isbn,int price){
        dao.updatePrice(isbn,price);
    }

}

@Service
public class MyService {

    @Autowired
    private BookService service;

    /*事务传播(多事务并发运行)
      如果是REQUIRED,则小事务的属性都是继承于大事务,而REQUIRES_NEW,则可以小事务自己调整
      REQUIRED: 将之前事务用的connection传递给当前方法使用
      REQUIRES_NEW: 让当前方法直接使用新的connection
    */
    @Transactional
    public void update(){
        //传播行为就是用来设置这个事务方法需不需要和之前的大事务共享一个事务(即使用同一连接)
        service.checkout("Tom","ISBN-001");
        service.updatePrice("ISBN-002",900);
        //int i = 1 / 0;
    }

}

@Test
public void test02(){
    MyService bean = context.getBean(MyService.class);
    bean.update();
}

6. 事务控制

<context:component-scan base-package="com.siki"/>

<context:property-placeholder location="classpath:db.properties"/>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="user" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
    <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>
    <property name="driverClass" value="${jdbc.driverClass}"/>
</bean>

<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<!--
        基于xml配置的事务
            1. 配置事务管理器
            2. 配置事务方法: 事务切面按照我们的切入点表达式去切入事务方法
    -->

<!--配置事务方法-->
<aop:config>
    <aop:pointcut id="point" expression="execution(* com.siki.service.*.*(..))"/>
    <!--配置事务增强-->
    <!--advice-ref: 指向事务管理器的配置-->
    <aop:advisor advice-ref="advice" pointcut-ref="point"/>
</aop:config>
<!--指定配置哪个事务管理器-->
<tx:advice transaction-manager="transactionManager" id="advice">
    <tx:attributes>
        <!--指明哪些方法是事务方法,切入点表达式只是说事务管理器要切入这些方法,但是哪些方法需要事务,由tx:method控制-->
        <tx:method name="*"/>
        <tx:method name="checkout" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
@Repository
public class BookDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void updateBalance(String username,int price){
        String sql = "update account set balance = balance - ? where username = ?";
        jdbcTemplate.update(sql,price,username);
    }

    public int getPrice(String isbn){
        String sql = "select price from book where isbn = ?";
        return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
    }

    public void updateStock(String isbn){
        String sql = "update book_stock set stock = stock - 1 where isbn = ?";
        jdbcTemplate.update(sql,isbn);
    }

    public void updatePrice(String isbn,int price){
        String sql = "update book set price = ? where isbn = ?";
        jdbcTemplate.update(sql,isbn,price);
    }

}

@Service
public class BookService {

    @Autowired
    private BookDao dao;

    public void checkout(String username,String isbn){
        dao.updateStock(isbn);
        int price = dao.getPrice(isbn);
        dao.updateBalance(username,price);
    }

    public void updatePrice(String isbn,int price){
        dao.updatePrice(isbn,price);
    }

}

@Test
public void test(){
    BookService bean = context.getBean(BookService.class);
    bean.checkout("Tom","ISBN-001");
}

SpringMVC

1. SpringMVC概述

  1. 关于SpringMVC

    1. Spring 为展现层提供的基于 MVC 设计理念的优秀的 Web 框架,是目前最主流的 MVC 框架之一
    2. Spring3.0 后全面超越 Struts2,成为最优秀的 MVC 框架
    3. 一种轻量级的、基于MVC的Web层应用框架, 偏前端而不是基于业务逻辑层, 是Spring框架的一个后续产品
    4. Spring MVC 通过一套 MVC 注解,让 POJO 成为处理请求的控制器,而无须实现任何接口
    5. 支持 REST 风格的 URL 请求
    6. 采用了松散耦合可插拔组件结构,比其他 MVC 框架更具扩展性和灵活性
  2. SpringMVC的功能

    1. 天生与Spring框架集成,如:(IOC,AOP)
    2. 支持Restful风格
    3. 进行更简洁的Web层开发
    4. 支持灵活的URL到页面控制器的映射
    5. 因为模型数据不存放在特定的API里,而是放在一个Model里(Map数据结构实现,因此很容易被其他框架使用)
    6. 非常灵活的数据验证、格式化和数据绑定机制、能使用任何对象进行数据绑定,不必实现特定框架的API
    7. 更加简单、强大的异常处理
    8. 对静态资源的支持
    9. 支持灵活的本地化、主题等解析
  3. 常用主要组件

    1. DispatcherServlet:前端控制器
    2. Controller:处理器/页面控制器,做的是MVC中的C的事情,但控制逻辑转移到前端控制器了,用于对请求进行处理
    3. HandlerMapping:请求映射到处理器,找谁来处理,如果映射成功返回一个HandlerExecutiongChain对象(包含一个Handler处理器(页面控制器)对象、多个HandlerInterceptor拦截器对象)
    4. ViewResolver: 视图解析器,找谁来处理返回的页面。把逻辑视图解析为具体的View,进行这种策略模式,很容易更换其他视图技术

      1. 如InternalResourceViewResolver将逻辑视图名映射为JSP视图
    5. LocalResolver:本地化、国际化
    6. MultipartResolver:文件上传解析器
    7. HandlerExceptionResolver:异常处理器
  4. SpringMVC的工作流程

2. HelloWorld

<!--1. 导入依赖-->
<dependencies>
    <!--spring的配置包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.8.RELEASE</version>
    </dependency>
    <!--spring的日志依赖包-->
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.1.1</version>
    </dependency>
    <!--spring的单元测试-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.8.RELEASE</version>
    </dependency>
    <!--AOP-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.9</version>
    </dependency>
    <dependency>
        <groupId>aopalliance</groupId>
        <artifactId>aopalliance</artifactId>
        <version>1.0</version>
    </dependency>
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.1</version>
    </dependency>
    <!--SpringMVC-->
    <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.0.8.RELEASE</version>
        </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.0.8.RELEASE</version>
    </dependency>
</dependencies>
<!--2. web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--配置前端控制器DispatcherServlet-->
    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--指定springmvc的配置文件位置-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <!--servlet启动加载: servlet默认第一次访问时创建对象
            我们可以设置为服务器启动的时候就创建对象,值越小优先级越高,越先创建对象-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <!--/*和/都会拦截所有请求-->
        <!--/*会拦截jsp请求,一旦拦截jsp请求,页面将无法正常显示-->
        <!--/不会拦截jsp请求,页面可以正常显示-->
        <!--处理*.jsp是Tomcat做的事-->
        <!--重点: 此时,我们可以访问jsp页面,但是无法访问index.html页面
                  因为index.html页面是静态资源,tomcat就会在服务器下找到这个资源并返回
                  而我们的前端控制器的/禁用了tomcat服务器中的DefaultServlet
                  但是我们并没有拦截tomcat服务器的JspServlet,所以可以访问jsp页面
         -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>


<!--3. springmvc.xml-->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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


</beans>
//4. 控制器
@Controller
public class HelloWorld {

    @RequestMapping("/hello")
    public String hello(){
        System.out.println("请求收到...");
        return "success";
    }

}
<!--5. index.jsp-->
<body>
    <a href="hello">helloworld</a>
</body>

<!--success.jsp-->
<body>
    <font color="#98fb98">成功</font>
</body>
  1. 如果项目启动报错,可能是缺少lib依赖

  2. HelloWorld运行细节

    1. 运行流程
  3. 客户端点击链接会发送http://localhost:8080/SpringMVC_HelloWorld/hello请求
  4. 来到Tomcat服务器
  5. SpringMVC的前端控制器会收到所有请求,看请求地址和@RequestMapping标注的哪个请求匹配,来找到到底使用哪个类的哪个请求
  6. 前端控制器找到了目标处理器和目标方法,直接利用反射执行目标方法
  7. 方法执行完以后会有一个返回值,SpringMVC认为这个返回值就是要去的页面地址
  8. 拿到返回值以后,用视图解析器进行拼串,得到完整的页面地址
  9. 拿到页面地址,前端控制器帮我们转发到相应的页面

    1. @RequestMapping

      1. 告诉SpringMVC,这个方法用来处理什么请求
      2. 这个”/“是可以省略的,也是默认从当前项目下开始
      3. 习惯性加上比较好
    2. 如果不指定配置文件的位置,也会默认去/WEB-INF/xxx-servlet.xml路径下找配置文件,如果resources文件夹下没有配置文件,可以在WEB-INF下创建一个名叫”前端控制器名-servlet.xml”的配置文件
    3. url-pattern

      1. /配置的是”/“
      2. Tomcat服务器的大web.xml中有一个DefaultServlet,它的url-pattern设置的是”/“
      3. DefaultServlet是用来处理Tomcat中的静态资源的,除jsp、servlet外,其余的页面都是静态资源
      4. jsp是由Tomcat服务器的JspServlet处理的,所以可以访问

3. @RequestMapping

  1. SpringMVC使用@RequestMapping注解为控制器指定可以处理哪些 URL 请求

  2. 在控制器的类定义及方法定义处都可标注 @RequestMapping

    1. 标记在类上:提供初步的请求映射信息。相对于 WEB 应用的根目录
    2. 标记在方法上:提供进一步的细分映射信息。相对于标记在类上的 URL
  3. 若类上未标注 @RequestMapping,则方法处标记的 URL 相对于 WEB 应用的根目录

  4. 作用:DispatcherServlet 截获请求后,就通过控制器上 @RequestMapping 提供的映射信息确定请求所对应的处理方法

    //@RequestMapping源码
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Mapping
    public @interface RequestMapping {
     String name() default "";
    
     @AliasFor("path")
     String[] value() default {};
    
     @AliasFor("value")
     String[] path() default {};
    
     RequestMethod[] method() default {};
    
     String[] params() default {};
    
     String[] headers() default {};
    
     String[] consumes() default {};
    
     String[] produces() default {};
    }
    
  1. RequestMapping支持Ant 路径风格

    1. ?:匹配文件名中的一个字符,0个或多个都不行,模糊和精确的情况下,精确优先

    2. *:匹配文件名中的任意字符,也可以匹配一层路径

    3. **:匹配多层路径

      /user/*/createUser
      匹配 /user/aaa/createUser、/user/bbb/createUser 等 URL
      /user/**/createUser
      匹配 /user/createUser、/user/aaa/bbb/createUser 等 URL
      /user/createUser??
      匹配 /user/createUseraa、/user/createUserbb 等 URL
      
  1. RequestMapping映射请求占位符PathVariable注解

    1. @PathVariable 映射 URL 绑定的占位符
    2. 带占位符的 URL 是 Spring3.0 新增的功能,该功能在 SpringMVC 向 REST目标挺进发展过程中具有里程碑的意义
    3. 通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中
    4. URL 中的 {xxx} 占位符可以通过 @PathVariable(“xxx”) 绑定到操作方法的入参中
@RequestMapping("/haha")  //为该类中的所有方法提供一个基准路径
@Controller
public class RequestMappingTest {

    @RequestMapping("/hello01")
    public String hello01(){
        return "success";
    }

    @RequestMapping("/hello02")
    public String hello02(){
        return "success";
    }

    /*
        1. @RequestMapping的其他属性
            1. method: 限定请求方式,默认是任意类型
                HTTP协议中的所有请求方式
                    POST GET HEAD PATCH PUT DELETE OPTIONS等
                    不是规定的方式就会报错
                    4xx: 都是客户端报错

            2. params: 规定请求参数,可以写一些简单的表达式
                1. param1: 表示请求必须包含名为param1的请求参数,没带就会404
                    params = {"username"}
                2. !param2: 表示请求不能包含名为param2的请求参数
                    params = {"!username"}
                3. param1 != value1: 表示请求必须包含名为param1的参数,但是其值不能为value1
                    params = {"username!=root"}

            3. headers: 规定请求头,也和params一样,可以写一些简单的表达式

            4. consumes: 只接受内容类型是哪一种的请求,规定请求头中的Content-Type
     */
    //表示只接收post请求,其他的方式就会报错
    @RequestMapping(value = "/hello03",method = RequestMethod.POST)
    public String hello03(){
        return "success";
    }

    @RequestMapping(value = "/hello04",params = {"username"})
    public String hello04(){
        return "success";
    }

    //表示只有谷歌能访问,其他浏览器不行
    @RequestMapping(value = "/hello05",headers = {"User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"})
    public String hello05(){
        return "success";
    }

    //路径上的占位符{username}只占一层路径
    @RequestMapping("/hello06/{username}")
    public String hello06(@PathVariable("username") String username){
        System.out.println("username = " + username);
        return "success";
    }

}

4. Rest风格的增删改查

  1. Rest风格:即 Representational State Transfer 资源表现层状态转化。是目前最流行的一种互联网软件架构。它结构清晰、符合标准、易于理解、扩展方便,所以正得到越来越多网站的采用

    1. 资源(Resources):网络上的一个实体,或者说是网络上的一个具体信息,它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的存在,可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的 URI 。获取这个资源,访问它的URI就可以,因此 URI 即为每一个资源的独一无二的识别符
    2. 表现层(Representation):把资源具体呈现出来的形式,叫做它的表现层(Representation)。比如,文本可以用 txt 格式表现,也可以用 HTML 格式、XML 格式、JSON 格式表现,甚至可以采用二进制格式
    3. 状态转化(State Transfer):每发出一个请求,就代表了客户端和服务器的一次交互过程。HTTP协议,是一个无状态协议,即所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”(State Transfer)而这种转化是建立在表现层之上的,所以就是 “表现层状态转化”
    4. 具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE,它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源
  2. HiddenHttpMethodFilter

    1. 浏览器 form 表单只支持 GET 与 POST 请求,而DELETE、PUT 等 method 并不支持,

    2. Spring3.0 添加了一个过滤器,可以将这些请求转换为标准的 http 方法,使得支持 GET、POST、PUT 与 DELETE 请求

<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--配置前端控制器-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!--配置过滤器,支持Rest风格的PUT和DELETE-->
    <filter>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <!--除了DispatcherServlet使用"/",其他的拦截使用"/*"-->
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>


<!--springmvc.xml-->
<!--包扫描-->
<context:component-scan base-package="com.siki"/>

<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/pages/"/>
    <property name="suffix" value=".jsp"/>
</bean>
@Controller
public class BookController {

    //增加图书
    @RequestMapping(value = "/book",method = RequestMethod.POST)
    public String addBook(){
        System.out.println("添加了新的图书");
        return "success";
    }

    //修改图书
    @RequestMapping(value = "/book/{id}",method = RequestMethod.PUT)
    public String updateBook(@PathVariable("id") Integer id){
        System.out.println("更新了 " + id + " 号图书");
        return "success";
    }

    //查询图书
    @RequestMapping(value = "/book/{id}",method = RequestMethod.GET)
    public String getBook(@PathVariable("id") Integer id){
        System.out.println("查询到了 " + id + " 号图书");
        return "success";
    }

    //删除图书
    @RequestMapping(value = "/book/{id}",method = RequestMethod.DELETE)
    public String deleteBook(@PathVariable("id") Integer id){
        System.out.println("删除了 " + id + " 号图书");
        return "success";
    }

}
<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true"%>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <font color="green">成功</font>
</body>
</html>


<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <title>$Title$</title>
</head>
<body>
  <!--
      发起图书的增删改查请求,使用Rest风格的URL地址
          1. 服务器支持POST、GET请求,但是不支持PUT、DELETE请求

          2. 如何从页面发起PUT、DELETE请求
            1. SpringMVC有一个Filter,它可以把普通的请求转换为规定形式的请求,我们可以在web.xml中配置这个Filter
            2. 按照以下要求
              1. 创建一个post类型的表单
              2. 表单项中携带一个_method的参数,这个_method的值设置为PUT或者DELETE

          3. 注意: 高版本的Tomcat(8.0及以上版本)对PUT、DELETE请求方式不支持
            解决办法: <%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
            在success.jsp中设置isErrorPage="true",默认为false
  -->
  <a href="book/1">查询图书</a><br>
  <form action="book" method="post">
    <input type="submit" value="添加图书"/>
  </form><br>
  <form action="book/1" method="post">
    <input name="_method" value="delete"/>
    <input type="submit" value="删除图书"/>
  </form><br>
  <form action="book/1" method="post">
    <input name="_method" value="put"/>
    <input type="submit" value="更新图书"/>
  </form>
</body>
</html>
//HiddenHttpMethodFilter源码分析
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS;
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = "_method";

    public HiddenHttpMethodFilter() {
    }

    public void setMethodParam(String methodParam) {
        Assert.hasText(methodParam, "'methodParam' must not be empty");
        this.methodParam = methodParam;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        //表单是一个Post
        if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
            //获取表单上_method带来的值
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                   //而且是PUT、DELETE、PATCH中的一种,就会转换为对应的请求方式
                if (ALLOWED_METHODS.contains(method)) {
                    //这里重写了request.getMethod()方法
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }

        filterChain.doFilter((ServletRequest)requestToUse, response);
    }

    static {
        ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }

        public String getMethod() {
            return this.method;
        }
    }
}

5. 请求数据传入

  1. 请求处理方法参数

    1. Spring MVC 通过分析处理方法的签名,HTTP请求信息绑定到处理方法的相应人参中
    2. Spring MVC 对控制器处理方法签名的限制是很宽松的,几乎可以按喜欢的任何方式对方法进行签名
    3. 必要时可以对方法及方法入参标注相应的注解( @PathVariable 、@RequestParam、@RequestHeader 等)
    4. Spring MVC 框架会将 HTTP 请求的信息绑定到相应的方法入参中,并根据方法的返回值类型做出相应的后续处理
<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--
        注意: 使用SpringMVC时,前端控制器写完就写字符编码过滤器
            1. 设置GET请求编码方式
                在Tomcat的conf文件夹中的server.xml中8080端口处添加URLEncoding="UTF-8"
            2. 设置字符编码过滤器
    -->

    <!--配置前端控制器-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!--配置字符编码过滤器  一定要注意: 字符编码过滤器一定要配置在其他过滤器之前,否则无法生效-->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!--指定解决POST请求乱码-->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <!--解决请求乱码和响应乱码-->
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!--配置请求方式过滤器-->
    <filter>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>


<!--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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

</beans>
<!--index.jsp-->
<html>
<head>
  <title>$Title$</title>
</head>
<body>
  <a href="hello">hello</a><br>
  <a href="hello01?username=xinxin">默认方式请求参数</a><br>
  <a href="hello02?user=root">@RequestParameter方式请求参数</a>
  <form action="book" method="post">
    书名: <input type="text" name="bookName"/><br>
    作者: <input type="text" name="authorName"/><br>
    价格: <input type="text" name="price"/><br>
    库存: <input type="text" name="stock"/><br>
    销量: <input type="text" name="sales"/><br>
    省: <input type="text" name="address.province"/><br>
    市: <input type="text" name="address.city"/><br>
    街道: <input type="text" name="address.street"/><br>
    <input type="submit" value="添加图书"/>
  </form>
  <a href="hello03">原生API</a>
</body>
</html>


<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <font color="green">成功</font><br>
    请求: ${requestScope.requestMsg}<br>
    会话: ${sessionScope.sessionMsg}<br>
</body>
</html>
@Controller
public class RequestController {

    @RequestMapping("/hello")
    public String hello(){
        return "success";
    }

    /*
        SpringMVC如何获取请求带来的各种信息
            1. @RequestParam: 获取请求参数
                1. 默认方式获取请求参数
                    直接给方法形参是写一个和请求参数名相同的变量,这个变量用来接收请求参数的值
                2. @RequestParam("user") String username  相当于username = request.getParameter("user")
                    1. 参数默认是必须带的
                    2. 属性
                        1. value: 指定要获取参数的key
                        2. required: 如果为true,表示参数必须要带
                        3. defaultValue: 如果没带参数,我们可以指定默认值
                    3. @RequestParam("user")-->user表示?后面的参数
                       @PathVariable("user")-->user表示/路径后面的值

            2. @RequestHeader: 获取请求头中某个key的值,如果请求头中没有这个值,就会报错
                @RequestHeader("User-Agent") String userAgent
                相当于userAgent = request.getParameter("User-Agent")

            3. @CookieValue: 获取某个Cookie的值
                @CookieValue("JSESESSIONID") String id
                相当于id = request.getCookies()[i].getName.equals("JSESESSIONID")

            4. 传入POJO,SpringMVC会自动封装
                1. 我们的请求参数是一个POJO,SpringMVC会自动的为这个POJO进行赋值
                2. 将POJO中的每一个属性,从request参数中尝试获取出来,并进行封装
                3. 还可以进行级联封装

            5. SpringMVC也支持传入原生API,但也不是所有的API都支持,只支持以下几种
                1. HttpServletRequest
                2. HttpServletResponse
                3. HttpSession
                4. java.security.Principal
                5. Local: 国际化有关的区域信息对象
                6. InputStream、OutputStream、Reader、Writer
                    request.getInputStream();
                    response.getOutputStream();
                    request.getReader();
                    response.getWriter();

            6. 当我们传入POJO的时候,可能会出现乱码
                1. 请求乱码
                    GET请求: 在Tomcat的conf文件夹中的server.xml中8080端口处添加URLEncoding="UTF-8"
                    POST请求: 在第一次获取请求参数之前设置
                        request.setCharacterEncoding("utf-8");
                        SpringMVC为我们准备了一个CharacterEncodingFilter过滤器,解决乱码问题,在web.xml中配置
                2. 响应乱码
                    response.setContentType("text/html;charset=utf-8");
     */
    //默认方式获取请求参数
    @RequestMapping("/hello01")
    public String hello01(String username){
        System.out.println("username = " + username);
        return "success";
    }

    @RequestMapping("/hello02")
    public String hello02(@RequestParam(value = "user",required = false) String username,
                          @RequestHeader("User-Agent") String userAgent,
                          @CookieValue(value = "JSESESSIONID",required = false) String id){
        System.out.println("username = " + username);
        System.out.println("User-Agent = " + userAgent);
        System.out.println("JSESESSIONID = " + id);
        return "success";
    }

    @RequestMapping("/book")
    public String addBook(Book book){
        System.out.println(book);
        return "success";
    }

    @RequestMapping("/hello03")
    public String hello03(HttpSession session, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
        request.setAttribute("requestMsg","我是HttpServletRequest");
        session.setAttribute("sessionMsg","我是HttpSession");
        return "success";
    }
}
//CharacterEncodingFilter源码
public class CharacterEncodingFilter extends OncePerRequestFilter {
    @Nullable
    private String encoding;
    private boolean forceRequestEncoding;
    private boolean forceResponseEncoding;

    public CharacterEncodingFilter() {
        //默认不解决请求乱码和响应乱码
        this.forceRequestEncoding = false;
        this.forceResponseEncoding = false;
    }

    public CharacterEncodingFilter(String encoding) {
        this(encoding, false);
    }

    public CharacterEncodingFilter(String encoding, boolean forceEncoding) {
        this(encoding, forceEncoding, forceEncoding);
    }

    public CharacterEncodingFilter(String encoding, boolean forceRequestEncoding, boolean forceResponseEncoding) {
        this.forceRequestEncoding = false;
        this.forceResponseEncoding = false;
        Assert.hasLength(encoding, "Encoding must not be empty");
        this.encoding = encoding;
        this.forceRequestEncoding = forceRequestEncoding;
        this.forceResponseEncoding = forceResponseEncoding;
    }

    public void setEncoding(@Nullable String encoding) {
        this.encoding = encoding;
    }

    @Nullable
    public String getEncoding() {
        return this.encoding;
    }

    public void setForceEncoding(boolean forceEncoding) {
        this.forceRequestEncoding = forceEncoding;
        this.forceResponseEncoding = forceEncoding;
    }

    public void setForceRequestEncoding(boolean forceRequestEncoding) {
        this.forceRequestEncoding = forceRequestEncoding;
    }

    public boolean isForceRequestEncoding() {
        return this.forceRequestEncoding;
    }

    public void setForceResponseEncoding(boolean forceResponseEncoding) {
        this.forceResponseEncoding = forceResponseEncoding;
    }

    public boolean isForceResponseEncoding() {
        return this.forceResponseEncoding;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String encoding = this.getEncoding();
        if (encoding != null) {
            if (this.isForceRequestEncoding() || request.getCharacterEncoding() == null) {
                //这里设置字符编码方式
                request.setCharacterEncoding(encoding);
            }

            if (this.isForceResponseEncoding()) {
                response.setCharacterEncoding(encoding);
            }
        }

        filterChain.doFilter(request, response);
    }
}

6. 响应数据传出

  1. SpringMVC输出模型数据概述

    1. ModelAndView: 处理方法返回值类型为 ModelAndView 时, 方法体即可通过该对象添加模型数据
    2. Map 及 Model: 入参为 org.springframework.ui.Model、org.springframework.ui.ModelMap 或 java.uti.Map 时,处理方法返回时,Map 中的数据会自动添加到模型中
    3. @SessionAttributes: 将模型中的某个属性暂存到 HttpSession 中,以便多个请求之间可以共享这个属性
    4. @ModelAttribute: 方法入参标注该注解后, 入参的对象就会放到数据模型中
<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>


<!--springmvc.xml-->
<!--包扫描-->
<context:component-scan base-package="com.siki"/>

<!--配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/pages/"/>
    <property name="suffix" value=".jsp"/>
</bean>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <title>$Title$</title>
</head>
<body>
  <a href="hello01">hello01</a><br>
  <a href="hello02">hello02</a><br>
  <a href="hello03">hello03</a><br>
  <a href="hello04">hello04</a><br>
  <a href="hello05">hello05</a><br>
</body>
</html>


<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <font color="green">成功</font><br>
    <!--
        SpringMVC如何将数据带到页面中来
            1.可以在方法处传入Map、Model或者ModelMap,给这些参数里面保存的所有数据都会放在request域中,可以在页面获取到
              1. 这三种方式最终都是BindingAwareModelMap进行的处理
              2. 相当于给BindingAwareModelMap中保存的东西都会被放到request域中
              3. Map: JDK中的接口
              4. Model: Spring中的接口
              5. ModelMap: 继承自LinkedHashMap的一个类
              6. BindingAwareModelMap: 继承自ExtendedModelMap,ExtendedModelMap继承自ModelMap,并实现了Model接口

           2. 方法的返回值还可以是ModelAndView类型
              1. 既包含视图信息(页面地址),也包含模型数据(带给页面的数据)
              2. 而且数据也是放在request域中的

           3. SpringMVC提供了一种可以临时给session域中保存数据的方式
              1. 使用@SessionAttributes注解(只能标在类上)
              2. @SessionAttributes(value = {"haha","msg"},types = {String.class})
                 表示给BindingAwareModelMap的request域中保存数据的同时,给session域中也放一份
                 value={"haha","msg"} 指定保存数据时,也要给session中存放数据,数据的key为"msg"和"haha"
                 types={String.class} 指定保存的数据类型为String
              3. 了解即可,因为不可控,所以基本上不用,如果想往session域中存放数据,推荐使用原生API

           4. @ModelAttribute注解(不会用)
              1. 在方法上使用@ModelAttribute注解,Spring MVC在调用目标处理方法前,
              会先逐个调用在方法级上标注了@ModelAttribute的方法
              2. 在方法的形参前使用@ModelAttribute注解,可以从隐含对象中获取隐含的模型数据中获取对象,
              再将请求参数绑定到对象中,再传给形参
              3. 由@ModelAttribute标记的方法, 会在每个目标方法执行之前被SpringMVC调用
    -->
    pageContext: ${pageScope.msg}<br>
    request: ${requestScope.msg}<br>
    session: ${sessionScope.msg}<br>
    application: ${applicationScope.msg}<br>
</body>
</html>
@SessionAttributes(value = {"haha","msg"},types = {String.class})
@Controller
public class ModelAndViewController {

    @RequestMapping("/hello01")
    public String hello01(Map<String,Object> map){
        map.put("msg","你好,昕昕");
        System.out.println("map的类型: " + map.getClass());
        return "success";
    }

    @RequestMapping("/hello02")  //Model是一个接口
    public String hello02(Model model){
        model.addAttribute("msg","我喜欢昕昕");
        System.out.println("model的类型: " + model.getClass());
        return "success";
    }

    @RequestMapping("/hello03")
    public String hello03(ModelMap modelMap){
        modelMap.addAttribute("msg","1314");
        System.out.println("modelMap的类型: " + modelMap.getClass());
        return "success";
    }

    @RequestMapping("/hello04")
    public ModelAndView hello04(){
        //之前的返回值就叫视图名,视图解析器会帮我们进行自动拼串,得到一个真实的地址
        //ModelAndView mv = new ModelAndView();
        //mv.setViewName("success");
        ModelAndView mv = new ModelAndView("success");
        mv.addObject("msg","520");
        return mv;
    }
}
//Map、Model、ModelMap的关系
public interface Map { 
}

public interface Model {   
}

public class ModelMap extends LinkedHashMap<String, Object> {    
}

public class BindingAwareModelMap extends ExtendedModelMap {
}

public class ExtendedModelMap extends ModelMap implements Model {
}

7. DispatcherServet运行流程源码分析

  1. 前端控制器(DispatcherServet)的运行流程

    1. DispatcherServlet收到请求后调用doDispatch()方法
    2. 执行getHandler()方法,根据当前所有控制器类找到能处理这个请求的目标控制器类
    3. 执行getHandlerAdapter()方法,根据当前处理器获取到能执行这个处理器方法的适配器
    4. 使用刚才获取到的适配器(RequestMappingHandlerAdapter)执行目标方法
    5. 目标方法执行后会返回一个ModelAndView对象
    6. 根据ModelAndView的信息转发到具体的页面,并可以在请求域中取出ModelAndView中的模型数据
//1. DispatcherServlet经过两层继承,最终继承自HttpServlet
public class DispatcherServlet extends FrameworkServlet {
}

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
}

public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
}

//2. FrameworkServlet类实现了HttpServlet的doGet()和doPost()方法
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

    //3. 请求一进来,必然经过doGet()或者doPost()方法
    @Override
    protected final void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        //4. 不管是get还是post方式,都会调用这个方法处理请求
        processRequest(request, response);
    }

    @Override
    protected final void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {

        processRequest(request, response);
    }

    @Override
    protected final void doPut(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }

    @Override
    protected final void doDelete(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        processRequest(request, response);
    }

    @Override
    protected void doOptions(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
            processRequest(request, response);
            if (response.containsHeader("Allow")) {
                return;
            }
        }
        super.doOptions(request, new HttpServletResponseWrapper(response) {
            @Override
            public void setHeader(String name, String value) {
                if ("Allow".equals(name)) {
                    value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
                }
                super.setHeader(name, value);
            }
        });
    }

}


//5. 处理请求
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;

    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();

    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();

    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    initContextHolders(request, localeContext, requestAttributes);

    try {
        //6. 抽象方法,留给子类实现的
        doService(request, response);
    }
    catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }

    finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }

        if (logger.isDebugEnabled()) {
            if (failureCause != null) {
                this.logger.debug("Could not complete request", failureCause);
            }
            else {
                if (asyncManager.isConcurrentHandlingStarted()) {
                    logger.debug("Leaving response open for concurrent processing");
                }
                else {
                    this.logger.debug("Successfully completed request");
                }
            }
        }
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}


//7. DispatcherServlet实现了父类的doService()方法
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (logger.isDebugEnabled()) {
        String resumed = WebAsyncUtils.getAsyncManager(request).hasConcurrentResult() ? " resumed" : "";
        logger.debug("DispatcherServlet with name '" + getServletName() + "'" + resumed +
                     " processing " + request.getMethod() + " request for [" + getRequestUri(request) + "]");
    }
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap<>();
        Enumeration<?> attrNames = request.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = (String) attrNames.nextElement();
            if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }
    }
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }
    try {
        //最终的处理方法
        doDispatch(request, response);
    }
    finally {
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            if (attributesSnapshot != null) {
                restoreAttributesAfterInclude(request, attributesSnapshot);
            }
        }
    }
}


//8. 最终的处理是这个方法,需要掌握
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            //1. 检查该请求是否是文件上传请求
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);
            //2. 根据当前的请求地址,去所有的Handler(Controller)中去找哪个Handler能够处理当前请求,找不到就直接404抛异常
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response); //404
                return;
            }
            //3. 拿到能执行这个类的所有方法的适配器(反射工具)->RequestMappingHandlerAdapter
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            //4. 获取请求方式
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (logger.isDebugEnabled()) {
                    logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                }
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
            //5. 重点: 这里调用了目标方法,真正的执行目标方法(执行目标方法)
            //适配器来执行目标方法,并且将目标方法执行完成后的返回值作为视图名,设置保存到ModelAndView中,目标方法无论怎么写,最终适配器执行完成后都会将执行后的信息封装层ModelAndView
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            //判断是否是异步处理请求,如果是,直接返回
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }
            //6. 如果没有视图名,SpringMVC会帮我们自动设置一个默认视图名
            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        //7. 重点: 根据方法最终执行完成后封装的ModelAndView,转发到目标页面,并且ModelAndView中的数据可以直接从请求域中获取到(转发到目标页面)
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                               new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}
  1. getHandler()方法分析

    1. 返回的是一个目标方法的执行链

    2. HandlerMapping:处理器映射

      1. handlerMap:IOC容器启动创建Controller对象的时候会扫描每个处理器都能够处理哪些请求,将这些信息保存到HandlerMapping的HandlerMap属性中,下一次请求过来的时候,就会从handlerMap中查找对应的请求映射信息
   @Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
       if (this.handlerMappings != null) {
        //它里面保存了每个处理器都能处理哪些方法的映射信息
           for (HandlerMapping hm : this.handlerMappings) {
               if (logger.isTraceEnabled()) {
                   logger.trace(
                    "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
               }
               HandlerExecutionChain handler = hm.getHandler(request);
               if (handler != null) {
                   return handler;
               }
           }
       }
       return null;
   }
  1. getHandlerAdapter()方法分析

    1. 要拿到适配器才能够执行目标方法

protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
//遍历所有的适配器
for (HandlerAdapter ha : this.handlerAdapters) {
if (logger.isTraceEnabled()) {
logger.trace(“Testing handler adapter [“ + ha + “]”);
}
//返回可以处理目标方法的适配器
if (ha.supports(handler)) {
return ha;
}
}
}
throw new ServletException(“No adapter for handler [“ + handler +
“]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler”);
}




4. ha.handle(processedRequest, response, mappedHandler.getHandler())方法分析

```java
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {

    return handleInternal(request, response, (HandlerMethod) handler);
}


@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ModelAndView mav;
    checkRequest(request);
    if (this.synchronizeOnSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized (mutex) {
                mav = invokeHandlerMethod(request, response, handlerMethod);
            }
        }
        else {
            mav = invokeHandlerMethod(request, response, handlerMethod);
        }
    }
    else {
        //通过适配器执行目标方法
        mav = invokeHandlerMethod(request, response, handlerMethod);
    }
    if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
        if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
            applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
        }
        else {
            prepareResponse(response);
        }
    }
    //返回ModelAndView对象
    return mav;
}
  1. DispatcherServlet中的九大组件

    1. SpringMVC在工作的时候,关键位置都是由这些组件来完成的
    2. 共同点:九大组件全部都是接口,接口就是默认规范,提供了非常强大的扩展性 ```java //文件上传解析器 public static final String MULTIPART_RESOLVER_BEAN_NAME = “multipartResolver”;

//区域信息解析器(跟国际化有关) public static final String LOCALE_RESOLVER_BEAN_NAME = “localeResolver”;

//主题解析器 public static final String THEME_RESOLVER_BEAN_NAME = “themeResolver”;

//Handler映射信息 public static final String HANDLER_MAPPING_BEAN_NAME = “handlerMapping”;

//Handler适配器 public static final String HANDLER_ADAPTER_BEAN_NAME = “handlerAdapter”;

//异常解析器: SpringMVC强大的异常解析功能 public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = “handlerExceptionResolver”;

//请求的视图名的转换器 public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = “viewNameTranslator”;

//视图解析器 public static final String VIEW_RESOLVER_BEAN_NAME = “viewResolver”;

//SpringMVC中允许重定向携带数据的功能 public static final String FLASH_MAP_MANAGER_BEAN_NAME = “flashMapManager”;


6. 
九大组件初始化

   1. 先去容器中找这个组件,如果没有找到就会使用默认的配置
   2. 有些组件是使用类型查找的,有些组件是使用id查找的
```java
//这个方法是Spring初始化IOC容器的时候留给子类实现的
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

//初始化九大组件
protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
}

//HandlerMapping初始化
private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    //detectAllHandlerMappings属性默认为true
    if (this.detectAllHandlerMappings) { 
        //从容器中去找这个HandlerMapping类型的组件
        Map<String, HandlerMapping> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList<>(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    }
    else {
        try {
            HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        }
        catch (NoSuchBeanDefinitionException ex) {
        }
    }
    //如果找不到,就会使用默认的配置
    if (this.handlerMappings == null) {
        this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
        if (logger.isDebugEnabled()) {
            logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
        }
    }
}
  1. 执行目标方法的细节(最难理解的执行流程)

    1. 通过反射定位到某个目标方法,还要准确获取到方法的每一个参数,才是最难的 ```java @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; checkRequest(request); //synchronizeOnSession默认为false if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) {
        Object mutex = WebUtils.getSessionMutex(session);
        synchronized (mutex) {
            //真正的执行目标方法
            mav = invokeHandlerMethod(request, response, handlerMethod);
        }
      
      } else {
        mav = invokeHandlerMethod(request, response, handlerMethod);
      
      } } else { mav = invokeHandlerMethod(request, response, handlerMethod); } if (!response.containsHeader(HEADER_CACHE_CONTROL)) { if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
        applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
      
      } else {
        prepareResponse(response);
      
      } } return mav; }

@Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

    ModelAndViewContainer mavContainer = new ModelAndViewContainer();
    mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
    modelFactory.initModel(webRequest, mavContainer, invocableMethod);
    mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

    AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
    asyncWebRequest.setTimeout(this.asyncRequestTimeout);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.setTaskExecutor(this.taskExecutor);
    asyncManager.setAsyncWebRequest(asyncWebRequest);
    asyncManager.registerCallableInterceptors(this.callableInterceptors);
    asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

    if (asyncManager.hasConcurrentResult()) {
        Object result = asyncManager.getConcurrentResult();
        mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
        asyncManager.clearConcurrentResult();
        if (logger.isDebugEnabled()) {
            logger.debug("Found concurrent result value [" + result + "]");
        }
        invocableMethod = invocableMethod.wrapConcurrentResult(result);
    }

    invocableMethod.invokeAndHandle(webRequest, mavContainer);
    if (asyncManager.isConcurrentHandlingStarted()) {
        return null;
    }
    return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
    webRequest.requestCompleted();
}

}


<a name="be8c604f"></a>
### 8. 视图解析

1. 
SpringMVC如何解析视图

   1. 不论控制器返回一个String,ModelAndView,View都会转换为ModelAndView对象,由视图解析器解析视图,然后,进行页面的跳转
2. 
视图和视图解析器

   1. 请求处理方法执行完成后,最终返回一个 ModelAndView 对象。对于那些返回 String,View 或 ModeMap 等类型的处理方法,Spring MVC 也会在内部将它们装配成一个 ModelAndView 对象,它包含了逻辑名和模型对象的视图
   2. Spring MVC 借助视图解析器(ViewResolver)得到最终的视图对象(View),最终的视图可以是 JSP ,也可能是 Excel、JFreeChart等各种表现形式的视图
   3. 对于最终究竟采取何种视图对象对模型数据进行渲染,处理器并不关心,处理器工作重点聚焦在生产模型数据的工作上,从而实现 MVC 的充分解耦
3. 
重定向和请求转发
```java
@Controller
public class MyController {

    @RequestMapping("/hello01")
    public String hello01(){
        return "success";
    }

    //forward请求转发  /hello.jsp表示转发到当前项目下的hello.jsp     注意: 转发和重定向不会进行视图解析
    @RequestMapping("/hello02")
    public String hello02(){
        return "forward:/hello.jsp";
        //return "forward:/hello01";   //两次转发
    }

    //redirect重定向,SpringMVC会为我们自动的拼接上项目名
    @RequestMapping()
    public String hello03(){
        return "redirect:/hello.jsp";
        //return "redirect:/hello02";   //两次重定向
    }
}
  1. 视图

    1. 视图的作用是渲染模型数据,将模型里的数据以某种形式呈现给客户
    2. 为了实现视图模型和具体实现技术的解耦,Spring 在 org.springframework.web.servlet 包中定义了一个高度抽象的 View接口
    3. 视图对象由视图解析器负责实例化。由于视图是无状态的,所以他们不会有线程安全的问题
  2. 视图解析器

    1. SpringMVC 为逻辑视图名的解析提供了不同的策略,可以在 Spring WEB 上下文中配置一种或多种解析策略,并指定他们之间的先后顺序。每一种映射策略对应一个具体的视图解析器实现类
    2. 视图解析器的作用比较单一:将逻辑视图解析为一个具体的视图对象
    3. 所有的视图解析器都必须实现 ViewResolver 接口
  3. 常用的视图解析器实现类

    1. 程序员可以选择一种视图解析器或混用多种视图解析器
  4. 每个视图解析器都实现了 Ordered 接口并开放出一个 order 属性,可以通过 order 属性指定解析器的优先顺序,order越小优先级越高
  5. SpringMVC 会按视图解析器顺序的优先顺序对逻辑视图名进行解析,直到解析成功并返回视图对象,否则将抛出 ServletException 异常
  6. JSP 是最常见的视图技术,可以使用 InternalResourceViewResolve作为视图解析器

  7. 视图解析器流程分析

    1. 方法执行完成后的返回值会作为页面地址进行参考,转发或者重定向到目标页面

    2. 视图解析器也可能会进行页面地址的拼串

    3. 任何方法的返回值,最终都会被包装成ModelAndView对象

    4. 视图渲染流程:将域中的数据在页面进行展示,页面就是用来渲染模型数据的

    5. View和ViewResolver

      1. View和ViewResolver的作用是根据视图名(方法的返回值)得到view对象
   private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
 @Nullable Exception exception) throws Exception {
       boolean errorView = false;
    if (exception != null) {
           if (exception instanceof ModelAndViewDefiningException) {
               logger.debug("ModelAndViewDefiningException encountered", exception);
               mv = ((ModelAndViewDefiningException) exception).getModelAndView();
           }
           else {
               Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
               mv = processHandlerException(request, response, handler, exception);
               errorView = (mv != null);
           }
       }
       if (mv != null && !mv.wasCleared()) {
           //渲染页面
           render(mv, request, response);
           if (errorView) {
               WebUtils.clearErrorRequestAttributes(request);
           }
       }
       else {
           if (logger.isDebugEnabled()) {
               logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +
                            "': assuming HandlerAdapter completed request handling");
           }
       }
       if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
           return;
       }
       if (mappedHandler != null) {
           mappedHandler.triggerAfterCompletion(request, response, null);
       }
   }


   protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
       //国际化
       Locale locale =
           (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
       response.setLocale(locale);
       View view;  //定义一个view对象
       String viewName = mv.getViewName();  //获取视图名称
       if (viewName != null) {
           view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
           if (view == null) {
               throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                                          "' in servlet with name '" + getServletName() + "'");
           }
       }
       else {
           view = mv.getView();
           if (view == null) {
               throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                                          "View object in servlet with name '" + getServletName() + "'");
           }
       }
       if (logger.isDebugEnabled()) {
           logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
       }
       try {
           if (mv.getStatus() != null) {
               response.setStatus(mv.getStatus().value());
           }
           view.render(mv.getModelInternal(), request, response);
       }
       catch (Exception ex) {
           if (logger.isDebugEnabled()) {
               logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
                            getServletName() + "'", ex);
           }
           throw ex;
       }
   }
  1. 如何根据方法的返回值(视图名)得到View对象

    1. 视图解析器得到View对象的流程:所有配置好的视图解析器都来尝试根据视图名(返回值)得到View对象,如果能得到就返回,得不到就换下一个解析器

      @Nullable
      protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,Locale locale, HttpServletRequest request) throws Exception {
      
      if (this.viewResolvers != null) {
        //遍历所有的viewResolver
        for (ViewResolver viewResolver : this.viewResolvers) {
            //获取view对象
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                return view;
            }
        }
      }
      return null;
      

}

  @Override
  @Nullable
  public View resolveViewName(String viewName, Locale locale) throws Exception {
      if (!isCache()) {
          return createView(viewName, locale);
      }
      else {
          Object cacheKey = getCacheKey(viewName, locale);
          View view = this.viewAccessCache.get(cacheKey);
          if (view == null) {
              synchronized (this.viewCreationCache) {
                  view = this.viewCreationCache.get(cacheKey);
                  if (view == null) {
                      //根据方法的返回值创建出视图对象
                      view = createView(viewName, locale);
                      if (view == null && this.cacheUnresolved) {
                          view = UNRESOLVED_VIEW;
                      }
                      if (view != null) {
                          this.viewAccessCache.put(cacheKey, view);
                          this.viewCreationCache.put(cacheKey, view);
                          if (logger.isTraceEnabled()) {
                              logger.trace("Cached view [" + cacheKey + "]");
                          }
                      }
                  }
              }
          }
          return (view != UNRESOLVED_VIEW ? view : null);
      }
  }


  @Override
  protected View createView(String viewName, Locale locale) throws Exception {
      if (!canHandle(viewName, locale)) {
          return null;
      }
      //是否是重定向
      if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
          String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
          //创建重定向视图对象
          RedirectView view = new RedirectView(redirectUrl,
                                               isRedirectContextRelative(), isRedirectHttp10Compatible());
          String[] hosts = getRedirectHosts();
          if (hosts != null) {
              view.setHosts(hosts);
          }
          return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
      }
      //是否是请求转发
      if (viewName.startsWith(FORWARD_URL_PREFIX)) {
          String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
          return new InternalResourceView(forwardUrl);
      }
      //如果没有前缀,就使用父类默认创建一个视图对象
      return super.createView(viewName, locale);
  }

8. 
综上所述:视图解析器只是为了得到视图对象,视图对象才能真正的转发或者重定向到目标页面(将模型数据全部放在请求域中),视图对象才能够真正的渲染视图

9. 
JstlView

   1. 若项目中使用了JSTL,则SpringMVC 会自动把视图由InternalResourceView转为JstlView
   2. 若使用 JSTL 的 fmt 标签则需要在 SpringMVC 的配置文件中配置国际化资源文件
   3. 若希望直接响应通过 SpringMVC 渲染的页面,可以使用 mvc:view-controller 标签实现
10. 
自定义视图

   1. 自定义视图(需要加入SpringMVC,那么,一定需要实现框架的接口)
```xml
<!--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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

    <!--自定义视图解析器,数字越小优先级越高-->
    <bean class="com.siki.view.MyViewResolver">
        <property name="order" value="1"/>
    </bean>

</beans>
/*
1. 让我们自定义的视图解析器工作
2. 得到我们自定义的视图对象
3. 自定义视图对象自定义渲染逻辑

自动逸视图和视图解析器的实现步骤
    1. 编写自定义视图和视图解析器
    2. 视图解析器必须放在IOC容器中
*/
//自定义视图
public class MyView implements View {

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("之前保存的数据: " + model);
        response.setContentType("text/html");  //设置编码类型
        response.getWriter().write("哈哈~~~~~");
        response.getWriter().write(model.get("msg").toString());
    }

}

//自定义视图解析器
public class MyViewResolver implements ViewResolver, Ordered {

    private Integer order;

    //根据视图名返回视图对象
    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        if(viewName.startsWith("view:")){
            return new MyView();    //返回自定义view
        }else{
            //如果不能处理,返回null即可
            return null;
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }

    //改变视图解析顺序的方法
    public void setOrder(Integer order){
        this.order = order;
    }
}

@Controller
public class MyViewResolverController {

    @RequestMapping("/hello")
    public String myViewResolver(Model model){
        List<String> list = new ArrayList<>();
        list.add("昕昕");
        list.add("1314");
        list.add("520");
        model.addAttribute("msg",list);
        return "view:/success";
    }

}
  1. mvc:view-controller标签

    1. 若希望直接响应通过 SpringMVC 渲染的页面,可以使用 mvc:view-controller 标签实现

    2. 直接配置响应的页面:无需经过控制器来执行结果

    3. 注意:配置mvc:view-controller会导致其他请求路径失效

    4. 解决办法:配置mvc:annotation-driven标签

      <mvc:view-controller path="/success" view-name="success"/>
      <mvc:annotation-driven />   <!--这个注解开启了mvc的开挂模式-->
      

9.RESTFUL-CRUD

<!--web.xml-->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--配置前端控制器-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!--配置字符编码过滤器  一定要注意: 字符编码过滤器一定要配置在其他过滤器之前,否则无法生效-->
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!--指定解决POST请求乱码-->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <!--解决请求乱码和响应乱码-->
        <init-param>
            <param-name>forceRequestEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>forceResponseEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!--配置请求方式过滤器-->
    <filter>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>hiddenHttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>


<!--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: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.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

    <!--默认前端控制器是拦截所有资源的(除了jsp),当我们导入js文件的时候,就被拦截了,js文件的请求应该是Tomcat来处理的-->
    <!--告诉SpringMVC,自己映射的请求就自己处理,不能处理的请求就交给Tomcat的默认处理器(DefaultServlet)处理-->
    <mvc:default-servlet-handler/>
    <!--上面的注解用来处理静态资源,下面的注解用来处理动态请求,这两个就是标配,少一个可能就会报错-->
    <mvc:annotation-driven/>

</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <title>$Title$</title>
</head>
<body>
  <!--访问项目就直接展示员工页面-->
  <jsp:forward page="/emps"></jsp:forward>
</body>
</html>


<!--list.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--导入jstl--%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <%
        request.setAttribute("path",request.getContextPath());
    %>
    <title>员工列表页面</title>
    <!--引入js-->
    <script type="text/javascript" src="${path}/js/jquery-1.9.1.min.js"></script>
</head>
<body>
    <h1>员工列表</h1>
    <table border="1" cellpadding="5" cellspacing="0">
        <tr>
            <th>ID</th>
            <th>lastName</th>
            <th>email</th>
            <th>gender</th>
            <th>departmentName</th>
            <th>edit</th>
            <th>delete</th>
        </tr>
        <c:forEach items="${emps}" var="emp">
            <tr>
                <td>${emp.id}</td>
                <td>${emp.lastName}</td>
                <td>${emp.email}</td>
                <td>${emp.gender == 0 ? "女" : "男"}</td>
                <td>${emp.department.departmentName}</td>
                <td>
                    <a href="${path}/emp/${emp.id}">edit</a>
                </td>
                <td>
                    <!--删除的简单方式,只不过删除变成了按钮,不是超链接-->
                    <%--<form action="${path}/emp/${emp.id}" method="post">
                        <input type="hidden" name="_method" value="delete"/>
                        <input type="submit" value="delete"/>
                    </form>--%>
                    <a href="${path}/emp/${emp.id}" class="delBtn">delete</a>
                </td>
            </tr>
        </c:forEach>
    </table>
    <a href="${path}/toAddPage">添加员工</a>

    <form id="deleteForm" action="" method="post">
        <input type="hidden" name="_method" value="delete"/>
    </form>
    <!--我们可以使用js,给删除链接绑定一个事件-->
    <script type="text/javascript">
        $(function(){
            $(".delBtn").click(function () {
                //我们通过修改表单的action,让其跟删除链接的页面跳转地址一样,就可以实现通过点击删除链接
                //让表单替我们提交post请求
                $("#deleteForm").attr("action",this.href);
                //提交表单
                $("#deleteForm").submit();
                //阻止页面跳转
                return false;
            })
        })
    </script>

</body>
</html>


<!--add.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!--导入表单标签-->
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <%--<form action="/emp" method="post">
        lastName: <input type="text" name="lastName"/><br>
        email: <input type="text" name="email"/><br>
        gender: 男:<input type="radio" name="gender" value="1"/>&nbsp;&nbsp;&nbsp;
                女:<input type="radio" name="gender" value="0"/><br>
        dept: <select name="department.id">
                    <c:forEach items="${depts}" var="dept">
                        <!--标签体中的是在页面的提示选项信息,value才是真正提交的信息-->
                        <option value="${dept.id}">${dept.departmentName}</option>
                    </c:forEach>
              </select>
        <input type="submit" value="提交"/>
    </form>--%>

    <!--
        SpringMVC使用表单标签可以实现将模型数据中的属性和html表单元素互相绑定,以实现表单数据更便捷的编辑和表单值的回显
            注意: 1. SpringMVC认为,表单数据中的每一项最终都是要回显的
                 2. path指定的属性是从隐含模型(请求域)中取出的某个对象中的属性
                 3. path指定的每一个属性,请求域中必须有一个对象,拥有这个属性,这个对象就是请求域中的command
                    解决办法1:
                           model.addAttribute("command",
                           new Employee(null, "xinxin", "1314520@163.com", 0, departmentDao.getDepartment(101)));
                   解决办法2:
                           form:form action="" modelAttribute="employee"
                           model.addAttribute("employee",new Employee());  属性刚好默认全为空
                           在表单标签中添加modelAttribute=""
                           以前我们表单标签会从请求域中获取一个command对象,把这个对象中的每一个属性对应的显示出来
                           现在,我们可以告诉SpringMVC,不要去取command的值,我们自己放了一个modelAttribute指定的值
                           取对象用的key,就用modelAttribute指定的值
    -->
    <%
        //请求尽量都使用绝对路径
        request.setAttribute("ctp",request.getContextPath());
    %>
    <form:form action="${ctp}/emp" modelAttribute="employee" method="post">
        <!--
            path: 就是原来html-input中的name属性
                1. 当做原生的name
                2. 自动回显隐含模型中某个对象对应的某个属性的值
        -->
        lastName: <form:input path="lastName"/><br>
        email: <form:input path="email"/><br>
        gender: 男: <form:radiobutton path="gender" value="1"/>
                女: <form:radiobutton path="gender" value="0"/><br>
        <!--items: 指定要遍历的集合,自动遍历-->
        <!--itemLabel: 指定遍历出的这个对象的哪个属性是作为option标签体的值-->
        <!--itemValue: 指定遍历出的这个对象的哪个属性是作为要提交的value值-->
        dept: <form:select path="department.id" items="${depts}"
                           itemLabel="departmentName" itemValue="id"><br>
              </form:select><br>
        <input type="submit" value="提交"/><br>
    </form:form>
</body>
</html>


<!--update.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
    <title>员工修改页面</title>
</head>
<body>
    <%
        request.setAttribute("path",request.getContextPath());
    %>
    <!--这个表单的所有内容显示绑定的是请求域中的updateEmp的值-->
    <form:form action="${path}/emp/${updateEmp.id}" modelAttribute="updateEmp" method="post">
        <input type="hidden" name="_method" value="put"/>
        <input type="hidden" name="id" value="${updateEmp.id}"/>
        email: <form:input path="email"/><br>
        gender: 男: <form:radiobutton path="gender" value="1"/>&nbsp;&nbsp;&nbsp;
                女: <form:radiobutton path="gender" value="0"/><br>
        dept: <form:select path="department.id" items="${depts}"
                           itemLabel="departmentName" itemValue="id">
              </form:select><br>
        <input type="submit" value="修改"/><br>
    </form:form>
</body>
</html>
//dao层
@Repository
public class EmployeeDao {

    private static Map<Integer, Employee> employees = null;

    @Autowired
    private DepartmentDao departmentDao;

    static{
        employees = new HashMap<Integer, Employee>();
        employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1, new Department(101, "D-AA")));
        employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1, new Department(102, "D-BB")));
        employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0, new Department(103, "D-CC")));
        employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0, new Department(104, "D-DD")));
        employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1, new Department(105, "D-EE")));
    }

    //添加员工时,初始化员工的id
    private static Integer initId = 1006;

    //员工保存/更新
    public void save(Employee employee){
        if(employee.getId() == null){
            employee.setId(initId++);
        }
        employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
        employees.put(employee.getId(), employee);
    }

    public Collection<Employee> getAll(){
        return employees.values();
    }

    public Employee get(Integer id){
        return employees.get(id);
    }

    public void delete(Integer id){
        employees.remove(id);
    }
}


@Repository
public class DepartmentDao {

    private static Map<Integer, Department> departments = null;

    static{
        departments = new HashMap<Integer, Department>();

        departments.put(101, new Department(101, "D-AA"));
        departments.put(102, new Department(102, "D-BB"));
        departments.put(103, new Department(103, "D-CC"));
        departments.put(104, new Department(104, "D-DD"));
        departments.put(105, new Department(105, "D-EE"));
    }

    public Collection<Department> getDepartments(){
        return departments.values();
    }

    public Department getDepartment(Integer id){
        return departments.get(id);
    }

}


//controller层
@Controller
public class RestControllerTest {

    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private DepartmentDao departmentDao;

    //查询所有员工
    @RequestMapping("/emps")
    public String getEmps(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        model.addAttribute("emps",employees);
        return "list";
    }

    //员工添加,添加之前需要查询出所有的部门信息,再页面进行展示
    @RequestMapping("/toAddPage")
    public String toAddPage(Model model){
        //先查询出所有部门
        Collection<Department> departments = departmentDao.getDepartments();
        //再放到请求域中
        model.addAttribute("depts",departments);
//        model.addAttribute("command",
//                new Employee(null, "xinxin", "1314520@163.com", 0, departmentDao.getDepartment(101)));
        model.addAttribute("employee",new Employee());  //属性刚好默认全为空
        return "add";
    }

    @RequestMapping(value = "/emp",method = RequestMethod.POST)
    public String addEmp(Employee employee){
        employeeDao.save(employee);
        //直接重定向到查询所有员工的请求
        return "redirect:/emps";
    }

    //来到修改页面
    @RequestMapping(value = "/emp/{id}",method = RequestMethod.GET)
    public String getEmp(@PathVariable("id") Integer id,Model model){
        Employee employee = employeeDao.get(id);
        model.addAttribute("updateEmp",employee);
        //注意: 还要将部门信息放入请求域中
        model.addAttribute("depts",departmentDao.getDepartments());
        return "update";
    }

    //修改员工之前,先将员工的完整信息获取到
    @ModelAttribute
    public void myModelAttribute(@RequestParam(value = "id",required = false) Integer id,Model model){
        //因为所有的处理器方法执行之前都会先执行这个方法,而/emps、/emp这样的请求是没有携带id参数的
        //所以需要设置required = false,否则会报错
        if(id != null){   //这里必须要判断,否则添加员工时,没有携带id属性,这个时候就会创建一个空的employee
            Employee employee = employeeDao.get(id);
            model.addAttribute("employee",employee);
        }
    }

    //修改员工
    @RequestMapping(value = "/emp/{id}",method = RequestMethod.PUT)
    public String updateEmp(Employee employee){
        //此时的员工是缺少lastName的,因为页面跳转过来的时候并没有携带lastName
        //所以我们可以使用@ModelAttribute提前获取到lastName的值,再放入请求域中,供修改方法使用
        System.out.println(employee);
        employeeDao.save(employee); //修改完后保存
        return "redirect:/emps";
    }

    //删除员工
    @RequestMapping(value = "/emp/{id}",method = RequestMethod.DELETE)
    public String deleteEmp(@PathVariable("id") Integer id){
        employeeDao.delete(id);
        return "redirect:/emps";
    }
}

10. 数据转换、数据绑定、数据校验

  1. 数据绑定流程原理

    1. Spring MVC 将 ServletRequest 对象及目标方法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder 实例对象
    2. DataBinder 调用装配在 Spring MVC 上下文中的 ConversionService 组件进行数据类型转换、数据格式化工作。将 Servlet 中的请求信息填充到入参对象中
    3. 调用 Validator 组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果 BindingData 对象
    4. Spring MVC 抽取 BindingResult 中的入参对象和校验错误对象,将它们赋给处理方法的响应入参
    5. 综上所述:Spring MVC 通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder
  2. 自定义类型转换器

    1. ConversionService 是 Spring 类型转换体系的核心接口

    2. 可以利用 ConversionServiceFactoryBean 在 Spring 的 IOC 容器中定义一个
      ConversionService,Spring 将自动识别出 IOC 容器中的 ConversionService,并在 Bean 属性配置及 SpringMVC 处理方法入参绑定等场合使用它进行数据的转换

    3. 可通过 ConversionServiceFactoryBean 的converters属性注册自定义的类型转换器

<!--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: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.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

    <!--自定义ConversionService-->
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <!--在converters中添加我们自己定义的converter,让它生效-->
        <property name="converters">
            <set>
                <bean class="com.siki.component.MyStringToEmployee"></bean>
            </set>
        </property>
    </bean>
    <mvc:default-servlet-handler/>
    <!--使用自己配置的conversionService类型转换组件-->
    <mvc:annotation-driven conversion-service="conversionService" />

</beans>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
    <%
      request.setAttribute("path",request.getContextPath());
    %>
    <form action="${path}/quickAdd">
      <input name="empInfo" value="1-xinxin-123456"/>
      <input type="submit" value="提交"/>
    </form>
  </body>
</html>
/*
1. 实现Converter接口,自定义类型转换器
2. Converter只是ConversionService中的组件
    1. 将自定义的Converter放入ConversionService中
    2. 将WebDataBinder中的ConversionService设置为我们自己定义的ConversionService
3. 在配置文件中配置ConversionService
*/

//自定义类型转换器
public class MyStringToEmployee implements Converter<String, Employee> {
    //自定义转换规则
    @Override
    public Employee convert(String s) {
        System.out.println("s = " + s);
        Employee employee = new Employee();
        if(s.contains("-")){
            String[] split = s.split("-");
            employee.setId(Integer.parseInt(split[0]));
            employee.setName(split[1]);
            employee.setPassword(split[2]);
        }
        return employee;
    }
}


@Controller
public class EmpController {

    //我们需要自定义类型转换器,将String转换为Employee类型
    @RequestMapping("/quickAdd")
    public String test01(@RequestParam("empInfo") Employee employee){
        System.out.println(employee);
        return "success";
    }

}
  1. Spring 支持的转换器类型:Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到 ConversionServiceFactoryBean 中(学会第一种就够了)

    1. Converter:将 S 类型对象转为 T 类型对象
    2. ConverterFactory:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
    3. GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换
  2. mvc:annotation-driven配置在什么时候必须配置

    1. 直接配置响应的页面:无需经过控制器来执行结果 ;但会导致其他请求路径失效,需要配置mvc:annotation-driven标签

    2. RESTful-CRUD操作,删除时,通过jQuery执行delete请求时,找不到静态资源,需要配置mvc:annotation-driven标签

      1. mvc:default-servlet-handler 将在 SpringMVC 上下文中定义一个
        DefaultServletHttpRequestHandler,它会对进入 DispatcherServlet 的请求进行筛查,如果发现是没有经过映射的请求,就将该请求交由 WEB 应用服务器默认的 Servlet 处理,如果不是静态资源的请求,才由 DispatcherServlet 继续处理
    3. 配置类型转换器服务时,需要指定转换器服务引用

      1. 会将自定义的ConversionService 注册到 Spring MVC 的上下文中
    4. 后面完成JSR 303数据验证,也需要配置
  3. 作用

    1. 会自动注册:RequestMappingHandlerMapping 、RequestMappingHandlerAdapter与ExceptionHandlerExceptionResolver 三个bean
    2. 支持使用 ConversionService 实例对表单参数进行类型转换
    3. 支持使用 @NumberFormat、@DateTimeFormat 注解完成数据类型的格式化
    4. 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证
    5. 支持使用 @RequestBody @ResponseBody 注解
  4. 日期格式化

    1. @DateTimeFormat 注解可对 java.util.Date、java.util.Calendar、java.long.Long 时间类型进行标注

      1. pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:”yyyy-MM-dd hh:mm:ss”
      2. iso 属性:类型为 DateTimeFormat.ISO。指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用) — 默认、ISO.DATE(yyyy-MM-dd) 、ISO.TIME(hh:mm:ss.SSSZ)、 ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)
      3. n style 属性:字符串类型。通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二位表示时间的格式:S:短日期/时间格式、M:中日期/时间格式、L:长日期/时间格式、F:完整日期/时间格式、-:忽略日期或时间格式
        <!--
        日期格式化
           1. 方式一: 去掉conversion-service
           <mvc:annotation-driven />
           2. 方式二: 将conversion-service配置改为FormattingConversionServiceFactoryBean
           <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
               <property name="converters">
                   <set>
                       <bean class="com.siki.component.MyStringToEmployee"></bean>
                   </set>
               </property>
           </bean>
           <mvc:annotation-driven conversion-service="conversionService" />
           因为FormattingConversionService里面也有converters,还有formatters,既可以实现类型转换器,还可以实现日期格式化
        所以以后要使用自定义类型转换的时候,就使用FormattingConversionServiceFactoryBean
        -->
        <mvc:annotation-driven />
        
        <%@ page contentType="text/html;charset=UTF-8" language="java" %>
        <html>
        <head>
        <title>Title</title>
        </head>
        <body>
        <%
        request.setAttribute("path",request.getContextPath());
        %>
        <form action="${path}/addBirth">
        id: <input type="text" name="id"/><br>
        name: <input type="text" name="name"/><br>
        password: <input type="password" name="password"/><br>
        birth: <input type="text" name="birth"/>
        <!--
           1. JSR303进行数据校验的时候,可以用这个form表单回显错误信息
           2. 如果是原生表单,该如何回显?
                  可以将错误信息放到一个map中,然后将map放到请求域中,最后带到目标页面进行回显
        -->
        <form:errors path="password" />
        <br>
        <input type="submit" value=""提交/><br>
        </form>
        </body>
        </html>
        
        ```java public class Employee {

      private Integer id;

      private String name;

      private String password;

      //规定页面提交的日期格式 @DateTimeFormat(pattern = “yyyy-MM-dd”) private Date birth;

}

//日期格式化 @RequestMapping(“/addBirth”) public String addBirth(Employee employee,Model model){ System.out.println(employee); model.addAttribute(employee); return “success”; }


7. 
数值格式化概述

   1. [@NumberFormat ](/NumberFormat ) 可对类似数字类型的属性进行标注,它拥有两个互斥的属性 
   2. style:类型为 NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型)、 Style.CURRENCY(货币类型)、 Style.PERCENT(百分数类型)
   3. pattern:类型为 String,自定义样式,如pattern="#,###"
8. 
数据校验

   1. 
如何校验

      1. 使用JSR 303验证标准
      2. 加入hibernate validator验证框架
      3. 在SpringMVC配置文件中增加[mvc:annotation-driven/]()
      4. 需要在bean的属性上增加对应验证的注解
      5. 在目标方法bean类型的前面增加@Valid注解
   2. 
JSR 303

      1. 
是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中

      2. 
JSR 303 (Java Specification Requests)意思是Java 规范提案)通过在 Bean 属性上标注类似于 @NotNull、[@Max ](/Max ) 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证 

   3. 
Hibernate Validator 扩展注解

      1. Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解
   4. 
Spring MVC 数据校验

      1. Spring 4.0 拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架
   5. 
Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验<br />
3. Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义了一个 **LocalValidatorFactoryBean**,即可将其注入到需要数据校验的 Bean 中<br />
4. Spring 本身并没有提供 JSR303 的实现,所以必须将 JSR303 的实现者的 jar 包放到类路径下<br />
5. [mvc:annotation-driven/]() 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 [@Valid ](/Valid ) 注解即可让 Spring MVC 在完成数据绑定后执行数据校验的工作 <br />
6. 在已经标注了 JSR303 注解的表单/命令对象前标注一个 @Valid,Spring MVC 框架在将请求参数绑定到该入参对象后,就会调用校验框架根据注解声明的校验规则实施校验


```java
/*
1. 导包
2. 只需要给JavaBean的属性添加上校验注解
3. 在SpringMVC封装对象的时候,告诉SpringMVC这个JavaBean需要校验
4. 如何知道校验结果
    给需要校验的JavaBean后面紧跟一个BindingResult,这个BindingResult就是封装前一个bean的     校验结果,根据不同的校验结果决定后面该怎么做
*/
public class Employee {

    @NonNull
    private Integer id;

    //也可以直接在message属性中写,但是这个方式不能使用国际化
    @NotNull(message = "name不能为null")
    private String name;

    @Length(min = 6,max = 18)
    private String password;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    //@Future   必须是一个将来的时间
    @Past  //生日必须是一个过去的时间
    private Date birth;

}


@RequestMapping("/addBirth")
public String addBirth(@Valid Employee employee, BindingResult bindingResult,Model model){  //进行校验
    System.out.println(employee);
    //获取是否有校验错误
    boolean b = bindingResult.hasErrors();
    System.out.println(b);
    if(b){
        Map<String,Object> map = new HashMap<>();  //存放错误信息
        List<FieldError> errors = bindingResult.getFieldErrors();
        for(FieldError error : errors){
                map.put(error.getField(),error.getDefaultMessage());
                System.out.println(error.getField() + "--->" + error.getDefaultMessage());
            }
            model.addAttribute("map",map);   //将错误信息带到页面进行回显
        return "error";
    }else{
        return "success";
    }
}
  1. 错误消息的显示及国际化

    1. 在页面上显示错误

      1. Spring MVC 除了会将表单/命令对象的校验结果保存到对应的 BindingResult 或 Errors 对象中外,还会将所有校验结果保存到 “隐含模型”
      2. 即使处理方法的签名中没有对应于表单/命令对象的结果入参,校验结果也会保存在 “隐含对象” 中
      3. 隐含模型中的所有数据最终将通过 HttpServletRequest 的属性列表暴露给 JSP 视图对象,因此在 JSP 中可以获取错误信息
      4. 在 JSP 页面上可通过 显示错误消息
    2. 自定义国际化错误消息的显示

      1. 每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的 FieldError 对象
      2. 当一个属性校验失败后,校验框架会为该属性生成 4 个消息代码,这些代码以校验注解类名为前缀,结合 modleAttribute、属性名及属性类型名生成多个对应的消息代码:例如 User 类中的 password 属性标注了一个 @Pattern 注解,当该属性值不满足 @Pattern 所定义的规则时, 就会产生以下 4 个错误代码:

        1. Pattern.user.password 校验规则.隐含模型中这个对象的key.对象的属性名
        2. Pattern.password 校验规则.属性名
        3. Pattern.java.lang.String 校验规则.属性类型
        4. Pattern 校验规则
      3. 当使用 Spring MVC 标签显示错误消息时, Spring MVC 会查看 WEB 上下文是否装配了对应的国际化消息,如果没有,则显示默认的错误消息,否则使用国际化消息
      4. 若数据类型转换或数据格式转换时发生错误,或该有的参数不存在,或调用处理方法时发生错误,都会在隐含模型中创建错误消息。其错误代码前缀说明如下:

        1. required:必要的参数不存在。如 @RequiredParam(“param1”) 标注了一个入参,但是该参数不存在
        2. typeMismatch:在数据绑定时,发生数据类型不匹配的问题
      5. methodInvocation:Spring MVC 在调用处理方法时发生了错误 ```properties

        errors_zh_CN.properties

        NotNull.id=id不能为空 Length.password=密码必须为6到18位

errors_en_US.properties

NotNull.id=id is not null Length.password=password must between 6 and 18

```xml
<!--管理国际化资源文件-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basename" value="errors"/>
</bean>
/*
1. 先编写国际化配置文件
2. 让SpringMVC管理国际化资源配置文件
*/

11. SpringMVC支持ajax

<!--json依赖,支持ajax-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.11.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.11.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.11.1</version>
</dependency>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
    <script type="text/javascript" src="js/jquery-1.9.1.min.js"></script>
</head>
<body>
    <%
        request.setAttribute("path",request.getContextPath());
    %>
    <a href="${path}/getAllajax">ajax获取所有员工</a>
    <div></div>
    <script type="text/javascript">
        $("a:first").click(function () {
            //1. 发送ajax获取所有员工
            $.ajax({
                url:"${path}/getAllajax",
                type:"get",
                success:function (data) {
                    $.each(data,function () {
                        var empInfo = this.lastName + "--->" + this.birth;
                        $("div").append(empInfo + "<br/>");
                    });
                }
            })
            return false;
        });
    </script>
</body>
</html>
@Controller
public class AjaxTestController {

    @Autowired
    private EmployeeDao employeeDao;

    @ResponseBody  //想返回的数据放进响应体中,如果是对象,将会自动转为json格式
    @RequestMapping("/getAllajax")
    public Collection<Employee> ajaxAll(){
        Collection<Employee> all = employeeDao.getAll();
        return all;
    }

}

12. 文件上传和下载

  1. Spring MVC 为文件上传提供了直接的支持,这种支持是通过即插即用的 MultipartResolver 实现的
  2. Spring 用 Jakarta Commons FileUpload 技术实现了一个 MultipartResolver 实现类:CommonsMultipartResovler
  3. Spring MVC 上下文中默认没有装配 MultipartResovler,因此默认情况下不能处理文件的上传工作,如果想使用 Spring 的文件上传功能,需现在上下文中配置 MultipartResolver

    1. MultipartResolver的defaultEncoding属性: 必须和用户 JSP 的 pageEncoding 属性一致,以便正确解析表单的内容,为了让 CommonsMultipartResovler 正确工作,必须先将 Jakarta Commons FileUpload 及 Jakarta Commons io 的类包添加到类路径下
<!--文件上传和下载-->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>
<?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: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.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

    <mvc:default-servlet-handler/>
    <mvc:annotation-driven />

    <!--配置文件上传解析器-->
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!--设置最大的文件上传大小为20MB-->
        <property name="maxUploadSize" value="#{1024 * 1024 * 20}"/>
        <!--设置默认编码-->
        <property name="defaultEncoding" value="utf-8"/>
    </bean>

</beans>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <title>$Title$</title>
</head>
<body>
  <!--
      文件上传:
          1. 文件上传表单准备: enctype="multipart/form-data"
          2. 导入jar包
          3. 在配置文件中配置文件上传解析器MultipartResolver的实现类CommonsMultipartResolver
          4. 文件上传请求处理
            在处理器方法上写一个@RequestParam("headerImg") MultipartFile file,封装当前文件的信息,可以直接保存
  -->
  <%
      request.setAttribute("path",request.getContextPath());
  %>
  ${msg}
  <!--单文件上传-->
  <form action="${path}/upload" method="post" enctype="multipart/form-data">
      用户头像: <input type="file" name="headerImg"/><br>
      用户名: <input type="text" name="username"/><br>
      <input type="submit" value="提交"/>
  </form>
  <!--多文件上传-->
  <form action="${path}/uploads" method="post" enctype="multipart/form-data">
      用户头像: <input type="file" name="headerImg"/><br>
      用户头像: <input type="file" name="headerImg"/><br>
      用户头像: <input type="file" name="headerImg"/><br>
      用户头像: <input type="file" name="headerImg"/><br>
      用户名: <input type="text" name="username"/><br>
      <input type="submit" value="提交"/>
  </form>
</body>
</html>
@Controller
public class FileController {

    //单文件上传
    @RequestMapping("/upload")
    public String upload(@RequestParam(value = "username",required = false) String username,
                         @RequestParam("headerImg") MultipartFile file, Model model){
        System.out.println("========上传的文件的信息========");
        System.out.println("文件项的名字: " + file.getName());
        System.out.println("文件的名字: " + file.getOriginalFilename());
        System.out.println("文件的大小: " + file.getSize());
        //文件保存
        try {
            file.transferTo(new File("E:\\IDEA\\" + file.getOriginalFilename()));
            model.addAttribute("msg","文件上传成功...");
        } catch (IOException e) {
            model.addAttribute("msg","文件上传失败..." + e.getMessage());
        }
        return "forward:/index.jsp";
    }

    //多文件上传
    @RequestMapping("/uploads")
    public String uploads(@RequestParam(value = "username",required = false) String username,
                         @RequestParam("headerImg") MultipartFile[] multipartFiles, Model model){
        System.out.println("========上传的文件的信息========");
        for(MultipartFile file : multipartFiles){
            if(!file.isEmpty()){
                //进行文件保存
                try {
                    file.transferTo(new File("E:\\IDEA\\" + file.getOriginalFilename()));
                    model.addAttribute("msg","文件上传成功...");
                } catch (IOException e) {
                    model.addAttribute("msg","文件上传失败..." + e.getMessage());
                }
            }
        }
        return "forward:/index.jsp";
    }

}

13. 拦截器

  1. 自定义拦截器概述

    1. SpringMVC提供了拦截器机制,允许我们在运行目标方法之前进行一些拦截工作,或者在目标方法运行之后进行一些其他的处理
    2. Spring MVC可以使用拦截器对请求进行拦截处理,用户可以自定义拦截器来实现特定的功能,自定义的拦截器必须实现HandlerInterceptor接口
    3. preHandle():这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求 request 进行处理。如果程序员决定该拦截器对请求进行拦截处理后还要调用其他的拦截器,或者是业务处理器去进行处理,则返回true;如果程序员决定不需要再调用其他的组件去处理请求,则返回false
    4. postHandle():这个方法在业务处理器处理完请求后,但是DispatcherServlet 向客户端返回响应前被调用,在该方法中对用户请求request进行处理
    5. afterCompletion():这个方法在 DispatcherServlet 完全处理完请求后被调用,可以在该方法中进行一些资源清理的操作
  2. 单拦截器的正常运行流程
    拦截器的preHandle——->目标方法——->拦截器的postHandle——->页面——->拦截器的afterCompletion

  3. 其他流程

    1. 只要preHandle方法返回false,表示不放行,后面的流程就不会有了
    2. 只要放行了,不管中间有没有报错,afterCompletion方法都会执行
  4. 多拦截器运行流程

    1. 正常流程:跟Filter过滤器一样,谁先配置,谁就先执行
      /*
      MyFirstInterceptor--->preHandle...
      MySecondInterceptor--->preHandle...
      testInterceptor...
      MySecondInterceptor--->postHandle...
      MyFirstInterceptor--->postHandle...
      success.jsp...
      MySecondInterceptor--->afterCompletion...
      MyFirstInterceptor--->afterCompletion...
      */
      
  1. 异常流程

    1. 任何一个不放行,都不会来到目标方法及页面
    2. 如果MySecondInterceptor不放行,但是它前面已经放行了的那些拦截器还是会执行afterCompletion方法
    3. 总结
  2. 拦截器的preHandle方法是按照顺序执行的

  3. 拦截器的postHandle方法是逆序执行的
  4. 拦截器的afterCompletion也是按照逆序执行的
  5. 不管当前拦截器有没有放行,前面已经放行了的拦截器的afterCompletion方法总会执行
<!--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: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.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

    <mvc:default-servlet-handler/>
    <mvc:annotation-driven />

    <!--自定义拦截器-->
    <mvc:interceptors>
        <!--配置某个拦截器,默认拦截所有请求-->
        <bean class="com.siki.interceptor.MyFirstInterceptor"></bean>
        <!--配置某个拦截器更详细的信息-->
        <mvc:interceptor>
            <!--path: 表示拦截哪个请求-->
            <mvc:mapping path="/testInterceptor"/>
            <bean class="com.siki.interceptor.MySecondInterceptor"></bean>
        </mvc:interceptor>
    </mvc:interceptors>

</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <title>$Title$</title>
</head>
<body>
  <%
    request.setAttribute("path",request.getContextPath());
  %>
  <a href="${path}/testInterceptor">测试拦截器</a>
</body>
</html>


<!--success.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <%
        System.out.println("success.jsp...");
    %>
    <font color="#98fb98">成功</font>
</body>
</html>
/*
    自定义拦截器
        1. 实现HandlerInterceptor接口
        2. 在SpringMVC配置文件中注册这个拦截器
            配置这个拦截器用来拦截哪些目标方法
 */
public class MyFirstInterceptor implements HandlerInterceptor {

    //目标方法运行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyFirstInterceptor--->preHandle...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyFirstInterceptor--->postHandle...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyFirstInterceptor--->afterCompletion...");
    }
}


public class MySecondInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MySecondInterceptor--->preHandle...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MySecondInterceptor--->postHandle...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MySecondInterceptor--->afterCompletion...");
    }

}


//测试
@Controller
public class InterceptorTestController {

    @RequestMapping("/testInterceptor")
    public String testInterceptor(){
        System.out.println("testInterceptor...");
        return "success";
    }

}

14. 国际化

  1. 页面中获取国际化资源信息

    1. 在页面上能够根据浏 览器语言设置的情况对文本, 时间, 数值进行本地化处理

    2. 可以在 bean 中获取国际化资源文件 Locale 对应的消息

    3. 可以通过超链接切换 Locale, 而不再依赖于浏览器的语言设置情况

    4. 实现

      1. 使用 JSTL 的 fmt 标签
      2. 在 bean 中注入 ResourceBundleMessageSource 的实例, 使用其对应的getMessage 方法即可
      3. 配置 LocalResolver 和 LocaleChangeInterceptor
    5. 通过超链接切换Locale

      1. 默认情况下,SpringMVC 根据 Accept-Language 参数判断客户端的本地化类型

      2. 当接受到请求时,SpringMVC 会在上下文中查找一个本地化解析器(LocalResolver),找到后使用它获取请求所对应的本地化类型信息

      3. SpringMVC 还允许装配一个动态更改本地化类型的拦截器,这样通过指定一个请求参数就可以控制单个请求的本地化类型

      4. 本地化解析器和本地化拦截器

        1. AcceptHeaderLocaleResolver:根据 HTTP 请求头的 Accept-Language 参数确定本地化类型,如果没有显式定义本地化解析器, SpringMVC 默认使用该解析器
    6. CookieLocaleResolver:根据指定的 Cookie 值确定本地化类型
  2. SessionLocaleResolver:根据 Session 中特定的属性确定本地化类型

    1. LocaleChangeInterceptor:从请求参数中获取本次请求对应的本地化类型
#login_zh_CN.properties
welcomeInfo=欢迎来到siki学院
username=用户名
password=密码
loginBtn=登录


#login_en_US.properties
welcomeInfo=welcome to siki.com
username=username
password=password
loginBtn=login
<!--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: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.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

    <mvc:default-servlet-handler/>
    <mvc:annotation-driven />

    <!--配置国际化解析器-->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="login"/>
        <property name="defaultEncoding" value="utf-8"/>
    </bean>

    <!--配置自定义区域信息解析器-->
    <!--<bean id="localeResolver" class="resolver.MyLocaleResolver" />-->

    <!--通过SessionLocaleResolver + LocaleChangeInterceptor获取区域信息-->
    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>
    <mvc:interceptors>
        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
    </mvc:interceptors>

</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
    <a href="toLoginPage">去登录页面</a>
  </body>
</html>


<!--login.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!--导入fmt标签-->
<%@taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <font color="#98fb98">
        <fmt:message key="welcomeInfo"/>
    </font>
    <form action="" method="post">
        <fmt:message key="username"/>: <input type="text"/><br>
        <fmt:message key="password"/>: <input type="password"/><br>
        <input type="submit" value="<fmt:message key="loginBtn"/>"/><br>
    </form>
    <a href="toLoginPage?locale=zh_CN">中文</a>&nbsp;|&nbsp;<a href="toLoginPage?locale=en_US">English</a>
</body>
</html>
//自定义区域信息解析器
public class MyLocaleResolver implements LocaleResolver {

    //解析后返回locale
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale l = null;
        String locale = request.getParameter("locale");
        //如果带了参数,就用参数指定的区域信息
        //区域信息是从session中获取的,使用的是SessionLocaleResolver
//        if(locale != null && !"".equals(locale)){
//            String[] s = locale.split("_");
//            l = new Locale(s[0],s[1]);
//        }else{
//            //没带的话就直接用请求头中的
//            l = request.getLocale();
//        }

        //我们也可以创建一个对象,存储区域信息,然后将这个对象放入session域中,再取出来,也可以实现同样的效果
        //只需要配置SessionLocaleResolver + LocaleChangeInterceptor,帮我们指定区域信息
        //这种方式挺麻烦,了解即可

        return l;
    }

    //修改Locale
    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}


//测试
@Controller
public class I18nTestController {

    @RequestMapping("/toLoginPage")
    public String testI18n(Locale locale){  //locale: 国际化信息
        System.out.println(locale);
        return "login";
    }

}

15. 异常处理

  1. 异常处理概述

    1. Spring MVC 通过 HandlerExceptionResolver 处理程序的异常,包括 Handler 映射、数据绑定以及目标方法执行时发生的异常

    2. HandlerExceptionResolver

      1. DispatcherServlet 默认装配的 HandlerExceptionResolver
    3. ExceptionHandlerExceptionResolver

      1. 主要处理 Handler 中用 @ExceptionHandler 注解定义的方法
    4. @ExceptionHandler 注解定义的方法的优先级问题:例如发生的是NullPointerException,但是声明的异常有 RuntimeException 和 Exception,此候会根据异常的最近继承关系找到继承深度最浅的那个 @ExceptionHandler 注解方法,即标记了 RuntimeException 的方法
  2. ExceptionHandlerMethodResolver 内部若找不到@ExceptionHandler 注解的话,会找@ControllerAdvice 中的@ExceptionHandler 注解方法

    1. ResponseStatusExceptionResolver

      1. 在异常及异常父类中找到 @ResponseStatus 注解,然后使用这个注解的属性进行处理
    2. 定义一个 @ResponseStatus 注解修饰的异常类
  3. 若在处理器方法中抛出了上述异常:若ExceptionHandlerExceptionResolver 不解析上述异常。由于触发的异常 UnauthorizedException 带有@ResponseStatus 注解。因此会被ResponseStatusExceptionResolver 解析到。最后响应HttpStatus.UNAUTHORIZED 代码给客户端。HttpStatus.UNAUTHORIZED 代表响应码401,无权限

    1. @ResponseStatus专门用来给自定义异常类上标注的
  4. DefaultHandlerExceptionResolver

    1. 对一些特殊的异常进行处理
  HttpMediaTypeNotSupportedException、

  HttpMediaTypeNotAcceptableException等
  1. SimpleMappingExceptionResolver
    如果希望对所有异常进行统一处理,可以使用 SimpleMappingExceptionResolver,它将异常类名映射为视图名,即发生异常时使用对应的视图报告异常
<!--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: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.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--包扫描-->
    <context:component-scan base-package="com.siki"/>

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

    <mvc:default-servlet-handler/>
    <mvc:annotation-driven />

    <!--配置SimpleMappingExceptionResolver-->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <!--public void setExceptionMappings(Properties mappings)-->
        <property name="exceptionMappings">
            <props>
                <!--key: 异常的全类名 value: 目标页面-->
                <prop key="java.lang.NullPointerException">myError</prop>
            </props>
        </property>
        <!--指定错误信息取出时的key-->
        <property name="exceptionAttribute" value="exception"/>
    </bean>

</beans>
<!--index.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>
    <%
      request.setAttribute("path",request.getContextPath());
    %>
    <a href="${path}/test01?i=10">测试异常1</a><br>
    <a href="${path}/test02?i=10">测试异常2</a><br>
    <a href="${path}/test03">测试异常2</a><br>
    <a href="${path}/test04">测试异常4</a><br>
  </body>
</html>


<!--myError.jsp-->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    <h1>出错啦~~~</h1>
    <h2>错误信息: ${exception}</h2>
</body>
</html>
@Controller
public class ExceptionTestController {

    @RequestMapping("/test01")
    public String test01(Integer i){
        System.out.println(10 / i);
        return "success";
    }

    /*
        告诉SpringMVC,这个方法专门用来处理ArithmeticException异常
            1. 方法参数中写Exception对象,获取异常信息
            2. 参数位置只能写Exception对象,不能写Model或者其他的对象
            3. 如果想将异常信息带到页面上显示,可以将返回值设置为ModelAndView,携带异常信息
            4. 如果有多个@ExceptionHandler都能处理某个异常,精确优先
            5. 全局异常处理和本类异常处理同时存在,本类优先
     */
    @ExceptionHandler(value = {ArithmeticException.class})
    public ModelAndView handleException(Exception e){  //参数e可以获取异常信息
        System.out.println("本类的handleException..." + e);
        ModelAndView view = new ModelAndView("myError");
        view.addObject("exception",e);
        return view;
    }


    @RequestMapping("/test02")
    public String test02(Integer i){
        if(i != 10){
            System.out.println("i不是合法的...");
            throw new MyResponseStatusException();
        }
        System.out.println("i是合法的...");
        return "success";
    }

    //如果某个异常是由spring自己引起的,而且没有ExceptionHandlerExceptionResolver和ResponseStatusExceptionResolver
    //进行相应的处理,则会由spring默认的DefaultHandlerExceptionResolver进行处理
    @RequestMapping(value = "/test03",method = RequestMethod.POST)
    public String test03(){
        return "success";
    }

    //SimpleMappingExceptionResolver处理空指针异常
    @RequestMapping("/test04")
    public String test04(){
        String s = null;
        System.out.println(s.length());
        return "success";
    }

}


//我们可以编写一个类,集中处理所有的异常,要加入到IOC容器中
@ControllerAdvice //这是一个专门用来处理异常的类
public class MyException {

    @ExceptionHandler(value = {ArithmeticException.class})
    public ModelAndView handleException(Exception e){  //参数e可以获取异常信息
        System.out.println("全局的handleException..." + e);
        ModelAndView view = new ModelAndView("myError");
        view.addObject("exception",e);
        return view;
    }

}


//自定义异常类
@ResponseStatus(reason = "i值不合法",value = HttpStatus.NOT_ACCEPTABLE)
public class MyResponseStatusException extends RuntimeException{

}

16. SpringMVC运行流程

  1. 流程图

  2. 流程描述

    1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获

    2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)
      判断请求URI对应的映射

      1. 不存在

        1. 再判断是否配置了mvc:default-servlet-handler
        2. 如果没配置,则控制台报映射查找不到,客户端展示404错误
        3. 如果有配置,则执行目标资源(一般为静态资源,如:JSP,HTML)
    3. 如果存在,则根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
      DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter

    4. 拿到HandlerAdapter之后,就会执行目标方法

      1. ModelAttribute注解标注的方法会提前运行
      2. 执行目标方法的时候,确定目标方法用的参数

        1. 有注解
        2. 没注解

          1. 看是否有Model、Map或者其他的
          2. 如果是自定义类型

            1. 从隐含模型中看有没有,如果有就从隐含模型中直接拿
            2. 如果没有,就要看是否有SessionAttributes注解标注的属性,如果是从session中拿,拿不到就会抛异常
            3. 如果都不是,就会利用反射创建对象
    5. 如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler()方法【正向】

    6. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作

      1. HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
      2. 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
      3. 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
      4. 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
    7. HandlerAdapter执行完成后,向DispatcherServlet 返回一个ModelAndView对象

    8. 此时将开始执行拦截器的postHandle()方法【逆向】

    9. 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet,根据Model和View,来渲染视图

    10. 在返回给客户端时需要执行拦截器的AfterCompletion方法【逆向】

    11. 将渲染结果返回给客户端

/*
以注解的方式来启动SpringMVC,需要继承AbstractAnnotationConfigDispatcherServletInitializer
实现抽象方法指定DispatcherServlet的配置信息
*/

//在web容器启动的时候创建对象,调用方法来初始化前端控制器
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //获取根容器的配置类,类似Spring的配置文件,创建一个父容器
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{RootConfig.class};
    }

    //获取web容器的配置类,类似SpringMVC的配置文件,创建一个子容器
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ServletConfig.class};
    }

    //获取DispatcherServlet的映射信息
    @Override
    protected String[] getServletMappings() {
        // /表示拦截所有请求(包括静态资源,但是不包括jsp页面),jsp页面是由tomcat的jsp引擎解析的
        // /*表示拦截所有请求,包括jsp页面
        return new String[]{"/"};
    }
}


//根配置类(Spring配置文件)  Spring容器不扫描Controller注解
@ComponentScan(value = "com.siki",excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,
        classes = Controller.class)})
public class RootConfig {

}


//子配置类(SpringMVC配置文件)  SpringMVC只扫描Controller注解,默认扫描全部,所以需要禁用默认过滤规则
@ComponentScan(value = "com.siki",includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,
        classes = Controller.class)},useDefaultFilters = false)
public class ServletConfig {

}


@Service
public class HelloService {

    public String helloService(String name){
        return "hello" + name;
    }

}


@Controller
public class HelloController {

    @Autowired
    private HelloService helloService;

    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello...");
        String s = helloService.helloService("xinxin");
        return s;
    }

}
/*
定制SpringMVC:
    1. 定义配置类,标注@EnableWebMvc注解,开启SpringMVC定制配置功能
            相当于<mvc:annotation-driven/>
    2. 配置组件(视图解析器、视图映射、静态资源映射、拦截器等)
            实现WebMvcConfigurer接口,并实现特定的方法
*/
@EnableWebMvc
public class MyConfig implements WebMvcConfigurer {

    //配置视图解析器
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        //底层return jsp("/WEB-INF/", ".jsp");
        //默认所有的页面都位于/WEB-INF/xxx.jsp
        //registry.jsp();
        registry.jsp("/WEB-INF/views/",".jsp");   //也可以自己定义路径
    }

    //配置静态资源访问
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}


@Controller
public class HelloController {

    @Autowired
    private HelloService helloService;

    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello...");
        String s = helloService.helloService("xinxin");
        return s;
    }

    @RequestMapping("/suc")
    public String success(){
        System.out.println("succ");
        return "success";
    }

}
/*
SpringMVC的异步请求(学完多线程及JUC之后再回来理解)
    1. 通过返回Callable进行异步处理
    2. 通过返回DeferredResult进行异步处理
*/
//处理异步处理请求
@Controller
public class AsyncController {

    /*
        1. 控制器返回Callable<>对象
        2. SpringMVC进行的异步处理,就是将Callable提交到TaskExecutor,使用一个隔离的线程进行执行
        3. DispatcherServlet和所有的Filter退出web容器的线程,但是响应保持打开状态
        4. Callable返回的结果,SpringMVC将请求重新派发给容器,恢复之前的处理
        5. 跟据Callable返回的结果,SpringMVC会继续进行视图渲染等流程
     */
    @ResponseBody
    @RequestMapping("/async01")
    public Callable<String> async01(){
        System.out.println("主线程开启..." + System.currentTimeMillis());
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println("副线程开启..." + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("副线程结束..." + System.currentTimeMillis());
                return "async01";
            }
        };
        System.out.println(callable);
        System.out.println("主线程结束..." + System.currentTimeMillis());
        return callable;
    }


    public DeferredResult<Object> createOrder(){
        DeferredResult<Object> result = new DeferredResult<>(2000L,"超时,创建失败...");
        return result;
    }

}