一、Spring

1、IOC(容器)

1.1 简单定义

  • IOC(Inversion Of Control):控制反转

    • 控制:资源的获取方式(主动式、被动式)
      • 主动式:需要什么资源就自己创建(new)
      • 被动式:将所需要的所有资源交给一个容器来创建和管理
        • 容器:负责管理所有的组件(有功能的类),只要告诉容器需要什么,容器就会帮助你创建;可以说类似一个大型的婚介所,你需要什么样的女(男)朋友,只要告诉该婚介所你的要求,那么婚介所就可以帮助你去寻找,安排你们约会,一切都为你们准备好了。
    • DI(Dependency Injection):依赖注入
      • 容器可以知道某个组件在运行的时候需要依赖另外哪个组件,然后通过反射的形式为其属性赋值。

        1.2 在容器中注册对象

        1.2.1 实验一(重点)

        通过 IOC 容器创建对象,并为属性赋值
  • 利用框架编写程序的流程

    1. 导包
      ```java org.springframework:spring-beans:5.2.5.RELEASE org.springframework:spring-context:5.2.5.RELEASE org.springframework:spring-core:5.2.5.RELEASE org.springframework:spring-expression:5.2.5.RELEASE

commons-logging:commons-logging:1.2 junit:junit:4.11

  1. 2. 写配置文件
  2. - 首先定义一个Person
  3. ```java
  4. package com.glutnn.bean;
  5. public class Person {
  6. private String name;
  7. private int age;
  8. private String email;
  9. private String gender;
  10. /*
  11. 此处写
  12. setter和getter方法
  13. toString方法
  14. */
  15. }
  -  在 Spring 配置文件 ioc.xml 中注册Person  
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        注册Person对象
        id:是该组件的唯一标识
        class:需要写该组件的全类名
        一个bean标签注册一个对象
    -->
    <bean id="person01" class="com.glutnn.bean.Person">
        <!--利用property标签为属性赋值-->
        <property name="name" value="张三"/>
        <property name="gender" value="男"/>
        <property name="email" value="zhangsna@qq.com"/>
        <property name="age" value="18"/>
    </bean>
</beans>
  1. 单元测试

    • 单元测试类

      public class PersonTest {
      @Test
      public void test01(){
      /**
      * ApplicationContext:ioc容器的接口
      * 通过配置文件拿到容器对象
      * 从容器中拿到该组件
      */
      ApplicationContext ioc = new ClassPathXmlApplicationContext("ioc.xml");
      Person person01 = (Person) ioc.getBean("person01");
      System.out.println(person01);
      // Person{name='张三', age=18, email='zhangsna@qq.com', gender='男'}
      }
      }
      
    • 注意

      1. 容器中的对象在容器创建完成的同时就创建好了,并不需要等到getBean的时候才会创建
      2. 同一个组件在容器中是单实例
      3. 获取一个容器中不存在的组件,会出现异常
        • org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named ‘pe’ available
      4. property 标签是利用 setter 方法为 JavaBean 的属性赋值,name=”这里填写的就是setter方法去掉set后剩下单词首字母小写形式”,因此不要随意改动 setter/getter 方法,最好就是利用IDE自动生成

        1.2.2 实验二(重点)

        根据 bean 的类型从IOC容器中获取 bean 的实例 ```java public class PersonTest { ApplicationContext ioc = new ClassPathXmlApplicationContext(“ioc.xml”);

    @Test public void test(){ /*

     但是需要注意的是,如果ioc容器中当前类型的bean有多个,那么就会出现异常
     org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.glutnn.Person' available: expected single matching bean but found 2: person01,person02
     此时如果依然要使用类型来获取组件的话,可以使用以下方法:
     getBean("person02",Person.class)
     将该组件的id和类型一并作为参数传入
    

    */ Person bean = ioc.getBean(Person.class); System.out.println(bean); } } ```

    1.2.3 实验三

    通过构造器为 bean 的属性赋值

  • 方式一(掌握)

    <bean id="person03" class="com.glutnn.bean.Person">
     <!--
         - public Person(String name, int age, String email, String gender)
         - 一个constructor-arg标签对应一个属性,若有参构造器只传入了3个属性,那么也只需要三个
             constructor-arg标签即可     
         - property标签是调用无参构造器创建对象,通过setter方法为属性赋值
         - 而constructor-arg标签则是直接调用有参构造器创建对象并为属性赋值
     -->
     <constructor-arg name="name" value="李四"/>
     <constructor-arg name="age" value="23"/>
     <constructor-arg name="email" value="lisi@qq.com"/>
     <constructor-arg name="gender" value="女"/>
    </bean>
    
  • 方式二(了解)

    • 在标签中省略 “name”,只写 “value”,这样的话就必须按照有参构造器中各个属性的顺序来赋值
    • 如果还是不想按顺序写,也可以在标签中通过 “index” 指定各个属性的索引,从0开始,例如:

      <constructor-arg value="李四" index="0"/>
      <constructor-arg value="女" index="3"/>
      
    • 如果构造器重载的情况下,还可以通过 “type” 来明确指定要赋值的属性类型,防止赋值出现混乱

通过 p 名称空间为 bean 属性赋值(了解)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--
        首先需要导入:xmlns:p="http://www.springframework.org/schema/p"
        该方式赋值是调用setter方法
    -->
    <bean id="person04" class="com.glutnn.bean.Person"
        p:name="王五" p:age="30" p:email="ww@qq.com" p:gender="男"/>

</beans>

1.2.4 实验四

正确的为各种属性赋值

  • Person类中存在的属性
    ```java package com.glutnn.bean;

public class Person { private String name; private int age; private String email; private String gender;

private Car car; // 自定义的Car类型,其中有属性carName、price、color;
private List<Book> books; // Book(bookName、author)
private Map<String,Object> maps;
private Properties properties;

/*setter、getter方法,toString方法*/

}


-  **赋 null 值** 
   -  不赋值,引用数据类型默认就是null,基本数据类型就是它对应的默认值 
      - Person{name='null', age=0, email='null', gender='null', car=null, books=null, maps=null, properties=null}
   -  假设原来 name 属性有一个默认值 private String name = "小明",现在要将该属性赋值为 null  
```xml
<property name="name">
    <null/>
</property>
  • 对象类型赋值
    • ref 引用外部 bean
      <bean id="car01" class="com.glutnn.bean.Car">
      <property name="carName" value="宝马"/>
      <property name="price" value="300000"/>
      <property name="color" value="绿色"/>
      </bean>
      <bean id="person05" class="com.glutnn.bean.Person">
      <property name="car" ref="car01"/>
      </bean>
      <!--
      输出:
      Person{name='null', age=0, email='null', gender='null', car=Car{carName='宝马', price=300000,     color='绿色'}, books=null, maps=null, properties=null}
      -->
      
public class PersonTest {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("ioc.xml");

    @Test
    public void test(){
        Person person05 = (Person) ioc.getBean("person05");
        System.out.println(person05);

        /*
            ref 是严格的地址引用,从容器中拿到的car
            和person05中getBean到的car是同一个car
        */
        Car car = person05.getCar();
        Car car01 = (Car) ioc.getBean("car01");
        System.out.println(car == car01); // true
    }
}
  • 内部创建

    <bean id="person05" class="com.glutnn.bean.Person">
    <property name="car">
      <!-- 
          相当于 car = new Car() 
          而且该bean写id是没有用的,因为根本获取不到,只可以内部使用
      -->
      <bean class="com.glutnn.bean.Car">
          <property name="carName" value="奔驰"/>
          <property name="price" value="400000"/>
          <property name="color" value="红色"/>
      </bean>
    </property>
    </bean>
    <!--
    输出:
    Person{name='null', age=0, email='null', gender='null', car=Car{carName='奔驰', price=400000,     color='红色'}, books=null, maps=null, properties=null}
    
    这里的car和外部的car就不是一个car了!!
    -->
    
  • List 类型赋值
    ```xml
- **Map 类型赋值**xml - **Properties 类型赋值**xml root 123456 - **util 名称空间创建集合类型的bean**xml - **级联属性(属性的属性)赋值**xml <a name="62c94f89"></a> #### 1.2.5 实验五(重点) **_配置通过静态工厂方法创建的 bean、实例工厂方法创建的 bean、FactoryBean_** - bean 的创建默认是框架利用反射 new 出来的 - 工厂模式:指的是存在一个专门帮我们创建对象的类,这个类就是工厂 - 静态工厂:工厂本身不需要创建对象,对象 = 工厂类.工厂方法() - 实例工厂:工厂本身需要创建对象,然后利用工厂对象再去调用工厂方法 - 静态工厂 - 创建一个飞机类 AirPlane[planeName、planeLong、planeWidth] - 创建静态工厂类java package com.glutnn.factory; import com.glutnn.bean.AirPlane; public class AirPlaneStaticFactory { public static AirPlane getAirPlane(String planeName){ AirPlane airPlane = new AirPlane(); airPlane.setPlaneName(planeName); airPlane.setPlaneLong(“100m”); airPlane.setPlaneWidth(“50m”); return airPlane; } } - 配置beanxml - 实例工厂 - 创建实例工厂类java package com.glutnn.factory; import com.glutnn.bean.AirPlane; public class AirPlaneInstanceFactory { public AirPlane getAirPlane(String planeName){ AirPlane airPlane = new AirPlane(); airPlane.setPlaneName(planeName); airPlane.setPlaneLong(“100m”); airPlane.setPlaneWidth(“50m”); return airPlane; } } - 配置beanxml - **实现 FactoryBean 的工厂** - FactoryBean 是 Spring 规定的一个接口,只要是该接口的实现类,Spring 都认为是一个工厂类 - **这里不管是单实例还是多实例,IOC 容器启动的时候都不会创建对象,只有在获取的时候才创建对象** 1. 编写 FactoryBean 的实现类java package com.glutnn.factory; import com.glutnn.bean.Book; import org.springframework.beans.factory.FactoryBean; / 实现了FactoryBean接口的类是Spring认识的工厂类 Spring会自动调用工厂方法创建实例 这里是个泛型,里面填的就是你想创建的对象类型 */ public class MyFactoryBeanImple implements FactoryBean { / 工厂方法 @return 返回创建的对象 @throws Exception / @Override public Book getObject() throws Exception { Book book = new Book(); book.setBookName(“计算机网络”); book.setAuthor(“谢希仁”); return book; } / Spring会自动调用该方法来确认创建的对象是什么类型 @return 返回创建的对象的类型 */ @Override public Class<?> getObjectType() { return Book.class; } / 是单例模式吗? @return false:不是单例;true:是单例 / @Override public boolean isSingleton() { return false; } } 2. 在 Spring 配置文件中进行注册xml ``` #### 1.2.6 实验六 *通过继承实现bean配置信息的重用 ```xml <a name="3abea927"></a> #### 1.2.7 实验七 **_通过 abstract 属性创建一个模板 bean_**xml <a name="e24528d6"></a> #### 1.2.8 实验八 **_bean 之间的依赖_**xml <a name="469de430"></a> #### 1.2.9 实验九(重点) **_测试 bean 的作用域,分别创建单实例和多实例的 bean_** - bean 的作用域 - 默认是单实例的 - 可以修改,通过 **scope(4个值)** - **prototype**:多实例 - 容器启动默认不会创建多实例bean,获取的时候才会创建 - 每次获取时构造器都会被调用,每次创建的都是一个新的实例 - **singleton**:单实例 - 在容器启动完成之前就已经创建好对象,并且保存在容器中 - 任何时候获取都是相同的一个对象 - request:web环境下,同一次请求创建一个bean实例(没用) - session:web环境下,同一次会话创建一个bean实例(没用) <a name="a29d4462"></a> #### 1.2.10 实验十 **_创建带有生命周期(bean的创建到销毁)方法的 bean_** - IOC容器中注册的 bean - 单例 bean:在容器启动的时候就会创建,容器关闭的时候就会销毁 - 多实例 bean:在获取的时候才会创建 - 可以为 bean 自定义一些生命周期方法,Spring 在创建或者销毁的时候就会调用指定的方法java / 在Book类中自定义两个方法 / public void myInit(){ System.out.println(“这是Book的初始化方法…”); } public void myDestory(){ System.out.println(“这是Book的销毁方法…”); } ```xml <bean id="book" class="com.glutnn.bean.Book" init-method="myInit" destroy-method="myDestory"></bean> <!-- 容器初始化会调用myInit(),容器关闭会调用myDestory() --> - 单例 bean 的生命周期:(容器启动)构造器—->初始化方法—->(容器关闭)销毁方法 - 多实例 bean 的生命周期:获取bean(构造器—->初始化方法)—->容器关闭不会调用bean的销毁方法 #### 1.2.11 实验十一 测试 bean 的后置处理器 1. 编写后置处理器接口(BeanPostProcessor)的实现类
java package com.glutnn.bean; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; public class MyBeanPostProcessorImple implements BeanPostProcessor { /** * 初始化之前调用 * @param bean 将要初始化的bean * @param beanName 该bean的名字 * @return * @throws BeansException */ @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { System.out.println("【"+beanName+"】将要调用初始化方法了...这个bean是【"+bean+"】"); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { System.out.println("【"+beanName+"】初始化方法调用完了..."); // 初始化之后返回的bean,这里返回的是什么,容器中保存的就是什么 return null; } } 2. 注册在配置文件中
xml <bean id="book" class="com.glutnn.bean.Book" init-method="myInit" destroy-method="myDestory"></bean> <bean id="myBeanPostProcessorImple" class="com.glutnn.bean.MyBeanPostProcessorImple"></bean> <!-- Book被创建 【book】将要调用初始化方法了...这个bean是【Book{bookName='null', author='null'}】 这是Book的初始化方法... 【book】初始化方法调用完了... 注意:无论bean有没有初始化方法,后置处理器都会工作 --> #### 1.2.12 实验十二(重点) 引用外部属性文件 - 数据库连接池作为单实例是最好的,一个项目只有一个连接池,管理很多连接,直接从连接池中拿出连接 - 可以让Spring帮助我们创建连接池对象(管理连接池) 1. 导包
java com.mchange:c3p0:0.9.5.5 mysql:mysql-connector-java:5.1.9 org.springframework:spring-aop:5.2.5.RELEASE 2. 编写 Properties 文件
properties jdbc.username=root jdbc.password=2000 jdbc.jdbcUrl=jdbc:mysql://localhost:3306/springdb jdbc.driverClass=com.mysql.jdbc.Driver 3. 在Spring配置文件中注册
xml <!--引用外部配置文件,固定写法classpath:表示引用类路径下的一个资源--> <context:property-placeholder location="classpath:dbconfig.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> #### 1.2.13 实验十三 基于XML的自动装配(自动赋值,仅对自定义类型有效) - autowire=”default”:不自动装配 - autowire=”no”:不自动装配 - autowire=”byName“:以实体类中的属性名作为id去容器中找到这个组件,为其赋值;找不到就赋值为null - 相当于 car = ioc.getBean(“car”); - autowire=”byType“:以属性的类型作为查找依据去容器中找这组件 - 相当于 car = ioc.getBean(Car.class); - 如果容器中有多个这种类型的组件,则会出现异常 - 如果没找到,则装配null - autowire=”constructor“:按照构造器进行赋值 - 先按照有参构造器的参数类型进行装配,没有该类型则装配null - 如果按照类型找到多个的话,会继续按照参数名作为id进行装配 - 好处是不会报异常 ```xml <a name="fcf1d9d6"></a> #### 1.2.14 实验十四 **_SpEL(Spring Expression Language)Spring表达式语言测试_**xml
<a name="044a1124"></a>
#### 1.2.15 实验十五(重点)
**_通过注解分别创建 Dao、Service、Controller_**

- 通过给 Bean 上添加注解,可以快速地将 bean 加入到容器中
- **四个注解** 
   - [**@Controller  **](/Controller ):控制器,给控制器层(servlet包下)的组件加这个注解
   - [**@Service  **](/Service ):业务逻辑,给业务逻辑层的组件加这个注解
   - [**@Repository  **](/Repository ):给数据库层(持久化层、Dao层)的组件加这个注解
   - [**@Component  **](/Component ):给不属于以上几层的组件加这个注解
- 虽然注解可以随便加,就是说你可以在业务逻辑组件上加控制器注解,这样也可以将组件加入到容器中,Spring 是不会去验证的。但是,注解是给我们程序员看的,该组件执行什么功能就应该加上对应的注解。乱加注解肯定会导致混乱,就会分不清这个组件原来的功能是什么。
- **使用注解将组件快速加入到容器中的步骤** 
   1. 在组件上添加你需要的注解
   1. 告诉 Spring ,在 Spring 配置文件中利用**<context:component-scan base-package="">**标签自动扫描加了组件的注解 
      - **base-package**:指定要扫描的基础包,Spring 会将基础包下的所有加了注解的组件都扫描到容器中
      - **注解依赖于aop包**
- 使用注解加入到容器中的组件和使用配置文件加入到容器中的组件默认行为都是相同的 
   - **组件的 id 默认就是组件的类名首字母小写**,如果要修改默认id的话,就直接 **@Component("newname")**,引号中就写你想要的该组件的新的 id
   - **组件的作用域默认都是单例的**,可以利用 [**@Scope(value  = "") **](/Scope(value ) 进行修改
<a name="3d23c92e"></a>
#### 1.2.16 实验十六
**_使用 context:include-filter指定扫描包时要包含的类_**
```xml
<context:component-scan base-package="com.glutnn" use-default-filters="false">
        <!--
            默认是将该基础包下加了注解的组件全部扫描
            这个标签的作用则是指定只扫描哪些组件
            use-default-filters="false":禁用默认规则,一定要禁用默认规则,下面的标签才会起作用
            以下代码的意思是:只扫描标注了Component注解的组件到容器中
        -->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/></context:component-scan>

1.2.17 实验十七

使用context:exclude-filter指定扫描包时不包含的类

<context:component-scan base-package="com.glutnn">
    <!--
            type:指定排除规则
                - annotation:按照注解进行排除
                - assignable:按照类进行排除
                下面三个没啥用...
                - aspectj
                - custom
                - regex
            expression:写全类名,注解规则下就写注解的全类名,类规则下就写类的全类名

            以下代码的意思是:将标注了Component注解的组件排除,也就是说不用将标注了该注解的组件扫描进容器
     -->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Component"/></context:component-scan>

1.2.18 实验十八(重点)

使用@Autowired注解实现属性自动装配(DI:依赖注入)

  • 假设容器中有两个组件,一个是BookServlet类,一个是BookService类,BookService中有一个方法save(),这个BookService类是BookServlet的属性,那么直接在该属性上加上@Autowired注解,Spring 就会去容器中找该属性对应的组件,然后自动为该属性赋值,如此就可以不用自己 bookService = new BookService() 而直接使用BookService中的方法了

    @Controller
    public class BookServlet{
     /*
         @Qualifier:可以自己指定一个名字作为id,这样Spring就不会默认按照变量名作为id去找组件了
         如此的话,变量名取什么就无所谓了,因为Spring会按照你指定的名字作为id去容器中寻找对应组件
         找到就装配,找不到就报异常
     */
     @Qualifier("bookService")
     @Autowired(required=false)
     private BookService bookService;
    
     public void doGet(){
         bookService.save();
     }
    
     @Autowired
     public void f(@Qualifier("bookService")BookService bookService){
         System.out.println(bookService);
     }
    }
    
  • @Autowired原理

    1. 先按照类型去容器中找相应的组件,bookService = ioc.getBean(BookService.class);
      1. 找到一个就赋值
      2. 没找到就报异常
      3. 找到多个,例如还有一个组件BookServiceExt继承于BookService,那么该组件也是BookService类型的
        • 就按照变量名作为id继续匹配,这两个组件的默认id分别是bookService、bookServiceExt,如果变量名是bookService,那么就为其装配BookService组件,反之则装配BookServiceExt组件
          • 匹配上就装配
          • 没匹配上就报异常
        • 或者,如果该属性标注了@Qualifier 注解,那么Spring就会按照这个注解指定的名字作为id去找对应组件
          • 匹配上就装配
          • 没匹配上就报异常
  • 可以发现,@Autowired 标注的属性,Spring默认情况下一定会给你装配上对应的组件,否则一定会报异常。想要避免这种情况的出现,可以在@Autowired 中加上 required=false,这样的话,即使Spring按照以上步骤都没有找到对应的组件,也不会报异常,最后只会给你装配一个 null 值
  • 如果在某个方法上使用 @Autowired 注解,那么Spring 将会给这个方法的形参进行自动装配注入,注入规则和上面的相同。而且还可以在参数上使用@Qualifier 注解。
  • @Autowired 和 的区别 @Resource
    • 前者是Spring规定的注解,功能更加强大;后者则是J2EE规定的标准,功能虽然比不上前者,但是扩展性更加强,因为如果我们不使用Spring容器框架的话,前者就没用了,而后者还是会被支持。

      1.2.23 实验十九(重点)

      测试泛型依赖注入
      Spring - 图1

2、AOP(面向切面编程)

2.1 简单定义

  • AOP(Aspect Oriented Programming):面向切面编程
    • 是基于OOP(面向对象编程)基础之上的一种新的编程思想,指的是在程序运行期间,将某段代码动态地切入到指定方法的指定位置
    • 以一个具体的场景来说明这个思想:计算器运行计算方法的时候进行日志记录
      • 定义一个接口
        ```java package com.glutnn.inter;

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);//除 }


      -  定义一个该接口的实现类  
```java
package com.glutnn.impl;

import com.glutnn.inter.Calculator;

public class MyMathCalculator implements Calculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}
  -  现在要给每个方法都添加日志记录,可以在每个方法内部添加日志方法,但是会发现十分麻烦,很难维护,而且和业务逻辑方法耦合了。最理想的方法就是在业务逻辑运行的期间,日志模块可以自己动态地加入。这就需要**动态代理**技术 

2.2 动态代理

package com.glutnn.proxy;

import com.glutnn.inter.Calculator;
import com.glutnn.utils.LogUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 帮Calculator生成代理对象的类
 */
public class CalculatorProxy {

    /**
     * 为传入的参数对象创建一个动态代理对象
     * @param calculator 被代理的对象
     * @return 返回这个被代理对象的代理对象
     */
    public static Calculator getProxy(Calculator calculator) {

        ClassLoader loader = calculator.getClass().getClassLoader(); // 被代理对象的类加载器
        Class<?>[] interfaces = calculator.getClass().getInterfaces(); // 被代理对象实现的接口
        InvocationHandler h = new InvocationHandler() { // 方法执行器,帮助目标对象执行目标方法
            /**
             *
             * @param proxy 给jdk使用的代理对象,我们不用动这个对象
             * @param method 当前将要执行的目标对象的方法
             * @param args 这个方法调用时外界传入的参数值
             * @return
             * @throws Throwable
             */
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = null;
                try {
                    // 添加方法开始日志
                    LogUtils.logStart(method,args);
                    // 利用反射执行目标方法,result:目标方法执行后的返回值
                    result = method.invoke(calculator, args);
                    // 添加方法结束日志
                    LogUtils.logReturn(method,result);
                } catch (Exception e) {
                    // 添加方法出现异常时的日志
                    LogUtils.logException(method,e);
                }finally {
                    // 添加方法最终结束时的日志
                    LogUtils.logEnd(method);
                }

                return result;
            }
        };

        /**
            Proxy为目标对象创建代理对象
         */
        Object proxy = Proxy.newProxyInstance(loader,interfaces,h);

        return (Calculator) proxy;
    }
}
package com.glutnn.utils;

import java.lang.reflect.Method;
import java.util.Arrays;

public class LogUtils {
    public static void logStart(Method method,Object...args){
        System.out.println("【"+method.getName()+"】方法开始执行,用的参数列表【"+ Arrays.asList(args) +"】");
    }

    public static void logReturn(Method method,Object result){
        System.out.println("【"+method.getName()+"】方法执行结束...执行的结果是:"+result);
    }

    public static void logException(Method method, Exception e) {
        System.out.println("【"+method.getName()+"】方法出现异常了,异常信息是:"+e.getCause());
    }

    public static void logEnd(Method method) {
        System.out.println("【"+method.getName()+"】方法最终结束了");
    }
}
package com.glutnn;

import com.glutnn.impl.MyMathCalculator;
import com.glutnn.inter.Calculator;
import com.glutnn.proxy.CalculatorProxy;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.sql.SQLException;

public class PersonTest {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("ioc.xml");

    @Test
    public void test() throws SQLException {
        Calculator calculator = new MyMathCalculator();
        // 拿到这个对象的代理对象,然后代理对象执行加减乘除方法
        Calculator proxy = CalculatorProxy.getProxy(calculator);
        proxy.add(1,2);
        System.out.println("============================================");
        proxy.div(3,0);
    }
    /*
    【add】方法开始执行,用的参数列表【[1, 2]】
    【add】方法执行结束...执行的结果是:3
    【add】方法最终结束了
    ============================================
    【div】方法开始执行,用的参数列表【[3, 0]】
    【div】方法出现异常了,异常信息是:java.lang.ArithmeticException: / by zero
    【div】方法最终结束了
    */
}
  • 存在的问题
    • 写起来很复杂…
    • JDK 默认的动态代理,如果目标对象没有实现任何接口,那么是无法为它创建代理对象的
  • AOP 的底层就是动态代理,利用Spring可以很简单的创建动态代理,而且没有强制要求目标对象必须实现接口

    2.3 AOP专业术语

    Spring - 图2

    2.4 AOP简单配置

  • 现在以上面那个场景为例子,利用Spring来实现AOP,将LogUtils(切面类)中的方法(通知方法)动态地在目标方法运行的同时切入指定位置

  • AOP使用步骤

    1. 导包

      org.springframework:spring-aspects:5.2.5.RELEASE // 基础版
      // 加强版,即使目标对象没有实现接口也可以创建动态代理
      net.sourceforge.cglib:com.springsource.net.sf.cglib:2.2.0
      org.aopalliance:com.springsource.org.aopalliance:1.0.0
      org.aspectj:com.springsource.org.aspectj.weaver:1.7.2.RELEASE
      
    2. 写配置文件

      1. 将目标类MyMathCalculator和切面类LogUtils(封装了通知方法(在目标方法执行前后执行的方法))加入到IOC容器中,分别利用注解@Service、@Component,并在Spring配置文件中配置自动扫描
      2. 利用注解 @Aspect 告诉Spring 这是一个切面类
      3. 告诉 Spring 切面类中的每个目标方法都是何时何地运行,在每个目标方法之上添加对应的通知注解(5个)和写切入点表达式【execution(访问权限符 返回值类型 方法签名)】
        • @Before :在目标方法之前执行(前置通知)
        • @After :在目标方法之后执行(后置通知)
        • @AfterReturning :在目标方法正常返回之后执行(返回通知)
        • @AfterThrowing :在目标方法抛出异常之后运行(异常通知)
        • @Around :环绕(环绕通知) ```java package com.glutnn.utils;

import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays;

@Aspect @Component public class LogUtils { @Before(“execution(public int com.glutnn.impl.MyMathCalculator.*(int,int))”) public static void logStart(JoinPoint joinPoint){ Object[] args = joinPoint.getArgs(); // 获取到目标方法运行时使用到的参数 Signature signature = joinPoint.getSignature();// 获取到目标方法的签名 String name = signature.getName(); // 获取到目标方法名 System.out.println(“【”+name+”】方法开始执行,用的参数列表【”+ Arrays.asList(args) +”】”); }

/**
 *
 * @param joinPoint
 * @param result 这个参数就是用来接收返回值的
 * returning = "result":负责告诉 Spring 利用 result 来接收返回值,下面的异常也是类似
 */
@AfterReturning(value = "execution(public int com.glutnn.impl.MyMathCalculator.*(int,int))",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
    Signature signature = joinPoint.getSignature();
    String name = signature.getName();
    System.out.println("【"+name+"】方法执行结束...执行的结果是:"+result);
}

@AfterThrowing(value = "execution(public int com.glutnn.impl.MyMathCalculator.*(int,int))",throwing = "e")
public static void logException(JoinPoint joinPoint,Exception e) {
    Signature signature = joinPoint.getSignature();
    String name = signature.getName();
    System.out.println("【"+name+"】方法出现异常了,异常信息是:"+e);
}

@After("execution(public int com.glutnn.impl.MyMathCalculator.*(int,int))")
public static void logEnd(JoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();
    String name = signature.getName();
    System.out.println("【"+name+"】方法最终结束了");
}

}


      4. 在 Spring 配置文件中开启基于注解的 AOP 功能
```xml
<aop:aspectj-autoproxy/>
  1. 测试

    public class PersonTest {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("ioc.xml");
    
    @Test
    public void test() throws SQLException {
     // 如果要用类型去容器中找,一定要用它的接口类型
     Calculator myMathCalculator = (Calculator) ioc.getBean("myMathCalculator");
     myMathCalculator.add(1,4);
     System.out.println("=================================");
     myMathCalculator.div(3,0);
    }
    /*
    【add】方法开始执行,用的参数列表【[1, 4]】
    【add】方法最终结束了
    【add】方法执行结束...执行的结果是:5
    =================================
    【div】方法开始执行,用的参数列表【[3, 0]】
    【div】方法最终结束了
    【div】方法出现异常了,异常信息是:java.lang.ArithmeticException: / by zero
    */
    }
    

    2.5 AOP细节

  1. AOP 底层就是动态代理,虽然看起来是将被代理的那个对象加入到了容器中,但是容器中保存的不是那个被代理的对象,而是被代理的那个对象的代理对象,也就是说容器启动会为被代理对象,也就是目标对象创建代理对象。因此如果要用类型去容器中找这个组件的话,就一定不能使用目标对象的类型,只能用它的接口类型,因为目标对象和代理对象唯一的联系就是它们实现了一个相同的接口。
  2. cglib 可以为没有接口的组件创建代理对象,此时容器中就存在目标对象了,按类型找的话就可以用目标对象的类型。
  3. 切入点表达式的写法
    • 固定格式:execution(访问权限符 返回值类型 方法全类名(参数表))
    • 通配符
      • 星号【*】
        • 匹配一个或多个字符
        • 匹配任意一个参数
        • 只能匹配一层路径
        • 不能写在权限位置,不写权限就代表任意权限了
      • 双点【..】
        • 匹配任意多个参数,任意类型参数
        • 匹配任意多层路径
    • 支持”&&”、“||”、“!”
      • &&:必须同时满足两个表达式
      • ||:满足任意一个表达式即可
      • !:只要不是这个表达式的都进行切入
  4. 通知方法的执行顺序
    • 正常执行:前置通知—->后置通知—->返回通知
    • 异常执行:前置通知—->后置通知—->异常通知
  5. 在通知方法运行的时候拿到目标方法的具体信息
    • 只需要在通知方法的参数列表上添加一个参数:JoinPoint joinPoint,其中就封装了当前目标方法的详细信息
  6. 指定哪个参数用来接收返回值、异常
    • 利用 returning、throwing
  7. Spring 对通知方法的约束
    • 唯一的要求就是方法的参数列表一定不可以乱写,因为通知方法是 Spring 通过反射来调用的,每一次调用都得确认参数,对于Spring 不认识的参数就得告诉Spring,例如接收返回值和异常的参数
  8. 抽取可重用的切入点表达式
    • 首先定义一个返回void的空方法 F
    • 在该方法上利用注解 @Pointcut(“抽取的切入点表达式”)
    • 最后在原来写切入点表达式的位置用该方法代替:@Before(“F()”)
  9. 环绕通知

    • 是Spring中最强大的通知
    • 前置通知、返回通知、后置通知、异常通知,四合一就是环绕通知
      ```java @Around(“execution(public int com.glutnn.impl.MyMathCalculator.*(int,int))”) public Object myAround(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); String name = pjp.getSignature().getName();

    Object proceed = null; try {

    System.out.println("【环绕前置通知】【"+name+"方法开始】,用的参数列表是:"+Arrays.asList(args));
    proceed = pjp.proceed(args); // 此处其实就是利用反射调用目标方法,相当于method.invoke(obj,args);
    System.out.println("【环绕返回通知】【"+name+"方法返回】,返回值是:"+proceed+"");
    

    } catch (Exception e) {

    System.out.println("【环绕异常通知】【"+name+"方法出现异常】,异常信息是:"+e);
    throw new RuntimeException(e);
    

    }finally {

    System.out.println("【环绕后置通知】【"+name+"方法最终结束...】");
    

    }

    return proceed;// 返回反射调用后的返回值 }

// 测试 public class PersonTest { ApplicationContext ioc = new ClassPathXmlApplicationContext(“ioc.xml”);

@Test
public void test() throws SQLException {
    // 如果要用类型去容器中找,一定要用它的接口类型
    Calculator myMathCalculator = (Calculator) ioc.getBean("myMathCalculator");
    myMathCalculator.add(1,4);
    System.out.println("=================================");
    myMathCalculator.div(3,0);
}
/*
【环绕前置通知】【add方法开始】,用的参数列表是:[1, 4]
【环绕返回通知】【add方法返回】,返回值是:5
【环绕后置通知】【add方法最终结束...】
=================================
【环绕前置通知】【div方法开始】,用的参数列表是:[3, 0]
【环绕异常通知】【div方法出现异常】,异常信息是:java.lang.ArithmeticException: / by zero
【环绕后置通知】【div方法最终结束...】
*/

}


   -  环绕通知**优先于**普通通知执行,一定是先走完环绕流程,不过对于环绕前置和普通前置会产生一种随机顺序,有时候会是环绕前置在前,有时候又会是普通前置在前,除了这点,其他都是严格按照顺序。因此,如果环绕出现异常就必须抛出去,否则其他普通通知就感知不到这个异常了。 
   -  环绕通知其实就是一个动态代理,如果你希望切入的时候影响目标方法那就写环绕通知,否则就写普通通知 
10.  多切面运行顺序 
   - 先切入的后出去,后切入的先出去
   - Spring 是根据切面类的首字母顺序来指定哪个切面类先切入,还可以利用注解 **@Order(1)** 改变切面顺序,数值越小优先级越高
   - 而且环绕通知只是影响当前切面
<a name="625edbf4"></a>
### 2.6 AOP 使用场景

- 加日志保存到数据库
- 做权限验证,安全检查
- 事务控制
<a name="fbdb8c5a"></a>
### 2.7 基于XML配置的AOP
```xml
 <!--1、将目标类和切面类都加入到容器中-->
<bean id="myMathCalculator" class="com.glutnn.impl.MyMathCalculator"></bean>
<bean id="logUtils" class="com.glutnn.utils.LogUtils"></bean>

<aop:config>
    <!--2、告诉Spring哪个是切面类,相当于@Aspect-->
    <aop:aspect ref="logUtils">
        <!--3、配置切面类中的各个通知方法何时何地运行-->
        <!--配置前置通知和当前切面可以使用的切入点表达式-->
        <aop:pointcut id="mypoint" expression="execution(public int com.glutnn.impl.MyMathCalculator.*(int,int))"/>
        <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>
</aop:config>
  • 重要的用配置,不重要的就用注解

    2.8 声明式事务

  • 事务有声明式事务编程式事务

    • 编程式事务

      TransactionFilter{
      try{
        // 获取连接
        // 设置非自动提交
        chain.doFilter();
        // 提交
      }catch(Exception e){
        // 回滚
      }finally{
        // 关闭连接并释放资源
      }
      }
      
    • 声明式事务

      • 以前需要通过复杂的编程来编写一个事务,但是现在只需要告诉 Spring 哪一个方法是事务方法即可,Spring 会自动进行事务的控制。事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助 Spring AOP 框架实现声明式事务管理。
      • Spring 提供了一个事务切面(事务管理器接口 PlatformTransactionManager),可以在目标方法运行的前后进行事务控制。其下有许多实现类,可以根据你使用的操作数据库的持久化层工具的不同,去选择不同的事务管理器。
  • 事务控制步骤
    ```xml
- 事务细节java / 事务的传播行为(事务的传播+事务的行为) - 如果有多个事务进行嵌套运行,子事务是否要和大事务共用一个事务 REQUIRED:如果有事务在运行,那么当前方法就在这个事务内运行;否则,就启动一个新事务,并在这个自己的事务内运行 REQUIRES_NEW:当前方法必须自己启动一个新事务,并在自己的事务内运行;如果当前有事务,则需要将它挂起 SUPPORTS:如果有事务在运行,那么就运行在该事务内;否则,可以不运行在事务内 NOT_SUPPORTED:当前方法不应该运行在事务中,如果有运行的事务,需要将其挂起 MANDATORY:当前方法必须运行在事务内,如果没有正在运行的事务就抛出异常 NEVER:当前方法不应该运行在事务中,如果有运行的事务就抛异常 NESTED:如果有事务在运行,当前方法就应该在这个事务的嵌套事务内运行,否则就启动一个新事务,并在自己的事务内运行 / Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; // 设置事务的隔离级别 int timeout() default -1; // 事务超出指定执行时长后自动终止并回滚,以秒为单位 / 设置事务为只读,默认为false 可以进行事务优化,加快查询速度,设置为true就无需管事务那一堆操作了 但只能用于查询方法 / boolean readOnly() default false; / 异常分类 - 运行时异常(非检查异常):可以不用处理;默认都回滚 - 编译时异常(检查异常):要么try-catch,要么在方法上声明throws;默认都不回滚 / // 哪些异常事务需要回滚,可以使得默认不回滚的异常回滚 Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; // 哪些异常事务可以不回滚,可以使得默认回滚的异常不回滚 Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {};ClassName - 如果某个业务逻辑存在事务,那么在IOC容器中保存的是它的代理对象 - 如果事务的传播行为是 REQUIRED ,那么它的属性都是继承于大事务的。REQUIRED 是将之前事务使用的 Connection 传递给当前方法使用,REQUIRED_NEW 是直接使用新的 Connection - 本类方法的嵌套调用就只是一个事务 - 基于XML配置的事务xml


---

<a name="9b4f8ff4"></a>
# 二、SpringMvc

-  是 Spring 的 web 模块,用来简化 web 开发的<br />![](https://cdn.nlark.com/yuque/0/2021/png/2396573/1621573103782-737e84e3-8b9f-4f9d-a685-3b7ddcdffb8f.png#clientId=u46d3a0ac-11a3-4&from=paste&height=310&id=u3ae9a341&margin=%5Bobject%20Object%5D&originHeight=620&originWidth=1126&originalType=url&status=done&style=stroke&taskId=u5b84533a-dbee-4fa2-8dfe-03b1bdeff0e&width=563) 
<a name="0d44e577"></a>
## 1、HelloWorld

-  项目结构图示

 ![](https://cdn.nlark.com/yuque/0/2021/png/2396573/1621573130242-16fbf91f-1e60-47a3-bc33-a0b5e50cd704.png#clientId=u46d3a0ac-11a3-4&from=paste&height=403&id=ub68096c9&margin=%5Bobject%20Object%5D&originHeight=805&originWidth=523&originalType=url&status=done&style=stroke&taskId=ue6b5c6e8-56c8-427e-9d3a-c203db393d0&width=261.5)

1.  导包 
   -  SpringMVC 是 Spring 的 web 模块;所有模块的运行都依赖核心模块(IOC模块) 
      -  核心容器模块  
```java
org.springframework:spring-beans:5.2.5.RELEASE
org.springframework:spring-context:5.2.5.RELEASE
org.springframework:spring-core:5.2.5.RELEASE
org.springframework:spring-expression:5.2.5.RELEASE
commons-logging:commons-logging:1.2
org.springframework:spring-aop:5.2.5.RELEASE
  -  web模块  
org.springframework:spring-web:5.2.5.RELEASE
org.springframework:spring-webmvc:5.2.5.RELEASE
  1. 写配置

    • SpringMVC 提供一个前端控制器 DispatcherServlet,负责拦截所有请求,并执行派发;这个前端控制器是一个 Servlet,所以需要在 web.xml 文件中对其进行配置。

      <!--配置前端控制器-->
      <servlet>
      <servlet-name>dispatcherServlet</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
       <!--指定SpringMVC配置文件的位置-->
       <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页面就无法显示了
       -->
      <url-pattern>/</url-pattern>
      </servlet-mapping>
      
    • 配置 SpringMVC 的配置文件 springmvc.xml
      ```xml


3.  测试 
   -  创建一个主页面 index.jsp 和一个成功页面 success.jsp   
```html
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<a href="hello">HelloWorld</a>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>成功页面</title>
</head>
<body>
    成功!
</body>
</html>
  • 创建一个处理器类
    ```java package com.glutnn.controller;

import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping;

/**

  • 1、告诉SpringMVC这是一个处理器,可以处理请求 @Controller */ @Controller public class MyFirstController {

    /**

    • 2、/ 代表从当前项目开始,处理当前项目下的hello请求
    • @return / @RequestMapping(“/hello”) public String myFirstRequest(){ System.out.println(“请求收到了,正在处理中…”); /*
       返回成功页面的地址
       这里视图解析器会自动拼串
       前缀(/WEB-INF/pages/)+返回值(success)+后缀(.jsp)
      
      */ return “success”; } } ```
    • 运行结果
      • 启动Tomcat服务器,点击主页面超链接,发送hello请求,跳转到成功页面,控制台输出 “请求收到了,正在处理中…”

        2、HelloWorld细节

  1. 运行流程
    1. 客户端点击链接,会发送 http://localhost:8888/SpringMVC/hello 请求
    2. 来到 Tomcat 服务器
    3. SpringMVC 的前端控制器收到所有请求
    4. 看请求地址和 @RequestMapping 标注的哪个方法匹配,以此来决定使用哪个类的哪个方法来处理该请求
    5. 前端控制器找到了目标处理器和目标方法,直接利用反射执行目标方法
    6. 方法执行完成后产生一个返回值,SpringMVC 就认为该返回值就是要去的页面地址
    7. 拿到方法返回值之后,利用视图解析器进行拼串,得到完整的页面地址
    8. 得到完整的页面地址后,前端控制器帮我们转发到页面
  2. @RequestMapping注解
    • value属性作用:告诉某个方法处理某个请求,其中开始的斜杠【/】可以省略,但是习惯还是加上。需要注意的是,一个方法只能处理一个请求,两个方法不能处理同一个请求。
    • 可以标注在方法上,也可以标注在类上。标注在类上代表为当前类的所有方法指定一个基准路径
    • 其他属性表示的含义
      • method限定请求方式。例如【method = RequestMethod.POST】,默认是什么请求方式都接收。HTTP协议规定的所有请求方式【GET、HEAD、POST、PUT、PATCH、DELETE、OPTIONS、TRACE】,如果没有按照限定设置请求方式,就会出现【HTTP状态 405 - 方法不允许,Request method ‘GET’ not supported】错误
      • params规定请求参数
        • 支持简单的表达式
          • param1:表示请求必须包含名为param1的请求参数,例如【params = {“username”}】
          • !param1:表示请求不能携带名为param1的请求参数,不按规定会404
          • param1!=value:表示请求必须包含名为param1的请求参数,但其值不能为value,例如【params = {“username!=123”}】,这种情况可以是:不带username或者username不是123
          • {param1=”value1”,”param2”}:请求必须同时包含名为param1和param2的请求参数
      • headers规定请求头。和params一样,可以写简单的表达式。
      • consumes:规定请求头中的Content-Type,只接收指定的内容类型的请求
      • produces:给响应头中加上Content-Type,告诉浏览器返回的内容类型
    • ant 风格的 URL,意思是 URL 可以写模糊的通配符,以下按精确程度排名:【?】>【】>【*
      • 【?】:能代替任意一个字符,例如某个请求【”/antTest0?”】,那么就代表antTest0后面加上任意一个合法字符的请求都可以处理,而且是必须加上一个字符,不能够不加。例如【”/antTest0a”、”/antTest01”、…】。需要注意:总是更加精确的请求优先,例如还有一个方法的请求是精确写法的【”/antTest0a”】,那么就优先使用这个方法来处理该请求。
      • 【*】:能替代任意多个字符,和一层路径。和上面那个差不多,区别是这个也可以不带字符。还可以代替一层路径,例如:【”/a/星/antTest”】,那么【”/a/aaaa/antTest”、”/a/abc/antTest”、…】这些请求都可以处理。
      • 【**】:能替代多层路径
  3. @PathVariable ,获取路径上的占位符

    /*
    意思是说:/user/admin、/user/asbhbv、...这类的请求都可以处理
    并且还可以通过@PathVariable注解获取这个占位符上的值
    但是需要注意:只能占一层路径
    */
    @RequestMapping("/user/{id}")
    public String myFirstRequest(@PathVariable("id")String id){
    System.out.println("路径上的占位符的值:"+id);
    return "success";
    }
    
  4. REST 风格的 URL 地址约束

    • 浅显的理解,就是系统希望以一种简洁的URL地址来发请求,然后对于某个资源的增删改查就以请求方式来区分。
      ```java /* 例如,之前要发送增删改查的请求是这样的: /getBook?id=1:查询图书 /deleteBook?id=1:删除图书 /updateBook?id=1:更新图书 /addBook:增加图书

但是REST风格就推荐按照以下规定对URL地址进行命名:/资源名/资源标识符,风格更加统一 /book/1 —-如果此时发送的是GET请求,那么就认为是对图书进行查询操作 /book/1 —- PUT,更新1号图书 /book/1 —- DELETE,删除1号图书 /book —- POST,添加图书

以简洁的URL地址提交请求,然后以请求方式来区分对资源的操作 */


   -  但是存在一个问题,从页面只能发起GET或者POST请求,那么如何解决PUT和DELETE请求呢? 
      -  Spring 提供了对 REST 风格的支持,SpringMVC 中存在一个**Filter**,可以把普通的请求转换为规定形式的请求,需要在 web.xml 中对其进行配置  
```xml
<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>
  -  创建一个POST类型的表单,表单项中携带一个_method的参数,这个参数的值就是DELETE或者PUT,不区分大小写  
<form action="book/1" method="post">
    <input name="_method" value="delete"/>
    <input type="submit" value="删除一号图书">
</form>
  -  注意:高版本的Tomcat需要在jsp页面加入**isErrorPage="true"**,否则会出问题  
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
  1. 如果不手动指定 SpringMVC 配置文件的位置
    • 那么 SpringMVC 会默认去找这个地址:【/WEB-INF/dispatcherServlet-servlet.xml
    • dispatcherServlet-servlet.xml 这个文件其实就是【 servlet-name标签中的值-servlet.xml
    • 此时如果你不想手动指定该配置文件位置,那么就在这个目录下按照以上规则创建一个配置文件
  2. url-pattern

    • 在 Tomcat 服务器的 web.xml 中有一个 DefaultServlet,它的 url-pattern 配置的是【/】,这个 DefaultServlet 是用来处理静态资源的,除了 jsp 和 Servlet ,剩下的都是静态资源。我们项目中的 web.xml 继承于服务器中的 web.xml,现在如果把项目中web.xml 的 url-pattern 也配置成【/】的话,这个操作就相当于前端控制器把服务器中的 DefaultServlet 给禁用掉了,如此一来,项目中的静态资源就无效了。但是因为我们没有覆盖服务器中的JspServlet,所以可以访问 jsp 文件。而【/*】这个玩意不存在覆盖服务器 web.xml 中的某个 Servlet ,因此直接就是拦截所有请求。所以,这里应该配置【/】,这也是为了迎合 REST 风格的 URL 地址

      3、请求处理

  3. SpringMVC 如何获取请求带来的各种信息

    • @RequestParam :获取请求参数。
      • 默认获取请求参数的方法就是给方法的参数列表设置一个和请求参数名相同的变量即可,这个变量就可以用来接收请求参数的值,如果没有值就默认接收 null;
      • 第二种方法就是使用注解@RequestParam,可以获取【?】后面的值【/book/{id}?user=”123”】
    • @RequestHeader :获取请求头中某个key的值
    • @CookieValue :可以获取某个cookie的值 ```java /* @RequestParam(“user”)String username 相当于 username = request.getParameter(“user”) 如果是使用该注解,那么请求默认是必须要带参数的,如果没带的话就会出现错误 也可以修改该默认设置 value:指定要获取的参数的key required:设置请求中是否必须带该参数,false为非必须,不带默认就是null defaultValue:原来参数默认值是null,这个属性可以设置参数默认的值

@RequestHeader(“User-Agent”)String userAgent 相当于 userAgent = request.getHeader(“User-Agent”); 如果请求头中不存在你要获取的这个参数,那么就会报错,当然也可以修改该默认设置,和@RequestParam相同

以前获取某个cookie的值的方法: Cookie[] cookies = request.getCookies(); for(Cookie cookie : cookies){ if(cookie.getName.equals(“JSESSIONID”)){ String cv = cookie.getValue();
} }

现在可以使用注解@CookieValue 如果没有你要获取的那个参数,就会报错,和上两个注解一样,也可以修改默认设置,属性都是一样的 */ @RequestMapping(“/handle01”) public String myFirstRequest( @RequestParam(value=”user”,required=”false”,defaultValue=”新的值”)String username, @RequestHeader(“User-Agent”)String userAgent, @CookieValue(“JSESSIONID”)String jid){

System.out.println("这个变量的值:"+username);
System.out.println("请求头中浏览器的信息:"+userAgent);
   System.out.println("cookie中jid的值:"+jid);

return "success";

}


2.  **SpringMVC自动封装-传入POJO** 
   -  搞一个form表单,此时请求所带的参数过多,处理该请求的方法就要定义很多的变量来接收,不方便  
```html
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<form action="book" method="post">
    书名:<input type="text" name="bookName"/><br/>
    作者:<input type="text" name="author"/><br/>
    价格:<input type="text" name="price"/><br/>
    <hr/>
    省份:<input type="text" name="address.sheng">
    市级:<input type="text" name="address.shi">
    <input type="submit" value="提交"/>
</form>
</body>
</html>
  • 此时可以定义一个Book类型的JavaBean,Book类中还有一个对象类型Address
    ```java package com.glutnn.bean;

public class Book { private String bookName; private String author; private Double price; private Address address; // 对象类型

/ 此处写getter/setter/toString方法 / }


```java
package com.glutnn.bean;

public class Address {
    private String sheng;
    private String shi;

    /*
   此处写getter/setter/toString方法
   */
}
  • 最后定义一个处理该请求的方法

    @Controller
    public class MyFirstController {
    /*
    如果请求参数是一个POJO(普通JavaBean),那么SpringMVC会自动为其属性赋值
      将POJO中的每个属性,尝试从request参数中获取,并封装
      只要请求参数的参数名和对象中的属性名一一对应即可
    而且还可以级联封装
      意思是说,假设Book类中还有一个对象类型,这个对象类型也有一系列的属性,
      那么SpringMVC也会为属性的属性进行赋值封装,例如这例子中的Address
    */
    @RequestMapping("/book")
    public String addBook(Book book){
      /*
      我要保存的图书:Book{bookName='apache', author='zhangsan', price=70.0, address=Address{sheng='guangdong', shi='shengzhen'}}
      */
      System.out.println("我要保存的图书:"+book);
    
      return "success";
    }
    }
    
  1. 乱码问题解决

    • 提交的数据可能存在乱码问题

      • 请求乱码

        • GET请求
          • 修改Tomcat的server.xml文件,在Connector标签中加上:URIEncoding=”UTF-8”
        • POST请求
          • 第一次获取请求参数之前设置:request.setCharacterEncoding(“UTF-8”)
          • 可以自己写一个Filter,但是SpringMVC已经提供了一个Filter来解决此问题,需要在web.xml中配置
            <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>forceEncoding</param-name>
            <param-value>true</param-value>
            </init-param>
            </filter>
            <filter-mapping>
            <filter-name>characterEncodingFilter</filter-name>
            <url-pattern>/*</url-pattern>
            </filter-mapping>
            
      • 响应乱码

        • response.setContentType(“text/html;charset=utf-8”);
    • 特别注意:字符编码的Filter必须在其他任何Filter之前设置!!!
  2. 可以传入的原生API
    • HttpServletRequest
    • HttpServletResponse
    • HttpSession
    • java.security.Principal
    • Locale:国际化有关的区域信息对象
    • InputStream:ServletInputStream inputStream = request.getInputStream();
    • OutputStream:ServletOutputStream outputStream = response.getOutputStream();
    • Reader:request.getReader();
    • Writer:response.getWriter()
      @Controller
      public class MyFirstController {
      @RequestMapping("/apiTest")
      public String addBook(HttpSession session, 
                          HttpServletRequest request, 
                          HttpServletResponse response){
        request.setAttribute("reqParam","我是请求域中的");
        session.setAttribute("sessionParam","我是session域中的");
        return "success";
      }
      }
      
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<html>
<head>
    <title>成功页面</title>
</head>
<body>
    成功!<br/>
    请求域:${requestScope.reqParam}<br/>
    session:${sessionScope.sessionParam}
</body>
</html>
  • 发送请求,成功页面显示
    /*
    成功!
    请求域:我是请求域中的
    session:我是session域中的
    */
    

    4、数据输出

  • SpringMVC可以使用传入原生API的方法将数据带给页面,还可以使用以下方法将数据传输给页面

    • 在处理请求的方法处传入Map、Model、ModelMap参数,在这些参数中保存的数据会保存在请求域(request)中,可以从页面获取
      ```java / 处理请求的类 / @Controller public class OutputController {

      @RequestMapping(“/handle01”) public String handle01(Map map){ map.put(“msg”,”你好”); //class org.springframework.validation.support.BindingAwareModelMap System.out.println(map.getClass()); return “success”; }

      @RequestMapping(“/handle02”) public String handle02(Model model){ model.addAttribute(“msg”,”你好坏”); //class org.springframework.validation.support.BindingAwareModelMap System.out.println(model.getClass()); return “success”; }

      @RequestMapping(“/handle03”) public String handle03(ModelMap modelMap){ modelMap.addAttribute(“msg”,”你好棒”); //class org.springframework.validation.support.BindingAwareModelMap System.out.println(modelMap.getClass()); return “success”; }

}


```html
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<html>
<head>
    <title>主页面</title>
</head>
<body>
<a href="handle01">handle01</a>
<a href="handle02">handle02</a>
<a href="handle03">handle03</a>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" isErrorPage="true" %>
<html>
<head>
    <title>成功页面</title>
</head>
<body>
pageContext:${pageScope.get("msg")}<br/>
request:${requestScope.get("msg")}<br/>
session:${sessionScope.get("msg")}<br/>
application:${applicationScope.get("msg")}
</body>
</html>
/*
三个处理方法在成功页面分别输出:

pageContext:
request:你好
session:
application:

pageContext:
request:你好坏
session:
application:

pageContext:
request:你好棒
session:
application:

*/
  • Map、Model、ModelMap三者间的关系
    • 最终都是 BindingAwareModelMap 在工作,相当于 BindingAwareModelMap 中保存的数据都会被放在请求域中

图片.png

  • 方法的返回值可以变为 ModelAndView 类型
    • 既包含视图信息(页面地址)也包含模型数据(给页面带的数据),数据也是放在请求域
      @Controller
      public class OutputController {
      @RequestMapping("/handle04")
      public ModelAndView handle04(){
      /**
          "success"就是视图名,视图解析器会将其进行拼串得到完整的页面地址
       */
      ModelAndView mv = new ModelAndView("success");
      mv.addObject("msg","你好吗?");
      return mv;
      }
      }
      
/*
pageContext:
request:你好吗?
session:
application:
*/

5、DispatcherServlet分析

Spring - 图4

  • 结构
  • 请求处理的大致流程

    1. DispatcherServlet收到请求
    2. 根据当前请求地址在HandlerMapping中找到这个请求的映射信息,获取到能处理该请求的目标处理器类(处理器),如果没有找到可以处理该请求的控制器(处理器),那么就404,或者抛异常

      mappedHandler = getHandler(processedRequest);
      
    3. 拿到能执行这个类中所有方法的适配器(反射工具),RequestMappingHandlerAdapter

      // ha 即拿到的适配器 RequestMappingHandlerAdapter
      HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
      
    4. 适配器来执行目标方法,将目标方法执行后的返回值作为视图名保存在ModelAndView中;无论目标方法怎么写,适配器执行完目标方法以后,都会将信息封装成ModelAndView;如果你的目标方法没有设置返回一个指定的视图名,那么SpringMVC会自动设置一个默认的视图名。

      // mv 就是 ModelAndView
      mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
      
    5. 根据方法最终执行完成后封装的ModelAndView;转发到对应页面,而且ModelAndView中的数据可以从请求域中获取

      processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
      
  • 确定方法每个参数的值

    1. 标了注解
      • 保存注解的信息;最终得到这个注解对应解析的值
    2. 没标注解
      1. 看是否是原生API
      2. 看是否是Model或者Map,或者其他类型等等
      3. 如果都不是,则看是否是简单类型;如果是简单类型,则会给paramName赋一个值,否则会给attrName赋一个值。attName(参数标了@ModelAttribute(“”)注解的,那么attrName的值就是该注解指定的值;否则就是一个空串)
        • 确定自定义类型参数的值
          • attrName如果是空串,那么就使用参数类型首字母小写作为值;否则就使用@ModelAttribute注解指定的值
          • 先看隐含模型中是否有以这个attrName作为key对应的值,如果有,那么就从隐含模型中获取并赋值;否则再看是否是@SessionAttributes(value=””)标注的属性,如果是,则从session中拿,如果拿不到就抛异常,因此最好不使用该注解。
          • 如果以上都没有,那么利用反射创建一个对象
      4. 拿到创建的对象,使用数据绑定器(WebDataBinder)将请求中的每个数据绑定到这个对象中。

        6、视图解析

  • forward前缀指定一个转发操作

    /*
    如果你想跳转到项目下任意一个页面的话,可以利用相对路径的写法
    也可以使用forward前缀,一定要加/,否则就是相对路径的转发,容易出问题
    还可以这样写:forward:/hello01,表示转发给hello01请求
    前缀的转发不会由视图解析器拼串
    */
    @RequestMapping("/hello")
    public String hello(){
     return "forward:/hello.jsp";
    }
    
  • redirect前缀指定重定向到页面

    /*
    redirect:/重定向的路径
    redirect:/hello.jsp代表就是从当前项目下开始,SpringMVC会为路径自动拼接上项目名
    但是原生的servlet重定向,路径需要手动加上项目名才能成功response.sendRedirect("")
    类似forward,也可以这样写:redirect:/hello01,表示重定向到某请求
    */
    @RequestMapping("/hello")
    public String hello(){
     return "redirect:/hello.jsp";
    }
    
  • 视图解析流程

    1. 任何方法的返回值都会被包装成ModelAndView对象
    2. 视图渲染流程:将域中的数据在页面展示;页面就是用来渲染模型数据的
    3. 调用render(mv,request,response)渲染页面
    4. 调用ViewResolver接口中的实现方法resolveViewName,根据视图名(方法的返回值)得到View(视图,这也是个接口)对象
    5. 如何根据视图名得到视图对象?
      1. 先是遍历所有的ViewResolver(SpringMVC九大组件之一)视图解析器,如果配置了就用配置的视图解析器,例如InternalResourceViewResolver,如果没配置就用默认的,默认还是用InternalResourceViewResolver。
      2. ViewResolver视图解析器根据方法的返回值得到一个View对象
      3. 其实总的来说就是:所有配置的视图解析器都来尝试根据视图名(返回值)的带View对象,如果能得到就返回,否则就换下一个视图解析器。
    6. 调用View对象的render方法,渲染要给页面输出的所有数据:将模型中的数据全部取出来放到request域中,然后转发或者重定向到页面
    7. 一句话总结:视图解析器只是为了得到视图对象;视图对象才能真正的转发(将模型数据全部放在请求域中)或者重定向页面。也就是视图对象才能真正渲染视图

      8、数据绑定

  • 数据绑定原理和思想

    • SpringMVC封装自定义类型对象的时候,JavaBean需要和页面提交的数据进行一一绑定。我们知道,页面提交的所有数据都是字符串,而JavaBean中存在很多类型的属性,那么这就必然会涉及到以下操作:
      • 数据绑定期间的数据类型转换
      • 数据绑定期间的数据格式化问题,例如提交的日期(Date)类型的转换
      • 数据校验,检查提交的数据是否合法
        • 前端校验(JS+正则表达式),但是前端校验存在一个问题,就是:浏览器有禁用JS的功能,如此一来前端校验就会失效
        • 后端校验,因此还需要后端校验
    • WebDataBinder数据绑定器负责数据绑定工作,也就是数据绑定期间产生的类型转换、格式化、数据校验等问题;
      • ConversionService组件:负责数据类型的转换以及格式化功能;
      • Validator:负责数据校验工作;
      • bindingResult:负责保存以及解析数据绑定期间数据校验产生的错误;
  • 关于【mvc:annotation-driven】标签
    • SpringMVC解析该标签时添加了一堆东西;
    • 需要和【mvc:default-servlet-handler】标签搭配使用
      • 如果这两个标签都没配,那么动态资源(@RequestMapping映射的资源)可以访问,静态资源不能访问;
      • 如果只配了后者,那么静态资源可以访问,而动态资源不能访问;
      • 如果只配了前者,静态资源也是无法访问的;
      • 只有在两个标签都有配置的情况下,动态资源和静态资源才都可以被访问;
  • 日期格式化
    • 若页面提交的数据格式不正确,则会出现400错误