Java Web 高级编程 - 第十二章 介绍Spring Framework

本章内容:

  • 了解Spring Framework
  • Spring Framework的优点
  • 应用上下文的定义
  • 启动Spring Framework的方法
  • 配置Spring Framework
  • 使用Bean definition profile

 

12.1 Spring Framework简介

Spring Framework是一个Java应用程序容器,它提供了许多有用的特性,例如控制反转、依赖注入、抽象数据访问、事务管理等。

12.1.1 控制反转和依赖注入

Spring Framework的核心特性之一就是对两个紧密相关观念的支持:控制反转(IoC)和依赖注入(DI)。

IoC是一个软件设计模式:组装器将在运行时而不是编译时绑定对象。当某些程序逻辑组件,例如ServiceA,依赖于拎一个程序逻辑组件ServiceB时,改以来将在应用程序运行时实现,而不是由ServiceA直接实例化ServiceB。通过使用这种方式,应用程序开发者可以针对一组接口进行编程,这样可以在不同的环境中进行切换,而无须重新编译代码。一个很方便的场景就是测试环境:可以再运行单元测试时提供“模拟”和“测试”服务;然后在部署到生产环境中时可以使用相同的代码,但是用“真正的”服务。

尽管理论上可以通过许多种方式实现Ioc,但DI是最常见的技术。通过使用DI,一段程序代码可以声明它依赖于另一块程序代码,然后组装器可以再运行时注入它以来的实例。

12.1.2 面向切面编程

因为Spring Framework负责处理实例化和依赖注入,所以它可以通过封装注入依赖的实例,使用其他行为对方法调用进行装饰。

例如,使用Spring Security时,它可以对方法进行注解,表示在这些方法上添加的安全限制,Spring Framework将使用必要的安全检查封装这些方法,以实现这些安全限制。还可以使用切面定义自己的横切关注点,Spring Framework将使用这些关注点装饰所有合适的方法。横切关注点是影响程序多个组件的关注点,通常与这些组件无关。面向切面编程式面向对象编程的补充,通过定义切面可以在应用程序中启用这些关注点,该定义指定了如何以及何时应用这些关注点。

12.1.3 数据访问和事务处理

Spring Framework提供了一组数据访问工具,它们可以简化关系数据库中Java对象的读取和持久化。尽管这些特性使单元测试数据访问变得极其容易,但是我们仍然需要使用特定供应商的SQL。通过Spring Data项目,在关系数据库以及MongoDB和Redis这样的NoSQL数据库中持久化对象变成了一个简单的任务。最后,Spring Framework还支持声明实物模型,添加了注解的方法的执行将被封装在事物中,如果方法抛出异常,事物也将会滚。使用Spring Framework的AOP支持可以实现该任务。

12.1.4 应用程序消息

在许多应用程序中,消息都是一个需要解决的重要关注点。例如,程序的特定部分可能需要知道程序的另一个部分是否执行了特定的操作。执行此操作的程序部分可以简单地依赖于所有对该操作感兴趣的程序,并调用它们的方法通知它们,但这种类型的紧耦合是难于维护的,并且很容易就变得混乱。

Spring Framework提供了一个松耦合的消息系统,它使用的是发布-订阅模式:系统中的组件通过订阅消息,声明它对该消息感兴趣,然后这些消息的生产者将会发布该消息,而无须关心谁对消息感兴趣。使用Spring Framework时,一个由Spring管理的bean可以通过实现一个通用接口订阅特定的消息类型,其他由Spring管理的对象可以发布这些消息到Spring Framework中,然后由Spring Framework将消息发送到已订阅的bean中。该系统也可以被扩展和配置为向跨应用程序的集群中发布消息。

12.1.5 Web应用程序的模型-视图-控制器模式

Spring Framework提供了一个模型-视图-控制器模式框架,它可以简化创建交互式Web应用程序的过程。控制器类的每个方法都被映射到了一个不同的请求URL、方法或请求的其他属性上。模型将以Map<String, Object>的形式从控制器传递到视图。控制器返回的视图或视图名称将使Spring把模型转发到合适的JSP视图。请求和URL路径参数将被自动转换为原始或复杂的控制器方法参数。

 

12.2 使用Spring Framework的原因

12.3 了解应用上下文

Spring Framework容器以一个或多个应用上下文的形式存在,由org.springframework.context.ApplicationContext接口表示。

一个应用上下文管理着一组bean、执行业务逻辑的Java对象、执行任务、持久化和获取持久化数据、响应HTTP请求等。由Spring管理的bean可以自动进行依赖注入、消息通知、定时方法执行、bean验证和执行其他关键的Spring服务。

http://docs.spring.io/spring/docs/current/javadoc-api/

Spring Framework Reference Documentation

一个Spring应用程序至少需要一个应用上下文,并且有时这就是所有的要求。不过,它也可以使用由多个应用上下文组成的层次结构。在这样的层次结构中,任何由Spring管理的bean都可以访问相同的应用上下文、父亲应用上下文、父亲的父亲应用上下文的bean(依次类推)。它们不能访问兄弟或者孩子应用上下文中的bean。这对于定义一组共享应用模块来说是非常有用的,它可以将应用模块彼此隔离开。

在Java EE Web应用程序中,Spring将使用派发器Servlet处理Web请求,该Servlet将把进入的请求委托给合适的控制器,并按需要对请求和响应实体进行转换。Web应用程序中可以使用任意数量的org.springframework.web.servlet.DispatcherServlet类实例。

每个DispatcherServlet实例都有自己的应用上下文,其中包含了对Web应用程序的ServletContext和它自己的ServletConfig的引用。

因为没有一个DispatcherServlet可以访问其他DispatcherServlet的应用上下文,所以通常可以在一个公共的根应用上下文中共向特定的bean。整个Web应用程序的全局应用上下文是所有DispatcherServlet应用上下文的父亲,它将通过org.springframework.web.context.ContextLoaderListener创建。它也有Web应用程序的ServletContext的引用,但因为它不属于任何特定的Servlet,所以它没有ServletConfig的引用。

 

12.4 启动Spring Framework

Spring Framework是一个容器,它可以运行在任何Java SE和EE容器中,并且作为应用程序的运行时环境。另外,如果其他容器一样,Spring必须被启动,它必须被启动并且需要得到如何运行它所包含的应用程序的指令。

配置和启动Spring Framework是两个不同的任务,并且相互独立,都可以通过多种不同的方式实现。当配置告诉Spring如何运行它所包含的应用程序时,启动进程将启动Spring并将配置指令传递给它。

在Java SE应用程序中,只有一种方式启动Spring:通过在应用程序的public static void main(String...) 方法中以编程的方式启动。

在Java EE应用程序中,有两种选择:可以通过XML撞见部署描述符启动Spring,也可以在ServletContainerInitializer中通过编程的方式启动Spring。

 

12.4.1 通过部署描述符启动Spring

<servlet>
    <servlet-name>springDispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/servletContext.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springDispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

 

该代码将为DispatcherServlet创建出单个Spring应用上下文,并指示Servlet在启动时初始化DispatcherServlet。在初始化的时候,DispatcherServlet将从/WEB-INF/servletContext.xml文件中加载上下文配置并启动应用上下文。

当然,这只会为应用程序创建出一个上下文,如之前所解释的,并不是很灵活。一个完整的部署描述符应如下所示:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/rootContext.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
    <servlet-name>springDispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/servletContext.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springDispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

 

ContextLoaderListener将在Web应用程序启动时被初始化,然后从contextConfigLocation上下文初始化参数指定的/WEB-INF/rootContext.xml文件中加载根应用上下文,并启动根应用上下文。

注意:contextConfigLocation上下文初始化参数不同于DispatcherServlet的contextConfigLocation Servlet初始化参数。它们并不冲突;前者作用于整个Servlet上下文,而后者只作用于它所指定的Servlet。由监听器创建的根应用上下文将自动被设置为所有通过DispatcherServlet创建的应用上下文的父亲上下文。

 

12.4.2 在初始化器中使用编程的方式启动Spring

WebApplicationInitializer

Interface to be implemented in Servlet 3.0+ environments in order to configure the ServletContext programmatically -- as opposed to (or possibly in conjunction with) the traditional web.xml-based approach.

Implementations of this SPI will be detected automatically by SpringServletContainerInitializer, which itself is bootstrapped automatically by any Servlet 3.0 container. See its Javadoc for details on this bootstrapping mechanism.

public class Bootstrap implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext rootContext = new XmlWebApplicationContext();
        rootContext.setConfigLocation("/WEB-INF/rootContext.xml");
        container.addListener(new ContextLoaderListener(rootContext));
        XmlWebApplicationContext servletContext = new XmlWebApplicationContext();
        servletContext.setConfigLocation("/WEB-INF/servletContext.xml");
        ServletRegistration.Dynamic dispatcher = container.addServlet(
                "springDispatcher", new DispatcherServlet(servletContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }
}

 

这个启动类在功能上等同于之前使用的部署描述符。

下面的启动类将Spring Java配置通过纯Java的方式启动和配置Spring。

public class Bootstrap implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext container) {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(com.wrox.config.RootContextConfiguration.class);
        container.addListener(new ContextLoaderListener(rootContext));
        AnnotationConfigWebApplicationContext servletContext = new AnnotationConfigWebApplicationContext();

        servletContext.register(com.wrox.config.ServletContextConfiguration.class);
        ServletRegistration.Dynamic dispatcher = container.addServlet("springDispatcher", new DispatcherServlet(servletContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }
}

 

 

12.5 配置Spring Framework

12.5.1 创建XML配置

bean是由Spring Framework管理的,所以这是在配置Spring Framework时我们主要需要完成的配置。

为了告诉Spring如何配置这些bean,我们需要使用<beans>XML命名空间。

这个简单的XML文件将告诉Spring实例化GreetingServiceImpl和HelloController,并将greetingServiceImpl bean注入到helloController bean的greetingService属性中。

元素<mvc:annotation-driven />将指示Spring使用@RequestMapping;@ResponseBody;@RequestParam;@ResponseBody这样的注解将请求映射到控制器方法上。

可以使用@RequestMapping注解和它的同伴一起完成许多事情。

@Controller
public class HelloController
{
    private GreetingService greetingService;

    @ResponseBody
    @RequestMapping("/")
    public String helloWorld()
    {
        return "Hello, World!";
    }

    @ResponseBody
    @RequestMapping("/custom")
    public String helloName(@RequestParam("name") String name)

    {
        return this.greetingService.getGreeting(name);
    }

    public void setGreetingService(GreetingService greetingService)
    {
        this.greetingService = greetingService;
    }
}

 

public class GreetingServiceImpl implements GreetingService
{
    @Override
    public String getGreeting(String name)
    {
        return "Hello, " + name + "!";
    }
}

 

现在,知道URL模式/gets将被映射到HelloController的helloWorld方法上,URL模式/custom将映射到helloName方法上。

这些URL模式是相对于DispatcherServlet的URL模式的,而不是Web应用程序根的URL。不过,因为此时DispatcherServlet被映射到了应用程序根上,所以该URL模式也将是相对于应用程序根的。如果将DispatcherServlet映射到/do/*,那么@RequestMapping注解中的URL模式也不回改变,但在浏览器的地址栏中它们之前都将添加上/do。

 

通常我们可能需要在Web应用程序中使用两个不同的应用上下文。这里已经创建了一个。一种典型的模式是:将把所有业务逻辑类放在根应用上下文中,将控制器放在Servlet上下文中。

我们可以,将原本存放在servletContext.xml中的greetingServiceImple声明,移动到一个新的XML文件中。

然后只需要在部署描述符中添加ContextLoaderListener即可。

现在就有了第二个应用上下文了:根应用上下文包含了greetingServiceImpl;DispatcherServlet的应用上下文则继承了所有根上下文的bean,它包含了helloController bean,并初始化了注解驱动的控制器映射。

为了让事情变得更加有趣,我们稍微调整控制器的helloName方法的请求映射。

现在该方法被映射到了与helloWorld方法相同的URL上,但它要求请求中必须有name参数。参数name的存在与否将决定由哪个方法处理请求。

 

12.5.2 创建混合配置

混合配置的核心是组件扫描和注解配置的概念。通过使用组件扫描,Spring将扫描通过特定注解指定的包查找类。

所有标注了@Component的类,都将编程由Spring管理的bean,这意味着Spring将实例化它们并注入它们的依赖。

其他符合组件扫描的注解:任何标注了@Component的注解都将编程组件注解,任何标注了另一个组件注解的注解也将编程组件注解。因此,标注了@Controller@Repository@Service的类也将成为Spring管理的bean。

Package org.springframework.stereotype

与注解配置结合使用的另一个关键注解是:@Autowired。可以为任何私有、保护和公开字段或者接受一个或多个参数的公开设置方法标注@Autowired@Autowired声明了Spring应该在实例化之后注入的依赖,并且它也可以用于标注构造器。通常由Spring管理的bean必须有无参构造器,但对于只含有一个标注了@Autowired的构造器的类,Spring将使用该构造器并注入所有的构造器参数。

在任何一种情况中,如果Spring无法为依赖找到匹配的bean,它将抛出并记录一个异常,然后启动失败。同样,如果它为以来找到多个匹配的bean,它也将抛出并记录一个异常,然后启动失败。

可以使用@Qualifier@Primary注解避免第二种情况。在使用@Autowired字段、方法、方法参数或者构造器参数时,通过@Qualifier可以指定应该使用的bean的名字。相反,可以使用@Primary标记一个组件标注的bean,表示在出现多个符合依赖的候选bean时应该优先使用它。

初始化组件扫描和注解配置是一个简单的任务,使用部署描述符配置和rootContext.xml以及servletContext.xml文件即可。

在Spring-Hybrid-Config项目中可以看到这个改动。

在rootContext.xml配置中,移除所有的bean定义,并替换上了<context:annotation-config>和<context:component-scan>元素即可:

特性base-package将指示Spring Framework扫描包com.wrox的类路径上的所有类或子包,查找@Component@Controller@Repository@Service

还有两个小改动需要完成:

(1)必须告诉Spring实例化GreetingServiceImpl,所以需要使用@Service标注该类。

(2)必须告诉Spring将GreetingService注入到HelloController中,所以需要使用@Autowired标注设置方法。

组件扫描默认将扫描所有的@Component注解,并且这两个应用上下文都启动了组件扫描。我们真正希望做的是分离bean,根应用上下文应该保存服务、仓库和其他业务逻辑片段,而DispatcherServlet的应用上下文应该包含Web控制器。幸运的时,有一种简单的方式可以修改默认的组件扫描算法。

exclude-filter元素将告诉Spring扫描所有的@Component,但@Controller除外。除了该排除元素,默认的扫描模式仍然将正常执行。

此外,还有另一种稍微不同的方式,它使用白名单而不是黑名单告诉Spring该扫描哪个组件:

上例中,use-default-filters特性被设置为false,这将告诉Spring忽略它的标准扫描模式。与根上下文中指定exclude-filter相反,include-filter告诉Spring只扫描@Controller。即使use-defalut-filters被设置为true,也仍然可以使用include-filters添加默认的过滤扫描器。

 

12.5.3 使用@Configuration配置Spring

Spring-Java-Config项目演示了纯Java配置的使用,从配置WebApplicationInitializer启动类型为根和DispatcherServlet应用上下文配置@Configuration类。

GreetingService、GreetingServiceImpl和HelloController类已经被移到了com.wrox.site包中,从而将它们与com.wrox.config包中的配置类分隔开。这种方式将保证组件扫描不回检测到@Configuration类。

@ComponentScan注解将告诉Spring扫描com.wrox.site包中的类以及所有子包中的类,同时排除@Controller类。

与前文讲解的混合配置一样新的ServletContextConfiguration类是非常类似的,它关闭了默认的过滤器,只扫描@Controller类,并启动了注解驱动的WebMVC特性。

@EnableWebMvc激活了注解驱动的控制器请求映射,它替代的是<mvc:annotation-driven>。

这两个类都没有@Bean方法。现在所有的bean都将通过组件扫描和@EnableWebMvc自动配置,所以不需要使用任何@Bean方法。

当然这些配置类都无法自己启动,所以实现WebApplicationInitializer接口。

本节之前在部署描述符中添加的初始化参数、监听器和DispatcherServlet都已经被移除了,现在web.xml中包含的只有基本的JSP和会话设置。

 

12.6 使用bean definition profile

通过Spring的bean definition profile,在命令行中使用简单的切换就可以轻松实现对整个配置的启动和关闭。

以下是一些用例:

  • 在一个多层次的应用环境中,需要让一些bean运行在一个层次中,而让另外一些bean运行在另一个层次中。
  • 可能需要针对许多不同类型的数据存储编写一个应用程序,用于重复销售。当你的终端用户购买并安装应用程序时,他们将指定希望使用的数据存储类型。你的应用程序可以使用一个包含了JPA仓库的Java Persistence API profile用于持久化关系数据库,以及一个包含了NoSQL仓库的Spring Data NoSQL profile用于编写无模式数据存储。用户可以安装相同的可执行文件,但只需要使用简单的配置启用正确的profile即可。
  • 你可能希望创建不同的开发、质量保证和生产profile。在生产profile中,可以硬编码某些设置,例如到本地数据库的连接。同样,质量保证环境中也可以有一些硬编码的设置,它们将与开发profile中的设置不同。毫无疑问,生产环境团队需要修改应用程序的设置,而无须等待你来修改和重新编译,生产profile将从属性文件中加载这些配置,而你的技术人员可以修改它。

12.6.1 了解profile的工作原理

类似于其他提供配置profile的技术(例如Maven),Spring bean definition profile有两个组件:声明和激活。

可以再XML配置文件中使用<bean>元素声明profile,或者在@Configuration类或@Component上使用@Profile注解,或者同时使用这两种方式。

<?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-4.0.xsd">

    <beans profile="development,qa">
        <jdbc:embedded-database id="dataSource" type="HSQL">
            <jdbc:script location="classpath:com/wrox/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/wrox/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>
    <beans profile="production">
        <context:property-placeholder location="file:/settings.properties"/>
        <jee:jndi-lookup id="dataSource" jndi-name="java:/comp/env/${production.dsn}" />
    </beans>

</beans>

 

上例中,两个使用了不同配置的DataSource bean被注册到两个不同的profile上。如果当前激活的是开发或测试环境,那么创建的数据库僵尸内嵌内存数据库HyperSQL,如果当前激活的是生产环境,那么应用程序将从JNDI上下文中根据配置的名称查找目标数据源。因为这两个bean都实现了DataSource,并且有着相同的ID,随意任何<bean>都可以引用该数据源bean,任何@Component也都可以自动注入正确的DataSource。如下面的代码所示,我们还可以通过使用@Configuration完成该任务:

interface DataConfiguration {
    DataSource dataSource();
}

@Configuration
@Import({DevQaDataConfiguration.class, ProductionDataConfiguration.class})
@ComponentScan(
        basePackages = "com.wrox.site",
        excludeFilters = @ComponentScan.Filter(Controller.class)
)
public class RootContextConfiguration {
}

@Configuration
@Profile({"development", "qa"})
public class DevQaDataConfiguration implements DataConfiguration {
    @Override
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).addScript("classpath:com/wrox/config/sql/schema.sql").addScript("classpath:com/wrox/config/sql/test-data.sql").build();
    }
}

@Configuration
@Profile("production")
@PropertySource("file:settings.properties")
public class ProductionDataConfiguration implements DataConfiguration {
    @Value("production.dsr")
    String dataSourceName;

    @Override
    @Bean
    public DataSource dataSource() {
        return new JndiDataSourceLookup().getDataSource("java:/comp/env/" + this.dataSourceName);
    }
}

 

在声明了自己的profile之后,可以通过一种或多种不同的方式激活它们。首先,可以使用spring.profile.active上下文参数:

<context-param> 
    <param-name>spring.profiles.active</param-name> 
    <param-value>development</param-value>
</context-param>

 

或者Servlet初始化参数:

<servlet>
    <init-param> 
        <param-name>spring.profiles.active</param-name> 
        <param-value>development</param-value>
    </init-param>
</servlet>

 

上下文参数将影响该Web应用程序中运行的所有Spring应用上下文,而Servlet初始化参数只会影响使用它的DispatcherServlet应用上下文。

也可以调用ConfigurableEnvironment.setActiveProfiles(java.lang.String...)方法,通过编程的方式激活一个或多个profile。

configurableEnvironment.setActiveProfiles("development"); 
configurableEnvironment.setActiveProfiles("profile1", "profile2");

 

这种方法将影响包含了该环境设置的应用上下文和它的所有子应用上下文。使用上下文、Servlet初始化参数或命令行属性通常是更加常见的。

通过编程的方式设置profile在集成测试时是非常有用的。

还有一件需要了解的事情是@Profile注解。

@Documented
@Retention(value=RetentionPolicy.RUNTIME) 
@Target(value={ElementType.TYPE, ElementType.METHOD})
@Profile("development") public @interface Development { }

 

这个自定义的@Development注解与profile名为development的@Profile注解的作用相同。现在使用@Development就等同于使用@Profile("development")。

 

12.6.2 考虑反模式和安全问题

 

 

 

Chapter 12
Updated on 9/4/14.

225.17 KB

Click to Download

 

 

摘录自:[美]Nicholas S.Williams著,王肖峰译 Java Web高级编程 [M]、清华大学出版社,2015、295-322、

posted @ 2016-11-26 17:46  guqiangjs  阅读(221)  评论(0)    收藏  举报