一、Spring简介

1、简介

Spring是一个实现J2EE的开源框架,为企业级应用的开发提供了一个简单、健壮、高效的解决方案。Spring提供容器的功能,使用容器来管理对象的生命周期和对象间的依赖关系。Spring框架有以下特点:

  • 控制反转(IOC)

依赖关系的转移,由原来的依赖于实现反转为依赖于抽象。

  • 依赖注入(DI)

指对象之间依赖关系的实现,包括:接口注入、构造方法注入、set方法注入。

  • 面向方面编程(AOP)

将之前写到业务逻辑中的日志、安全、事务管理等服务(功能)分离出来形成一个方面,实现复用,并通过动态插入的方式将方面插入到业务逻辑中。

2、示例

  • 新建Java工程,并导入Spring和commons-logging的jar

  • 新建测试使用的Java类
package com.spring.model;

public class HelloWorld {

	public void print(){
		System.out.println("Hello World!");
	}
}
  • 在src目录下新建applicationContext.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">

    <bean id="hi" class="com.spring.model.HelloWorld"></bean>

</beans>
  • 编写测试类
package com.spring.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.spring.model.HelloWorld;

public class TestSpring {

	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		HelloWorld hi = (HelloWorld) context.getBean("hi");
		hi.print();
	}
}

输出:Hello World!

二、依赖注入与控制反转

1、简介

Spring的核心机制就是依赖注入(DI)与控制反转(IOC),它们含义相同。通过依赖注入,程序中的各个组件不再需要通过硬编码方式耦合,实现了组件间的解耦。

Spring提倡面向接口的编程,明确定义组件接口,独立开发各个组件,然后根据组件的依赖关系组装运行。

2、示例

  • Student Model
package com.spring.model;

public class Student {

	private String name;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public void introduce(){
		System.out.println("Hi, I'm " + name);
	}
	@Override
	public String toString() {
		return getName();
	}
}
  • 新建DAO和Service相关类
package com.spring.dao;

import com.spring.model.Student;

public interface StudentDAO {

	public void insert(Student student);
}
package com.spring.dao;

import com.spring.model.Student;

public class StudentDAOImpl implements StudentDAO{

	@Override
	public void insert(Student student) {
		System.out.println("增加一条学生记录..." + student);
	}

}
package com.spring.service;

import com.spring.model.Student;

public interface StudentService {

	public void addStudent(Student student);
}
package com.spring.service;

import com.spring.dao.StudentDAO;
import com.spring.model.Student;

public class StudentServiceImpl implements StudentService{

	private StudentDAO studentDAO;
	
	@Override
	public void addStudent(Student student) {
		studentDAO.insert(student);
	}

	/**
	 * studentDAO的set方法,用于依赖注入
	 * @param studentDAO
	 */
	public void setStudentDAO(StudentDAO studentDAO) {
		this.studentDAO = studentDAO;
	}
}
  • applicationContext.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">
    
          <bean id="student" class="com.spring.model.Student">
              <property name="name" value="Tom"></property>
          </bean>
    
          <bean id="stDAO" class="com.spring.dao.StudentDAOImpl"></bean>
          <bean id="stService" class="com.spring.service.StudentServiceImpl">
              <property name="studentDAO" ref="stDAO"></property>
          </bean>
    
      </beans>
    

    其中:

    • <bean>标签用来定义Bean的实例化信息

    • <bean>标签的id属性指定生成的Bean的实例名称

    • <bean>标签的class属性指定类全名

    • <property>标签用来调用Bean实例中的相关Set方法完成属性的赋值,实现依赖的注入

    • <property>标签的name属性为Bean实例中对应的属性名称,其值可以通过ref或value属性来定义

    • <property>标签的ref属性为XML配置中Bean的引用(id属性值)

  • 测试类

public static void main(String[] args) {
	ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
	StudentService service = (StudentService) context.getBean("stService");
	Student student = (Student) context.getBean("student");
	student.introduce();
	service.addStudent(student);
}

输出:

Hi, I'm Tom
增加一条学生记录...Tom

三、面向方面编程

1、简介

类似日志、事务管理、安全验证等通用的、散布在系统各处的需要在业务逻辑中关注的事情被称为方面或关注点。AOP(Aspect-Oriented Programming),即面向方面编程,就是将方面从业务逻辑中分离出来集中实现,从而独立的编写业务逻辑和方面,在系统运行时再将方面织入到系统中。这样做能够减少方面代码中的错误,在方面处理策略改变时还能统一修改;而且在编写业务代码时可以专心于业务逻辑。

2、核心概念

  • 切面(Aspect)

切面是方面(日志、事务、安全验证等)的实现,例如:日志切面、权限切面等。通常是存放方面代码的普通Java类,需要在配置中指定才能被AOP容器识别为切面。

  • 通知(Advice)

    通知是切面的具体实现。以目标方法为参照点,根据放置的位置不同,分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、环绕通知(Around)和最终通知(After)。切面类中的某个方法具体属于哪种通知,需要在配置中指定。

    • 前置通知

      在目标方法执行前执行

    • 后置通知

      在目标方法执行后执行

    • 环绕通知

      可以在方法调用前执行通知代码,还可以决定是否继续调用目标方法

    • 异常通知

      在目标方法抛出异常时执行

  • 切入点(Pointcut)

切入点用于定义通知应该织入到哪些连接点上。

  • 目标对象(Target)

指将要织入切面的对象,即被通知的对象。这些对象中只包含核心业务逻辑代码,所有的日志等方面功能会被AOP容器织入。

  • 代理对象(Proxy)

将通知应用到目标对象之后,动态创建的对象。其功能相当于目标对象的核心业务逻辑功能加方面(日志、事务等)的功能。

  • 织入(Weaving)

将切面应用到目标对象并创建一个新的代理对象的过程被称为织入。

3、使用ProxyFactoryBean实现AOP

  • 前置通知

前置通知需要实现org.springframework.aop.MethodBeforeAdvice接口:

package com.spring.advice;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.MethodBeforeAdvice;

public class BeforeLogAdvice implements MethodBeforeAdvice{

	private Log log = LogFactory.getLog(getClass());
	
	@Override
	public void before(Method method, Object[] args, Object target) throws Throwable {
		String targetClsName = target.getClass().getName();
		String targetMethodName = method.getName();
		log.info("[前置通知] 开始执行 " + targetClsName + " 类的 " + targetMethodName + " 方法, 参数:" + args[0]);
	}

}
其中:

* 参数method为目标方法对象

* 参数args为调用方法时传入的参数

* 参数target为被调用的目标方法所属的对象实例

Spring使用代理的方式(使用ProxyFactoryBean代理类)将通知织入到原来的对象中,需要在XML配置中将业务逻辑和方面组装到代理类中:

<bean id="beforeLogAdvice" class="com.spring.advice.BeforeLogAdvice"></bean>
	
<bean id="proxyService" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces">
		<value>com.spring.service.StudentService</value>
	</property>
	<property name="interceptorNames">
		<list>
			<value>beforeLogAdvice</value>
		</list>
	</property>
	<property name="target" ref="stService"></property>
</bean>
其中:

* proxyInterfaces表示被代理的接口

* interceptorNames表示要织入的通知列表

* target表示被代理的原来的Bean

ProxyFactoryBean是Spring框架APO中的代理组件类,通过这个代理类访问原来的Bean时,就能在方法调用时自动执行通知的代码。测试类主要代码如下:

ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
StudentService proxyService = (StudentService) context.getBean("proxyService");
Student student = (Student) context.getBean("student");
proxyService.addStudent(student);

输出:

信息: [前置通知] 开始执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法, 参数:Tom
增加一条学生记录...Tom
  • 后置通知

后置通知需要实现org.springframework.aop.AfterReturningAdvice接口:

package com.spring.advice;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.AfterReturningAdvice;

public class AfterLogAdvice implements AfterReturningAdvice{

	private Log log = LogFactory.getLog(getClass());
	
	@Override
	public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
		String targetClsName = target.getClass().getName();
		String targetMethodName = method.getName();
		log.info("[后置通知] " + targetClsName + " 类的 " + targetMethodName + " 方法已执行完成! 参数:" + args[0] + " 返回值:" + returnValue);
	}

}

其中参数returnValue为目标方法的返回值,其余参数同前置通知before()方法的参数。

在XML中配置后置通知:

<bean id="afterLogAdvice" class="com.spring.advice.AfterLogAdvice"></bean>

<bean id="proxyService" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces">
		<value>com.spring.service.StudentService</value>
	</property>
	<property name="interceptorNames">
		<list>
			<value>beforeLogAdvice</value>
			<value>afterLogAdvice</value>
		</list>
	</property>
	<property name="target" ref="stService"></property>
</bean>

运行测试类,输出:

信息: [前置通知] 开始执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法, 参数:Tom
增加一条学生记录...Tom
信息: [后置通知] com.spring.service.StudentServiceImpl 类的 addStudent 方法已执行完成! 参数:Tom 返回值:null
  • 异常通知

异常通知需要实现org.springframework.aop.ThrowsAdvice接口,并定义afterThrowing()方法:

package com.spring.advice;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.ThrowsAdvice;

public class ErrorLogAdvice implements ThrowsAdvice{

	private Log log = LogFactory.getLog(getClass());
	
	public void afterThrowing(Method method, Object[] args, Object target, Throwable exception) {
		String targetClsName = target.getClass().getName();
		String targetMethodName = method.getName();
		log.info("[异常通知] 执行 " + targetClsName + " 类的 " + targetMethodName + " 方法时发生了异常, 参数:" + args[0] + ", 异常信息:" + exception);
	}
}

其中参数exception为发生的异常信息,其余参数同前置通知before()方法的参数。

在XML中配置异常通知:

<bean id="errorLogAdvice" class="com.spring.advice.ErrorLogAdvice"></bean>

<bean id="proxyService" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces">
		<value>com.spring.service.StudentService</value>
	</property>
	<property name="interceptorNames">
		<list>
			<value>beforeLogAdvice</value>
			<value>afterLogAdvice</value>
			<value>errorLogAdvice</value>
		</list>
	</property>
	<property name="target" ref="stService"></property>
</bean>

修改StudentServiceImpl类的addStudent()方法,使其抛出异常:

@Override
public void addStudent(Student student) {
	studentDAO.insert(student);
	throw new RuntimeException("Unpredictable error...");
}

运行测试类,输出:

信息: [前置通知] 开始执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法, 参数:Tom
增加一条学生记录...Tom
信息: [异常通知] 执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法时发生了异常, 参数:Tom, 异常信息:java.lang.RuntimeException: Unpredictable error...
Exception in thread "main" java.lang.RuntimeException: Unpredictable error...
	at com.spring.service.StudentServiceImpl.addStudent(StudentServiceImpl.java:13)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
  • 环绕通知

环绕通知需要实现org.aopalliance.intercept.MethodInterceptor接口:

package com.spring.advice;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class AroundLogAdvice implements MethodInterceptor{

	private Log log = LogFactory.getLog(getClass());
	
	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		long begin = System.currentTimeMillis();
		//执行目标方法
		Object returnValue = invocation.proceed();
		long end = System.currentTimeMillis();
		String targetClsName = invocation.getThis().getClass().getName();
		String targetMethodName = invocation.getMethod().getName();
		log.info("[环绕通知] 执行 " + targetClsName + " 类的 " + targetMethodName + " 方法耗时 " + (end - begin) + "ms, 方法参数:" + invocation.getArguments()[0] + " 返回值:" + returnValue);
		return null;
	}

}

在XML中配置环绕通知:

<bean id="aroundLogAdvice" class="com.spring.advice.AroundLogAdvice"></bean>

<bean id="proxyService" class="org.springframework.aop.framework.ProxyFactoryBean">
	<property name="proxyInterfaces">
		<value>com.spring.service.StudentService</value>
	</property>
	<property name="interceptorNames">
		<list>
			<value>beforeLogAdvice</value>
			<value>afterLogAdvice</value>
			<value>errorLogAdvice</value>
			<value>aroundLogAdvice</value>
		</list>
	</property>
	<property name="target" ref="stService"></property>
</bean>

修改StudentServiceImpl类的addStudent()方法,模拟方法的执行耗时:

@Override
public void addStudent(Student student) {
	studentDAO.insert(student);
	try {
		Thread.sleep(500);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
}

运行测试类,输出:

信息: [前置通知] 开始执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法, 参数:Tom
增加一条学生记录...Tom
信息: [环绕通知] 执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法耗时 501ms, 方法参数:Tom 返回值:null
信息: [后置通知] com.spring.service.StudentServiceImpl 类的 addStudent 方法已执行完成! 参数:Tom 返回值:null

4、使用配置方式实现AOP

此种方式需要使用aspectj,因此还需引入相关的jar(aspectjweaver.jar)。

在定义通知时,通知方法中有一个JoinPoint的参数,通过此参数可以获取目标对象、目标方法和目标方法参数等信息。

  • 包含各种通知的类:LogAdvice
package com.spring.advice;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class LogAdvice {

	private Log log = LogFactory.getLog(getClass());
	
	/**
	 * 前置通知
	 * @param joinPoint
	 */
	public void beforeAdvice(JoinPoint joinPoint){
		String targetClsName = joinPoint.getTarget().getClass().getName();
		String targetMethodName = joinPoint.getSignature().getName();
		log.info("[前置通知] 开始执行 " + targetClsName + " 类的 " + targetMethodName + " 方法, 参数:" + joinPoint.getArgs()[0]);
	}
	
	/**
	 * 后置通知
	 * @param joinPoint
	 */
	public void afterAdvice(JoinPoint joinPoint){
		String targetClsName = joinPoint.getTarget().getClass().getName();
		String targetMethodName = joinPoint.getSignature().getName();
		log.info("[后置通知] " + targetClsName + " 类的 " + targetMethodName + " 方法已执行完成! 参数:" + joinPoint.getArgs()[0]);
	}
	
	/**
	 * 异常通知
	 * @param joinPoint
	 * @param exception
	 */
	public void errorAdvice(JoinPoint joinPoint, Exception exception){
		String targetClsName = joinPoint.getTarget().getClass().getName();
		String targetMethodName = joinPoint.getSignature().getName();
		log.info("[异常通知] 执行 " + targetClsName + " 类的 " + targetMethodName + " 方法时发生了异常, 参数:" + joinPoint.getArgs()[0] + ", 异常信息:" + exception);
	}
	
	/**
	 * 环绕通知
	 * @param joinPoint
	 * @throws Throwable
	 */
	public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable{
		String targetClsName = joinPoint.getTarget().getClass().getName();
		String targetMethodName = joinPoint.getSignature().getName();
		long begin = System.currentTimeMillis();
		//执行目标方法
		Object returnValue = joinPoint.proceed();
		long end = System.currentTimeMillis();
		log.info("[环绕通知] 执行 " + targetClsName + " 类的 " + targetMethodName + " 方法耗时 " + (end - begin) + "ms, 方法参数:" + joinPoint.getArgs()[0] + " 返回值:" + returnValue);
	}
}
  • 在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/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

	<bean id="student" class="com.spring.model.Student">
		<property name="name" value="Tom"></property>
	</bean>

    <bean id="stDAO" class="com.spring.dao.StudentDAOImpl"></bean>
    <bean id="stService" class="com.spring.service.StudentServiceImpl">
    	<property name="studentDAO" ref="stDAO"></property>
    </bean>

	<bean id="logAdvice" class="com.spring.advice.LogAdvice"></bean>
	
	<!-- aop配置 -->
	<aop:config>
		<aop:aspect id="logAop" ref="logAdvice">
			<!-- 定义切入点,使用正则,对 com.spring.service.StudentService 中的所有方法都拦截-->
			<aop:pointcut expression="execution(* com.spring.service.StudentService.*(..))" id="logPointcut"/>
			<!-- 指定方法的切入类型 -->
			<aop:before method="beforeAdvice" pointcut-ref="logPointcut"/>
			<aop:after-returning method="afterAdvice" pointcut-ref="logPointcut"/>
			<aop:after-throwing method="errorAdvice" pointcut-ref="logPointcut" throwing="exception"/>
			<aop:around method="aroundAdvice" pointcut-ref="logPointcut"/>
		</aop:aspect>
	</aop:config>
	 
</beans>
  • 测试类
package com.spring.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.spring.model.Student;
import com.spring.service.StudentService;

public class TestAOP {

	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		StudentService service = (StudentService) context.getBean("stService");
		Student student = (Student) context.getBean("student");
		service.addStudent(student);
	}
}

运行输出:

信息: [前置通知] 开始执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法, 参数:Tom
增加一条学生记录...Tom
信息: [后置通知] com.spring.service.StudentServiceImpl 类的 addStudent 方法已执行完成! 参数:Tom
信息: [环绕通知] 执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法耗时 500ms, 方法参数:Tom 返回值:null

异常通知测试输出:

增加一条学生记录...Tom
信息: [异常通知] 执行 com.spring.service.StudentServiceImpl 类的 addStudent 方法时发生了异常, 参数:Tom, 异常信息:java.lang.RuntimeException: Unpredictable error...
Exception in thread "main" java.lang.RuntimeException: Unpredictable error...
	at com.spring.service.StudentServiceImpl.addStudent(StudentServiceImpl.java:13)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
参考资料:
  • 《SSH框架技术与项目实战》