zoukankan      html  css  js  c++  java
  • 【已转移】【Java架构:基础技术】一篇文章搞掂:Spring

    本文篇幅较长,建议合理利用右上角目录进行查看(如果没有目录请刷新)。

    本文是对《SPRING实战第4版》的总结,大家也可以去仔细研读该书

    【------------------------Spring 核心------------------------】

    一、Spring的由来和简介

    1.1、Spring的使命:简化Java开发

    几个概念:

    • POJO:Plain Old Java Object,普通的Java对象。指只有属性、get、set等方法,不包含复杂逻辑的Java类。
    • JavaBean:一种可重用组件,是指符合JavaBean规范的类。
      • JavaBean规范主要有以下几条(仅从网络信息摘取过来,有待商榷)
        • 类必须是具体的和公共的,并且具有无参数的构造器。
        • 提供符合一致性设计模式的公共方法将内部域暴露成员属性,set和get方法获取
        • 属性名称符合这种模式,其他Java 类可以通过自省机制(反射机制)发现和操作这些JavaBean 的属性。
    • EJB:Enterprice JavaBean,是指符合Java企业级应用规范的类
      • 为了快速开发企业级应用,Java提出了EJB,但是太重量级,并不好用;Spring技术则提供了一种方案,仅基于POJO又能实现EJB或其它企业级规范JAVA对象才有的功能,使企业级开发变得更加轻量级,简单;另外Java也看到了Spring技术的优势和好处,也使用Spring技术的原理,改进了原来的EJB,使其更加简单好用;目前为止,Spring技术还是比EJB技术更加好用。
      • 另外,Spring还在不断进步中,移动开发、社交API集成、NoSQL数据库,云计算以及大数据等领域,Spring都一直创新,用更加高效简单的方式来提供这些领域的解决方案
    • Spring中把应用组件(类)也称为Bean或者JavaBean,但是Spring指的是任何POJO,所以文章中所有Bean或JavaBean不一定符合JavaBean规范,可能只是一个POJO

    Spring的几个关键策略

    为了简化Java开发,Spring使用了以下几个关键策略

    • 1、基于POJO的轻量级和最小侵入编程

      • 通常一个类应用Spring技术,不需要引用Spring;即使是最坏的情况,也只需要用到Spring注解,不会有Spring的代码出现在一个类中。从而保证应用中的类仍是POJO
    • 2、基于依赖注入和面向接口实现松耦合

      • DI:Dependency Injection 依赖注入

      在传统编程中,难免会有以下这种编程情景(多个类交互)

    public class DamselRescuingKnight implements Knight {
      private RescueDamselQuest quest;
      public DamselRescuingKnight() {
        this.quest = new RescueDamselQuest();
      }
      public void embarkOnQuest() {
        quest.embark();
      }
    }

      DamselRescuingKnight中,通过new 创建了一个RescueDamselQuest实例,2个类形成了紧耦合。

      其中的依赖关系是DamselRescuingKnight依赖RescueDamselQuest,而且限制了embarkOnQuest方法的实现

      而且,无法对DamselRescuingKnight进行单元测试,因为embarkOnQuest方法需要调用RescueDamselQuest的embark方法,而仅在这个方法内,并没有这个方法的实现

      耦合的两面性:必须的(复杂逻辑必然有多个类进行交互)、过于耦合将导致难以测试、复用、难以理解

      依赖注入:将对象的依赖关系交给第三方来进行创建和管理

    public class BraveKnight implements Knight {
      private Quest quest;
      public BraveKnight(Quest quest) {
        this.quest = quest;
      }
      public void embarkOnQuest() {
        quest.embark();
      }
    }

      上面使用了依赖注入的一种方式:构造器注入;而且传入的是一个Quest接口,对象和依赖对象的具体实现没有耦合关系,就形成了松耦合。

      而且,此情况下,可以使用mock(一种测试方式,具体自行学习)实现单元测试。

      Spring可以作为一个依赖注入容器,通过不同方式注入,也有相应的配置,装配策略,留到后面讲解。

    • 3、基于切面和惯例进行声明式编程

      • AOP:Aspect Oriented Programming,面向切面编程

      DI负责让互相协作的组件实现松耦合,而AOP则允许把遍布程序各处的功能分离出来形成可重用组件。

      可以把切面想象成很多组件上的一个外壳,借助AOP,可以使用各种功能层包裹核心业务层,以声明的方式灵活应用到系统中,核心应用中与这些功能层没有耦合,保证了POJO的简单性

      例如,你的代码中可能会出现以下情景

    public class BraveKnight implements Knight {
        private Quest quest;
        private Minstrel minstrel;
        public BraveKnight(Quest quest, Minstrel minstrel) {
            this.quest = quest;
            this.minstrel = minstrel;
        }
        public void embarkOnQuest() {
            minstrel.singBeforeQuest();
            quest.embark();
            minstrel.singAfterQuest();
        }
    }

      minstrel类可以看作一个日志功能,在某个其它类中,需要调用这个日志类来记录日志,导致这个功能代码与业务代码混淆在一起

      而Spring提供的AOP方案,可以通过配置文件等方式,将这个日志类的相关代码从这个业务类中去除,从而实现解耦;具体方式后面介绍

    • 4、通过切面和模板减少样板式代码

      例如:以往使用JDBC进行操作数据库时,每次操作,都有很多连接数据库,断开连接等代码和业务代码交织在一齐

      而Spring则提供了如jdbcTemplate等类,对这些样板式代码进行简化

    1.2、使用Spring管理Bean

    Spring框架中对象是由Spring创建和管理的,本节讨论Spring如何管理这些Bean

    Spring提供一个或多个Spring容器,Spring容器负责创建、装配、配置、管理对象的整个生命周期

    Spring有多个容器实现,可以分为2种类型:

    • bean工厂:是最简单的容器,提供基本的DI支持(由于bean工厂对于大部分程序来说太低级,所以只讨论应用上下文)
    • 应用上下文:基于BeanFactory构建,提供应用框架级别的服务

    使用应用上下文创建Spring容器:

    Spring自带多种应用上下文:

    • AnnotationConfigApplicationContext:从一个或多个Java配置类加载Spring应用上下文
    • AnnotationConfigWebApplicationContext:从一个或多个Java配置类加载Spring Web应用上下文
    • ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加载上下文定义,把应用上下文的定义文件作为类资源
    • FileSystemXmlapplicationcontext:从文件系统下的一个或多个XML配置文件中加载上下文定义
    • XmlWebApplicationContext:从Web应用下的以恶搞或多个XML配置文件加载上下文定义

    创建Spring容器例子:ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/knight.xml");

    通过context.getBean()方法即可获取容器内的Bean

    bean生命周期:

    普通Java应用程序中,bean的生命周期很简单,就是通过new实例化,就可以使用了,当bean不再被使用,就会被自动回收。

    而Spring中的bean的生命周期就很复杂,而且非常重要,因为有时候对这个生命周期的扩展点进行自定义

    1.3、Spring框架包含的内容

    由前面的内容知道,Spring框架致力于通过DI、AOP和消除模板式代码来简化企业级JAVA开发

    而在整个Spring框架范畴内,Spring不仅提供了多种简化开发的方式,还构建了一个在核心框架上的庞大生态圈,将Spring扩展到不同领域,如Web服务、REST、移动开发,NoSQL等

    Spring框架中的模块:

    如Spring4.0中,Spring框架发布版本中包含20个不同模块,每个模块有3个JAR文件(二进制类库、源码、JavaDoc)

    Spring发布版本中lib目录下的JAR文件

    Spring Portfolio:

    Spring Portfolio包含构建于核心Spring框架之上的框架和类库,概括地说,Spring Portfolio为每一个领域的Java开发都提供了Spring编程模型

    • Spring Web Flow:为基于流程的会话式Web应用提供支持,如购物车或向导系统
    • Spring Web Service:提供契约优先的Web Service模型,服务的实现都是为了满足服务的契约而编写的
    • Spring Security:为Spring应用提供声明式安全机制
    • Spring Integration:提供多种通用应用集成模式的声明式风格实现
    • Spring Batch:对数据进行大量操作
    • Spring Data:无论是关系型数据库还是NoSQL、图形数据库等数据模式,Spring Data都为这些持久化提供了一种简单的编程模型
    • Spring Social:一个社交网络扩展模块
    • Spring Mobile:支持移动Web应用开发
    • Spring for Android:旨在通过Spring框架为开发基于Android设备的本地应用提供某些简单的支持
    • Spring Boot:依赖自动配置技术,消除大部分Spring配置,减小Spring工程构建文件大小

    二、装配Bean

    2.1、装配Bean的可行方式

    Spring框架支持以下3种主要装配Bean的方式:

    • 1、XML中进行显式配置
    • 2、Java中进行显式配置
    • 3、隐式的bean发现机制和自动装配

    最佳实践:建议是尽可能使用自动配置,减少显式配置;当需要显式配置,推荐使用类型安全的JavaConfig;最后再使用XML配置

    2.2、自动化装配bean

    Spring从3个步骤完成自动化装配:

    • 创建组件(@Component):在类上面进行标注,告知Spring这个类需要创建Bean
    • 组件扫描(@ComponentScan):为Spring指定组件扫描范围,告知Spring从哪些地方发现Bean
    • 自动装配(@Autowiring):在属性、set方法、构造函数上指定,告知Spring这个属性或方法的参数,需要Spring来装配对应的Bean

    @Component:

    标明这个类是一个组建类,告知Spring要为这个类创建bean

    也可以用@Named注解(Java依赖规范/Java Dependency Injection 提供)代替,但一般不推荐

    public interface CompactDisc {
      void play();
    }
    @Component
    public class SgtPeppers implements CompactDisc {
      private String title = "Sgt. Pepper's Lonely Hearts Club Band";  
      private String artist = "The Beatles";
      public void play() {
        System.out.println("Playing " + title + " by " + artist);
      }
    }

    可以为bean命名id(默认自动生成id,是类名首字母小写)

    @Component("myName")

    @ComponentScan:

    Spring组件扫描默认是关闭的,可以通过Java设置或者是XML设置来开启

    会搜索配置类所在的包以及子包

    Java配置形式:

    @Configuration
    @ComponentScan
    public class CDPlayerConfig { 
    }

    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:c="http://www.springframework.org/schema/c"
      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
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
      <context:component-scan base-package="soundsystem" />
    
    </beans>

    指定组件扫描的基础包,可以通过类名,或者包中的类或接口:

    @ComponentScan("soundsystem")
    @ComponentScan(basePackages = "soundsystem")
    @ComponentScan(basePackages = { "soundsystem", "video" })
    @ComponentScan(basePackages = { CDPlayer.class, MediaPlayer.class })

    推荐在包中创建标识接口,这样在重构系统的时候,不会对这个配置造成影响

    @Autowired:

    可以用在类中任何方法内(包括构造方法、setter)

    这样,调用这个方法的时候,Spring就会去查找适合方法参数的bean并装配到方法中

    如果找不到,或者有多个符合,则会报错

    可以通过@Autowired(required=false)使找不到时不报错,那么传入的参数值为null,需要自己手动处理代码

    可以使用@Inject(Java依赖规范/Java Dependency Injection 提供)代替,但一般不推荐

    public class CDPlayer implements MediaPlayer {
      private CompactDisc cd;
      @Autowired
      public CDPlayer(CompactDisc cd) {
        this.cd = cd;
      }
      public void play() {
        cd.play();
      }
    }

    2.3、通过Java代码装配bean

    例如,需要将第三方库的组件加载到你的应用中,此时无法给他的类上添加@Component和@Autowired注解,此时不能使用自动化装配了。

    这种情况下,就必须使用显式装配的形式,可以选择Java代码装配或Xml装配

    建议:显式配置是优先使用JavaConfig装配,因为他强大、类型安全且对重构友好;因为他和业务代码无关,应放到单独的包中

    @Configuration:

    告诉Spring,这是一个Spring配置类,用来配置Spring应用上下文如何配置bean

    @Bean:

    创建一个方法,用来产生类的实例,并告诉Spring,这个实例要注册为Spring应用上下文中的bean

    @Configuration
    public class CDPlayerConfig {
        @Bean
        public CompactDisc sgtPeppers() {
            return new SgtPeppers();
        }
        @Bean
        public CDPlayer cdPlayer1() {
            return new CDPlayer(sgtPeppers());
        }
        @Bean
        public CDPlayer cdPlayer2(CompactDisc compactDisc) {
            return new CDPlayer(compactDisc);
        }
    }
    • 首先,使用new SgtPeppers()的实例,为CompactDisc类型创建一个bean
    • cdPlayer1中,通过直接调用SgtPeppers()方法获取实例,看似每次调用cdPlayer1,都会产生一个新的CompactDisc实例,但实际上,因为SgtPeppers()方法添加了@bean备注,Spring会拦截其调用,如果已创建bean则直接返回bean,不会重新创建。
    • cdPlayer2中,给这个方法添加了@Bean注解,Spring会在调用的时候为参数找到对应类型的实例,自动注入
    • 其中cdPlayer2的方式更符合实际代码的运行,建议使用这种方式,方便理解。

    2.4、通过XML装配bean

    Spring刚出现时,用XML描述配置是主要方式;现在有了强大的自动化配置和Java配置,XML配置不再是首选;但是以往的项目仍然存在大量的XML配置,所以有必要掌握这种方式。

    创建XML配置规范:

    Java配置需要@Configuration注解,而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"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context">
    
        <!-- 配置内容 -->
    
    </beans>

    <bean>:

    用来声明一个简单的bean

    spring检测到这个设置,会调用该类的默认构造器来创建实例,并注册为bean

    使用字符串作为类名设置,会出现写错等问题;可以使用有Spring感知的IDE来确保XML配置的正确性,如Spring Tool Suite

    <bean class="soundsystem.BlankDisc" />
    <bean id="compactDisc" class="soundsystem.BlankDisc" />

    如果不设置id,会自动给予id:class+#0,#1,#2...

    为了减小XML文件的繁琐性,建议只对需要按名字引用的bean添加id属性

    通过构造器注入初始化bean:

    2种方式:使用<constructor-arg>元素、使用Spring3.0引入的c-命名空间

    区别:<constructor-arg>元素冗长繁琐;但有些事情<constructor-arg>元素能做到,c-命名空间做不到

    使用<constructor-arg>元素:

    引用bean注入:

    <bean id="compactDisc" class="soundsystem.SgtPeppers" />
          
    <bean id="cdPlayer" class="soundsystem.CDPlayer">
      <constructor-arg ref="compactDisc" />
    </bean>
    • 第一个bean:使用soundsystem.SgtPeppers的默认构造器创建实例作为bean,id为compactDisc
    • 第二个bean:把id为compactDisc的bean,传递到soundsystem.CDPlayer的带参数构造器中,使用这个bean作为参数,创建实例后作为bean,id为cdPlayer

    用字面量注入:

    <bean id="compactDisc" class="soundsystem.BlankDisc">
        <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
        <constructor-arg value="The Beatles" />
    </bean>

    装配集合:

    传入空值:

    <bean id="compactDisc" class="soundsystem.collections.BlankDisc">
        <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
        <constructor-arg value="The Beatles" />
        <constructor-arg><null/></constructor-arg>
    </bean>

    使用List传入字面量:

    <bean id="compactDisc" class="soundsystem.collections.BlankDisc">
        <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
        <constructor-arg value="The Beatles" />
        <constructor-arg>
            <list>
                <value>Sgt. Pepper's Lonely Hearts Club Band</value>
                <value>With a Little Help from My Friends</value>
            </list>
        </constructor-arg>
    </bean>

    使用List传入引用bean:

    <bean id="compactDisc" class="soundsystem.collections.BlankDisc">
        <constructor-arg value="Sgt. Pepper's Lonely Hearts Club Band" />
        <constructor-arg value="The Beatles" />
        <constructor-arg>
            <list>
                <ref bean="cd1"/>
                <ref bean="cd2"/>
            </list>
        </constructor-arg>
    </bean>

    List可以替换成Set,区别是所创建的是List还是Set

    使用c-命名空间:

    声明c-模式:

    首先,使用这个命名控件要在beans标签增加声明此模式:xmlns:c="http://www.springframework.org/schema/c"

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

    引用bean注入:

    <bean id="compactDisc" class="soundsystem.SgtPeppers" />
    
    <bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />

    其中,cd是指对应构造方法的参数名,按照这种写法,如果修改了构造方法,可能就会出错

    替代方案:

    <bean id="cdPlayer" class="soundsystem.CDPlayer" c:_0-ref="compactDisc" />
    
    <bean id="cdPlayer" class="soundsystem.CDPlayer" c:_-ref="compactDisc" />

    _0,_1:使用参数顺序来注入

    _:如果只有一个参数,可以不用标示参数,用下划线代替

    用字面量注入:

    <bean id="compactDisc" class="soundsystem.BlankDisc" c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles" />

    装配集合:

    c-的方式无法实现装配集合

    使用属性注入

    怎么选择构造器注入和属性注入?建议对强依赖使用构造器注入,对可选依赖使用属性注入。

    假设有一个类CDPlayer:

    public class CDPlayer implements MediaPlayer {
        private CompactDisc compactDisc;
        @Autowired
        public void setCompactDisc(CompactDisc compactDisc) {
            this.compactDisc = compactDisc;
        }
        public void play() {
            compactDisc.play();
        }
    }

    使用<property>元素:

    引用bean注入:

    <bean id="cdPlayer" class="soundsystem.properties.CDPlayer">
        <property name="compactDisc" ref="compactDisc" />
    </bean>

    代表通过setCompactDisc方法把id为compactDisc的bean注入到compactDisc属性中

    用字面量注入以及装配集合:

    <bean id="compactDisc" class="soundsystem.properties.BlankDisc">
        <property name="title" value="Sgt. Pepper's Lonely Hearts Club Band" />
        <property name="artist" value="The Beatles" />
        <property name="tracks">
            <list>
                <value>Sgt. Pepper's Lonely Hearts Club Band</value>
                <value>With a Little Help from My Friends</value>
            </list>
        </property>
    </bean>

    使用p-命名空间

    声明p-模式:

    首先,使用这个命名控件要在beans标签增加声明此模式:xmlns:p="http://www.springframework.org/schema/p"

    <?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
          http://www.springframework.org/schema/context">
    
        <!-- 配置内容 -->
    
    </beans>

    然后使用p-命名空间来装配属性

    引用bean注入:

    <bean id="cdPlayer" class="soundsystem.properties.CDPlayer" p:compactDisc-ref="compactDisc" />

    用字面量注入以及装配集合:

    <bean id="compactDisc" class="soundsystem.properties.BlankDisc" p:title="Sgt. Pepper's Lonely Hearts Club Band" p:artist="The Beatles">
        <property name="tracks">
            <list>
                <value>Sgt. Pepper's Lonely Hearts Club Band</value>
                <value>With a Little Help from My Friends</value>
            </list>
        </property>
    </bean>

    使用util-命名空间进行简化:

    声明util-模式:

    首先,使用这个命名控件要在beans标签增加声明此模式:xmlns:util="http://www.springframework.org/schema/util"和2个http

    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns: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.xsd
        http://www.springframework.org/schema/context">
    
        <!-- 配置内容 -->
    
    </beans>

    使用util:list简化:

    <bean id="compactDisc" class="soundsystem.properties.BlankDisc"
        p:title="Sgt. Pepper's Lonely Hearts Club Band" 
        p:artist="The Beatles"
        p:tracks-ref="trackList" />
    
    <util:list id="trackList">
        <value>Sgt. Pepper's Lonely Hearts Club Band</value>
        <value>With a Little Help from My Friends</value>
    </util:list>
    
    <bean id="cdPlayer" class="soundsystem.properties.CDPlayer"
        p:compactDisc-ref="compactDisc" />

    util-命名控件的全部元素:

    2.5、导入和混合配置

    1、拆分JavaConfig:

    其中一个JavaConfig:

    @Configuration
    public class CDConfig {
        @Bean
        public CompactDisc compactDisc() {
            return new SgtPeppers();
        }
    }

    使用@Import注解引入另外一个JavaConfig:

    @Configuration
    @Import(CDConfig.class)
    public class CDPlayerConfig {
        @Bean
        public CDPlayer cdPlayer(CompactDisc compactDisc) {
            return new CDPlayer(compactDisc);
        }
    }

    或使用一个更高级别的JavaConfig引用2个JavaConfig

    @Configuration
    @Import({ CDPlayerConfig.class, CDConfig.class })
    public class SoundSystemConfig {
    }

    2、JavaConfig中引用XML配置

    1中把CDPlayer和CompactDisc分开了,假设出于某些原因,需要把CompactDisc用XML来配置

    <bean id="compactDisc" class="soundsystem.BlankDisc"
        c:_0="Sgt. Pepper's Lonely Hearts Club Band" c:_1="The Beatles">
        <constructor-arg>
            <list>
                <value>Sgt. Pepper's Lonely Hearts Club Band</value>
                <value>With a Little Help from My Friends</value>
                <value>Lucy in the Sky with Diamonds</value>
                <value>Getting Better</value>
                <value>Fixing a Hole</value>
                <!-- ...other tracks omitted for brevity... -->
            </list>
        </constructor-arg>
    </bean>

    JavaConfig引用XML配置

    @Configuration
    @Import(CDPlayerConfig.class)
    @ImportResource("classpath:cd-config.xml")
    public class SoundSystemConfig {
    }

    这样,CDPlayer和BlankDisc都会作为bean被加载到Spring容器中;而CDPlayer添加了@Bean注解,所需参数CompactDisc也会把BlanDisc加载进来

    3、拆分XML配置

    <import resource="cd-config.xml" />
    <bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />

    4、XML配置中引用JavaConfig

    <bean class="soundsystem.CDConfig" />
    <bean id="cdPlayer" class="soundsystem.CDPlayer" c:cd-ref="compactDisc" />

    推荐无论使用JavaConfig还是XML配置,都加入一个更高层次的配置文件,负责组合这些配置文件

    <bean class="soundsystem.CDConfig" />
    <import resource="cdplayer-config.xml" />

    三、高级装配

    3.1、环境与profile

    程序运行有多个环境,比如开发环境、测试环境、生产环境等。

    在每个环境中,很多东西有不同的做法,如使用的数据库等。

    为了避免在切换运行环境是需要对程序进行大量修改,Spring提供了profile设置,使程序能对不同环境做出对应的处理。

    使用profile,可以使一个部署单元(如war包)可以用于不同的环境,内部的bean会根据环境所需而创建。

    配置profile bean:

    Java配置profile bean:

    使用@Profile注解指定bean属于哪个profile

    针对类,表明这个类下所有的bean都在对应profile激活时才创建:

    @Configuration
    @Profile("dev")
    public class DataSourceConfig {
      @Bean(destroyMethod = "shutdown")
      public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder().build();
      }
    }

    针对方法,表明只有注解的方法才会根据profile来创建bean

    @Configuration
    public class DataSourceConfig {
      @Bean(destroyMethod = "shutdown")
      @Profile("dev")
      public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder().build();
      }
      @Bean
      @Profile("prod")
      public DataSource jndiDataSource() {
        JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();return (DataSource) jndiObjectFactoryBean.getObject();
      }
    }

     XML配置profile:

    设置整个XML文件属于某个profile, 每一个环境创建一个XML文件,把这些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:jdbc="http://www.springframework.org/schema/jdbc"
      xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
      xsi:schemaLocation="
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd
        http://www.springframework.org/schema/jdbc
        http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd"
        profile="dev">
        
        <jdbc:embedded-database id="dataSource" type="H2">
          <jdbc:script location="classpath:schema.sql" />
          <jdbc:script location="classpath:test-data.sql" />
        </jdbc:embedded-database>
        
    </beans>

     也可以通过嵌套<beans>元素,在同一个XML文件下创建不同profile 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:jdbc="http://www.springframework.org/schema/jdbc"
      xmlns:jee="http://www.springframework.org/schema/jee" xmlns:p="http://www.springframework.org/schema/p"
      xsi:schemaLocation="
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd
        http://www.springframework.org/schema/jdbc
        http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
      <beans profile="dev">
        <jdbc:embedded-database id="dataSource" type="H2">
          <jdbc:script location="classpath:schema.sql" />
          <jdbc:script location="classpath:test-data.sql" />
        </jdbc:embedded-database>
      </beans>
      
      <beans profile="prod">
        <jee:jndi-lookup id="dataSource"
          lazy-init="true"
          jndi-name="jdbc/myDatabase"
          resource-ref="true"
          proxy-interface="javax.sql.DataSource" />
      </beans>
    </beans>

    激活profile:

    bean根据profile的创建步骤:

    • 1、没有指定profile的bean任何时候都会创建
    • 2、根据spring.profiles.active属性,找到当前激活的profile
    • 3、如果active属性没有设置,则找spring.profiles.default属性,找到默认的profile
    • 4、如果2个值都没有设置,则不会创建任何定义在profile里面的bean

    spring.profiles.active,spring.profiles.default2个参数的设置方式:

    • 1、作为DispatcherServlet的初始化参数;
    • 2、作为Web应用的上下文参数;
    • 3、作为JNDI条目;
    • 4、作为环境变量;
    • 5、作为JVM的系统属性;
    • 6、在集成测试类上,使用@ActiveProfiles注解设置

    具体写法,后面的例子会讲到

    使用profile进行测试:

    使用@ActiveProfiles注解:

      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration(classes=DataSourceConfig.class)
      @ActiveProfiles("prod")
      public static class ProductionDataSourceTest {
        @Autowired
        private DataSource dataSource;
        
        @Test
        public void shouldBeEmbeddedDatasource() {
          // should be null, because there isn't a datasource configured in JNDI
          assertNull(dataSource);
        }
      }

    3.2、条件化Bean

    使用@Conditional注解:

    @Conditional注解中给定一个类,这个类实现Condition接口,其中实现的matches方法的值代表改bean是否生成。

    @Configuration
    public class MagicConfig {
    
      @Bean
      @Conditional(MagicExistsCondition.class)
      public MagicBean magicBean() {
        return new MagicBean();
      }
      
    }
    public class MagicExistsCondition implements Condition {
    
      @Override
      public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("magic");
      }
      
    }

    ConditionContext接口:

    AnnotatedTypeMetadata接口:

    3.3、处理自动装配的歧义性

    出现歧义的情景:

    使用自动装配时,如果有多个bean能匹配上,会产生错误

    例如:

    //某方法
    @Autowired
    public void setDessert(Dessert dessert){
        this.dessert = dessert;
    }
    
    //有3个类实现了该接口
    @Component
    public class Cake implements Dessert{...}
    @Component
    public class Cookies implements Dessert{...}
    @Component
    public class IceCream implements Dessert{...}

    Spring无法从3者中做出选择,抛出NoUniqueBeanDefinitionException异常。

    使用@Primary注解标注首选bean:

    //使用@Component配置bean时,可以使用@Primary注解
    @Component
    @Primary
    public class Cake implements Dessert{...}
    
    //使用@Bean配置bean时,可以使用@Primary注解
    @Bean
    @Primary
    public Dessert iceCream{
        return new IceCream();
    }
    <!-- 使用XML配置时,设置primary属性 -->
    <bean id="iceCream" class="com.desserteater.IceCream" primary="true" />

     使用@Primary时,也会出现多个匹配的bean都标注了primary属性,同样会让Spring出现无法选择的情况,导致错误

     使用@Qualifier注解限定装配的bean:

    1、使用默认限定符:

    @Autowired
    @Qualifier("iceCream")
    public void setDessert(Dessert dessert){
        this.dessert = dessert;
    }

    @Qualifier注解设置的参数是要注入的bean的ID,如果没有为bean设定ID,则为首字母小写的类名(也称这个bean的默认限定符)

    2、为bean设置自定义限定符:

    @Component
    @Qualifier("soft")
    public class Cake implements Dessert{...}
    
    @Bean
    @Qualifier("cold")
    public Dessert iceCream{
        return new IceCream();
    }

    在bean上使用@Qualifier注解,表示为bean设置自定义的限定符。那么自动装载时,就可以使用自定义的限定符进行限定

    @Autowired
    @Qualifier("cold")
    public void setDessert(Dessert dessert){
        this.dessert = dessert;
    }

    3、使用自定义的限定符注解:

    假设已经有一个bean使用了限定符cold,结果另外一个bean也需要使用限定符cold,这样也出现了多个匹配的bean也会报错。

    解决这个问题的思路是,再为这2个bean增加限定符,继续细化;但是@Qualifier注解并不支持重复注解,不能在一个bean上使用多个@Qualifier注解。

    为了解决这个问题,可以使用自定义的限定符注解:

    //代替@Qualifier("cold")
    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Cold{}
    
    //代替@Qualifier("creamy")
    @Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface Creamy{}

     这样,以下代码,自动装配式使用了2个自定义限定符注解,也可以找到唯一匹配的bean。

    @Bean
    @Cold
    @Creamy
    public Dessert iceCream{
        return new IceCream();
    }
    
    @Bean
    @Cold
    public Dessert ice{
        return new Ice();
    }
    
    @Autowired
    @Cold
    @Creamy
    public void setDessert(Dessert dessert){
        this.dessert = dessert;
    }

    3.4、bean的作用域

    默认情况下,bean是单例的形式创建的,既整个应用程序使用的bean是同一个实例。

    有些情况,如果想重用这个bean,结果这个bean被之前的操作污染了,会使程序发生错误。

    Spring支持为bean设置作用域,提供了以下几种作用域:

    • 单例(Singleton):整个应用中,只创建bean的一个实例。
    • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。
    • 会话(Session):Web应用中,为每个会话创建一个bean实例。
    • 请求(Request):Web应用中,为每个请求创建一个bean实例。
    //可以使用组件扫描时,声明作用域;作用域可以使用ConfigurableBeanFactory表示
    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class Cake implements Dessert{...}
    
    //可以在Java配置Bean时,声明作用域;作用域也可以使用字符串表示,不过建议使用ConfigurableBeanFactory更不容易出错
    @Bean
    @Scope("prototype")
    public Dessert iceCream{
        return new IceCream();
    }
    <!-- 使用XML配置时,设置bean作用域 -->
    <bean id="iceCream" class="com.desserteater.IceCream" scope="prototype" />

    使用会话和请求作用域

     某些情景下,某个bean(例如Web应用中的购物车bean),不应该是对于程序单例的,而是对于每一个用户有对应一个bean。

    这种情况下,适合使用会话作用域的bean

    @Component
    @Scope(
        value=WebApplicationContext.SCOPE_SESSION,
        proxyMode=ScopedProxyMode.INTERFACES)
    public ShopingCart cart(){...}
    • WebApplicationContext:SCOPE_SESSION属性表明Spring为每一个Web会话创建一个实例
    • ScopedProxyMode:解决会话作用域的bean注入到单例bean的问题。当会话作用域的bean注入到单例bean的时候,Spring改为注入一个bean代理,当单例bean需要调用会话作用域的bean时,该代理才针对当前会话,找到对应的bean,进行调用。
      • ScopedProxyMode.INTERFACES:当该bean是一个接口的时候,设置值为INTERFACES,表示该代理要实现该接口,并讲调用委托给实现的bean。
      • ScopedProxyMode.TARGET_CLASS:当该bean是一个类的时候,设置值为TARGET_CLASS,因为SPring无法创建基于接口的代理,需要使用CGLib来生成基于类的代理,通过生成目标类扩展的方式来进行代理。

    同样,请求作用域也是有一样的问题,同样处理即可。

    在XML中声明作用域代理

    引用aop命名空间

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

    使用aop命名控件声明作用域代理:

    <aop:scoped-proxy />是和@Scope注解的proxyMode属性相同的XML元素

    它告诉Spring为bean创建一个作用域代理,默认状态下,会使用CGLib创建目标类代理。

    <bean id="cart"
        class="com.myapp.ShoppingCart"
        scope="session">
        <aop:scoped-proxy />
    </bean>

    设置为生成基于接口的代理

    <bean id="cart"
        class="com.myapp.ShoppingCart"
        scope="session">
        <aop:scoped-proxy proxy-target-class="false" />
    </bean>

    3.5、运行时值注入

    之前讨论的依赖注入,主要关注将一个bean引用注入到另一个bean的属性或构造器参数中。

    依赖注入还有另外一方面:将值注入到bean的属性或构造器参数中。

    当然可以使用之前说过的硬编码,但是我们这里希望这些值在运行时确定。

    Spring提供2种运行时求值方式:属性占位符(Property placeholder)、Spring表达式语言(SpEL)

    1、注入外部的值

    使用@PropertySource注解和Environment:

    @Configuration
    @PropertySource("classpath:/com/soundsystem/app.properties")
    public class EnvironmentConfig {
    
      @Autowired
      Environment env;
      
      @Bean
      public BlankDisc blankDisc() {
        return new BlankDisc(
            env.getProperty("disc.title"),
            env.getProperty("disc.artist"));
      }
      
    }

    通过@PropertySource注解引用一个名为app.properties的文件,文件内容如下

    disc.title=Sgt. Peppers Lonely Hearts Club Band
    disc.artist=The Beatles

    然后通过Spring的Environment,使用getProperty()方法,进行检索属性。

    深入Spring的Environment:

    getProperty()方法的重载

    String getProperty(String key)
    String getProperty(String key, String defaultValue)
    T getProperty(String key, class<T> type)
    T getProperty(String key, class<T> type,T defaultValue)
    • getProperty()方法如果没有找到对应的属性,会返回null值
    • getRequiredProperty()方法,如果没有找到属性,会抛出IllegalStateException异常
    • containsProperty()方法:检查某个属性是否存在
    • getPropertyAsClass()方法:将属性解析为类

    检查profile的激活状态

    • String[] getActiveProfiles():返回激活profile名称的数组;
    • String[] getDefaultProfiles():返回默认profile名称的数组;
    • boolean acceptsProfiles(String... profiles):如果environment支持给定profile的话,返回true

    解析属性占位符“${...}”以及注解@Value:

    使用Environment检索属性很方便,Spring同时也提供了占位符装配属性的方法

    a、配置PropertyPlaceholderConfigurer bean或PropertySourcesPlaceholderConfigurer bean(Spring3.1开始推荐用这个) ,这个bean可以基于Spring Environment以及其属性源来解析占位符

    Java配置

    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer(){
        return new PropertySourcesPlaceholderConfigurer();
    }

    XML配置,引用Spring context命名空间中的<context:property-placeholder />即可自动生成PropertySourcesPlaceholderConfigurer 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: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-beans.xsd">
    
        <context:property-placeholder />
    
    </beans>

    b、注入

    Java注入

    public BlandKisc(
        @Value("${disc.title}") String title,
        @Value("${disc.artist}") String artist){
        this.title = title;
        this.artist = artist;
    }

    XML注入

    <bean id="sgtPeppers"
          class="soundsystem.BlandDisc"
          c:_title="${disc.title}"
          c:_artist="${disc.artist}" />

    2、使用Spring表达式语言进行装配

    Spring 3引入了Spring表达式语言(Spring Expression Language,SpEL),能够强大和简洁地把值装配到bean属性和构造器参数中。

    SpEL的一些特性:

    • 使用bean的ID引用bean;
    • 调用方法和访问对象的属性;
    • 对值进行算术、关系和逻辑运算;
    • 正则表达式匹配;
    • 集合操作;

    2.1、表示字面值:

    #{1}//整型
    #{3.14159}//浮点数
    #{9.87E4}//科学计数法
    #{'Hello'}//String类型字面值
    #{false}//Boolean类型

    2.2、引用bean、属性和方法:

    #{sgtPeppers}//引用ID为sgtPeppers的bean
    #{sgtPeppers.artist}//引用bean的属性
    #{sgtPeppers.selectArtist()}//调用bean的方法
    #{sgtPeppers.selectArtist().toUpperCase()}//调用bean的方法的值的方法,例如方法值是String类型,则可以调用String对象的toUpperCase方法
    #{sgtPeppers.selectArtist()?.toUpperCase()}//类型安全的运算符“?.”,当值为null时返回null,否则猜执行toUpperCase方法

    2.3、表达式中使用类型:

    主要作用是调用类的静态方法和变量。使用T()运算符,获得Class对象。

    #{T(java.lang.Math)}//得到一个class对象
    #{T(java.lang.Math).PI}//类的常量
    #{T(java.lang.Math).random()}//类的静态方法

    2.4、SpEL运算符:

    用于在SpEL表达式上做运算

    #{2 * T(java.lang.Math).PI * circle.radius}//计算周长
    #{T(java.lang.Math).PI * circle.radius ^ 2}//计算面积
    #{disc.title + ' by ' + disc.artist}//拼接字符串
    #{counter.total == 100}//比较运算符 结果为布尔值
    #{counter.total eq 100}//比较运算符 结果为布尔值
    #{scoreboard.score ? 1000 ? "Winner!" : "Loser"}//三元运算符
    #{disc.title ?: 'Rattle and Hum'}//Elvis运算符(Elvis是猫王的名字,?:符号像猫王的头发),判断是否为null,是null则给默认值

    2.5、计算正则表达式:

    使用matches运算符,返回Boolean类型值

    #{admin.email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.com'}//验证有效邮件

     2.6、计算集合:

    #{jukebox.songs[4].title}//songs集合第5个元素的title属性
    #{'this is a test'[3]}//String中的一个字符
    #{jukebox.songs.?[artist eq 'Jay']}//使用查询运算符(.?[])过滤子集
    #{jukebox.songs.^[artist eq 'Jay']}//使用第一个匹配符(.^[])找到第一个匹配的项
    #{jukebox.songs.$[artist eq 'Jay']}//使用最后一个匹配符(.$[])找到最后一个匹配的项
    #{jukebox.songs.![title]}//投影运算符(.![])选择属性投影到一个新的集合

    2.7、这里介绍的SpEL只是冰山一角,有需要的去查阅相关资料

    四、面向切面的Spring

    4.1、什么是面向切面编程

    什么是AOP:

    为什么需要面向切面编程(AOP)技术:

    • 在软件开发中,有一些需求需要散步在应用中的多处,称为横切关注点。
    • 例如希望每一次操作,都记录下日志;当然我们可以在每一次操作都加上记录日志的代码,但是这样变得十分复杂和繁琐。
    • 面向切面编程(AOP)的目的就是把这些横切关注点和业务逻辑相分离。
    • 依赖注入(DI)实现了应用对象之间的解耦;而面向切面编程(AOP)实现了横切关注点和它们影响的对象之间的解耦。

    什么是面向切面编程(AOP)技术:

    如上所述,切面可以帮助我们模块化横切关注点。

    一般如果我们需要重用通用功能的话,常见的面向对象技术是继承或委托。

    但是如果整个应用中都使用同样的基类,继承往往会导致一个脆弱的对象体系;

    而使用委托可能需要对委托对象进行复杂的调用。

    而切面提供了另一种可选方案:在一个独立的地方定义通用功能,通过声明的方式定义此功能用何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化特殊的类,这些类称为切面。

    这样做有2个好处:

    • a、每个关注点集中在一个地方,而不是分散到多出代码中;
    • b、服务模块更简洁,因为它们只关注核心代码,次要代码被转移到切面中。

    AOP术语:

    通知(Advice):

    通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。

    Spring切面有5种类型的通知:

    • a、前置通知(Before):在目标方法被调用之前调用通知功能;
    • b、后置通知(After):在目标方法完成之后调用通知,此事不会关心方法的输出是什么;
    • c、返回通知(After-returning):在目标方法成功执行之后调用通知;
    • d、异常通知(After-throwing):在目标方法抛出异常后调用通知;
    • e、环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

    连接点(Join point):

    连接点是我们的程序可以应用通知的实际,是应用执行过程中能够插入切面的一个点。

    切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

    切点(Poincut):

    切点指切面所通知的连接点的范围。

    一个切面不需要通知整个程序的连接点,通过切点,定义了切面所应用的范围。

    通知定义了切面是“什么”以及“何时”执行,切点定义了“何处”需要执行切面。

    切面(Aspect):

     切面是通知和切点的结合,定义了需要做什么,在何时,何处完成该功能。

    引入(Introduction):

    引入允许我们想现有的类添加新的方法或属性。

    可以在不修改现有类的情况下,将新的方法和实例变量引入到该类中。

    织入(Weaving):

    织入是把切面应用到目标对象并创建新的代理对象的过程。

    切面在指定的连接点被织入到目标对象中。

    在目标对象的声明周期有几个点可以进行织入:

    • a、编译期:切面在目标类编译时被织入。这种方式需要特殊的编译期。AspectJ的织入编译器就是用这种方式织入切面。
    • b、类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。
    •       AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
    • c、运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会以目标对象动态创建一个代理对象。Spring AOP就是以这种方式织入切面的。

    Spring对AOP的支持:

    • 不是所有的AOP框架都相同,比如在连接点模型上可能有强弱之分,有些允许在字段修饰符级别应用通知,而另一些只支持方法调用相关的连接点;织入切面的方式和时机也不同。
    • 但是,AOP框架的基本功能,就是创建切点来定义切面所织入的连接点。
    • 本书关注Spring AOP,同时Spring和AspectJ项目之间有大量协作,Spring对AOP支持有很多方面借鉴了AspectJ项目

    Spring支持4种类型的AOP支持:

    • a、基于代理的经典Spring AOP
      • 经典Spring AOP过于笨重和复杂,不作介绍。
    • b、纯POJO切面
      • 借助Spring的aop命名空间,可以将纯POJO转换为切面。
      • 实际上这些POJO知识提供了满足切点条件时所调用的方法。
      • 但这种技术需要XML配置。
    • c、@AspectJ注解驱动的切面
      • 提供以注解驱动的AOP。本质上还是Spring基于代理的AOP,但是编程模型修改为和AspectJ注解一致。
      • 这种方式好处在于不用使用XML。
    • d、注入式AspectJ切面(适用于Spring各版本)
      • 如果AOP需求超过了简单的方法调用(如构造器或属性拦截),则需要使用AspectJ来实现切面。
    • 前3种都是Spring AOP实现的变体,基于动态代理,所以Spring对AOP的支持局限于方法拦截

    Spring通知是Java编写的:

    Spring所创建的通知是标准的Java类编写的,我们可以使用Java开发IDE来开发切面。

    而且定义通知所应用的切点通常会用注解或Spring XML编写,Java开发者都非常熟悉。

    AspectJ与之相反,AspectJ最初是以Java语言扩展的方式实现的。

    优点是通过特有的AOP语言,我们可以获得更强大和细粒度的控制,以及更丰富的AOP工具集。

    缺点是需要额外学习新的工具和语法。

    Spring在运行时通知对象:

    通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。

    代理类封装目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。

    代理拦截到方法调用时,会在调用目标bean方法之前执行切面逻辑。

    知道应用需要被代理的bean时,Spring才会创建代理对象。如果使用的是ApplicationContext的话,在ApplicationContext从BeanFactory中加载所有bean的时候,Spring才会创建被代理的对象。

    因为Spring运行时才创建代理对象,所以我们不需要特殊的编译器来织入Spring AOP的切面。

    Spring只支持方法级别的连接点:

    因为Spring是基于动态代理,所以只支持方法连接点;但也足以满足绝大部分需求。

    如果需要拦截字段和构造器,可以使用AspectJ和JBoss

    4.2、通过切点来选择连接点

    Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。

    Spring仅支持AspectJ切点指示器的一个子集。

    Spring是基于代理的,而某些切点表达式是与基于代理的AOP无关的。

    下面是Spring AOP所支持的AspectJ切点指示器:

    其中,只有execution指示器是实际执行匹配的,其它指示器都是用来限制匹配的。

    Spring中尝试使用AspectJ其它指示器时,会抛出IllegalArgument-Exception异常

    编写切点:

    假设有一个接口

    public interface Performance{
        public void perform();
    }

     使用切点表达式设置当perform()方法执行时触发通知的调用:

    使用within()指示器限制仅匹配concert包

    支持的关系操作符有且(&&),或(||),非(!)

    如果用XML配置,因为&在XML中有特殊含义,所以可以使用and,or,not来作为关系操作符

    切点中选择bean:

    Spring引入了一个新的bean()指示器,通过bean的ID来限制bean

     限制id为woodstock的bean才应用通知

     
     限制id非woodstock的bean才应用通知

    4.3、使用注解创建切面

    AspectJ5之前,编写AspectJ切面需要学习一种Java语言的扩展。

    AspectJ5引入了使用注解来创建切面的关键特性,AspectJ面向注解的模型可以非常简便地通过注解把任意类转变为切面。

    1、定义切面:

    @Aspect
    public class Audience{
        //表演前
        @Before("execution(** concert.Performance.perform(..))")
        public void silenceCellPhones(){
            System.out.println("Silencing cell phones");
        }
        //表演前
        @Before("execution(** concert.Performance.perform(..))")
        public void takeSeats(){
            System.out.println("Taking seats");
        }
        //表演后
        @AfterReturning("execution(** concert.Performance.perform(..))")
        public void applause(){
            System.out.println("Clap!!");
        }
        //表演失败后
        @AfterThrowing("execution(** concert.Performance.perform(..))")
        public void demandRefund(){
            System.out.println("Refund!!");
        }
    
    }

    @Aspect注解表示Audience不仅是一个POJO,还是一个切面。

    @Before,@AfterReturning等注解,用来声明通知方法。

    使用@Pointcut注解定义可重用切点:

    @Aspect
    public class Audience{
        //可重用切点
        @Pointcut("excution(** concert.performance.perform(..))")
        public void performance(){}
        //表演前
        @Before("performance()")
        public void silenceCellPhones(){
            System.out.println("Silencing cell phones");
        }
        //表演前
        @Before("performance()")
        public void takeSeats(){
            System.out.println("Taking seats");
    }

    使用一个空的方法,作为标记,使用@Pointcut注解定义成一个可重用的节点,然后通过“方法名()”来进行引用

    启用自动代理:

    以上仅仅是定义了切面,要启动切面功能,需要启动切面的代理

    Java配置方法:

    @Configuration
    @EnableAspectJAutoProxy//启动AspectJ自动代理
    @ComponentScan
    public class ConcertConfig{
        @Bean
        Public Audience audience(){//声明Audience bean
            return new Audience();
        }
    }

     XML配置方法(使用aop命名空间)

    <?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:aop="http://www.springframework.org/schema/aop"
        xsi:schemaLocation="
          http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop.xsd
          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-beans.xsd">
    
        <context:conponent-scan base-package="concert" />
    
        <!-- 启用AspectJ自动代理 -->
        <aop:aspectj-autoproxy />
        
        <!-- 声明Audience bean -->
        <bean class="concert.Audience" />
        
    </beans>

    代理的作用:

    使用上述2种方法,会给Concert bean创建一个代理,Audence类中的通知方法会在perform()调用前后执行。

    注意!Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的知道,切面本质上还是Spring基于代理的切面,仅局限于代理方法的调用。

    如果想要使用AspectJ的所有能力,必须运行时使用AspectJ并且不依赖Spring。

    2、创建环绕通知

    @Aspect
    public class Audience{
        //可重用切点
        @Pointcut("excution(** concert.performance.perform(..))")
        public void performance(){}
    
        //环绕通知方法
        @Around("performance()")
        public void watchPerformance(ProceedingJoinPoint jp){
            try{
                System.out.println("Silencing cell phones");
                System.out.println("Taking seats");
                jp.proceed();
                System.out.println("CLAP!!");
            }catch(Throwable e){
                System.out.println("refund!!");
            }
        }
    }

     可以将前置和后置通知写在一个方法中,并使用ProceedingJoinPoint对象来进行对目标方法的调用。

    3、处理通知中的参数

    @Aspect
    public class TrackCounter{
        private Map<Interger,Integer> trackCounts = new HashMap<Integer,Integer>();
    
        @Pointcut(
            "execution(* soundsystem.CompactDisc.playTrack(int))" +
            "&& args(trackNumber)")
        public  void trackPlayed(int trackNumber){}
    
        @Before("trackPlayed(trackNumber)")
        public void countTrrack(int trackNumber){
            int currentCount = getPlayCount(trackNumber);
            trackCounts.put(trackNumber,currentCount + 1);
        }
    
        public int getPlayCount(int trackNumber){
            return trackCounts.containsKey(trackNumber)
                    ? trackCounts.get(trackNumber) : 0;
        }
    }

    4、通过注解引入新功能

    假设情景:有一个类,希望让其以及其实例实现某个接口,但这个类是不可以修改的(如没有源码)。我们可以通过AOP为这个类引入新的方法,实现该接口。

    例如

    我们有一个类concert.Performance

    希望能通过AOP实现接口

    public interface Encoreable{
        void performEncore();
    }

    切面

    @Aspect
    public class EncoreableIntroducer{
        @DeclareParents(value="concert.performance+",defaultImpl=DefaultEncoreable.class)
        public static Encoreable encoreable;
    }

    通过声明一个切面,使用@DeclareParents注解,讲Encoreable接口引入到Performance bean中。

    @DeclareParents的组成部分

    1、Value属性指定哪种类型bean要引入该接口。本例中,指所有实现Performance的类型。(加号表示是Performance的所有子类型,而不是Performance本身。)

    2、defaultImpl属性指定为引入功能提供实现的类。在这里我们指定的是DefaultEncoreable提供实现。

    3、@DeclareParents注解所标注的惊天属性知名了要引入的接口。在这里我们引入的是Encoreable接口。

    【------------------------Spring Web------------------------】

    五、构建Spring Web应用程序

    5.1、Spring MVC起步

    在基于HTTP协议的Web应用中,Spring MVC将用户请求在调度Servlet、处理器映射、控制器以及视图解析器之间移动,再将用户结果返回给用户。

    我们将介绍请求如何从客户端发起,经过Spring MVC中的组件,最终回到客户端。

    1、跟踪Spring MVC的请求

    请求:请求离开浏览器时,带有用户请求内容的信息。一般包含请求的URL、用户提交的表单信息等。

    • 1、前端控制器:前端控制器是一种常用的Web应用程序模式。服务器使用一个单例的Servlet(Spring的DispatcherServlet)作为前端控制器,将请求委托给应用程序的其它组件来执行实际的处理。
    • 2、查询映射并分发请求:DispatcherServlet的任务是将请求发送给Spring MVC控制器。DispatcherServlet收到请求后,根据请求携带的URL信息,查询处理器映射,确定请求对应的控制器,并发送给控制器。
    • 3、控制器处理请求:控制器得到请求后,会获取请求中用户提交的信息,并等待控制器处理这些信息。
    • 4、模型和视图名:控制器处理完数据后,会产生一些信息(称为模型),为了显示,这些模型需要发送给一个视图。控制器把模型数据打包,并标示出用于渲染输出的视图名,然后将请求、模型、视图逻辑名发送回给DispatcherServlet
    • 5、视图解析器:DispatcherServlet根据逻辑视图名找到真正的视图实现。
    • 6、视图渲染:根据找到的视图实现,并将模型数据交付给视图实现,即可将数据渲染输入。
    • 7、传递到客户端:这个输出会通过响应对象传递给客户端。

    2、搭建Spring MVC

    2.1、配置DispatcherServlet:

    传统方式:配置在web.xml中,这个文件放到应用的WAR包中。

    现在方法:使用Java将DispatcherServlet配置到Servlet容器中。

    public class SpitterWebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
      
      @Override
      protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
      }
    
      @Override
      protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };//指定配置类
      }
    
      @Override
      protected String[] getServletMappings() {
        return new String[] { "/" };//将DispatcherServlet映射到“/”
      }
    
    }

    扩展AbstractAnnotationConfigDispatcherServletInitializer的任意类都会自动配置DipatcherServlet和Spring应用上下文,Spring的应用上下文会位于应用程序的Servlet上下文之中。

    getServletMappings()方法将一个或多个路径映射到DispatcherServlet上。本例中“/”,代表是应用的默认Servlet,会处理进入应用的所有请求。

    要理解另外2个方法,需要先理解DispatcherServlet和一个Servlet监听器(ContextLoaderListener)的关系。

    2.2、两个应用上下文之间的关系

    DispatcherServlet启动时,创建Spring应用上下文,加载配置文件或配置类中声明的bean。

    getServletConfigClasses()方法要求DispatcherServlet加载应用上下文时,使用定义在WebConfig配置类中的bean。

    ContextLoaderListener会创建另外一个应用上下文

    我们希望DispatcherServlet记载包含Web组件的bean,如控制器、视图解析器以及处理器映射;而ContextLoaderListener要加载应用中其它bean,通常是驱动应用后端的中间层和数据层组件。

    AbstractAnnotationConfigDispatcherServletInitializer 会同时创建DispatcherServlet和ContextLoaderListener。

    getServletConfigClasses()方法返回的带有@Configuration注解的类会用来定义DispatcherServlet应用上下文的bean。

    getRootConfigClasses()方法返回的带有@Configuration注解的类将会用来配置ContextLoaderListener创建的应用上下文中的bean。

    Servlet3.0之前的服务器,只能支持web.xml的配置方式;Servlet3.0及以后版本才支持AbstractAnnotationConfigDispatcherServletInitializer 的配置方式。

    2.3、启动Spring MVC

    传统方法:使用XML配置Spring MVC组件

    现在方法:使用Java配置Spring MVC组件

    最简单的Spring MVC配置

    @Configuration
    @EnableWebMvc
    public class WebConfig {
    }

    这样就能启动Spring MVC,但是有以下问题:

    • 1、没有配置视图解析器,Spring使用默认的BeanNameViewResolver,这个视图解析器会查找ID与视图名称匹配的bean,并且查找的bean要实现View接口,以这样的方式解析视图。
    • 2、没有启动组件扫描。导致Spring只能找到显式声明在配置类中的控制器。
    • 3、DispatcherServlet会映射为默认的Servlet,所以会处理所有的请求,包括对静态资源的请求,如图片和样式表(一般不希望这样)。

    以下配置即可解决上述问题

    @Configuration
    @EnableWebMvc//启用Spring MVC
    @ComponentScan("spittr.web")//启用组件扫描
    public class WebConfig extends WebMvcConfigurerAdapter {
      @Bean
      public ViewResolver viewResolver() {//配置JSP视图解释器
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
      }
      @Override
      public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
      }
      @Override
      public void addResourceHandlers(ResourceHandlerRegistry registry) {//配置静态资源处理
        // TODO Auto-generated method stub
        super.addResourceHandlers(registry);
      }
    }
    • 1、@ComponentScan注解,会扫描spittr.web包查找组件;通常我们编写的控制器会使用@Controller注解,使其成文组件扫描时的houxuanbean。所以我们不需要在配置类显式声明任何控制器。
    • 2、ViewResolver bean。后面会详细讨论,这个的作用是查找JSP文件。例如home的视图会被解析为/WEB-INF/views/home.jsp。
    • 3、新的WebConfig类扩展了WebMvcConfigurerAdapter并重写了其configureDefaultServletHandling()方法。通过调用enable()方法,要求DispatcherServlet将对静态资源的请求转发到Servlet容器中默认的Servlet上,而不是使用DispatcherServlet本身来处理此类请求。

    RootConfig的配置

    @Configration
    @ComponentScan(basePackages={"spitter"},
        excludeFilters={
            @filter{type=FilterType.ANNOTATION, value=EnableWebMvc.class)
        })
    public class RootConfig{
    }

     使用@ComponentScan注解,后期可以使用很多非Web组件来完善RootConfig。

    5.2、编写基本的控制器

    1、控制器的基本形式

    @RequestMapping注解:

    声明这个控制器所要处理的请求

    @Controller
    public class HomeController{
        @RequestMapping(value="/", method=GET)
        public String home(){
            return "home";
        }
    }

    1.1、声明控制器

    2种方式让控制器类能被扫描称为组件:

    • a、在类上使用@Controller声明这是一个控制器
    • b、在类上使用@Component声明这是一个组件,并且类名使用Controller作为结束

    1.2、指定处理请求路径

    @RequestMapping(value="/",method=GET)

    value代表要处理的请求路径,method属性指定所处理的HTTP方法

    1.3、返回视图名称

    return "home";

    返回一个字符串,代表需要渲染的视图名称。DispatcherServlet会要求视图解析器将这个逻辑名称解析为实际的视图。

    基于我们在InternalResourceViewResolver的配置,视图名“home”将被解析为“/WEB-INF/views/home.jsp”路径的JSP。

    2、测试控制器

    控制器本身也是一个POJO,可用普通POJO的测试方法测试,但是没有太大的意义。

    Public class HomeControllerTest{
        @Test
        public void testHomePage() throws Exception{
            HomeController controller = new HomeController();
            assertEquals("home", controller.home());
        }
    }

    这个测试只是测试home()方法的返回值,没有站在SpringMVC控制器的角度进行测试。

    Spring 3.2开始可以按照控制器的方式来测试控制器。

    Spring 3.2开始包含一种mock Spring MVC并针对控制器执行HTTP请求的机制,这样测试控制器就不用启动Web服务器和Web浏览器了。

    Public class HomeControllerTest{
        @Test
        public void testHomePage() throws Exception{
            HomeController controller = new HomeController();
            MockMvc mockMvc = standaloneSetup(controller).build();//搭建MockMvc
            mockMvc.perform(get("/"))//对"/"执行GET请求
                          .andExpect(view().name("home"));//预期得到home视图
        }
    }

    先传递一个HomeController实例到standaloneSetup()并调用build()来构建MockMvc实例,然后用这个实例来执行针对“/”的GET请求并设置期望得到的视图名称。

    3、定义类级别的请求处理

    对类使用@RequestMapping注解,那么这个注解会应用到所有处理器方法中。

    @Controller
    @RequestMapping("/")
    public class HomeController{
        @RequestMapping( method=GET)
        public String home(){
            return "home";
        }
    }

    路径还可以是一个数组,下面例子代表home()方法可以映射到对“/”和“homepage”的GET请求。

    @Controller
    @RequestMapping({"/", "/homepage"})
    public class HomeController{
        @RequestMapping( method=GET)
        public String home(){
            return "home";
        }
    }

    4、传递模型数据到视图中

    使用Model传递数据

    @RequestMapping(method=RequestMethod.GET)
    public String spittles(Model model){
        model.addAttribute(spittleRepository.findSpittles(Long.MAX_VALUE,20));
        return "spittles";
    }

    通过Model参数,可以将控制器里面的值,传递到视图中,渲染到客户端。

    Model实际上是一个Map(Key-Value对集合)

    使用addAttribute并且不指定key的时候,Model会根据类型自动生成key,例如上面是一个List<Spittle>,那么key值就是spittleList

    最后控制器返回视图逻辑名,标明需要渲染的视图。

    改用Map传递数据

    如果不想使用Spring类型,把Model改成Map类型也是可以的

    @RequestMapping(method=RequestMethod.GET)
    public String spittles(Map model){
        model.put("spittleList",spittleRepository.findSpittles(Long.MAX_VALUE,20));
        return "spittles";
    }

    直接返回数据

    @RequestMapping(method=RequestMethod.GET)
    public List<Spittle> spittles(){
        return spittleRepository.findSpittles(Long.MAX_VALUE,20);
    }

    这种写法,没有返回视图名称,也没有显式设定模型。

    模型:当处理器方法直接返回对象或集合时,这个值会放进模型中,模型的key由类型推断出来。

    视图:而视图的逻辑名称会根据请求路径推断得出,如/spittles的GET请求,逻辑视图名称就是spittles(去掉开头斜线)。

    视图的渲染

    无论使用哪种方法,结果是一样的:

    在控制器中,将数据定义为模型,并发送到指定的视图,根据视图的逻辑名称,按照我们配置的InternalResourceViewResolver视图解析器,找到对应的视图文件(如"/WEB-INF/views/spittles.jsp")。

    当视图是JSP的时候,模型数据会作为请求属性放到请求(request)之中。

    因此,在jsp文件中可以使用JSTL(JavaServer Pages Standard Tag Library)的<c:forEach>标签进行渲染。

    测试控制器视图名以及传递的模型数据

    @Test
    public void houldShowRecentSpittles() throws Exception {
    List<Spittle> expectedSpittles = createSpittleList(20);
    SpittleRepository mockRepository = mock(SpittleRepository.class);//使用mock,利用接口创建一个实现,并创建一个实例对象
    when(mockRepository.findSpittles(Long.MAX_VALUE, 20))
        .thenReturn(expectedSpittles);//调用mock实现,创建20个Spittle对象
    
    SpittleController controller = new SpittleController(mockRepository);
    MockMvc mockMvc = standaloneSetup(controller)//搭建MockMvc
        .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp"))
        .build();
    
    mockMvc.perform(get("/spittles"))//对/spittles发起GET请求
       .andExpect(view().name("spittles"))//断言视图名为spittles
       .andExpect(model().attributeExists("spittleList"))
       .andExpect(model().attribute("spittleList", 
                  hasItems(expectedSpittles.toArray())));
    }

     

    5.3、接受请求的输入

    1、处理查询参数

    场景:我们需要分页查找Spittle列表,希望传入2个参数:上一页最后项的id、每页的数据数量,从而找到下一页应该读取的数据。

    获取参数

    使用@RequestParam注解获取参数

    @RequestMapping(method=RequestMethod.GET)
    public List<Spittle> spittles(
            @RequestParam("max") long max,
            @RequestParam("count") int count){
        return spittleRepository.findSpittles(max,count);
    }

    给定默认值

    private static final String MAX_LONG_AS_STRING = Long.toString(Long.MAX_VALUE);
    @RequestMapping(method=RequestMethod.GET)
    public List<Spittle> spittles(
            @RequestParam(value="max",defaultValue=MAX_LONG_AS_STRING) long max,
            @RequestParam(value="count",defaultValue="20") int count){
        return spittleRepository.findSpittles(max,count);
    }

    注意,查询参数都是String类型,所以需要把Long.MAX_VALUE转换为String类型才能制定默认值。

    当绑定到方法的参数时,才会转换为对应的参数的类型。

    测试方法

    @Test
    public void shouldShowPagedSpittles() throws Exception {
        List<Spittle> expectedSpittles = createSpittleList(50);
        SpittleRepository mockRepository = mock(SpittleRepository.class);
        when(mockRepository.findSpittles(238900, 50)).thenReturn(expectedSpittles);//预期的max和count参数
    
        SpittleController controller = new SpittleController(mockRepository);
        MockMvc mockMvc = standaloneSetup(controller)
                .setSingleView(new InternalResourceView("/WEB-INF/views/spittles.jsp")).build();
    
        mockMvc.perform(get("/spittles?max=238900&count=50")).andExpect(view().name("spittles"))//传入max和count参数
                .andExpect(model().attributeExists("spittleList"))
                .andExpect(model().attribute("spittleList", hasItems(expectedSpittles.toArray())));
    }

    2、通过路径接收参数

    按上面讲解的写法:

    @RequestMapping(value = "/show", method = RequestMethod.GET)
    public String spittle(@RequestParam("spittleId") long spittleId, Model model) {
        model.addAttribute(spittleRepository.findOne(spittleId));
        return "spittle";
    }

    这个控制器,可以处理请求是这样的:"/spittle/show/?spittle_id=12345"

    但是现在流行面向资源的方式,我们查找一个spittle的行为,相当于获取一个spittle资源,更希望URL的方式是:对"/spittle/12345"这样的URL进行GET请求。

    使用@RequestMapping的占位符与@PathVariable注解

    @RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
    public String spittle(@PathVariable("spittleId") long spittleId, Model model) {
        model.addAttribute(spittleRepository.findOne(spittleId));
        return "spittle";
    }

    这样在"/spittle/"后面的参数将会传递到spittleId变量中

    如果变量名和占位符名称相同,可以省掉@PathVariable中的变量

    @RequestMapping(value = "/{spittleId}", method = RequestMethod.GET)
    public String spittle(@PathVariable("spittleId") long spittleId, Model model) {
        model.addAttribute(spittleRepository.findOne(spittleId));
        return "spittle";
    }

    测试方法

    @Test
    public void testSpittle() throws Exception {
        Spittle expectedSpittle = new Spittle("Hello", new Date());
        SpittleRepository mockRepository = mock(SpittleRepository.class);
        when(mockRepository.findOne(12345)).thenReturn(expectedSpittle);
    
        SpittleController controller = new SpittleController(mockRepository);
        MockMvc mockMvc = standaloneSetup(controller).build();
    
        mockMvc.perform(get("/spittles/12345")).andExpect(view().name("spittle"))//通过路径请求资源
                .andExpect(model().attributeExists("spittle")).andExpect(model().attribute("spittle", expectedSpittle));
    }

    使用路径传参只使用于少量参数的情况,如果需要传递大量数据,则需要使用表单。

    5.4、处理表单(待补充)

    【暂略】六、渲染Web视图

    【暂略】七、Spring MVC的高级技术

    【暂略】八、使用Spring Web Flow

    九、Spring Security保护Web应用

    Spring Security是一种基于Spring AOP和Servlet规范中filter实现的安全框架

    • 提供声明式安全保护
    • 提供了完整的安全解决方案

    Spring Security从2个角度解决安全问题

    • 使用Servlet规范中的filter保护Web请求并限制URL级别的访问(本章)
    • 使用Spring的AOP,借助对象代理和使用通知,确保有适当权限的用户才能访问受保护的方法(14章)

    Spring Security分为11个模块

    要使用Spring Security,至少要引入Core和Configuration2个模块

    9.1、Spring Security简介

    开发方式的演进:

    • Spring Security基于Servlet Filter,在web.xml或webApplicationInitializer中配置多个filter

    简化:使用一个特殊的filter:DelegatingFilterProxy,这个filter是一个代理类,会把工作交给Spring上下文中的javax.servlet.filter实现类

     

    • Spring 3.2带来了新的Java配置方式

    在实现了WebSecurityConfigurer的bean中使用注解@EnableWebSecurity

    更简化:在扩展类WebSecurityConfigurerAdapter中使用注解@EnableWebSecurity;如果使用Spring MVC,则需要使用注解@EnableWebMvcSecurity

    Spring Security的配置

    可以通过重写以下3个方法,对Spring Security的行为进行配置

    9.2、配置用户信息服务

    Spring Security需要配置用户信息服务,表明哪些用户可以进行访问

    有以下几种方式,暂时先不详细介绍

    • 基于内存设置
    • 基于数据库表设置
    • 基于LDAP设置
    • 配置自定义用户服务,实现UserDetailsService接口

    9.3、拦截请求

    【---------------------后端中的Spring----------------------】

    【暂略】十、通过Spring和JDBC征服数据库

    【暂略】十一、使用对象-关系映射持久化数据库

    十二、Spring中使用NoSQL数据库

    12.1、使用MongoDB持久化文档数据

    待补充

    12.2、使用Noe4j操作图数据

    待补充

    12.3、使用Redis操作key-value数据

    Redis是一种基于key-value存储的数据库,Spring Data没有把Repository生成功能应用到Redis中,而是使用面向模版的数据访问的方式来支持Redis的访问。

    Spring Data Redis通过提供一个连接工厂来创建模版

    12.3.1、连接到Redis

    创建连接工厂:

      为了连接和访问Redis,有很多Redis客户端,如Jedis、JRedis等。

      Spring Data Redis为4种Redis客户端实现提供了连接工厂:

      • JedisConnectionFactory
      • JredisConnectionFactory
      • LettuceConnectionFactory
      • SrpConnectionFactory

      选择哪个客户端实现取决于你,对于Spring Data Redis,这些连接工厂的适用性是一样的

      我们使用一个bean来创建这个工厂

    @Bean
    public RedisConnectionFactory redisCF() {
        JedisConnectionFactory cf = new JedisConnectionFactory();
        cf.setHostName("redis-server");
        cf.setPort(7379);
        cf.setPassword("123456");
        return cf;
    }

      对于不同的客户端实现,他的工厂的方法(如setHostName)也是相同的,所以他们在配置方面是相同的操作。

    12.3.2、Redis模版RedisTemplate

    获取RedisTemplate

      其中一种访问Redis的方式是,从连接工厂中获取连接RedisConnection,使用字节码存取数据(不推荐):

    RedisConnectionFactory cf = ...;
    RedisConnection conn = cf.getConnection();
    conn.set("greeting".getBytes(),"Hello World".getBytes());
    byte[] greetingBytes = conn.get("greeting".getBytes());
    String greeting = new String(greetingBytes);

      Spring Date Redis提供了基于模版的较高等级的数据访问方案,其中提供了2个模版:

      • RedisTemplate:直接持久化各种类型的key和value,而不是只局限于字节数组
      • StringRedisTemplate:扩展了RedisTemplate,只关注String类型
    RedisConnectionFactory cf = ..;
    RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
    redis.setConnectionFactory(cf);
    
    RedisConnectionFactory cf = ..;
    StringRedisTemplate redis = new StringRedisTemplate(cf);
    
    //设置为bean
    @Bean
    public RedisTemplate<String, Product> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
        redis.setConnectionFactory(cf);
        return redis;
    }
    
    @Bean
    public StringRedisTemplate redisTemplate(RedisConnectionFactory cf) {
        return new StringRedisTemplate(cf);
    }

    使用RedisTemplate存取数据

      

    //redis是一个RedisTemplate<String,Product>类型的bean
    
    //【使用简单的值】
    //通过product的sku进行存储和读取product对象
    redis.opsForValue().set(product.getSku(),product);
    Product product = redis.opsForValue().get("123456");
    
    //【使用List类型的值】
    //在List类型条目尾部/头部添加一个值,如果没有cart这个key的列表,则会创建一个
    redis.opsForList().rightPush("cart",product);
    redis.opsForList().leftPush("cart",product);
    //弹出一个元素,会移除该元素
    Product product = redis.opsForList().rightPop("cart");
    Product product = redis.opsForList().leftPop("cart");
    //只是想获取值
    //获取索引2到12的元素;如果超出范围,则只返回范围内的;如果范围内没有,则返回空值
    List<Product> products = redis.opsForList().range("cart",2,12);
    
    //在Set上执行操作
    //添加一个元素
    redis.opsForSet().add("cart", product);
    //求差异、交集、并集
    Set<Product> diff = redis.opsForSet().difference("cart1", "cart2");
    Set<Product> union = redis.opsForSet().union("cart1", "cart2");
    Set<Product> isect = redis.opsForSet().intersect("cart1", "cart2");
    //移除元素
    redis.opsForSet().remove(product);
    //随机元素
    Product random = redis.opsForSet().randomMember("cart");
    
    //绑定到某个key上,相当于创建一个变量,引用这个变量时都是针对这个key来进行的
    BoundListOperations<String, Product> cart = redis.boundListOps("cart");
    Product popped = cart.rightPop(); 
    cart.rightPush(product);
    cart.rightPush(product2);
    cart.rightPush(product3);

    12.3.3、使用key和value的序列化器

    当某个条目保存到Redis key-value存储的时候,key和value都会使用Redis的序列化器进行序列化。

    Spring Data Redis提供了多个这样的序列化器,包括:

    • GenericToStringSerializer:使用Spring转换服务进行序列化
    • JacksonJsonRedisSerializer:使用Jackson1,讲对象序列化为JSON
    • Jackson2JsonRedisSerializer:使用Jackson2,讲对象序列化为JSON
    • JdkSerializationRedisSerializer:使用Java序列化
    • OxmSerializer:使用Spring O/X映射的编排器和解排器实现序列化,用于XML薛丽华
    • StringRedisSerializer:序列化String类型的key和value

    这些序列化器都实现了RedisSerializer接口,如果其中没有符合需求的序列化器,可以自行创建。

    RedisTemplate默认使用JdkSerializationRedisSerializer,那么key和value都会通过Java进行序列化。

    StringRedisTemplate默认使用StringRedisSerializer,实际就是实现String和byte数组之间的转化。

    例子:

       使用RedisTemplate,key是String类型,我们希望使用StringRedisSerializer进行序列化;value是Product类型,我们希望使用JacksonJsonRedisSerializer序列化为JSON

    @Bean
    public void RedisTemplate<String,Product> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, Product> redis = new RedisTemplate<String, Product>();
        redis.setConnectionFactory(cf);
        redis.setKeySerializer(new StringRedisSerializer());
        redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
        return redis;
    }

    十三、使用Spring缓存技术

    什么叫缓存:

      一些变动不频繁或者不变动的数据,当每次获取的时候,都需要从数据库中提取或者计算,每次都需要消耗资源。

      我们可以把这些计算后的结果,在某个地方存放起来,当下次访问的时候直接返回,就可以避免了多次的资源消耗,这就叫缓存技术。

    13.1、启用缓存支持

    13.1.1、2种方式启用Spring对注解驱动缓存的支持:

    • 在一个配置类上使用@EnableCaching注解
    • 使用XML进行配置

    @EnableCaching注解方式:

    @Configuration
    @EnableCaching
    public class CachingConfig {
        @Bean
        public CacheManager cacheManager() {
            return new ConcurrentMapCacheManager();
        }
    }

    XML方式:

      使用Spring cache命名空间中的<cache:annotation-driven>元素启动注解驱动的缓存

    <?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:cache="http://www.springframework.org/schema/cache"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/cache
            http://www.springframework.org/schema/cache/spring-cache.xsd">
        
        <!-- 启用缓存-->
        <cache:annotation-driven />
    
        <!-- 声明缓存管理器-->
        <bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager" />
    
    <beans>

     代码解析:

      2种方式本质上是一样的,都创建一个切面并触发Spring缓存注解的切点

      根据所使用的注解和缓存状态,切面会从缓存中获取数据,并将数据添加到缓存中或者从缓存删除某个值。

      其中不仅启用了注解驱动的缓存,还声明了一个缓存管理器(cache manager)的bean。

      缓存管理器是SPring缓存抽象的狠心,可以和不同的缓存实现进行集成。

      2个例子都是用ConcurrentHashMap作为缓存存储,是基于内存的,用在开发或者测试可以,但是在大型企业级应用程序就有其他更好的选择了。

    13.1.2、 配置缓存管理器

    Spring3.1内置五个缓存管理器实现:

    • SimpleCacheManager
    • NoOpCacheManager
    • ConcurrentMapCacheManager
    • CompositeCacheManager
    • EhCacheCacheManager

    Spring3.2引入了另外一个缓存管理器实现

    • 这个管理器可以用于基于JCache(JSR-107)的缓存提供商之中

    除了核心Spring框架,Spring Data提供了2个缓存管理器实现

    • RedisCacheManager(来自于Spring Data Redis项目)
    • GemfireCacheManager(来自于Spring Data GemFire项目)

    我们选择一个缓存管理器,然后在Spring应用上下文中,以bean的形式进行设置。使用不同的缓存管理器会影响数据如何存储,但是不会影响Spring如何声明缓存。

    例子——配置EhCache缓存管理器:

      此例子暂时省略。。。

    例子——配置Redis缓存管理器

      使用RedisCacheManager,它会与一个Redis服务器协作,并通过RedisTemplate存取条目

      RedisCacheManager要求:

    • 一个RedisConnectFactory实现类的bean
    • 一个RedisTemplate的bean
    @Configuration
    @EnableCaching
    public class CachingConfig {
    
        //Redis缓存管理器bean
        @Bean
        public CacheManager cacheManager(RedisTemplate redisTemplate) {
            return new RedisCacheManager(redisTemplate);
        }
    
        //Redis连接工厂bean
        @Bean
        public JedisConnectionFactory redisConnectionFactory() {
            JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
            jedisConnectionFactory.afterPropertiesSet();
            return jedisConnectionFactory;
        }
    
        //RedisTemplate bean
        @Bean
        public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisCF) {
            RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
            redisTemplate.setConnectionFactory(redisCF);
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    }

    例子——使用多个缓存管理器

    使用Spring的CompositeCacheManager

    以下例子创建一个CompositeCacheManager的bean,里面配置了多个缓存管理器,会按顺序迭代查找这些缓存管理器里面是否存在值

    @Bean
    public CacheManager cacheManager(net.sf.ehcache.CacheManager cm,javax.cache.CacheManager jcm){
        CompositeCacheManager cacheManager = new CompositeCacheManager();
        List<CacheManager> managers = new ArrayList<CacheManager>();
        managers.add(new JCacheCacheManager(jcm));
        managers.add(new EhcacheCacheManager(cm));
        manager.add(new RedisCacheManager(reisTemplate()));
        cacheManager.setCacheManagers(managers);
        return cacheManager;
    }

    13.2、为方法添加注解以支持缓存

    设置好了连接工厂和缓存管理器,我们就可以使用注解来设置缓存了。

    Spring的缓存抽象很大程度是基于切面构建的,启用了缓存,就会创建一个切面,触发Spring的缓存注解。

    这些注解可以用在方法或类上,用在类上,则类的所有方法都会应用该缓存行为。

    13.2.1、填充缓存

    @Cacheable和@CachePut

    最简单的情况下只需要使用value属性即可

    @Cacheable("spittleCache")
    public Spittle findOne(long id){
        return new Spittle ();//可以是具体的逻辑
    }

    缓存流程:

    • 调用findOne方法
    • 缓存切面拦截
    • 缓存切面在缓存中名为spittleCache的缓存数据中,key为id的值
      • 如果有值,则直接返回缓存值,不再调用方法。
      • 如果无值,则调用方法,并将结果放到缓存中。

    缓存注解也可以写在接口上,那么它的所有实现类都会应用这个缓存规则。

    将值放到缓存中:

    一个使用情景:我们保存一个数据,然后前台刷新或者其他用户获取,我们可以保存时直接更新缓存

    @CachePut("spittleCache")
    Spittle save(Spittle spittle);

    自定义缓存key:

    上面例子中,默认把Spittle参数作为缓存的key,这样并不合适,我们希望用Spittle的ID作为key值

    但是Spittle未保存情况下,又未有ID,这样我们可以通过SpEL表达式解决这个问题

    @CachePut(value="spittleCache",key="#result.id")
    Spittle save(Spittle spittle);

    条件化缓存:

    使用unless和condition属性,接受一个SpEL表达式

    unless:如果为false,调用方法时依然会在缓存中查找,只是阻止结果放进缓存;

    condition:如果是false,则禁用整个缓存,包括方法调用前的缓存查找和方法调用后把结果放进缓存都被禁用

    @Cacheable(value="spittleCache" 
    unless="#result.message.contains('NoCache')") Spittle findOne(long id);
    @Cacheable(value="spittleCache" 
    unless="#result.message.contains('NoCache')"
    condition="#id >= 10") Spittle findOne(long id);

    13.2.2、移除缓存条目

    @CacheEvict注解

    调用方法时,会删除该缓存记录,

    @CacheEvict("spittleCache")
    void remove(long spittleId);

     

    13.3、使用XML声明缓存

    有时候无法是注解,或者不想使用注解,可以使用XML声明,这里暂不介绍。

    【暂略】十四、保护方法应用

    【------------------------Spring集成------------------------】

    【暂略】十五、使用远程服务

    十六、使用Spring MVC创建REST API

    16.1、REST介绍

    1、什么是REST

    REST的名称解释:

    SOAP:简单对象访问协议(英文:Simple Object Access Protocol,简称SOAP)。

    REST:表述性状态传递(英文:Representational State Transfer,简称REST)。

    REST是比SOAP更简单的一个Web应用可选方案。

    REST是一种面向资源的架构风格,强调描述应用程序的事物和名词。

    • Representational :表述性,REST资源可以使用各种不同的形式进行表述,如XML,JSON,HTML;
    • State:状态,使用REST的时候,我们关注的是资源的状态,而不是行为;
    • Transfer:转移,REST的资源,通过某种形式的表述,在应用之间传递转移。

    简洁地说,REST就是将资源的状态,以最合适客户端或服务器的表述方式,在服务器与客户端之间转移。

    REST与HTTP方法:

    URL:REST中,资源通过URL定位和识别。虽然没有严格的URL格式定义,但是一个URL应该能识别资源,而不是简单的一个命令。因为REST的核心是资源,而不是行为。

    行为:REST中也有行为,但是不是在URL中体现,一般通过HTTP行为来定义,例如CRUD

    • Creat:POST
    • Read:GET
    • Update:PUT/PATCH
    • Delete:Delete

    2、Spring对REST的支持

    • a、控制器支持所有HTTP方法,包含POST/GET/PUT/DELETE,Spring3.2及以上版本还包含PATCH。
    • b、@PathVariable注解使控制器可以处理参数化URL
    • c、Spring的视图和视图解析器,资源可以以多种方式表述,包括将模型数据渲染成XML/JSON/Atom/RSS的View实现。
    • d、可以使用ContentNegotiatingViewResolver来选择客户端最适合的表述。
    • e、使用@Response注解的各种HttpMethodConverter实现,能够替换基于视图的渲染方式。
    • f、使用@Response注解的各种HttpMethodConverter可以将传入的HTTP数据转化为控制器处理方法的Java对象。
    • g、借助RestTemplate,Spring应用能够方便地使用REST资源。

    16.2、创建REST端点

    需要实现RESTful功能的Spring MVC控制器

    @Controller
    @RequestMapping("/spittle")
    public class SpittleApiController {
        private static final String MAX_LONG_AS_STRING = "9223372036854775807";
        private SpittleRepository spittleRepository;
        @Autowired
        public SpittleApiController(SpittleRepository spittleRepository) {
            this.spittleRepository = spittleRepository;
        }
        @RequestMapping(method = RequestMethod.GET)
        public List<Spittle> spittles(@RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
                @RequestParam(value = "count", defaultValue = "20") int count) {
            return spittleRepository.findSpittles(max, count);
        }
    }

    1、Spring提供了2种将Java表述形式转换为发给客户端的表述形式的方式

    内容协商

    选择一个视图,能够将模型渲染为呈现给客户端的表述形式

    这种方式一般不用

    消息转换器

    使用HTTP信息转换器

    这是一种直接的方式,将控制器的数据转换为服务于客户端的表述形式。

    当使用消息转换功能时, DispatcherServlet不用麻烦地将模型传递给视图中。

    这里没有视图,甚至没有模型,只有控制器产生的数据,然后经过消息转换器,产生资源表述,传到客户端。

    Spring中自带了各种各样的转换器,能够满足常见的对象转换为表述的需求。

    例如,客户端通过请求的Accept头信息表明它能接受"application/json",并且Jackson JSON在类路径下,那么处理方法返回的对象将交给MappingJacksonHttpMessageConverter,由他转换为返回客户端的JSON表述形式。

    或者,如果请求的头信息表明客户端想要"text/xml"格式,那么Jaxb2RootElementHttpMessageConverter将会为客户端产生XML表述形式。

    除了其中5个外,其它都是自动注册的,不需要Spring配置;但是为了支持他们,需要把对应的库添加到类路径中。

    2、消息转换器的具体用法

    在响应体中返回资源状态

    正常情况,如果控制器方法返回Java对象,这个对象会放到模型中,并在视图中渲染。

    为了使用消息转换功能,我们需要告诉Spring跳过正常的模型/视图流程,并使用消息转换器。

    最简单的方式:使用@ResponseBody

    @RequestMapping(method = RequestMethod.GET, produces = "application/json")
    public @ResponseBody List<Spittle> spittles(@RequestParam(value = "max", defaultValue = MAX_LONG_AS_STRING) long max,
            @RequestParam(value = "count", defaultValue = "20") int count) {
        return spittleRepository.findSpittles(max, count);
    }

    @ResponseBody会告诉Spring,我们要将返回的对象作为资源发送给客户端,并转换为客户端要求的表述形式。

    DispatcherServlet会根据请求中Accept头部信息,找到对应的消息转换器,然后把Java对象转换为客户端需要的表述形式。

    例如客户端请求的Accept头部信息表明它接收"application/json",且Jackson JSON库位于应用的类路径下,那么将选择MappingJacksonHttpMessageConverter或MappingJackson2HttpMessageConverter(取决于类路径下是哪个版本的Jackson)作为消息转换器,并将Java对象转换为JSON文档,写入到相应体中。

    @ RequestMapping中produces属性,代表该控制器只处理预期输出为JSON的请求,也就是Accept头信息包含"application/json"的请求。

    其它类型请求,即使URL匹配且为GET请求,也不会被处理。

    这样的请求会被其它的方法进行处理(有适当方法的情况下),或者返回HTTP 406(Not Acceptable)响应。

    请求体中接收资源状态

    上面只讨论了如何将一个REST资源转换为客户端所需要的表述,这里讨论如何将客户端发送过来的资源状态表述转换为JAVA对象。

    使用@RequestBody注解

    @RequestMapping(method = RequestMethod.POST, consumes = "application/json")
    public @ResponseBody Spittle saveSpittle(@RequestBody Spittle spittle) {
        return spittleRepository.save(spittle);
    }

    @RequestBody表明

    • a、这个控制器方法,只能处理/spittles(定义在类级别上了)的POST请求,而且请求中预期要包含一个Spittle的资源表述
    • b、Spring会根据请求的Content-Type头信息,查找对应的消息转换器,把客户端发送的资源的表述形式(JSON,或HTMl)转化为Java对象

    为控制器默认设置消息转换

    @RestController注解(Spring 4.0及以上)

    使用@RestController代替@Controller标注控制器,Spring会为所有方法应用消息转换功能,我们就不用每个方法添加@ResponseBody,当然@RequestBody如果需要使用到,是不能省略的

    16.3、提供资源之外的其它内容

    1、发送错误信息到客户端

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public @ResponseBody Spittle spittleById(@PathVariable Long id) {
        return spittleRepository.findOne(id);
    }

    上述方法是查找一个Spittle对象。

    假设找不到,则返回null,这时候,返回给客户端的HTTP状态码是200(OK),代表事情正常运行,但是数据是空的。

    而我们希望这种情况下或许可以使状态码未404(Not Found),这样客户端就知道发生了什么错误了。

    要实现这种功能,Spring提供了一下几种方式:

    • a、使用@ResponseStatus注解指定状态码
    • b、控制器方法可以返回ResponseEntity对象,改对象能够包含更多响应相关元数据
    • c、异常处理器能够应对错误场景,这样处理器方法就能关注于正常的状况

    使用ResponseEntity

    使用ResponseEntity对象替代@ResponseBody

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ResponseEntity<Spittle> spittleById(@PathVariable Long id) {
        return spittleRepository.findOne(id);
        HttpStatus status = spittle != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
        return new ResponseEntity<Spittle>(spittle, status);
    }

    使用ResponseEntity,可以指定HTTP状态码,而且本身也包含@ResponseBody的定义,相当于使用了@ResponseBody。

    接下来,我们希望如果找不到Spittle对象时,返回错误信息

    可以创建一个Error类,然后使用泛型,在找不到Spittle对象时,返回一个Error对象

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ResponseEntity<?> spittleById(@PathVariable Long id) {
        return spittleRepository.findOne(id);
        if (spittle == null){
            Error error = new Error(4, "Spittle [" + id + "] not found");
            return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
    }

    但是,这种写法貌似使代码变得有点复杂,我们可以考虑使用错误处理器。

    处理错误

    步骤1:创建一个异常类

    public class SpittleNotFoundException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        private long spittleId;
        public SpittleNotFoundException(long spittleId) {
            this.spittleId = spittleId;
        }
        public long getSpittleId() {
            return spittleId;
        }
    }

    步骤2:在控制器下创建一个错误处理器的处理方法

    @ExceptionHandler(SpittleNotFoundException.class)
    public ResopnseEntity<Error> spittleNotFound(SpittleNotFoundException e) {
        long spittleId = e.getSpittleId();
        Error error =  new Error(4, "Spittle [" + spittleId + "] not found");
        return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler注解加到控制器方法中,可以处理对应的异常。

    如果请求A发生异常,被这个方法捕获到该异常, 那么请求的返回值就是这个异常处理器的返回值。

    步骤3:原来的业务控制器方法可以得到简化

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ResponseEntity<Spittle> spittleById(@PathVariable Long id) {
        return spittleRepository.findOne(id);
        if (spittle == null) { throw new SpittleNotFoundException(id); }
        return new ResponseEntity<Spittle>(spittle, HttpStatus.OK);
    }

    又因为此时任何时候,这个控制方法都有数据返回,所以HTTP状态码始终是200,所以可以不使用ResponseEntity,二使用@ResponseBody;如果控制器上面还使用了@RestController,我们又可以把@ResponseBody省掉, 最后得到代码

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public Spittle spittleById(@PathVariable Long id) {
        return spittleRepository.findOne(id);
        if (spittle == null) { throw new SpittleNotFoundException(id); }
        return Spittle;
    }

    步骤4:同理,可以简化一下错误处理器

    @ExceptionHandler(SpittleNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Error spittleNotFound(SpittleNotFoundException e) {
        long spittleId = e.getSpittleId();
        return new Error(4, "Spittle [" + spittleId + "] not found");
    }

    在简化过程中,我们都希望避免使用ResponseEntity,因为他会导致代码看上来很复杂。

    但是有的情况必须使用ResponseEntity才能实现某些功能。

    在响应中设置头部信息  

    要使用到ResponseEntity,比如说我新建一个Spittle后,系统通过HTTP的Location头部信息,返回这个Spittle的URL资源地址。

    这里暂时不讨论

    【暂略】16.4、编写REST客户端

    【暂略】十七、Spring消息

    【暂略】十八、使用WebSocket和STMOP实现消息功能

    【暂略】十九、使用Spring发送Email

    【暂略】二十、使用JMX管理Spring Bean

    【暂略】二十一、使用Spring Boot简化Spring开发

    --

    实例

    实例01:@ResponseBody使用实体接收数据,发送实体内没有实体时,报错

    SpringMVC中默认的序列化器只支持子集传入,可以注入bean,使用fastJson作为MVC使用的序列化和反序列化器

    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters() {
        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        HttpMessageConverter<?> converter = fastConverter;
        return new HttpMessageConverters(converter);
    }
  • 相关阅读:
    POJ 1611 The Suspects
    POJ 2001 Shortest Prefixes(字典树)
    HDU 1251 统计难题(字典树 裸题 链表做法)
    G++ C++之区别
    PAT 乙级 1013. 数素数 (20)
    PAT 乙级 1012. 数字分类 (20)
    PAT 乙级 1009. 说反话 (20)
    PAT 乙级 1008. 数组元素循环右移问题 (20)
    HDU 6063 17多校3 RXD and math(暴力打表题)
    HDU 6066 17多校3 RXD's date(超水题)
  • 原文地址:https://www.cnblogs.com/LiveYourLife/p/9249321.html
Copyright © 2011-2022 走看看