从我接触到的两个项目,这两个项目真的很大。但它们的的确确用到了spring进行事务控制,同时其中一个中时整合了struts2和spring。虽然两个框架都是开源的,也是人们所说的轻量级j2ee所使用的组件,但其中的思想却是值得我们学习和使用的。
以下想就spring的IOC(DI)和AOP两个重要的概念通过实践进行理解,以期对spring有一个新的认识。
1、IOC
貌似通常说的DI方式有三种,但其中两种并不常用,对用setter方法的注入倒是使用的很频繁,也可以说是很成熟,那就以这个做为对IOC和理解进行实例演示。
首先来看一下我用作测试的整个工程的目录结构:
1)IOC依赖于一个配置文件beans,这个配置文件也是在服务器启动时就需要加载到内存中的,如果想知道如何在web工程中加载配置文件可以看我博客中《模拟struts架构》描述。beans.xml文件存储于src的根目录。
让我们看下beans文件的内容:
一个简单的xml文件,其中模仿spring的配置文件进行了相关的配置,如果学习过spring的同学应该很容易看懂其中内容,如果没学习过的,那就当作xml来看待就好了,如果xml也不知道,那还是先补一下吧。
<beans>
<bean id="user" class="com.yyb.model.User" />
<bean id="userDAO" class="com.yyb.dao.impl.UserDAOImpl" />
<bean id="userService" class="com.yyb.service.UserService">
<property name="userDAO" bean="userDAO" />
</bean>
</beans>
2)看一下在beans.xml文件中所涉及到的类:
其实这些类基本都是javabean,不过对用UserService类一看就知道了,此类是要用做业务处理的,必然有还相应的业务处理方法。而这个业务处理类,如果需要用到数据模型(也就是javabean),那这里(业务处理类)就需要声明一个数据模型的对象,那这个对象由谁来创建呢,我们交由spring来给我们创建,那这个创建的过程由我们手中交到了spring手中,这种产品类对象的方式就是所谓的IOC(控制反转),而生成这一对象后如何交给业务处理使用,我们采用set方法将对象初始化给业务处理类中已定义但未创建的对象,这一个过程就是DI(依赖注入)。ok,到这里我们就明白了什么是IOC和DI,这两个概念是经常被人们所谈到的。
业务处理类UserService.java
package com.yyb.service;
import com.yyb.dao.UserDAO;
import com.yyb.model.User;
public class UserService
{
private UserDAO userDAO;
void addUser(User u)
{
this.userDAO.save(u);
}
/**
* @return the userDAO
*/
public UserDAO getUserDAO()
{
return userDAO;
}
/**
* @param userDAO
* the userDAO to set
*/
public void setUserDAO(UserDAO userDAO)
{
this.userDAO = userDAO;
}
}
将会用作被注入到业务处理类中的类UserDAOImpl.java和它的父类UserDAO.java
package com.yyb.dao.impl;
import com.yyb.dao.UserDAO;
import com.yyb.model.User;
public class UserDAOImpl implements UserDAO {
@Override
public void save(User u) {
System.out.println("User:" + u.toString());
}
@Override
public void delete() {
System.out.println("delete User");
}
}
package com.yyb.dao;
import com.yyb.model.User;
public interface UserDAO {
void save(User u);
void delete();
}
不要忘记了这个javabean
package com.yyb.model;
public class User
{
private String userName;
private String password;
/**
* @return the userName
*/
public String getUserName()
{
return userName;
}
/**
* @param userName
* the userName to set
*/
public void setUserName(String userName)
{
this.userName = userName;
}
/**
* @return the password
*/
public String getPassword()
{
return password;
}
/**
* @param password
* the password to set
*/
public void setPassword(String password)
{
this.password = password;
}
public String toString()
{
StringBuffer sb = new StringBuffer();
sb.append(this.userName);
sb.append(this.password);
return sb.toString();
}
}
再回过头来看一下这几个类的关系,DAO类将会被注入到业务类中进行数据持久化,而刚才所说的数据模型却并没有作为注入的对象,原因是对于项目架构的设计,并不会将所以的类都进行注入,那样并没有太大的意义,而一些对于业务分层出现明显界线的会使用注入的方式进行管理,而javabean对于整个业务处理流程都会用到,如果进行注入就不合适了。这点很重要,不然我们使用这种框架一方面帮助我们加快开发速度,一方面是由于它从使用的方式上就可以让我们的项目各层次之间使用最少的耦合。
3)真正实现注入
之前的代码只是我们自己在项目中会写到的,主要是让大家注意到在写代码过程中一些地方用来做什么的,另一些地方又用来做什么的。下面是真正的实现了注入。以下内容大概是这样的:从我们的模拟的配置文件中读取内容到内存(注:这里使用jdom的方式)。然后按照配置文件中的内容产生类的实例,并将这个实例注入到依赖它的那个类里(这里其实就是执行了一个setXXX(Object)方法,但这个方法名和方法的参数及这个set方法的调用都是我们亲手根据配置文件一步一步来做的)。
当然这里还提供了一个getBean()方法,这个方法供我们传入相应的bean的名称,得到相应的类的实例,模拟的还是很真实的,且像类名和实现的接口都是依照spring而来的,与spring中的名称一样。
package com.yyb.spring;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
public class ClassPathXmlApplicationContext implements BeanFactory
{
private Map<String, Object> beans = new HashMap<String, Object>();
public ClassPathXmlApplicationContext() throws JDOMException, IOException,
InstantiationException, IllegalAccessException,
ClassNotFoundException, SecurityException, NoSuchMethodException,
IllegalArgumentException, InvocationTargetException
{
SAXBuilder sb = new SAXBuilder();
// 构造文档对象
Document doc = sb.build(ClassPathXmlApplicationContext.class
.getClassLoader().getResourceAsStream("beans.xml"));
// 获取根元素
Element root = doc.getRootElement();
// 取到根元素所有元素
List list = root.getChildren();
for (int i = 0; i < list.size(); i++)
{
Element element = (Element) list.get(i);
// 取id子元素
String beanid = element.getAttributeValue("id");
// 取class子元素
String clzss = element.getAttributeValue("class");
// 实例化
Object o = Class.forName(clzss).newInstance();
// 将所有bean放入map中
beans.put(beanid, o);
// 获取property 进行依赖注入
for (Element propertyElement : (List<Element>) element
.getChildren("property"))
{
String name = propertyElement.getAttributeValue("name");
String bean = propertyElement.getAttributeValue("bean");
// 从beans.xml中根据id取到类的对象
Object beanObj = this.getBean(name);
// 形成setXXX方法名
String methodName = "set" + name.substring(0, 1).toUpperCase()
+ name.substring(1);
// 反射机制对方法进行调用,将对象在加载bean时就注入到环境上下文中
Method m = o.getClass().getMethod(methodName,
beanObj.getClass().getInterfaces()[0]);
// 执行注入,相当于执行了一个setXXX(args..)的方法
m.invoke(o, beanObj);
}
}
}
@Override
public Object getBean(String name)
{
return beans.get(name);
}
}
package com.yyb.spring;
public interface BeanFactory {
Object getBean(String name);
}
4)测试
这里使用junit进行测试我们写的效果,你可以打印相应语句看看我们模拟的怎么。
package com.yyb.service;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import org.jdom.JDOMException;
import org.junit.Test;
import com.yyb.aop.LogInterceptor;
import com.yyb.dao.UserDAO;
import com.yyb.dao.impl.UserDAOImpl;
import com.yyb.model.User;
import com.yyb.spring.BeanFactory;
import com.yyb.spring.ClassPathXmlApplicationContext;
public class UserServieTest {
@Test
public void testAddUser() throws JDOMException, IOException,
InstantiationException, IllegalAccessException,
ClassNotFoundException, SecurityException,
IllegalArgumentException, NoSuchMethodException,
InvocationTargetException {
BeanFactory factory = new ClassPathXmlApplicationContext();
UserService userService = (UserService) factory.getBean("userService");
User u = new User();
u.setUserName("yyb");
u.setPassword("1234");
userService.addUser(u);
}
}
2、AOP
这一个概念不需要在实践的过程就应该能理解,其实就是在我们的业务流程中(这个流程打个比方是一条直线),然后我们在第1厘米和第10厘米处分别打了两个记号,这样你的流程就不得不经过我们的记号再住下执行。这就是AOP(切面)的大致理解。
所以可以这样说,我们如果要是想在执行一个方法的前面和后面都加入日志打印,那我们的通常想法是再写两个方法,一个在调用原来的方法前调用 ,一个在执行后调用 ,这样也是AOP的现实例子。那我们又应该如何来模拟呢,我们还是以这个记日志的例子为例来模拟,如果这个成功了那就可以改为事务的处理。那不就是我们常用的AOP的最好实践么。
再提示一上,设计模式中有一种模式叫做代理,而我们想的是让程序根据要执行的方法而打印出不同的日志,那你会想到什么,动态代理。对,我们就是想用动态代理来模拟AOP。
知道了这一利害,那就直接让我们看代码吧:
类中的invoke方法是必须要有这,在invoke方法中我们调用了beforeMethod方法,这个就打印出了方法执行前的日志,然后执行真正要执行的方法。这里没有调用类似afterMethod这样的方法,因为原理都是一样的。
这样就真的将我的想要组合在一起的两方法捆绑好了。那如何才能让他们一起执行呢。
package com.yyb.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class LogInterceptor implements InvocationHandler {
private Object target;
public void beforeMethod(Method m) {
System.out.println("before "+m.getName()+"method start");
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
beforeMethod(method);
method.invoke(target, args);
return null;
}
/**
* @return the target
*/
public Object getTarget() {
return target;
}
/**
* @param target the target to set
*/
public void setTarget(Object target) {
this.target = target;
}
}
看这里,这里让他们一起执行:
如果对于代理模式及反射熟悉的话,应该很好理解。这里直接在单元测试里产生代理的调用,从而让被代理也一起调用了
package com.yyb.service;
import java.lang.reflect.Proxy;
import org.junit.Test;
import com.yyb.aop.LogInterceptor;
import com.yyb.dao.UserDAO;
import com.yyb.dao.impl.UserDAOImpl;
import com.yyb.model.User;
public class UserServieTest {
@Test
public void testProxy() {
UserDAO userDAO = new UserDAOImpl();
LogInterceptor log = new LogInterceptor();
log.setTarget(userDAO);
UserDAO userProxy = (UserDAO) Proxy.newProxyInstance(UserDAO.class
.getClassLoader(), new Class[] { UserDAO.class }, log);
User u = new User();
userProxy.save(u);
userProxy.delete();
}
}
总结:
spring的这两个机制不管是在实际运用中还是面试中都需要我们深入的了解,那样才会事半功倍。我希望我们通过实践对这两个概念也好机制也好都有新的理解才是最好。