1 Spring4.x Learn Tutorial

本文仅作为学习 Spring 基础知识的一个记录,便于查阅。

1 Spring 基础

1.1 Spring概述

1.1.1 Spring 的简史

  • 第一阶段:xml配置

在 Spring 1.x 时代,使用 Spring 开发基本都是基于 xml 配置的 Bean。

  • 第二阶段:注解配置

在 Spring 2.x 时代,Spring 提供了声明 Bean 的注解(如 @Component、@Service),大大减少了配置量。一般采用 xml 和注解方式相结合的方式来配置应用,应用的基本配置(如 MySQL 配置)采用xml,业务配置采用注解。

  • 第三阶段:Java 配置

从 Spring 3.x 到现在,Spring 提供了 Java 配置的能力,使用 Java 配置可以让你更理解配置的 Bean。在 Spring 4.x 和 Spring Boot 中都推荐使用 Java 配置。

1.1.2 Spring 概述

Spring 框架是一个轻量级的企业级开发的一站式解决方案。

所谓解决方案就是可以基于 Spring 解决 Java EE 开发的所有问题,框架提供了 IoC容器、APO、数据访问、Web开发、消息、测试等相关技术的支持。

所以轻量级指的就是 Spring 是模块化的,这意味着你可以只使用你需要的 Spring 模块。

Spring Framework Modules

详见七大模块的说明

1.2 简化 Java 开发

Spring 框架使用以下4种策略来降低 Java 开发的复杂性:

  • 使用 POJO 进行轻量级和最小侵入式的开发
  • 通过依赖注入和基于接口编程实现松耦合
  • 通过 AOP 和默认习惯进行声明式编程
  • 使用模板减少样板式代码

1.2.1 使用 POJO 进行轻量级和最小侵入式的开发

很多框架通过强迫应用集成它们的类或实现它们的接口从而导致应用与框架绑死,这种编程方式便是侵入式的。
在 Spring 中,它不会强迫你实现 Spring 规范的接口或类。在基于 Spring 构建的应用中,它的类通常没有任何痕迹表明你使用了 Spring。(最坏的场景是,一个类或许会使用 Spring 注解,但它依旧是 POJO)

1.2.2 通过依赖注入和基于接口编程实现松耦合

下面我们看一个紧耦合的例子

package com.kaifei.spring.tutorial.api.manager.impl;

import com.kaifei.spring.tutorial.api.manager.IKnight;

public class DamselRescuingKnight implements IKnight {

    private RescueDamselQuest quest;

    // DamselRescuingKnight在它的构造函数中自行创建了RescueDamselQuest。
    // 这使得DamselRescuingKnight紧密地和RescueDamselQuest耦合到了一起
    public DamselRescuingKnight() {
        this.quest = new RescueDamselQuest();
    }

    public void embarkQuest() {
        quest.embark();
    }
}

可以看到 DamselRescuingKnight 所依赖的类是由它自己创建和管理的。

再看一个松耦合的例子

package com.kaifei.spring.tutorial.api.manager.impl;

import com.kaifei.spring.tutorial.api.manager.IKnight;
import com.kaifei.spring.tutorial.api.manager.IQuest;

public class DamselRescuingKnight implements IKnight {

    private IQuest quest;

    public DamselRescuingKnight(IQuest quest) {
        this.quest = quest;
    }

    public void embarkQuest() {
        this.quest.embark();
    }
}

可以看到 DamselRescuingKnight 只是通过接口来表明依赖关系(也没有自行去管理依赖关系),那么这种依赖就能够在对象毫不知情的情况下,用不同的具体实现来进行替换。

在此便可引出 DI 的作用:

对象的依赖关系交由系统中负责协调各对象的第三方组件在创建对象时进行设定。
DI 会将所依赖的关系自动交给目标对象,而不是让对象自己去获取依赖。

1.2.3 通过 AOP 和默认习惯进行声明式编程

借助 AOP,能确保 POJO 的简单性,使你的 POJO 专注于核心逻辑而不用去关注各种横切面(如日志、安全等模块)。这些横切面以声明的方式灵活的应用到系统中,你的核心甚至根本不用关心它们的存在。

1.2.4 使用模板减少样板式代码

在使用 JAVA API 时会导致我们写许多的样板式代码,例如使用 JDBC 时,我们在对数据库的每一次操作时,都得按 建立连接-获取连接-编写业务代码-异常捕获-关闭连接 的流程来组织我们的代码。

使用 Spring 提供的模板能够使我们更加专注在编写核心代码上,而不用去重复的编写样板式的代码。

1.3 容纳你的 Bean

1.3.1 使用应用上下文

Spring 自带了多种类型的应用上下文:

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

在应用上下文准备就绪之后,我们就可以调用上下文的 getBean() 方法从 Spring 容器中获取 bean。

1.3.2 bean 的生命周期

bean 在 Spring 容器中从创建到销毁经历了若干阶段,每一阶段都可以针对 Spring 如何管理 bean 进行个性化定制。

2 装配Bean

2.1 Spring 配置 Bean 的可选方案

Spring 容器负责创建 Bean 并通过 DI 来协调这些对象之间的关系,但是作为开发人员,你需要告诉 Spring 要创建哪些 Bean 并且如何将其装配在一起。

Spring 提供了三种主要的装配机制:

  • 在 XML 中进行显示的配置
  • 在 Java 中进行显示的配置
  • 隐世的 Bean 发现机制和自动装配

一般在项目中,建议尽可能的使用自动装配的机制,显示的配置越少越好。

2.2 自动装配

2.2.1 创建可被发现的Bean

实现组件扫描使用以下3个注解即可:

  • @Component:声明一个 POJO 类为组件类(Spring会为组件类创建 bean)
  • @ComponentScan:启用组件扫描,扫描包下所有的组件类(在配置类中声明,默认会扫描与配置类相同的包及其子包)
  • @Configuration:声明一个 POJO 为配置类

组件类:

package spring.tutorial.bean.assem.manager.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import spring.tutorial.bean.assem.manager.ICar;
import spring.tutorial.bean.assem.manager.IPlant;

@Component(value="specialTree")
public class Tree implements IPlant {

    @Autowired
    private ICar icar;

    private String name = "我是树木";

    @Override
    public void getName() {
        System.out.println(this.name);
    }

    public void printName() {
        this.getName();
        this.icar.sayName();
    }
}

配置类:

package spring.tutorial.bean.assem;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class JavaConfig {

}

[代码示例]

2.2.2 为组件扫描的bean命名

Spring 应用上下文中所有的 bean 都会给定一个 ID,默认 Spring 会根据类名来为 bean 指定一个 ID(即类名首字母小写)。

我们可也显示的为 bean 命名

@Component("specialTree")
public class Tree implements IPlant {...}

2.2.3 设置组件扫描的基础包

组件扫描的基础包默认为当前配置类所在的包及其子包。

显示的设置有以下2种方法:

  • @ComponentScan(basePackages={"com.kaifei.tutorial.spring", "otherPackage"})
  • @ComponentScan(basePackageClasses={IMarkerInterface.class, [...]}),basePackageClasses 中类所在的包将会作为组件扫描的基础包

2.2.4 自动装配

所谓自动装配就是让 Spring 自动满足 bean 依赖的一种方法,在满足依赖的过程中,会在 Spring 应用上下文中寻找匹配某个 bean 需求的其他的 bean。

我们可以使用 @Autowired 注解来声明自动装配。

@Autowired 注解可以放置在属性、构造器、Setter 方法或其它的方法之上,Spring 都会尝试满足方法参数上所声明的依赖。

  • 若只有一个 bean 匹配依赖的需求,那么这个 bean 将会被装配进来
  • 若有多个 bean 匹配依赖的需求,Spring 会抛出一个异常,表明自动装配有歧义
  • 若没有匹配到的 bean,Spring 会抛出一个异常。(将 @Autowiredrequired 属性设置为 false,会避免异常,但此时 bean 处于未装配状态,若直接使用会报异常)

2.3 通过Java代码装配bean

在很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式,但
有时候自动化配置的方案行不通,因此需要明确配置Spring。比如说,你想要将第三方库中的
组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component和
@Autowired注解的,因此就不能使用自动化装配的方案了。

显示装配有以下两种方案:

  • 通过 java 代码装配
  • 通过 xml 装配

在进行显示配置时,javaConfig 是更好的方案,因为它更为强大、类型安全且对重构友好。

2.3.1 创建配置类

使用注解 @Component 标识 POJO 对象即可声明为配置类

2.3.2 声明简单的bean

在配置类中编写一个方法,方法会创建所需类型的实例,然后给这个方法添加 @Bean 注解即可声明 bean。

@Bean(name="phone")
public IMobilePhone getMobilePhone() {
    OnePlus onePlus = new OnePlus();
    return onePlus;
}

声明的 bean ID 默认为方法名(getMobilePhone),可通过注解 @Bean 的 name 属性修改。

2.3.3 借助JavaConfig实现注入

@Configuration
@ComponentScan
public class JavaConfig {

    @Bean(name="phone")
    public IMobilePhone getMobilePhone(ICar iCar) {
        OnePlus onePlus = new OnePlus(iCar);
        return onePlus;
    }
}

当 Spring 调用 getMobilePhone() 方法时,它会自动装配一个 ICar 到配置方法中,然后方法体就可以按照适合的方式来使用它。

2.4 通过XML装配bean

2.5 导入混合配置

2.5.1 在JavaConfig中引用XML配置

@Configuration
@ComponentScan
@Import({FlowerConfig.class})
public class JavaConfig {   
    /**
     * 注入其他配置类中的 bean
     * @param iFlower
     * @return
     */
    @Bean(name="car")
    public ICar getCar(IFlower iFlower) {
        Bwm bwm = new Bwm();
        bwm.setIflower(iFlower);
        return bwm;
    }
}

当配置类显得笨重时,我们可将在配置类中声明的bean拆分到多个配置类中,再使用 @Import 注解引入其他的配置类。

使用注解 @ImportResource,可将 XML 中声明的bean装配进来。

【装配bean的代码示例】

3 高级装配

3.1 环境与profile

应用的配置文件内容会因环境的不同而不同,那么我们需要应用能够在不同的环境下选择最为合适的配置。

其中的一个解决方案就是在单独的配置类(或XML文件)中配置每个bean,然后在构建阶段(如使用 Maven 的 profiles)确定要将哪一个配置编译到可部署的应用中。这种方式的问题就在于要为每个环境构建应用。

而Spring 所提供的方案并不需要重新构建。

3.1.1 配置 profile bean

Spring 所提供的解决方案与构建时的方案并没有太大的却别,只不过 Spring 并不是在构建的时候做出决策,而是等到运行时再来决定。

在 Java 配置中,可以使用 @Profile 注解指定某个 bean 属于哪一个 profile。

配置 profile bean

XML 中配置多个 profile

XML 配置bean

3.1.2 激活profile

Spring 在确定哪个 profile 处于激活状态时,需要依赖两个独立的属性:

spring.profiles.activespring.profiles.default

有多种方式来设置这两个属性:

  • 作为 DispatcherServlet 的初始参数
  • 作为 WEB 应用的上下文参数
  • 作为 JNDI 条目
  • 作为 JVM 的系统属性
  • 在集成测试类上,使用 @ActiveProfiles 注解设置

这里写图片描述

3.2 条件化的bean

条件化的 bean 指的就是条件化的创建 bean,使用 @Conditional 注解指定实现了 Condition 接口的类即可实现(在 matches() 方法内通过返回 boolean 值类决定 bean 的创建与否)。

具体实现和详细内容可查阅相关资料。

3.3 处理自动装配的歧义性

自动装配能帮我们减少显示配置的数量,不过仅有一个 bean 所匹配所需的结果时,自动装配才是有效的。如果不仅有一个 bean 能够满足匹配结果的话,这种歧义性会阻碍 Spring 自动装配属性、构造器参数或方法参数。

当发生歧义性时,Spring 提供了多种可选方案来解决这样的问题。你可以将可选 bean 中的某一个设为首选(primary) 的 bean,或者使用限定符(qualifier)来帮助 Spring 将可选的 bean 的返回缩小到只有一个 bean。

3.3.1 标示首选的 bean

使用注解 @Component 能够将所标记的 bean 设置为首选的。

@Component 注解可应用在 组件扫描、JavaConfig、或 XML 上

标示首选的 bean

3.3.2 限定自动装配的 bean

在自动装配注解上 @Autowired 上使用 @Qualifier 注解可以限定自动装配的 bean。

当 bean 没有指定限定符时默认和 beanID 一致,这样将会导致自动装配下的 @Qualifier 注解所指定的限定符与 bean 的类名紧密的耦合在一起,对类名称的任意改动都会导致限定符失效。

限定自动装配的 bean 可通过以下两种方法:

  • 使用自定义的限定符
  • 使用自定义的限定符注解

代码示例(使用自定义的限定符)

指定限定符的 bean

package spring.tutorial.bean.assem.advanced.permission.impl;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import spring.tutorial.bean.assem.advanced.manager.impl.BasePermission;
import spring.tutorial.util.Constants;

@Component
@Qualifier(Constants.PERMISSION_MANAGE_SERVICE_REQUEST)
public class PermissionOfManagerServiceRequest extends BasePermission {

    @Override
    public Boolean checkPermission(String userType, String perssion) {
        return super.checkPermission(userType, perssion);
    }
}
package spring.tutorial.bean.assem.advanced.permission.impl;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import spring.tutorial.bean.assem.advanced.manager.impl.BasePermission;
import spring.tutorial.util.Constants;

@Component
@Qualifier(Constants.PERMISSION_OPERATE_OWNER)
public class PermissionOfOperateOwner extends BasePermission {
}

依赖注入

package spring.tutorial.bean.assem.advanced.manager.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import spring.tutorial.bean.assem.advanced.manager.IServiceRequest;
import spring.tutorial.bean.assem.advanced.permission.IPermission;
import spring.tutorial.util.Constants;

@Component
public class ServiceRequest implements IServiceRequest {

    @Autowired
    @Qualifier(Constants.PERMISSION_MANAGE_SERVICE_REQUEST)
    private IPermission iPermission;

    @Override
    public Boolean accessUri(String userType) {
        return iPermission.checkPermission(userType, Constants.PERMISSION_MANAGE_SERVICE_REQUEST);
    }
}

使用自定义的限定符注解

使用自定义限定符的注解会经历如下步骤:

1 声明自定义限定符注解

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Cold {
}

@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Creamy {
}

2 使用自定义限定符的注解标识 bean

@Component
@Cold
@Creamy
public class IceCream implements Desert {

}

3 装配指定限定符注解

@Autowired
@Cold
@Creamy
private Desert desert;

3.4 bean的作用域

Spring 定义了多种作用域,可以给予这些作用域创建 bean,包括:

  • 单例(Singleton):在整个应用中,只会创建 bean 的一个实例。
  • 原型(Prototype):每次注入或者通过 Spring 应用上下文获取的时候,都会创建一个新的 bean 实例
  • 会话(Session):在 WEB 应用中,为每个会话创建一个 bean 实例。
  • 请求(Request):在 WEB 应用中,为每个请求创建一个 bean 实例。

【示例代码】

3.5 运行时值注入

之前在讨论 DI 的时候,通常来将是将一个对象与另一个对象进行关联。

Bean 装配的另外一个方面指的是将一个值注入到 bean 的属性或构造器参数中。

有时候通过硬编码可以实现上述需求,但有时我们希望尽量避免硬编码,让这些值在运行时再确定。为了实现这些功能,Spring 提供了两种在运行时求值的方式:

  • 属性占位符(property placeholder)
  • Spring 表达式语言(SpEL)

3.5.1 注入外部的值

使用 Spring 的 Environment 来检索属性

实现 Spring 的 Environment 来检索属性,实现以下步骤即可:

  • 在配置类中,使用注解 @PropertySource 来声明属性源
  • 在配置类中,注入 Environment 对象
  • 在声明 bean 时,使用 env 对象来检索属性

示例代码:

配置类:

package spring.tutorial.bean.assem.advanced;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;

import spring.tutorial.bean.assem.advanced.manager.IMail;
import spring.tutorial.bean.assem.advanced.manager.impl.Mail;

@Configuration
@ComponentScan
@PropertySource("classpath:/application.properties") // 声明属性源
public class JavaConfig {

    @Autowired
    private Environment env; // 注入 Environment 对象

    @Bean
    public IMail mail() {
        Mail mail = new Mail();
        mail.setHost(env.getProperty("mail.host")); // 使用 Environment 对象来检索属性
        mail.setUser(env.getProperty("mail.user"));
        mail.setUser(env.getProperty("mail.password", Integer.class));
        return mail;
    }
}

getProperty() 方法的四种重载形式

  • String getProperty(String key)
  • String getProperty(String key, String defaultValue)
  • String getProperty(String key, Class<T> type)
  • String getProperty(String key, Class<T> type, T defaultValue)
解析属性占位符

待续

面向切面的 Spring

4.1 什么是面向切面编程

AOP 可以把横切关注点与业务逻辑相分离,从而实现这些横切关注点与它们所影响的对象之间的解耦。

横切关注点

横切关注点可以被模块化为特殊的类,这些类被称为切面。

使用 AOP 将切面与业务逻辑解耦有以下好处:

  • 每个关注点都集中于一个地方,而不是分散在多出代码中
  • 服务模块更加简洁了,它们只需要关注核心功能

4.1.1 定义 AOP 术语

通知(Advice):通知定义了切面是什么(完成什么工作)以及何时使用
连接点(Join point):应用执行过程中能够插入切面的一个点
切点(Poincut):定义了哪些连接点会得到通知,相当与定义了何处
切面(Aspect):切面是通知和切点的结合,通知和切点共同定义了切面的全部内容-它是什么、在何时和何处完成其功能

4.1.2 Spring 对 AOP 的支持

Spring 提供了4种类型的 AOP 支持:

  • 基于代理的经典 Spring AOP
  • 纯 POJO 切面
  • @AspectJ 注解驱动的切面
  • 注入式 AspectJ 切面(适用于 Spring 各版本)

在引入了简单的声明式 AOP 和基于注解的 AOP 之后,Spring 经典的 AOP 看起来就显得非常的笨重和复杂,所以采用后两者来替代基于代理的经典 Spring AOP。

Spring AOP 框架的一些关键点:

  • Spring 通知是 Java 编写的
  • Spring 在运行时通知对象
  • Spring 只支持方法级别的连接点

4.2 通过切点来选择连接点

切点用于准确定位应该在什么地方应用切面的通知。

关于 Spring AOP 的 AspectJ 切点,最重要的一点就是 Spring 仅支持 AspectJ 切点指示器(poincut)的一个子集。

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

Spring AOP 所支持的 AspectJ 切点指示器

通过上面的介绍,我们可以看到只有 execution 指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的。

4.2.1 编写切点

设置当 perform() 方法执行时触发通知的调用:

切点

配置的切点仅匹配 concert 包:

within()指示器限制切点范围

4.2.2 在切点中选择 bean

使用 bean() 指示器,我们可以在切点表达式中使用 bean 的 ID 或 bean 的名称作为参数来限制切点只匹配特定的 bean

在执行 Performance 的 perform() 方法时应用通知,但限定 bean 的 ID 为 woodstock:

切点中选择bean

排除特定 ID 的 bean 应用通知:

切点排除特定bean

4.3 使用注解创建切面

4.3.1 定义切面

定义切面:

  1. 使用 @Aspect 注解,声明一个 POJO 类为切面,在切面中定义切点(使用 @Pointcut 注解)以及通知。
  2. 在配置类中使用 @EnableAspectJAutoProxy 注解启用 AspectJ 注解的自动代理。

通知所使用的注解方法:

通知所使用的注解方法

程序示例:

切面的定义:

package spring.tutorial.aop.annotation;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class Audience {

    @Pointcut("execution(* spring.tutorial.aop.annotation.manager.IPerformance.perform(..))")
    public void performance() {}

    @Before("performance()")
    public void silenceCellPhone() {
        System.out.println("Silencing cell phone");
    }

    @Before("performance()")
    public void takeSeats() {
        System.out.println("Taking seats");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("CLAP CLAP CLAP!!");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("Demanding a refund");
    }
}

启用自动代理:

package spring.tutorial.aop.annotation;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
@ComponentScan
public class JavaConfig {

}

Note:proxyTargetClass 设置为 false,采用 JDK 动态代理;设置为 true,采用 CGLib 代理

4.3.2 创建环绕通知

环绕通知是最为强大的通知类型,它能够让你所编写的逻辑将被通知的目标方法完全包装起来,实际上就像在一个通知方法中同时编写前置通知和后置通知。

程序示例:

package spring.tutorial.aop.annotation;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AdviceAround {

    @Pointcut("execution(* spring.tutorial.aop.annotation.manager.IPerformance.sayHello(..))")
    public void sayHello() {}

    @Around("sayHello()")
    @param ProceedingJoinPoint jp 调用改参数将控制权交给被通知的方法
    public void watchSayHello(ProceedingJoinPoint jp) {
        try {
            System.out.println("before: first");
            System.out.println("before: second");
            jp.proceed();
            System.out.println("afterReturn: return");
        } catch(Throwable e) {
            System.out.println("throw exception");
        }
    }
}

4.3.3 处理通知中的参数

此处笔记待完善…

5 构建 Spring Web 应用程序

5.1 Spring MVC起步

5.1.1 跟踪 Spring MVC 的请求

DispatcherServlet 是 Spring MVC 的核心,它负责将请求路由到其他的组件中。
请求使用 Spring MVC 所经历的所有站点

这里写图片描述

前端控制器 DispatcherServlet ,会根据请求所携带的 URL 信息查询一个或多个处理器映射(handler maapping)来确定将请求发给哪个控制器。

5.1.2 搭建Spring MVC

搭建 SpringMVC 的第一步便是配置 DispatcherServlet。

在 Servlet 3 之前,配置 DispatcherServlet 的方式一般都是通过 web.xml 文件,而在 Servlet 3之后,这种方式已经不是唯一的方案了,我们采取使用 Java 来配置 DispatcherServlet。

代码示例:

配置DispatcherServlet

package spring.tutorial.web.base;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * 加载应用中的其他 bean,这些 bean 通常是驱动应用的后端中间层和数据层组件
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] {RootConfig.class};
    }

    /**
     * 加载配置类或配置文件中声明的 bean,通常是视图解析器、控制器等
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] {WebConfig.class};
    }

    /**
     * 将一个或多个路径映射到 DispatcherServlet 上
     * "/" 表示为应用的默认 servlet,它会处理进入应用的所有请求
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

初始化 ServletConfig,启用 Spring MVC

package spring.tutorial.web.base;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@Configuration
@ComponentScan
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    /**
     * 配置 JSP 视图解析器
     * @return
     */
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    /**
     * DispatcherServlet 将静态资源的请求转发到 servlet 容器中默认的 servlet 上,
     * 而不是使用 dispatcherServlet 本身来处理请求
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configure) {
        configure.enable();
    }

}

启用 Spring MVC 的方式:

  • 使用 XML 配置,使用 启用注解驱动的 Spring MVC
  • 使用注解配置,使用注解 @EnableWebMvc 标注配置类来启用 Spring MVC

RootConfig配置

package spring.tutorial.web.base;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class RootConfig {

}

一般 web 相关的配置,通过 DispatcherServlet 创建的应用上下文已经配置好,在 RootConfig 中这里只是最简单的配置。

5.2 编写基本的控制器

代码示例:

package spring.tutorial.web.base.controller;

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

@Controller
@RequestMapping(value="/user")
public class UserController {

    @ResponseBody
    @RequestMapping(value={"/", "/hello"}, method=RequestMethod.GET)
    public String sayHello() {
        return "say hello";
    }

    @RequestMapping(value="/helloWorld", method=RequestMethod.GET)
    public String helloWorld() {
        return "helloWorld";
    }

}
  • @Controller:声明一个 POJO 类为 bean
  • @RequestMapping:在类级别上,该注解对类下的所有方法适用;在方法级别上,标识方法的访问路径。
  • @ResponseBody:完成对象到响应报文的转换。上述示例代码中,sayHello() 方法将返回 “say hello” 字符串,而 helloWorld() 方法将返回 “helloWorld.jsp”

5.3 接受请求的输入

Spring MVC 允许以多种方式将客户端中的数据传送到控制器的处理器方法中,包括:

  • 查询参数:使用注解 @RequestParam 标识
  • 路径参数:使用注解 @PathVariable 标识
  • 表单参数:使用注解 @ResquestBody 标识,用于请求体到对象之间的转换

代码示例:

package spring.tutorial.web.base.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import spring.tutorial.web.base.dao.User;

@Controller
@RequestMapping("/param")
public class ParameterController {

    /**
     * `@RequestParam` 接收单个的查询参数
     * 
     * @param pageSize
     * @param pageNum
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/queryParam", method = RequestMethod.GET)
    public String getQueryParam(@RequestParam(value = "pageSize", required = true) Integer pageSize,
            @RequestParam(value = "pageNum", defaultValue = "1", required = false) Integer pageNum) {
        return String.format("pageSize is %s, pageNum is %s", pageSize, pageNum);
    }

    /**
     * `@PathVariable` 注解接收路径参数
     * 
     * @param pageNum
     * @param pageSize
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/pathVariableParam/pageNum/{pageNum}/pageSize/{pageSize}")
    public String getPathVariableParam(@PathVariable(value = "pageNum") Integer pageNum,
            @PathVariable(value = "pageSize") Integer pageSize) {
        return String.format("pageSize is %s, pageNum is %s", pageSize, pageNum);
    }

    /**
     * `@RequestBody` 注解将请求体转换为对象
     * 
     * @param user
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/requestBoay", method = RequestMethod.POST)
    public String getRequestBody(@RequestBody User user) {
        return "";
    }

    /**
     * `@CookieValue` 注解接收 Cookie 值
     * 
     * @param sessionId
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/cookieValue")
    public String getCookieValue(@CookieValue(value = "JSESSIONID", defaultValue = "") String sessionId) {
        return String.format("sessionId is %s", sessionId);
    }

    /**
     * `@RequestHeader` 注解接收请求头
     * 
     * @param authorization
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/headerValue", method = RequestMethod.GET)
    public String getHeaderValue(@RequestHeader(value = "Authorization") String authorization) {
        return String.format("Authorization is %s", authorization);
    }

    /**
     * 所有的请求信息都可以通过 HttpServletRequest 对象来获取
     * 
     * @param httpServletRequest
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/requestInfo", method = RequestMethod.GET)
    public String getRequestInfo(HttpServletRequest httpServletRequest) {
        return String.format("content-type is %s", httpServletRequest.getContentType());
    }

}

7 Spring MVC 的高级技术

7.1 Spring MVC配置的替代方案

7.1.1 自定义 DispatcherServlet 配置

我们通过集成 AbstractAnnotationConfigDispatcherServletInitializer 类并重载3个方法,便可以配置好 DispatcherServlet。
另外,我们还可以通过重写 customizeRegistration 方法来实现自定义的 DispatcherServlet。

代码示例

package spring.tutorial.web.base;

import javax.servlet.ServletRegistration.Dynamic;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * 加载应用中的其他 bean,这些 bean 通常是驱动应用的后端中间层和数据层组件
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] {RootConfig.class};
    }

    /**
     * 加载配置类或配置文件中声明的 bean,通常是视图解析器、控制器等
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] {WebConfig.class};
    }

    /**
     * 将一个或多个路径映射到 DispatcherServlet 上
     * "/" 表示为应用的默认 servlet,它会处理进入应用的所有请求
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * 通过该方法,我们可以对 DispatcherServlet 进行额外的配置,从而实现自定义 DispatcherServlet 配置
     * (1). 调用 setLoadOnStartup() 设置 load-on-startup 优先级
     * (2). 调用 setInitParameter() 设置初始化参数
     * (3). 调用 setMultipartConfig() 配置 Servlet 3.0 对 multipart的支持
     */
    @Override
    protected void customizeRegistration(Dynamic registration) {
        super.customizeRegistration(registration);
    }
}

7.1.2 添加其他的 Servlet 和 Filter

7.1.3 在 web.xml 中声明 DispatcherServlet

7.2 处理 multipart 形式的数据

multipart 格式的数据会将一个表单拆分为多个部分(part),每个部分对应一个输入域。在一般的表单输入域中,它所对应的部分中会放置文本型的数据,但是如果是上传文件的话,它所对应的部分则为二进制。

multipart 格式的请求体

这里写图片描述

7.2.1 配置 multipart 解析器

DispatcherServlet 并没有实现任何解析 multipart 请求数据的功能。在 Spring 中这个功能由实现了 MultipartResolver 接口的类来完成。

从 Spring 3.1 开始,Spring 内置了两个 MultipartResolver 的实现供我们选择:

  • CommonMultipartResolver:使用 Jakarta Commons FileUpload 解析 multipart 请求
  • StandardServletMultipartResolver:依赖于 Servlet 3.0 对 multipart 请求的支持(始于 Spring 3.1)

使用 Servlet 3.0 解析 multipart

使用 servlet 3.0 解析 multipart 请求只需遵循以下 2 步:

  1. 将 MultipartResolver 声明为 bean
  2. 重载 customizeRegistration 方法并配置 MultipartResolver 的工作方式

代码示例

声明 MultipartResolver 的实现类为 bean

package spring.tutorial.web.base;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;

@Configuration
@ComponentScan
public class RootConfig {

    @Bean
    public MultipartResolver multipartResolver() {
        return new StandardServletMultipartResolver();
    }

}

customizeRegistration 中配置 MultipartResolver

package spring.tutorial.web.base;

import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration.Dynamic;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * 加载应用中的其他 bean,这些 bean 通常是驱动应用的后端中间层和数据层组件
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] {RootConfig.class};
    }

    /**
     * 加载配置类或配置文件中声明的 bean,通常是视图解析器、控制器等
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] {WebConfig.class};
    }

    /**
     * 将一个或多个路径映射到 DispatcherServlet 上
     * "/" 表示为应用的默认 servlet,它会处理进入应用的所有请求
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    /**
     * 通过该方法,我们可以对 DispatcherServlet 进行额外的配置,从而实现自定义 DispatcherServlet 配置
     * (1). 调用 setLoadOnStartup() 设置 load-on-startup 优先级
     * (2). 调用 setInitParameter() 设置初始化参数
     * (3). 调用 setMultipartConfig() 配置 Servlet 3.0 对 multipart的支持
     */
    @Override
    protected void customizeRegistration(Dynamic registration) {
        /**
         * 配置 multipart 请求
         * 可以接受的参数有:
         * 临时目录:临时目录路径(必填)
         * 上传文件的最大容量:以字节为单位,默认是没有限制的。
         * 整个multipart请求的最大容量:以字节为单位,不会关心有多少个part以及每个part的大小,默认是没有限制的。
         * 是否写入临时文件:在上传的过程中,如果文件大小达到了一个指定最大容量,将会写入到临时文件路径中,默认值为0,也就是所有上传的文件都会写入到磁盘上。
         */
        registration.setMultipartConfig(new MultipartConfigElement("D:/workspace/kaifei/tmp"));
    }
}

配置Jakarta Commons FileUpload multipart解析器

若需要将应用部署到非 servlet 3.0 以上的容器中,我们就需要使用 CommonsMultipartResolver 来解析 multipart 的请求了。

配置 CommonsMultipartResolver 解析器,需要引入以下 jar 包:

这里写图片描述

声明 multipart 解析器

package spring.tutorial.web.base;

import java.io.IOException;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;

@Configuration
@ComponentScan
public class RootConfig {

//  @Bean
//  public MultipartResolver multipartResolver() {
//      return new StandardServletMultipartResolver();
//  }

    @Bean
    public MultipartResolver multipartResolver() throws IOException {
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
        // 临时目录
        commonsMultipartResolver.setUploadTempDir(new FileSystemResource("D:/workspace/kaifei/tmp"));
        // 上传文件大小
        commonsMultipartResolver.setMaxUploadSize(10240000);
        // 所占用的最大内存,0 表示不管文件大小如何,所有文件都会写入磁盘
        commonsMultipartResolver.setMaxInMemorySize(0);

        return commonsMultipartResolver;
    }
}

7.2.2 处理 multipart 请求

Spring 提供了 MultipartFile 接口,它相较与原始 byte[] 提供了更为丰富的内容。 另外,可以使用 @RequestPart 注解来接收上传文件参数。

代码示例

package spring.tutorial.web.base.controller;

import java.io.File;
import java.io.IOException;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import spring.tutorial.web.base.vo.UserVO;

@Controller
@RequestMapping(value="/test")
public class UserController {

    @ResponseBody
    @RequestMapping(value="/upload/byte", method=RequestMethod.POST)
    public String uploadFile1(
            @RequestPart("avatar") byte[] file,
            @RequestParam(value="name", required=true) String name,
            @RequestParam(value="gender", required=true) String gender) {
        return String.format("filesize is %s, name is %s, gender is %s", file.length, name, gender);
    }

    @ResponseBody
    @RequestMapping(value="/upload/multipartFile", method=RequestMethod.POST)
    public String uploadFile2(
            @RequestPart("avatar") MultipartFile file,
            @RequestParam(value="name", required=true) String name,
            @RequestParam(value="gender", required=true) String gender) {
        try {
            // 将文件保存到文件系统
            file.transferTo(new File("D:/workspace/kaifei/upload/" + file.getOriginalFilename()));
        } catch (IllegalStateException | IOException e) {
            e.printStackTrace();
        }
        return String.format("filename is %s, name is %s, gender is %s", file.getOriginalFilename(), name, gender);
    }


    @ResponseBody
    @RequestMapping(value="/upload/multipartFile2", method=RequestMethod.POST)
    public String uploadFile3(UserVO requestVO) {
        return String.format("filename is %s, name is %s, gender is %s", requestVO.getAvatar().getOriginalFilename(), requestVO.getName(), requestVO.getGender());
    }
}

如果你需要将应用部署到Servlet 3.0的容器中,那么会有 MultipartFile 的一个替代方案。
Spring MVC也能接受 javax.servlet.http.Part 作为控制器方法的参数。

在此省略其实现,可自行查阅相关资料。

7.3 处理异常

Spring 提供了多种方式将异常转化为响应:

  • 特定的 Spring 异常将会自动映射为指定的 HTTP 状态码
  • 异常上可以添加 @ResponseStatus 注解,从而将其映射为某一个 HTTP 状态码
  • 在方法上可以添加 @ExceptionHandler 注解,使其用来处理异常

控制器通知(controller advice)是任意带有 @ControllerAdvice 注解的类,这个类会包含一个或多个如下类型的方法:

  • @ExceptionHandler 注解标注的方法
  • @InitBinder 注解标注的方法
  • @ModelAttribute 注解标注的方法

在带有 @ControllerAdvice 注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有 @RequestMapping 注解的方法上。

使用控制器通知捕获异常的方式:

  • 使用注解 @ControllerAdvice 标注一个 POJO 类为控制器通知类
  • 使用注解 @ExceptionHandler 标注控制器通知类中的一个方法,用来处理异常

代码实例

控制器通知类

package spring.tutorial.web.base.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@ControllerAdvice
public class AppExceptionHandler {

    @ExceptionHandler(TutorialException.class)
    @ResponseBody
    public String handlerTutorialException(TutorialException e) {
        return String.format("statusCode is %s, errorCode is %s, errorMsg is %s", e.getStatusCode(), e.getErrorCode(), e.getErrorMsg());
    }
}

10 Spring 中使用 JDBC 操作数据库

10.1 数据访问模板化

Spring 将数据的访问过程中固定的和可变的部分明确划分为两个不同的类:模板和回调。模板管理过程中固定的部分,而回调处理自定义的数据访问代码。

这里写图片描述

10.2 配置数据源

10.2.1 使用 JNDI 数据源

这中配置的好处在于数据库的配置完全在应用程序之外,对开发人员透明,而且还支持热切换。

102.2 使用数据源连接池

这种配置的好处在于,连接不用频繁的打开和关闭,减少了数据库资源的消耗。

以下演示使用了 c3p0 数据源连接池

dependency

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
        </dependency>
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.2</version>
        </dependency>

        <!-- JdbcTemplate -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.3.8.RELEASE</version>
        </dependency>

dataSource

package spring.tutorial.jdbc;

import java.beans.PropertyVetoException;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jndi.JndiObjectFactoryBean;

import com.mchange.v2.c3p0.ComboPooledDataSource;

import spring.tutorial.util.Constants;

@Configuration
@ComponentScan
public class DataSourceConfig {

    @Bean
    public DataSource dataSource() {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        try {
            dataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
            dataSource.setJdbcUrl("jdbc:mysql://localhost/data?useUnicode=true&characterEncoding=UTF-8&useSSL=true&serverTimezone=UTC");
            dataSource.setUser("root");
            dataSource.setPassword("admino0o0oo0");
        } catch (PropertyVetoException e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    @Bean
    public JdbcOperations jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

10.2.3 使用 JDBC 驱动的数据源

此种连接方式要么是单线程的要么每次连接都会创建新的连接,消耗性能,所以在生产环境不可取,建议使用连接池

10.2.4 使用嵌入式数据库

嵌入式数据库作为应用的一部分运行,而不是应用连接的独立数据库服务器。尽管在生产环境的设
置中,它并没有太大的用处,但是对于开发和测试来讲,嵌入式数据库都是很好的可选方案。
这是因为每次重启应用或运行测试的时候,都能够重新填充测试数据。

10.3 Spring 中使用 JDBC

Spring 的 JDBC 模板承担了资源管理和异常处理的工作,从而简化了原始的 JDBC 代码,让我们只需要关心逻辑代码的实现。

代码示例

interface

package spring.tutorial.jdbc.dao.repository;

import java.util.List;

import spring.tutorial.jdbc.dao.User;

public interface IUserRepository {


    List<User> getUserList(Integer userId);

    User getUserDetail(Integer userId);

}

implement

package spring.tutorial.jdbc.dao.repository.impl;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import spring.tutorial.jdbc.dao.User;
import spring.tutorial.jdbc.dao.repository.IUserRepository;

@Repository
public class UserRepositoryImpl implements IUserRepository {

    @Autowired
    private JdbcOperations jdbcOperations;

    /**
     * 查询用户列表
     * 
     * @author hkf
     */
    @Override
    public List<User> getUserList(Integer userId) {
        List<User> users = new ArrayList<User>();

        String sql = "select * from user where id > ?";
        users = jdbcOperations.query(sql, new RowMapper<User>() {

            @Override
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                if (rs != null) {
                    User user = new User();
                    user.setId(rs.getInt("id"));
                    user.setMail(rs.getString("mail"));
                    return user;
                }
                return null;
            }

        }, new Object[]{userId});


        return users;
    }

    /**
     * 查询用户详情
     * 
     * @author hkf
     */
    @Override
    public User getUserDetail(Integer userId) {
        String sql = "select * from user where id = ?";
        User user = jdbcOperations.queryForObject(sql, new RowMapper<User>() {

            @Override
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                if (rs != null) {
                    User user = new User();
                    user.setId(rs.getInt("id"));
                    user.setMail(rs.getString("mail"));
                    return user;
                }
                return null;
            }

        }, new Object[]{userId});

        return user;
    }
}

11 使用 ORM 持久化数据

11.1 在 Spring 中集成 Hibernate

11.1.1 声明 Hibernate 的 Session 工厂

获取 Hibernate Session 对象的标准方式是借助于 Hibernate SessionFactory 接口的实现类,SessionFactory 主要负责 Hibernate Session 的打开、关闭以及管理。

从 Spring 3.1 开始,Spring 提供了三个 Session 工厂 bean 供我们选择

  • org.springframework.orm.hibernate3.LocalSessionFactoryBean
  • org.springframework.orm.hibernate3.annotation.AnnotationSessionFactory
  • org.springframework.orm.hibernate4.LocalSessionFactoryBean

在此我们使用 org.springframework.orm.hibernate4.LocalSessionFactoryBean 来声明 sessionFactory

示例代码

dependency

        <!-- spring-orm -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.3.9.RELEASE</version>
        </dependency>

        <!-- hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.2.10.Final</version>
        </dependency>

声明 sessionFactory

package spring.tutorial.orm;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.hibernate4.LocalSessionFactoryBean;
import spring.tutorial.jdbc.DataSourceConfig;

import javax.sql.DataSource;
import java.util.Properties;


@Configuration
@ComponentScan
@Import({DataSourceConfig.class})
public class ORMConfig {

    @Bean
    public LocalSessionFactoryBean sessionFactoryBean(DataSource dataSource) {
        LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
        sfb.setDataSource(dataSource);
        sfb.setPackagesToScan(new String[] {"spring.tutorial.orm.domain", "spring.tutorial.orm.repository"});
        Properties properties = new Properties();
        properties.setProperty("dialect", "org.hibernate.dialect.MySQLDialect");
        properties.setProperty("show_sql", "true");
        properties.setProperty("format_sql", "true");
        sfb.setHibernateProperties(properties);
        return sfb;
    }
}

11.1.2 构建不依赖于 Spring 的 Hibernate 代码

所谓构建不依赖于 Spring 的 HIbernate 代码指的就是:在早期,编写 Repository 类会涉及到使用 Spring 的 HibernateTemplate,HibernateTemplate 能够保证每个事物使用同一个 session,但是这种方式也会把我们 Repository 的实现和 Spring 耦合在一起。现在最佳实践便是将 Hibernate 的 SessionFactory 装配到 Repository 中,并使用它来获取 session。

代码示例

repository

package spring.tutorial.orm.repository.impl;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;
import org.springframework.stereotype.Repository;
import spring.tutorial.orm.domain.BlockTx;
import spring.tutorial.orm.repository.IBlockTxRepository;

import java.util.List;

@Repository
@SuppressWarnings({"unused", "unchecked"})
public class BlockTxRepository implements IBlockTxRepository {

    // 使用 SessionFactory 来获取 Session
    private SessionFactory sessionFactory;

    public BlockTxRepository(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    private Session currentSession() {
        // TODO: 使用 @EnableTransactionManagement 和 @Transactional 来管理事物
        try {
            return this.sessionFactory.getCurrentSession();
        } catch (HibernateException e) {
            return this.sessionFactory.openSession();
        }
//        return this.sessionFactory.getCurrentSession();
    }

    @Override
    public List<BlockTx> findAll() {
        return listAndCast(currentSession().createQuery("from BlockTx"));
    }

    private static <T> List<T> listAndCast(Query q) {
        return q.list();
    }
}

domain

package spring.tutorial.orm.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;

@Entity
@Table(name = "block_tx")
public class BlockTx {

    @Id
    private Integer id;

    @Column(name = "tx_id")
    private String txId;

    @Column(name = "account_sender")
    private String consumer;

    @Column(name = "account_receive")
    private String provider;

    @Column(name = "tx_time")
    private Date txTime;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTxId() {
        return txId;
    }

    public void setTxId(String txId) {
        this.txId = txId;
    }

    public String getConsumer() {
        return consumer;
    }

    public void setConsumer(String consumer) {
        this.consumer = consumer;
    }

    public String getProvider() {
        return provider;
    }

    public void setProvider(String provider) {
        this.provider = provider;
    }

    public Date getTxTime() {
        return txTime;
    }

    public void setTxTime(Date txTime) {
        this.txTime = txTime;
    }
}

关于 Hibernate 的用法示例,请自行查阅相关资料。

11.2 Spring 与 Java 持久化 API(JPA)

11.2.1 配置实体管理工厂

基于 JPA 的应用程序需要使用 EntityManagerFactory 的实现类来获取 EntityManager 实例。

JPA 定义了靓仔类型的实体管理器:

这里写图片描述

代码实例

配置实体管理工厂

package spring.tutorial.orm;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import spring.tutorial.jdbc.DataSourceConfig;

import javax.sql.DataSource;


@Configuration
@ComponentScan
@Import({DataSourceConfig.class})
public class ORMConfig {

//    @Bean
//    public LocalSessionFactoryBean sessionFactoryBean(DataSource dataSource) {
//        LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
//        sfb.setDataSource(dataSource);
//        sfb.setPackagesToScan(new String[] {"spring.tutorial.orm.domain", "spring.tutorial.orm.repository"});
//        Properties properties = new Properties();
//        properties.setProperty("dialect", "org.hibernate.dialect.MySQLDialect");
//        properties.setProperty("show_sql", "true");
//        properties.setProperty("format_sql", "true");
//        sfb.setHibernateProperties(properties);
//        return sfb;
//    }

    @Bean
    public JpaVendorAdapter JpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setDatabase(Database.MYSQL);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.MySQLDialect");
        return adapter;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(DataSource dataSource, JpaVendorAdapter adapter) {
        LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
        emfb.setDataSource(dataSource);
        emfb.setJpaVendorAdapter(adapter);
        emfb.setPackagesToScan("spring.tutorial.orm.domain");
        return emfb;
    }
}

11.2.2 编写基于 JPA 的 Repository

使用纯粹的 JPA 方式远胜于基于模板的 JPA。

代码实例

package spring.tutorial.orm.repository.impl;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import spring.tutorial.orm.domain.User;
import spring.tutorial.orm.repository.IUserRepository;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Repository
@Transactional
public class UserRepository implements IUserRepository {

    /**
     * 真正的EntityManager是与当前事务相关联的那一个,如果不
     * 存在这样的EntityManager的话,就会创建一个新的。这样的话,我们就能始终以线程安全
     * 的方式使用实体管理器。
     */
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public User getUserDetail(Integer id) {
        return entityManager.find(User.class, id);
    }
}

11.3 借助 Spring Data 实现自动化的 JPA Repository

借助 Spring Data 能够让我们只编写 Repository 接口就可以了,根本不再需要实现类。

使用 Spring Data JPA 只需要配置以下两项即可使用:

  • 在配置类上声明注解 @EnableJpaRepositories
  • 接口扩展自 JpaRepository

代码实例

dependency

        <!-- Spring data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
            <version>1.13.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>1.11.4.RELEASE</version>
        </dependency>

声明注解

package spring.tutorial.orm;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import spring.tutorial.jdbc.DataSourceConfig;

import javax.sql.DataSource;


@Configuration
@ComponentScan
// 如果发现了扩展自Repository的接口,它会自动生成(在应用启动的时候)这个接口的实现。
@EnableJpaRepositories(
        basePackages = "spring.tutorial.orm.repository",
        entityManagerFactoryRef = "entityManagerFactoryBean"
)
@Import({DataSourceConfig.class})
public class ORMConfig {

    /**
     * 配置 Hibernate 的 SessionFactory
     * @return
     */
//    @Bean
//    public LocalSessionFactoryBean sessionFactoryBean(DataSource dataSource) {
//        LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
//        sfb.setDataSource(dataSource);
//        sfb.setPackagesToScan(new String[] {"spring.tutorial.orm.domain", "spring.tutorial.orm.repository"});
//        Properties properties = new Properties();
//        properties.setProperty("dialect", "org.hibernate.dialect.MySQLDialect");
//        properties.setProperty("show_sql", "true");
//        properties.setProperty("format_sql", "true");
//        sfb.setHibernateProperties(properties);
//        return sfb;
//    }

    @Bean
    public JpaVendorAdapter JpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setDatabase(Database.MYSQL);
        adapter.setShowSql(true);
        adapter.setGenerateDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.MySQLDialect");
        return adapter;
    }

    /**
     * 配置 JPA 的 EntityManager
     *
     * @param dataSource
     * @param adapter
     * @return
     */
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(DataSource dataSource, JpaVendorAdapter adapter) {
        LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
        emfb.setDataSource(dataSource);
        emfb.setJpaVendorAdapter(adapter);
        emfb.setPackagesToScan("spring.tutorial.orm.domain");
        return emfb;
    }
}

扩展接口:JpaRepository

package spring.tutorial.orm.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import spring.tutorial.orm.domain.UserProfile;

import java.util.Date;
import java.util.List;

@Repository
@SuppressWarnings("unused")
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {

    List<UserProfile> findByLastAccessTimeGreaterThanAndPreferredContactMode(Date date, String perferredContactMode);
}

Note:Spring Data JPA 很棒的一点在于它能为持久化对象提供18个便利的方法来进行通用的 JPA 操作,而无需你编写任何持久化代码。Repository 实现类实在应用启动的时候生成的。

11.3.1 定义查询方法

在扩展了 JpaRepository 的接口中,我们遵循 Spring Data 的 DSL(domain specific language)规范来给方法命名,那么我们就不需要写方法的实现了,交由 Spring Data 来实现。而持久化的细节都是通过 Repository 方法的签名来描述的。

Repository 的方法是有一个动词、一个可选主题(Subject)、关键词By以及一个断言所组成。

Spring Data 允许在方法名中使用四种动词:get、read、find 和 count,其中前3种时同义,count会返回匹配对象的数量。

在断言中,会有一个或多个限制结果的条件,每个条件必须引用一个属性,并且还可以制定一种比较操作。

example:

  • List readByFirstNameOrLastName(String firstName, String lastName)
  • List readByFirstNameOrLastNameIgnoresCase(String firstName, String lastName)
  • List readByFirstNameOrLastNameOrderByLastNameDescFirstNameAsc(String firstName, String lastName)
  • List findPetsByBreedIn(List breed)
  • int countProductsByDiscontinuedTrue()
  • List findByShippingDateBetween(Date start, Date end)

代码示例

package spring.tutorial.orm.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import spring.tutorial.orm.domain.UserProfile;

import java.util.Date;
import java.util.List;

@Repository
@SuppressWarnings("unused")
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {

    List<UserProfile> findByLastAccessTimeGreaterThanAndPreferredContactMode(Date date, String perferredContactMode);
}

11.3.2 声明自定义查询

当 DSL 语言无法通过方法名进行恰当的描述时,我们可以使用 @Query 注解,为 Spring Data 提供要执行的查询。

代码示例

@Query("SELECT bt FROM BlockTx bt WHERE (bt.accountSend= :account OR bt.accountReceive = :account) AND (bt.txId = :keyword OR bt.blockId = :keyword)")
    Page<BlockTx> getAllBolckTxes(@Param("account") String account, @Param("keyword") String keyword, Pageable pageable);

11.3.2 混合自定义的功能

有些复杂的 SQL 无法通过 Spring Data 提供的 DSL 和 @Query 来实现,此时我们就需要使用 EntityManager 来实现,将 Spring Data JPA 和 EntityManager 混合起来使用。

这里写图片描述

18 使用 WebSocket 和 STOMP 实现消息功能

WebSocket 协议提供了一个套接字实现全双工通信的功能,这意味着服务器可以发送消息给浏览器,浏览器也可以发送消息给服务器。

Spring 4.0 为 WebSocket 通信提供了支持,包括:

  • 发送和接收消息的低层级 API
  • 发送和接收消息的高层 API
  • 用来发送消息的模板
  • 支持 SockJS,用来解决浏览器、服务器以及代理不支持 WebSocket 的问题

18.1 使用 Spring 的低层级 WebSocket API

使用 Spring WebSocket 只需要完成以下两步即可:

  1. 启用 WebSocket 并声明 handler
  2. 实现处理 WebSocket 的 handler

代码示例

dependency

        <!-- webSocket -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>4.3.9.RELEASE</version>
        </dependency>

        <!-- sockJS -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.3</version>
            <scope>runtime</scope>
        </dependency>

启用 WebSocket

package spring.tutorial.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(getMarcoHandler(), "/marco")
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Bean
    public MarcoHandler getMarcoHandler() {
        return new MarcoHandler();
    }

}

实现 Handler

package spring.tutorial.websocket;

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

public class MarcoHandler extends AbstractWebSocketHandler {


    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

        System.out.println("received message:" + message.getPayload());

        Thread.sleep(2000);

        session.sendMessage(new TextMessage("Polo!"));
    }
}

客户端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<button id="button1">click me</button>

<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/sockjs/1/sockjs.min.js"></script>
<script>
    $("#button1").click(function () {
//        var url = "ws://localhost:8080/spring/marco";
//        var sock = new WebSocket(url);
        var url = "http://localhost:8080/spring/marco";
        var sock = new SockJS(url);

        sock.onopen = function (event) {
            console.log('Opening');
            sayMarco();
        };

        sock.onmessage = function (e) {
            console.log('Received message:', e.data);
            setTimeout(function () {
                sayMarco();
            }, 2000)
        };

        sock.onclose = function (p1) {
            console.log('closing');
        };

        function sayMarco() {
            console.log('Sending Marco');
            sock.send('Marco');
        }
    });

</script>
</body>
</html>

18.2 应对不支持 WebSocket 的场景

SockJS 是 WebSocket 技术的一种模拟,在表面上,它尽可能的对应 WebSocket API,但是在低层它非常的智能,如果 WebSocket 技术不可用的话,就会选择另外的通信方式来模拟 WebSocket 的功能。

使用方式见 18.1 小节的代码示例

18.3 使用 STOMP 消息

STOMP 在 WebSocket 之上提供了一个基于帧的线路的格式层,用来定义消息的语义。

STOMP 帧由命令、一个或多个头消息以及负载所组成。

这里写图片描述

18.3.1 启用 STOMP 消息功能

使用 @EnableWebSocketMessageBroker 注解即可启用 STOMP 消息功能

代码示例

* 启用STOMP *

package spring.tutorial.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketMessageBrokerConfig extends AbstractWebSocketMessageBrokerConfigurer {

    /**
     * 注册 stomp 连接点
     * 客户端在发送消息或者订阅之前都得先连接到该 ndPoint
     *
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/stomp").setAllowedOrigins("*").withSockJS();
    }

    /**
     * 定义消息代理
     * 1. 定义应用目的地的前缀
     * 2. 定义代理目的地的前缀
     *
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/websocket");
    }
}

18.3.2 处理来自客户端的 STOMP 消息

Spring 引入 @MessageMapping 注解来处理 STOMP 消息。

代码示例

dependency

        <!-- jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.8.9</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.8.9</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.8.9</version>
        </dependency>

* 处理客户端消息并向客户端发送消息 *

package spring.tutorial.web.base.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import spring.tutorial.web.base.vo.MessageVO;
import spring.tutorial.web.base.vo.ResponseMessageVO;

import java.security.Principal;

@Controller
public class STOMPController {

    @Autowired
    private SimpMessageSendingOperations messaging;

    /**
     * send message to client
     * `@sendTo`:any alient which subscribe this theme will receive response
     *
     * @param messageVO
     * @return
     */
    @MessageMapping("/hello")
    @SendTo("/topic/hello")
    public ResponseMessageVO handlerHello(MessageVO messageVO) {
        String message = String.format("hello client! I have received message:%s", messageVO.getMessage());
        System.out.println(message);
        ResponseMessageVO responseMessageVO = new ResponseMessageVO();
        responseMessageVO.setResponseMessage(message);
        return responseMessageVO;
    }

    /**
     *  应用场景为:请求-响应模式
     *  客户端订阅某一个目的地,然后预期在这个目的地上获得一个一次性的响应(异步)
     *
     * @return
     */
    @SubscribeMapping("/subscribe/hello")
    public ResponseMessageVO handlerSubscribe(Principal principal) {
        String message = String.format("welcome!you have subscribe me。you are %s", principal.getName());
        ResponseMessageVO responseMessageVO = new ResponseMessageVO();
        responseMessageVO.setResponseMessage(message);
        return responseMessageVO;
    }

    @RequestMapping("/")
    public void broadcastMessage(MessageVO messageVO) {
        String message = String.format("hello %s, I an sending message to you", messageVO.getMessage());
        System.out.println(message);
        ResponseMessageVO responseMessageVO = new ResponseMessageVO();
        responseMessageVO.setResponseMessage(message);
        messaging.convertAndSend("/topic/broadcast", responseMessageVO);
    }
}

18.4 为目标用户发送消息

待完善…

18.5 处理消息异常

待完善…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值