经常做javaee开发的朋友基本上每天与mvc打交道,但是很多朋友却并没有自己写过mvc,甚至只会用,不知道其内部实现原理,同样地,我身边有很多朋友也都是知其然,不知其所以然。其实个人认为mvc实现起来不难,我想事实也是,笔者在兴趣的驱使下,自己写了一个小型的框架,实现了mvc,orm。虽然功能不是很强大,但基本可以在简单项目,至少是练手的项目使用,学习的项目使用是很好的。至于ioc目前正在实现中。Aop在酝酿中。
SpringMVC主要使用注解式,struts2也可以用convention插件来实现注解式。个人认为灵活性很好。不过对于配置文件其实也是不错的选择。不管哪种方式,其实只是初始化访问路径时的来源不一样而已,配置文件只用解析xml,对于注解自然是要扫描注解包。
当有了访问路径时,其他的事就是访问某一地址(请求路径)时如何转发到具体的方法(也就是对应的注解)。
注解不难理解,说白了说是一种注释。本身没有任何的意义,只是标记一下。比如:
@Resource @Autoware 等。最多带几个参数:@Table(name=”user”),这种说明一下对应关系或者其他的一些参数。使用不多说。
先上访问地址(也就是action方法)代码:
packagenet.javaing.mvc.annotation;
importjava.lang.annotation.Documented;
importjava.lang.annotation.ElementType;
importjava.lang.annotation.Retention;
importjava.lang.annotation.RetentionPolicy;
importjava.lang.annotation.Target;
/**
* 对实体属性与数据库字段的一个对应
* @author javaing
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interface At {
String value() default "";
String method() default "get";
String suffix() default"servlet";
}
说明:Java中提供了四种元注解,专门负责注解其他的注解,分别如下 1、@Retention元注解,表示需要在什么级别保存该注释信息(生命周期)。可选的RetentionPoicy参数包括: RetentionPolicy.SOURCE: 停留在java源文件,编译器被丢掉 RetentionPolicy.CLASS:停留在class文件中,但会被VM丢弃(默认) RetentionPolicy.RUNTIME:内存中的字节码,VM将在运行时也保留注解,因此可以通过反射机制读取注解的信息 2、@Target元注解,默认值为任何元素,表示该注解用于什么地方。可用的ElementType参数包括 ElementType.CONSTRUCTOR: 构造器声明 ElementType.FIELD: 成员变量、对象、属性(包括enum实例) ElementType.LOCAL_VARIABLE: 局部变量声明 ElementType.METHOD: 方法声明 ElementType.PACKAGE: 包声明 ElementType.PARAMETER: 参数声明 ElementType.TYPE: 类、接口(包括注解类型)或enum声明 3、@Documented将注解包含在JavaDoc中 4、@Inheried允许子类继承父类中的注解
这样首先就完成了action注解。
通过上面的文章,可以看到@At注解有三个属性,value为访问请求地址,method为请求方式,支持get/post,还有个suffix是后缀,也就是通过什么样的后缀访问请求,一个完整的请求地址(也就是浏览器中的址,或post请求控制器地址),由value和suffix构成。中间用“.”分开。
有了注解,我们当然要扫描到注解,什么时候扫描?其实用spring的朋友都知道,spring在web.xml中有这么一句话:
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
相信这个大家都不陌生。
那么同样地,我也用这个来实现:
package net.javaing.mvc.listener;
import java.lang.reflect.Method;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import net.javaing.mvc.annotation.At;
import net.javaing.mvc.exception.ExistActionException;
import net.javaing.mvc.mapping.ActionMapping;
import net.javaing.util.ClassUtil;
import net.javaing.util.PropertiesUtil;
import org.apache.log4j.Logger;
public class MvcContextListener implements ServletContextListener {
static Logger logger =Logger.getLogger(MvcContextListener.class.getName());
@Override
public voidcontextDestroyed(ServletContextEvent arg0){
}
@Override
public voidcontextInitialized(ServletContextEvent arg0){
for (Class<?> clazz :ClassUtil.getClasses(PropertiesUtil.readValue("config/mvc.properties","scan-base-package"))) {
Method[]methods=clazz.getDeclaredMethods();
for (Method method :methods) {
At at =method.getAnnotation(At.class);
if (at != null){
StringmethodName="";
if(!at.value().equals("")){
methodName=at.value();
}else{
methodName=method.getName();
}
if(at.method().equals("get")){
if(ActionMapping.getGetAction(methodName+"."+at.suffix())==null){ ActionMapping.addGetActionMapping(methodName+"."+at.suffix(),method);
logger.info("findnew action:type:get\t\tname:"+methodName+"."+at.suffix());
}else{
try{
throw newExistActionException(methodName+"."+at.suffix());
}catch (ExistActionException e) { e.printStackTrace();
}
}
}else{ if(ActionMapping.getPostAction(methodName+"."+at.suffix())==null){ ActionMapping.addPostActionMapping(methodName+"."+at.suffix(),method); logger.info("find newaction:type:post\t\tname:"+methodName+"."+at.suffix());
}else{
try{
thrownew ExistActionException(methodName+"."+at.suffix());
}catch (ExistActionException e) {
e.printStackTrace();
}
}
}
}
}
}
}
}
至于使用,同spring一样。只是将类名替换:
<listener>
<listener-class>net.javaing.mvc.listener. MvcContextListener.</listener-class>
</listener>
这样也就完成了@At注解的扫描。并且保存到了一个map集合对象ActionMapping中。
有的朋友会问,ActionMapping是个什么东东?
我只能说是个map(事实上他也是),用来存放扫描到的地址。先看看代码:
package net.javaing.mvc.mapping;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/***
* Action映射
*/
public class ActionMapping {
private staticMap<String,Method> postMapping = new HashMap<String,Method>();
private static Map<String,Method>getMapping = new HashMap<String,Method>();
public static voidaddPostActionMapping(String serletPath,Method method){
postMapping.put("/"+serletPath,method);
}
public static MethodgetPostAction(String servletPath){
returnpostMapping.get(servletPath);
}
public static voidaddGetActionMapping(String serletPath,Method method){
getMapping.put("/"+serletPath,method);
}
public static MethodgetGetAction(String servletPath){
returngetMapping.get(servletPath);
}
}
实现的办法就是,把扫描到的请求地址,按get/post分类。按请求地址(前面说过,由value+”.”+suffix构成)存放主键,对应的方法(method对象)为值,这样的方式存放。想法就是,当请求地址请求以后,取得请求地址,寻找对应方法。如果扫描出现地址完全一样(请求方式、请求名称、后缀完全一样)会抛出:ExistActionException。当请求地址找不到的时候(不存在的请求地址,有可能是请求方式不对或名称或后缀不一样)会抛出:NoSuchActionException。
以下为两异常定义:
ExistActionException.java:
package net.javaing.mvc.exception;
public class ExistActionException extends Exception {
private static final longserialVersionUID = -8608538729128495163L;
publicExistActionException(String msg) {
super("重复的请求地址!"+msg);
}
NoSuchActionException.java:
package net.javaing.mvc.exception;
public class NoSuchActionException extends Exception {
private static final longserialVersionUID = -8608538729128495163L;
publicNoSuchActionException(String msg) {
super("不存在的请求地址!"+msg);
}
这样也就完全地完成了地址扫描。
到现在为止,如果熟悉springmvc的朋友就会发现,笔者自己写的mvc有很多使用方式和spring很像。只不过注解名称不一样而已。多一个了个属性而已,之所以加一个属性,是因为本mvc主要设计为网站形式,尤其文章类型的,给用户呈现的和管理后台有点一样的情况,就是用户呈现的需要页面静态化,常用urlrewrite实现,但是开发的时候一般总是先开发功能,完了再统一做地址静态化,至少我一般这样。这样无形中添加了工作量。如果让action后缀(部分)变成html这样就不用再处理了,而如果直接全用html做后缀,有时感觉又不太方便或有可能冲突(与真实静态文件),而且后台管理方便又一般不那样做,所以,这纯粹是自己新加的一点功能(有些朋友可能不认同这种方式,或感觉没用)。
同样地,本mvc使用也和spring一样,在web.xml中加入一个servlet:
<servlet>
<servlet-name>mvc</servlet-name>
<servlet-class>net.javaing.mvc.Mvc</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
根据自己的一些功能,配置自己的mvc。和spring比起来简单多了,主要参数少。功能小。
关于地址转发,基本思想是根据请求方式,找到对应的所有方式下的请求地址集合,再从集合中根据请求地址,找到请求方法。然后执行,进行页面返回。中间涉及几个用户不会手动带参的,也就是request这样的参数。但是像springmvc这样直接在后台使用的参数的处理。以下是具体实现:
Mvc。Java:主要接收地址,进行地址分发,将请求下发
package net.javaing.mvc;
import java.io.IOException;
import java.lang.reflect.Method;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.javaing.mvc.mapping.ActionMapping;
public class Mvc extends HttpServlet {
private static Mvcs mvc = new Mvcs();
/**
*
*/
private static final long serialVersionUID = -6326149137476027612L;
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
Method method = ActionMapping.getGetAction(request.getServletPath());
Object obj=mvc.execute(request, response, method);
if(obj instanceof JspView){
request.setAttribute("obj", ((JspView) obj).getObject());
request.getRequestDispatcher(((JspView) obj).getPath()+".jsp").forward(request,response);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Method method = ActionMapping.getPostAction(request.getServletPath());
Object obj=mvc.execute(request, response, method);
if(obj instanceof JspView){
request.setAttribute("obj", ((JspView) obj).getObject());
request.getRequestDispatcher(((JspView) obj).getPath()+".jsp").forward(request,response);
}
}
}
在上面的类中可以看到,并没有进行真正的处理,只是转发给mvcs类。
Mvcs.java
package net.javaing.mvc;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.javaing.mvc.annotation.CheckSession;
import net.javaing.mvc.exception.NoSuchActionException;
import net.javaing.mvc.exception.NullCheckSessionIdException;
import net.javaing.util.MethodParamNamesScanner;
import net.javaing.util.SetterGetter;
public class Mvcs {
public Object execute(HttpServletRequest request,
HttpServletResponse response, Method method) {
if (method == null) {
try {
throw new NoSuchActionException("");
} catch (NoSuchActionException e) {
e.printStackTrace();
}
return null;
} else {
try {
if(checkSession(request, method)){
Class<?>[] parameterTypes = method.getParameterTypes();
List<String> paramNames = MethodParamNamesScanner
.getParamNames(method);
Object[] parameterObjects = new Object[parameterTypes.length];
for (int index = 0; index < parameterTypes.length; index++) {
Class<?> clazz = parameterTypes[index];
String paramName = paramNames.get(index);
if (clazz.getName().equals(
HttpServletRequest.class.getName())) {
parameterObjects[index] = request;
} else if (clazz.getName().equals(
HttpServletResponse.class.getName())) {
parameterObjects[index] = response;
} else if (clazz.getName().equals(Integer.class.getName())) {
parameterObjects[index] = Integer.parseInt(request
.getParameter(paramName));
} else if (clazz.getName().equals(Double.class.getName())) {
parameterObjects[index] = Double.parseDouble(request
.getParameter(paramName));
} else if (clazz.getName().equals(Float.class.getName())) {
parameterObjects[index] = Float.parseFloat(request
.getParameter(paramName));
} else if (clazz.getName().equals(String.class.getName())) {
parameterObjects[index] = request
.getParameter(paramName);
} else {
Object obj = clazz.newInstance();
for (Field filed : clazz.getDeclaredFields()) {
Method setMethod = clazz.getDeclaredMethod(
SetterGetter.setter(filed.getName()),
filed.getType());
setMethod.invoke(obj,
request.getParameter(filed.getName()));
}
parameterObjects[index] = obj;
}
}
return method.invoke(method.getDeclaringClass().newInstance(),
parameterObjects);
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
}
public boolean checkSession(HttpServletRequest request, Method method){
CheckSession checkSession = method.getAnnotation(CheckSession.class);
if(checkSession!=null){
if(checkSession.name()==null){
try {
throw new NullCheckSessionIdException("\"\"");
} catch (NullCheckSessionIdException e) {
e.printStackTrace();
}
return false;
}else if((checkSession.value()==null&&request.getSession().getAttribute(checkSession.name())!=null)||checkSession.value().equals(request.getSession().getAttribute(checkSession.name()))){
return true;
}else{
//不符合指定值当然是要返回指定页面了。
return false;
}
}else{
return true;
}
}
}
这个类会进行真正的处理,将请求转发给真正的我们要使用的控制器。主要处理:
1、地址分发
2、参数处理,主要表现为request和response。
3、将参数对象化,主要模仿spring的方式,自动装配对象,而不是像struts2一样,要通过user.username这种方式处理。
4、添加了一个过滤器,会检查会话状态。下次会讲到。
这样也就完成了从页面请求到控制器的处理。