“框架”一词对我们来说并不陌生,比如建房子的时候,经常都是先把基本的结构弄好,然后再往里面添加门窗之类的,基本的结构就是框架。
我觉得框架和工具是不同的作用,简单来说,工具就是我们使用它来帮我们完成某些工作,比如输入法。而框架是将我们写的东西给它调用来完成工作,两者是有区别的。
下面就简单的来完成一个WEB框架,基于注解方式完成。当然一个人的力量有限,所以很多问题没有考虑进去,比如上传文件,异常处理等等,只是一个非常简单初级的框架,也请大家多多提意见。
1. 配置servlet
对于WEB应用来说,主要有两种形式来进行用户请求拦截操作,一个是Filter,一个是servlet,这里我采用servlet来完成,所以应该在web.xml中对servlet进行配置,并且要在tomcat启动的时候完成初始化工作。
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>dispater</servlet-name> <servlet-class>com.jacksoft.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispater</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>
这里我的servlet的名称是不是有点熟悉?嘿嘿
2. 导入jar
我的项目采用maven来管理,所以需要导入servlet-api的包,在pom.xml里面进行配置,将该jar导入
<properties> <servlet-api.version>2.5</servlet-api.version> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>${servlet-api.version}</version> </dependency> </dependencies>
3. 编码
这里先进行分析具体的流程
当客户端发起请求的时候,比如:http://127.0.0.1:8080/web/test.do,其中web是项目的path,而test.do是访问的导航路径,所以我们需要根据这个test.do来查找对应的类,然后进行处理,最后再返回相应的页面得到结果,这就是一个大概的流程,当然这之间问题很多,都一一简单处理。
(1)这里采用注解的形式,所以得先写个注解来说明某个类是专门处理请求的,Controller
package com.jacksoft.web.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 指定controller配置
* 可以配置路径,默认为空
* @author Jack
*
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
String value() default "";
}
该注解名称是controller,用来定义处理请求的类
(2)定义URL 访问路径对应注解
package com.jacksoft.web.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UrlValue {
/**
* 指定具体路径信息
* @return
*/
String url();
Method method() default Method.GET;
}
注意: 这里的method方法返回的是一个枚举类型数据,也就是我们提交请求对应的方法,所以还需要创建一个枚举,便于管理数据
package com.jacksoft.web.annotation;
public enum Method {
GET, POST, DELETE, PUT
}
简单定义几种请求方式。
(3)编写DispatcherServlet
这里采用扫描的形式来查找那些类是标记了Controller注解的,这里就全部采用硬编码的形式来设置变量,由于初始扫描包的路径可能还要子包,所以需要递归的形式来读取所以的文件信息
private void initContext(){
try{
List<String> classList = new ArrayList<String>();
String filePath = SCAN_PACKAGE.replace(".", "/");
URL url = Thread.currentThread().getContextClassLoader().getResource(filePath);
File f = new File(url.toURI());
File[] files = f.listFiles();
for(File file : files){
listFile(file, classList, SCAN_PACKAGE);
}
for(String className : classList){
System.out.println(className);
}
}catch(Exception ex){
ex.printStackTrace();
}
}
/**
* 递归查找文件,发现是文件夹,那么继续查找
* @param f
* @param classList
* @param filePath
*/
private void listFile(File f,List<String> classList,String filePath){
if(f.isFile() && f.getName().endsWith(".class")){
classList.add(filePath + "." + f.getName().replace(".class", ""));
}else{
File[] files = f.listFiles();
String tmpFilePath = filePath + "." +f.getName();
for(File file : files){
listFile(file,classList,tmpFilePath);
}
}
}
首先将包名转换为路径,然后查找下面的文件,如果是目录,那么继续查找,同时路径名称也要变化。
这样就找到了指定包下面的所以class名称,然后再通过反射来查找那些类是我们需要的。
这里创建个Controller类
package com.jacksoft.web.controller;
import com.jacksoft.web.annotation.Controller;
import com.jacksoft.web.annotation.Method;
import com.jacksoft.web.annotation.UrlValue;
@Controller
public class TestController {
@UrlValue(url="test.do",method=Method.GET)
public String test(){
System.out.println("------TestController.test正在处理----------------");
System.out.println(req.getParameter("username"));
return "test";
}
}
该类已经加了controller标记,同时也添加了urlvalue的标记,先来处理controller标记,这里我们希望将访问路径,Controller以及对应的方法关联起来,所以定义了一个java bean
package com.jacksoft.web.utils;
import java.lang.reflect.Method;
public class MethodUtils {
private String path;
private String requestMethod;
private Object controller;
private Method method;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getRequestMethod() {
return requestMethod;
}
public void setRequestMethod(String requestMethod) {
this.requestMethod = requestMethod;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
@Override
public String toString() {
return "path=" + path + " requestMethod=" + requestMethod
+ " controller=" + controller + " method=" + method;
}
}
这样我们查找到需要的数据时,可以对象化后再放入map中,其中key为我们访问的servletUrl,value为刚刚创建的MethodUtils.
private static Map<String,MethodUtils> controllerMap = null;
在initContext方法中进行初始化操作
controllerMap = new HashMap<String, MethodUtils>();
将查找了又controller和urlvalue标记的信息放入mao中,因此initContext方法如下:
private void initContext(){
try{
controllerMap = new HashMap<String, MethodUtils>();
List<String> classList = new ArrayList<String>();
String filePath = SCAN_PACKAGE.replace(".", "/");
URL url = Thread.currentThread().getContextClassLoader().getResource(filePath);
File f = new File(url.toURI());
File[] files = f.listFiles();
for(File file : files){
listFile(file, classList, SCAN_PACKAGE);
}
for(String className : classList){
Class<?> clazz = Class.forName(className);
Object obj = clazz.newInstance();
//判断是否加了controller的注解
//只处理该注解,如果是,那么接着将其方法中的urlvalue注解拿到
if(clazz.isAnnotationPresent(Controller.class)){
Method[] methods = clazz.getMethods();
for(Method method : methods){
//判断方法是否加了UrlValue的注解,如果加了,那么获取对应的url和method方法
if(method.isAnnotationPresent(UrlValue.class)){
UrlValue value = method.getAnnotation(UrlValue.class);
MethodUtils util = new MethodUtils();
util.setPath(value.url());
util.setRequestMethod(value.method().name());
util.setMethod(method);
util.setController(obj);
controllerMap.put(value.url(), util);
}
}
}
}
}catch(Exception ex){
ex.printStackTrace();
}
}
这样就把信息连在一起了,当用户访问test.do的时候,那么取该map中查找key=test.do,找出来之后,再通过反射就可以执行对应的方法,同时返回数据。该方法只需要在启动tomcat的时候执行,所以把他给init方法调用
@Override
public void init() throws ServletException {
initContext();
}
接下来就是service方法了,将接受请求的路径到map中去获取,如果能拿得到数据,那么就可以正常调用,否则返回错误页面
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
try{
String servletUrl = req.getServletPath().substring(1);
MethodUtils utils = controllerMap.get(servletUrl);
if(utils == null){
req.getRequestDispatcher("/WEB-INF/jsp/error/error404.jsp");
return;
}
Method method = utils.getMethod();
Object obj = method.invoke(utils.getController());
req.getRequestDispatcher("/WEB-INF/jsp/"+obj+".jsp").forward(req, resp);
}catch(Exception ex){
ex.printStackTrace();
}
}
到这里一个简单的框架就算完成了,启动tomcat来进行访问,访问test.do 能够正确返回test.jsp页面
4. 加上service注解
请求分发给controller后,controller还需要调用service来处理具体的事情,这里也通过注解的形式来为字段赋值,设计原理就是Service注解的名称和controller字段注解的值一样,那么就进行赋值处理
(1)定义Service注解
package com.jacksoft.web.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
String value() default "";
}
该注解用于service上面
(2)定义myservice,在controller中标记字段
package com.jacksoft.web.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyService {
String value();
}
(3)创建service
既然用到service,那么就创建个,简单的打印一句话
package com.jacksoft.web.service;
import com.jacksoft.web.annotation.Service;
@Service("testService")
public class TestService {
public void sayHello(){
System.out.println("----------TestService.sayHello()--------------------------");
}
}
(4) 接下来接着处理DispaterServlet,将Service注解的类也找出来存储起来
定义一个Map<String,Object>来存储数据
private Map<String,Object> serviceMap = null;
(5)在initContext方法处理controller之前,就需要处理service注解类
/**
* 处理server注解
*/
for(String str : classList){
Class<?> clazz = Class.forName(str);
if(clazz.isAnnotationPresent(Service.class)){
Service service = clazz.getAnnotation(Service.class);
serviceMap.put(service.value(), clazz.newInstance());
}
}
这样我们在处理controller时,如果发现字段上面有MyService注解,并且名称一样,那么我们就为其设值
修改Controller,添加service字段
package com.jacksoft.web.controller;
import com.jacksoft.web.annotation.Controller;
import com.jacksoft.web.annotation.Method;
import com.jacksoft.web.annotation.MyService;
import com.jacksoft.web.annotation.UrlValue;
import com.jacksoft.web.service.TestService;
@Controller
public class TestController {
@MyService("testService")
private TestService service;
@UrlValue(url="test.do",method=Method.GET)
public String test(){
System.out.println("------TestController.test正在处理----------------");
service.sayHello();
return "test";
}
}
(6)处理Service注解
// 处理service注解
Field[] fields = clazz.getDeclaredFields();
for(Field field : fields){
if(field.isAnnotationPresent(MyService.class)){
MyService service = field.getAnnotation(MyService.class);
String serverName = service.value();
Object serverParam = serviceMap.get(serverName);
if(serverParam != null){
//为标记有注解 且名字相同的字段赋值
field.setAccessible(true);
field.set(obj, serverParam);
}
}
}
将Controller中标记有MyService注解的字段,看名称是否在map中存在,如果存在,那么就赋值
(7) 测试
其他的就不修改了,启动tomcat,进行访问test.do,调用service处理也能工作了。
5. 封装参数
在分发到controller对应方法时,还需要设置参数,在service中处理的时候,通过method对象来设置参数信息,最后通过反射将参数传递过去。
Method method = utils.getMethod();
Class<?>[] parameterClass = method.getParameterTypes();
Object[] parameter = new Object[parameterClass.length];
int i = 0;
for(Class<?> clazz : parameterClass){
if(clazz.equals(HttpServletRequest.class)){
parameter[i] = req;
}else if(clazz.equals(HttpServletResponse.class)){
parameter[i] = resp;
}
i++;
}
Object page = utils.getMethod().invoke(utils.getController(), parameter);
这里简单的写两种类型的参数,当然参数很多,可以通过javabean的特性来赋值。
到这里一个简单的框架就差不多了,接下来就是不断的去修改,添加,尽可能的完美。
附上DispaterServlet全部代码:
package com.jacksoft.web.servlet;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.jacksoft.web.annotation.Controller;
import com.jacksoft.web.annotation.MyService;
import com.jacksoft.web.annotation.Service;
import com.jacksoft.web.annotation.UrlValue;
import com.jacksoft.web.utils.MethodUtils;
public class DispatcherServlet extends HttpServlet {
/**
*
*/
private static final long serialVersionUID = 1L;
private static final String SCAN_PACKAGE = "com.jacksoft.web";
private static Map<String,MethodUtils> controllerMap = null;
private Map<String,Object> serviceMap = null;
@Override
public void init() throws ServletException {
initContext();
}
private void initContext(){
try{
controllerMap = new HashMap<String, MethodUtils>();
serviceMap = new HashMap<String, Object>();
List<String> classList = new ArrayList<String>();
String filePath = SCAN_PACKAGE.replace(".", "/");
URL url = Thread.currentThread().getContextClassLoader().getResource(filePath);
File f = new File(url.toURI());
File[] files = f.listFiles();
for(File file : files){
listFile(file, classList, SCAN_PACKAGE);
}
/**
* 处理server注解
*/
for(String str : classList){
Class<?> clazz = Class.forName(str);
if(clazz.isAnnotationPresent(Service.class)){
Service service = clazz.getAnnotation(Service.class);
serviceMap.put(service.value(), clazz.newInstance());
}
}
for(String className : classList){
Class<?> clazz = Class.forName(className);
//判断是否加了controller的注解
//只处理该注解,如果是,那么接着将其方法中的urlvalue注解拿到
if(clazz.isAnnotationPresent(Controller.class)){
Object obj = clazz.newInstance();
// 处理service注解
Field[] fields = clazz.getDeclaredFields();
for(Field field : fields){
if(field.isAnnotationPresent(MyService.class)){
MyService service = field.getAnnotation(MyService.class);
String serverName = service.value();
Object serverParam = serviceMap.get(serverName);
if(serverParam != null){
//为标记有注解 且名字相同的字段赋值
field.setAccessible(true);
field.set(obj, serverParam);
}
}
}
Method[] methods = clazz.getMethods();
for(Method method : methods){
//判断方法是否加了UrlValue的注解,如果加了,那么获取对应的url和method方法
if(method.isAnnotationPresent(UrlValue.class)){
UrlValue value = method.getAnnotation(UrlValue.class);
MethodUtils util = new MethodUtils();
util.setPath(value.url());
util.setRequestMethod(value.method().name());
util.setMethod(method);
util.setController(obj);
controllerMap.put(value.url(), util);
}
}
}
}
}catch(Exception ex){
ex.printStackTrace();
}
}
/**
* 递归查找文件,发现是文件夹,那么继续查找
* @param f
* @param classList
* @param filePath
*/
private void listFile(File f,List<String> classList,String filePath){
if(f.isFile() && f.getName().endsWith(".class")){
classList.add(filePath + "." + f.getName().replace(".class", ""));
}else{
File[] files = f.listFiles();
String tmpFilePath = filePath + "." +f.getName();
for(File file : files){
listFile(file,classList,tmpFilePath);
}
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
try{
String servletUrl = req.getServletPath().substring(1);
MethodUtils utils = controllerMap.get(servletUrl);
if(utils == null){
req.getRequestDispatcher("/WEB-INF/jsp/error/error404.jsp");
return;
}
Method method = utils.getMethod();
Object obj = method.invoke(utils.getController());
req.getRequestDispatcher("/WEB-INF/jsp/"+obj+".jsp").forward(req, resp);
}catch(Exception ex){
ex.printStackTrace();
}
}
}