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);
    }
  • 相关阅读:
    数据结构与算法系列——排序(6)_树形选择排序
    数据结构与算法系列——排序(7)_堆排序
    数据结构与算法系列——排序(5)_简单选择排序
    数据结构与算法系列——排序(4)_Shell希尔排序
    数据结构与算法系列——排序(3)_折半插入排序
    数据结构与算法系列——排序(2)_直接插入排序
    数据结构与算法系列——排序(1)_概述
    Java高级开发_性能优化的细节
    图形推理
    美团点评面试20190515
  • 原文地址:https://www.cnblogs.com/LiveYourLife/p/9249321.html
Copyright © 2011-2022 走看看