spring 第一篇(1-1):让java开发变得更简单(下)

切面(aspects)应用

  DI能够让你的软件组件间保持松耦合,而面向切面编程(AOP)能够让你捕获到在整个应用中可重用的组件功能。在软件系统中,AOP通常被定义为提升关注点分离的一个技术。系统由很多的组件组成,每个组件负责一部分的功能。但是这些组件往往除了核心功能外,还有些额外的责任。比如像日志,事务管理和安全这些系统服务会被引进到组件中。这些服务通常被称为横切关注点(cross-cutting-concerns),因为它们常常贯穿于多个组件中。

  在多个组件中传播这些概念,会有两个复杂层面内容将被引进到你的代码中

  •  实现系统级概念代码将会在多个组件中重复出现,这意味着如果你需要改变这些概念的话,你必须修改多个组件的内容。即使你将这些概念抽象到一个独立的方法中来使它成为一个单一的方法,但是方法的调用还是会出现在多个地方。
  •  你的组件现在充斥着和核心代码无关的功能。

  下图展示的它的复杂度。左边的业务对象和右边的系统服务紧密的关联在一起。spring 第一篇(1-1):让java开发变得更简单(下)

  AOP能够使这些服务成为模块并且很方便的使用它们(声明方式),来达到在组件中消除这些影响。这样让组件更有粘合性,使它们只关注于自己具体的业务,完全忽略系统服务。简而言之,切面让POJOS保持简单。

  你可以将切面设想为覆盖了很多组件的毛毯,如下图所示。一个应用由多个实现业务功能的模块组成。使用AOP,你可以使用这些功能来覆盖你的核心应用。这些层可以通过声明的方式来贯穿到你的应用中,而你核心代码根本不用知道它们的存在。这是一个强大的概念,因为它将应用核心业务和系统服务独立开来。

spring 第一篇(1-1):让java开发变得更简单(下)

下面我们来看个具体的例子,现在是个骑士的故事,故事如何流传下来呢,当然是通过歌手的歌声来传递

public class Minstrel {
private PrintStream stream;
public Minstrel(PrintStream stream) {
this.stream = stream;
}
public void singBeforeQuest() {
stream.println("Fa la la, the knight is so brave!");
}
public void singAfterQuest() {
stream.println("Tee hee hee, the brave knight " +
"did embark on a quest!");
}
}

  如你所见,Minstrel是个简单的类并有两个方法。singBeforeQuest()方法在骑士开始探索的之前被调用,而singAfterQuest()方法应当在骑士探索结束后调用。在这两个用例中,Minstrel通过在构造器中注入的PrintSteam来歌颂骑士的事迹。

  现在你只需要注入BraveKnight就能使用,让我们微调下BraveKnight来使用Minstrel。下面展示如何将他们合并在一起

 

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() throws QuestException {
minstrel.singBeforeQuest();
quest.embark();
minstrel.singAfterQuest();
}
}

  它应该能够运行,你要做的就是回到你的配置文件中,然后声明一个Minstrel bean并且注入到BraveKnight的构造器中。但是等等...是否需要在knight中来管理minstrel?minstrel类不是只要关注于自己的功能?因为knight需要知道minstrel存在。这将迫使你将Minstrel注入到BraveKnight中,这样不仅使BraveKnight的代码变得复杂,而且用户可能需要个没有minstrel的knight。如果Minstrel为空?是否需要进行空值逻辑检测?

  但是如果你使用AOP,minstrel和knight只需要关心各自的事情。要将Minstrel转化成一个切面,你只需要在Spring配置文件中声明它。下面在原来的knights.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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="knight" class="com.springinaction.knights.BraveKnight">
  <constructor-arg ref="quest" />
</bean>
<bean id="quest" class="com.springinaction.knights.SlayDragonQuest">
  <constructor-arg value="#{T(System).out}" />
</bean>
<bean id="minstrel" class="com.springinaction.knights.Minstrel">
  <constructor-arg value="#{T(System).out}" />
</bean>
<aop:config>
  <aop:aspect ref="minstrel">
    <aop:pointcut id="embark" expression="execution(* *.embarkOnQuest(..))"/>
    <aop:before pointcut-ref="embark" method="singBeforeQuest"/>
    <aop:after pointcut-ref="embark" method="singAfterQuest"/>
  </aop:aspect>
</aop:config>
</beans>

  上面使用spring aop命名空间配置来声明Minstrel作为一个切面。首先你声明Minstrel作为一个bean。然后在<aop:aspect>元素中引用它。接着进一步对切面定义,你使用<aop:before>来声明在执行embarkOnQuest()执行前调用singBeforeQuest方法,这个叫做前置通知。使用<aop:after>来声明在执行embarkOnQuest()执行后调用singAfterQuest方法,这个叫做后置通知。在这两个用例中,pointcut-ref属性都引用了叫embark的切点。这个切点在前面的<ponitcut>元素中定义,并且它有个叫做expression属性,该属性用来选择那些地方需要使用通知。这个表达式语法是AspectJ的切点表达式语言。

  现在先不要关心AspectJ表达式细节,以后的章节会详细谈论。现在你应该有点感知切面在spring中是如何使用的,是的只要一点点的XML配置,你就能将minstrel转成spring的切面。现在你应该从这个例子中能获取到两点认识。

  1. 首先,Minstrel仍然是一个POJO ---- 没有任何指示能表明它是一个切面。但实际上它却是spring context中的一个切面
  2. 其次,更重要的是,Minstrel可以被用到BraveKnight中,而不用BraveKnight明确的去调用。事实上,BraveKnight完全不关心Minstrel是否存在

这里需要指出的是,在使用Spring魔法将Minstrel转换成一个切面前,需要将它声明为Spring的bean。所以只要注入依赖就能就其他的spring bean也变成一个切面。当然这个例子只是简单的spring aop应用,后面我们将介绍它在声明式事务和安全方面中的应用。接下来我们再来看一种spring使java开发变得更简单的方式。

使用模板消除样板代码

  样板代码指的是你经常一次次编写相似的代码来完成普通、简单的任务。有很多Java APIs存在着样板代码。一个常见的样板代码就是使用JDBC来查询数据库操作。如果你有用过JDBC来操作的话,你可能会经常写下类似下面的一段代码

public Employee getEmployeeById(long id) {
  Connection conn = null;
  PreparedStatement stmt = null;
  ResultSet rs = null;
  try {
    conn = dataSource.getConnection();
    stmt = conn.prepareStatement(
    "select id, firstname, lastname, salary from " +
    "employee where id=?");
    stmt.setLong(1, id);
    rs = stmt.executeQuery();
    Employee employee = null;
  if (rs.next()) {
    employee = new Employee();
    employee.setId(rs.getLong("id"));
    employee.setFirstName(rs.getString("firstname"));
    employee.setLastName(rs.getString("lastname"));
    employee.setSalary(rs.getBigDecimal("salary"));
  }
    return employee;
  } catch (SQLException e) {
  } finally {
    if(rs != null) {
  try {
    rs.close();
  } catch(SQLException e) {}
 }
  if(stmt != null) {
  try {
    stmt.close();
  } catch(SQLException e) {}
 }if(conn != null) {
  try {
    conn.close();
  } catch(SQLException e) {}
  }
}
return null;
}

  如你所见,你只是想查询id为某个值的employee数据,而这里却要写一堆的JDBC操作。首先你要创建个connection,然后创建statement,接着获取查询结果。而且,你还要捕获SQLException异常。最终当所有操作完成后,你必须清除乱七八糟的东西,关闭connection,statement和result set。很显然,上面的代码你可能有经历过。查询employee只要一条语句,但是却要进行大量的JDBC样板操作。

  JDBC不是唯一的样板代码,很多活跃的代码也存在类似的问题,比如JMS,JNDI和REST客服端。Spring通过将样板代码封装到模板中来消除这些重复代码。Spring的JDBCTemplate使不用传统的JDBC来操作数据库成为可能。

  举个例子,使用Spring的SimpleJdbcTemplate(利用java 5特性来实现的一个JDBCTemplate),getEmployeeById()方法可以被重写为,如何获取employee数据而不用在考虑JDBC API操作。下面是更新后的代码

public Employee getEmployeeById(long id) {
  return jdbcTemplate.queryForObject(
    "select id, firstname, lastname, salary " +
    "from employee where id=?",
    new RowMapper<Employee>() {
    public Employee mapRow(ResultSet rs,
      int rowNum) throws SQLException {
      Employee employee = new Employee();
      employee.setId(rs.getLong("id"));
      employee.setFirstName(rs.getString("firstname"));
      employee.setLastName(rs.getString("lastname"));
      employee.setSalary(rs.getBigDecimal("salary"));
    return employee;
    }
  },id);
}

  如你所见,这个版本的getEmployeeById()更加的简单,它只关注于如何进行数据库查询employee操作。模板的queryForObject()只需要一个SQL query语句,以及如何将数据封装到domain object的RowMapper,你看不到任何的JDBC样板代码,所有的样板代码都会在模板中进行处理。

  这里我们使用spring DI、aspect、template来使复杂的java开发变得简单。上面我们举例说明如何配置bean和aspect。下面我们就来解释下这些文件如何加载以及将他们加载到什么地方去。

上一篇:Java开发最实用最好用的11个技术网站


下一篇:Java实现缓存(类似于Redis)