自定义springmvc

自定义springmvc

1、简单介绍

​ springmvc框架就是是在servlet基础之上进行了很多的封装、增加一些功能,让web开发更加方便,大大简化了开发的难度。但是springmvc本质上来说仍然是spring。所以学习springmvc之前,建议先看下spring的基础知识。当然我在未来还会进行写spring的知识点。

2、servlet的缺点

先说说javaweb中比较高级的处理方式,下面用案例来进行显示

前端的请求:

<a href="./user?action=queryAll">查询所有</a>
<a href="./user?action=add">添加</a>
<a href="./user?action=delete">删除</a>
<a href="./user?action=update">更新</a>
public class UserServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            this.doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       // this.queryAll(request,response);
       // this.add(request,response);
       // this.delete(request,response);
       // this.update(request,response);
    }
    
    public void queryAll(HttpServletRequest request, HttpServletResponse response){
        //  使用request和response来进行一些操作
    }
    public void add(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
    public void delete(HttpServletRequest request, HttpServletResponse response{
         //  使用request和response来进行一些操作
    }
    public void update(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
}

对应的web.xml文件

    <servlet>
        <servlet-name>userservlet</servlet-name>
        <servlet-class>guang.servlet.UserServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>userservlet</servlet-name>
        <url-pattern>/user</url-pattern>
    </servlet-mapping>

分析上面的问题:

UserServlet中有四个方法,但是不知道前端的请求是请求的哪个方法,而在这个servlet中只能调用一种方法,但是要是需要前端请求执行这四种方法中的一个,又该怎么进行操作?此时应该想到了之前学习HttpServlet中的service方法,使用的是if判断语句来进行的。

查看下HttpServet中的service方法

    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        long lastModified;
        if (method.equals("GET")) {
            lastModified = this.getLastModified(req);
            if (lastModified == -1L) {
                this.doGet(req, resp);
            } else {
                long ifModifiedSince;
                try {
                    ifModifiedSince = req.getDateHeader("If-Modified-Since");
                } catch (IllegalArgumentException var9) {
                    ifModifiedSince = -1L;
                }

                if (ifModifiedSince < lastModified / 1000L * 1000L) {
                    this.maybeSetLastModified(resp, lastModified);
                    this.doGet(req, resp);
                } else {
                    resp.setStatus(304);
                }
            }
        } else if (method.equals("HEAD")) {
            lastModified = this.getLastModified(req);
            this.maybeSetLastModified(resp, lastModified);
            this.doHead(req, resp);
        } else if (method.equals("POST")) {
            this.doPost(req, resp);
        } else if (method.equals("PUT")) {
            this.doPut(req, resp);
        } else if (method.equals("DELETE")) {
            this.doDelete(req, resp);
        } else if (method.equals("OPTIONS")) {
            this.doOptions(req, resp);
        } else if (method.equals("TRACE")) {
            this.doTrace(req, resp);
        } else {
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[]{method};
            errMsg = MessageFormat.format(errMsg, errArgs);
            resp.sendError(501, errMsg);
        }

    }

所以根据这种思想来进行改进

改进方法一:

public class UserServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            this.doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
     // 先获取请求的方法是哪一个
        String method = request.getParameter("action");
        if("queryAll".equals(method)){
            this.queryAll(request,response);
        }else if("add".equals(method)){
            this.add(request,response);
        }else if("delete".equals(method)){
             this.delete(request,response);
        }else if("update".equals(method)){
             this.update(request,response);
        }
    }
    
    public void queryAll(HttpServletRequest request, HttpServletResponse response){
        //  使用request和response来进行一些操作
    }
    public void add(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
    public void delete(HttpServletRequest request, HttpServletResponse response{
         //  使用request和response来进行一些操作
    }
    public void update(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
}

此时,觉得上面的方法完美吗?有没有继续可以改进的方法?

对于一个类说来,调用方法的几种方式:

1、使用类名调用静态方法
2、使用对象调用静态方法
3、使用对象调用非静态方法

这里的方法都是非静态的,所以只能选择对象来进行调用。那么如何确定是哪种方法呢?

通过请求对象可以获取得到请求参数的值(方法名称),而且方法中的参数类型还都是一样的。那么有没有想到反射????没有想到的话,好好思考下自己的问题吧。

复习下利用反射来调用方法

// 因为在doPost/Get被service所调用,那么调用方法的对象是当前的servlet的实例对象,也就是this,那么在方法中就可以使用这个对象了

// 得到字节码对象
Class clazz = this.getClass();
// 利用字节码对象获取得到指定的方法对象。这里需要得到方法的名字。怎么获取得到名字??
// 前端传递过来的参数值不就是我们所需要的嘛!所以先获取一下参数的值(也就是方法的名字)
String action = request.getParameter("action");
// 将方法名字填充到这里来
Method method = clazz.getMethod(action,HttpServletRequest.class,HttpServletResponse.class);
// 方法对象执行,需要传递进去实际的参数。需要的是request和response。obj是方法的返回值
Object obj = method.invoke(this,request,reponse); // doGet/Post方法参数中就有这两个值 

所以填充到代码中去。

那么又有一个问题来了!此时是不能抛出异常的,为什么?因为父类(HttpServlet)中的doGet/Post方法抛了两个异常,子类不能比父类抛出更多的异常,多以此时就需要抓异常了。

public class UserServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            this.doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 得到字节码对象
	Class clazz = this.getClass();
	// 利用字节码对象获取得到指定的方法对象。这里需要得到方法的名字。怎么获取得到名字??
	// 前端传递过来的参数值不就是我们所需要的嘛!所以先获取一下参数的值(也就是方法的名字)
	String action = request.getParameter("action");
	// 将方法名字填充到这里来
	Method method = clazz.getMethod(action,HttpServletRequest.class,HttpServletResponse.class);
	// 方法对象执行,需要传递进去实际的参数。需要的是request和response。obj是方法的返回值
	Object obj = method.invoke(this,request,reponse); // doGet/Post方法参数中就有这两个值 
    }
    
    public void queryAll(HttpServletRequest request, HttpServletResponse response){
        //  使用request和response来进行一些操作
    }
    public void add(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
    public void delete(HttpServletRequest request, HttpServletResponse response{
         //  使用request和response来进行一些操作
    }
    public void update(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
}

在下面进行抓异常

public class UserServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            this.doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
   try{
    // 得到字节码对象
	Class clazz = this.getClass();
	// 利用字节码对象获取得到指定的方法对象。这里需要得到方法的名字。怎么获取得到名字??
	// 前端传递过来的参数值不就是我们所需要的嘛!所以先获取一下参数的值(也就是方法的名字)
	String action = request.getParameter("action");
	// 将方法名字填充到这里来
	Method method = clazz.getMethod(action,HttpServletRequest.class,HttpServletResponse.class);
	// 方法对象执行,需要传递进去实际的参数。需要的是request和response。obj是方法的返回值
	Object obj = method.invoke(this,request,reponse); // doGet/Post方法参数中就有这两个值 
   }catch(Exception e){
            e.printStackTrace()
        }
    }
    
    public void queryAll(HttpServletRequest request, HttpServletResponse response){
        //  使用request和response来进行一些操作
    }
    public void add(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
    public void delete(HttpServletRequest request, HttpServletResponse response{
         //  使用request和response来进行一些操作
    }
    public void update(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
}

改进方案二:

上面的分析来说,在javaweb阶段是足够进行使用了,但是在springmvc中还是不够使用的。

分析原因:

比如说商城管理系统中,有关于订单处理的servlet、有关于用户处理的servlet、有关于权限处理的servlet等等。servlet一多就不好容易管理,但是每个servlet又都有其特殊的功能。但是就是书写起来太麻烦了

想办法进行改进,观察后发现,每个servlet中大量重复的代码是doPost/Get方法。那么能否想到对这里的doGet/Post方法进行抽取呢?很好的想法。

来进行抽取

在这里插入图片描述

对应的代码如下

public class BaseServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            //1.接收参数:action--要执行的方法名称
            String action = request.getParameter("action");
            //2.反射调用名称为action的方法
            Class clazz = this.getClass();
            Method method = clazz.getMethod(action, HttpServletRequest.class, HttpServletResponse.class);
            method.invoke(this, request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}

额外注意一点,我在这里想的是将BaseServlet抽取成抽象类,但是不能进行抽取,因为BaseServlet需要通过全限定类名反射得到对象,抽象类是不可以生成对象的。在这里说明一下

而对于之前的类来说,只需要继承上面的BaseServlet即可

public class UserServlet extends BaseServlet {
    
    public void queryAll(HttpServletRequest request, HttpServletResponse response){
        //  使用request和response来进行一些操作
    }
    public void add(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
    public void delete(HttpServletRequest request, HttpServletResponse response{
         //  使用request和response来进行一些操作
    }
    public void update(HttpServletRequest request, HttpServletResponse response){
         //  使用request和response来进行一些操作
    }
}

显示一波体系图

在这里插入图片描述

分析:此时的UserServlet在调用service方法的时候,首先会去爷爷类HttpServlet中调用service方法,然后调用对应的doGet/Post方法,然后通过获取得到的参数得到执行的方法,然后执行userservlet类中的四大方法。

画图分析一波更加清楚。

在这里插入图片描述

那么分析这个BaseServlet类

自己没有进行处理核心的业务功能,只是通过反射来调用子类的方法的。

子类,如UserServlet是用来处理业务的

这种方式就决定了一下的几个特点:

1:所有的请求携带的参数都得是action,需要对获取的方法名字进行获取

2、所有的业务处理类(UserServlet处理用户请求)都需要继承BaseServlet

通过继承体系图,发现这种依赖性太强了!凡事皆有弊端。虽然增强了,但是也有弊端,那么需要考虑的是,还有解决的方式吗?

改进方法三

目标:想要脱离这种强烈的依赖性,怎么去进行脱离?也就是说怎么处理耦合问题?

针对于上面的携带参数的问题还需要进一步的解决,难道每个请求中都需要携带参数action来进行请求的吗?

用一种特殊的表示来表示请求的方式是要访问servlet的。struct2使用的是.do来进行标识的,好像有一个是以.action来进行结尾的,表示的是请求的是servlet的业务处理类

思路:

1、将业务处理类和BaseServlet进行分离,不再使用继承的方式;

2、扫描src目录下所有的java类,当然其他包名也是可以的,只要是把所有的java类放在了一个文件夹中即可;

3、利用循环的方式扫描每一个类,创建一个类的对象,并保存类中所有的方法,存放在一个容器中。这样的容器很容易想到,因为是键值对的形式,所以很容易先到是map集合来进行存储。

4、针对于上面的携带参数的问题还需要进一步的解决,难道每个请求中都需要携带参数action来进行请求的吗?用一种特殊的表示来表示请求的方式是要访问业务处理类的。我是在是想不出来更好的办法了,只能沿用之前老的框架中的方式俩进行解决。struct2使用的是.do来进行标识的,好像有一个是以.action来进行结尾的,表示的是请求的是servlet的业务处理类

那么又产生了新的问题:对于一个请求来说,如:http://localhost:8080/test/user/queryAll.do,如何去判断使用的哪一个业务处理类中的哪个方法去进行处理请求呢?

思路:

什么来作为key?

对于URI来说,URI是/test/user/queryAll.do,真正有用的是后面的/user/queryAll.do。因为前面的/test是Context路径,所以没有太大的效果。对于一个context来说,每个业务类都会有这种。用来作为一种区分是不适合的。
那么获取得到了/user/queryAll.do,我们至少可以明白一点,我们要用的是user中的queryAll方法,沿着这样的逻辑来进行思考。那么要这个.do也是没有作用的,可以使用字符串中的方法进行截取,最终得到的字符串就是/user/queryAll,那么离我们之前的queryAll又近了一步,在进一步的进行思考,我们也可以获取得到user和queryAll这两个值。选取哪一个?我选择user,因为其他的业务处理类说,订单类来说queryAll方法,商品类也有这个方法,而唯一没有的就是user,用这个可以来作为区分,可以让其充当map中的key。顺着这种思路来进行分析,其实也不难理解。

用代码来实现一下上面的分析过程:

第一步:获取得到URI

第二步:对URI进行截取获取得到字符串

第三步:对URI截取得到的字符串进行再截取

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
            // 获取得到URI
        String requestURI = request.getRequestURI();
        System.out.println("requestURI----------:"+requestURI);
        // 获取得到context路径
        String contextPath = request.getContextPath();
        System.out.println("contextPath----------:"+contextPath);
        // 将URI进行截取
        String requestURI1 = requestURI.substring(contextPath.length());
        System.out.println("requestURI1----------:"+requestURI1);
        // 再次对subString进行截取。将.do去掉
        String requestURI2 = requestURI1.substring(0,requestURI1.lastIndexOf("."));
        System.out.println("requestURI2----------:"+requestURI2);
        // 再次将字符串进行截取
        String[] split = requestURI2.split("/");
        for (int i = 0; i < split.length; i++) {
            System.out.println("split["+i+"]------:"+split[i]);
        }
    }

控制台信息:

requestURI----------:/test/user/queryAll.do
contextPath----------:/test
requestURI1----------:/user/queryAll.do
requestURI2----------:/user/queryAll
split[0]------:          // 将/前面的作为一个,截取的是空的
split[1]------:user
split[2]------:queryAll

那么这里有必要的去思考一个问题了。做为key的有必要是user吗?万一有多层路径该怎么处理?

这里不再进行深入探讨,为了追求简洁,可以选择一个简单的模式,可以选择requestURI1,也可以选择requestURI2,在这里我选择requestURI1

那么value是什么?
最初的想法是让value成为该类的对象(通过反射机制可以获取),然后通过反射来调用其方法。
但是更好的方式是将方法名字、对象封装成一个对象作为value,采用的是面向对象的思想来解决问题。这里的思想真的是非常的棒。

但是其实springmvc采用的也不是这种方法来进行处理的,我没有看过源码,所以现在也是不清楚的。只能沿用以前过时框架中的方式来进行操作的。比如说struct2中采用.do,.action来进行处理请求业务逻辑类中的处理方法

这里说明的是让BaseServlet来拦截访问业务处理类,并且后缀名称是以".do"结尾的请求,那么这里和上面差不多,只不过这里要求的是访问servlet的时候,要求添加的是.do,上面要求的是在参数中添加action而已。

说明也没有根本性的解决问题。不过也是,要不然我们随随便便的也可以自定义框架了,肯定是没有别人考虑的那么周全。

说了这么多,还是使用图解的方式来进行说明

在这里插入图片描述

那么这里重点是doGet方法里面的书写方式了。为什么不需要重写doPost方法。我在这里给出两个理由

1、doPost方法不常用
2、可以在doPost方法中调用doGet方法,实现一劳永逸的效果。这也正是我在前面博客中提到的一个技巧

终极版本

针对于上面的思考方式进行再次改进

思路:

第一步:对业务处理类加上一种特殊的标识,能够让BaseServlet去识别

第二步:对业务处理类中的处理方法加上表示,能够让BaseServet去进行识别

在这里思考了许久,找不到解决的方法。如果不是别人告诉我,我是永远都不会想到使用注解来进行解决的

解决方案:

标识处理业务的类:使用注解@Controller
使用方法来处理参数:使用注解@RequestMapping,映射处理的方法

采用其中的一种方法或者两个都采用(推荐的是两个都采用,因为方便)

其实这里的注解可以随便的进行定义,但是这里考虑到的是后面真正的springmvc框架中采用的注解方式,采用的是上面的注解方式进行使用。

这里不需要由疑问,因为学过了springmvc才知道,在这里进行说明。

在这里实现自定springmvc的最终版本

第一步:自定义注解

对于Controller的注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
    String value() default "";  // default表示默认可以不写
}

对于RequestMapping的注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
     String value() default "";  // default表示默认可以不写
}

获取得到key

// 获取得到URI
String requestURI = request.getRequestURI();
System.out.println("requestURI----------:"+requestURI);
// 获取得到context路径
String contextPath = request.getContextPath();
System.out.println("contextPath----------:"+contextPath);
// 将URI进行截取
String requestURI1 = requestURI.substring(contextPath.length());
System.out.println("requestURI1----------:"+requestURI1);
// 再次对subString进行截取。将.do去掉
String requestURI2 = requestURI1.substring(0,requestURI1.lastIndexOf("."));
System.out.println("requestURI2----------:"+requestURI2);

将这里的requestURI1或者requestURI2作为key

获取得到value,根据上面的分析,value是一个类,

public class MvcMethod {
    private Method method;  // 方法对象
    private Object object;	// 当前类对象

    public MvcMethod(Method method, Object object) {
        this.method = method;
        this.object = object;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }
}

编写核心servlet类:

将反射机制获取得到key和value放到init方法中进行执行。希望在启动的时候就来加载执行这个方法,而不是说在请求第一次访问的时候再来进行启动加载。需要在web.xml中设置DispatcherServlet的启动时间

public class DispatcherServlet extends HttpServlet {

    private Map<String, MvcMethod> mvcMethodMap = new HashMap<>();

    @Override
    public void init() throws ServletException {
        try {
            //扫描guang.servlet包里所有的类
            List<Class<?>> classList = ClassScannerUtils.getClasssFromPackage("guang.servlet");
            for (Class<?> clazz : classList) {
                // 因为在业务处理类中加载了@Controller注解,那么利用这个可以减少扫描次数
                boolean isController = clazz.isAnnotationPresent(Controller.class);
                // 没有@Controller,继续扫描下一个类
                if (!isController) {
                    continue;
                }
                // 程序执行到这里,说明了类上肯定是有@Controller的
			   // 既然有了@Controller,那么里面肯定有@RequestMapping。如果在下面循环中反射得到对象
                // 肯定是不合适的,因为创建的对象中要有四个方法。也有节约内存的效果
                Object object = clazz.newInstance();

                //获取类里每个方法,因为都是public修饰的,不需要暴力反射使用方法
                Method[] methods = clazz.getMethods();
                // 遍历方法
                for (Method method : methods) {
                    boolean isMappingMethod = method.isAnnotationPresent(RequestMapping.class);
                    if (isMappingMethod) {
                        //把方法Method和类实例对象封装成MvcMethod对象
                        MvcMethod mvcMethod = new MvcMethod(method, object);

                        //得到每个方法上RequestMapping注解配置映射路径:mappingPath
                        RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                        String mappingPath = requestMapping.value();

                        //以mappingPath为key,MvcMethod对象为value,保存到map中
                        mvcMethodMap.put(mappingPath, mvcMethod);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            //1. 获取请求的路径  /mymvc/linkman/queryAll.do
            String uri = request.getRequestURI();
            String contextPath = request.getContextPath();
            String path = uri.substring(contextPath.length(), uri.lastIndexOf("."));

            //2. 找到路径对应的MvcMethod
            MvcMethod mvcMethod = mvcMethodMap.get(path);

            //3. 反射执行其中的方法
            Method method = mvcMethod.getMethod();
            Object object = mvcMethod.getObject();
            method.invoke(object, request, response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.doPost(request, response);
    }
}

总结:上面的只是一种思想,明显的存在着很多的缺陷。是一种不完全的框架,我将在会下一篇中博客中具体对比分析自定义的springmvc框架和真正的springmvc。

      //2. 找到路径对应的MvcMethod
        MvcMethod mvcMethod = mvcMethodMap.get(path);

        //3. 反射执行其中的方法
        Method method = mvcMethod.getMethod();
        Object object = mvcMethod.getObject();
        method.invoke(object, request, response);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    this.doPost(request, response);
}

}




总结:上面的只是一种思想,明显的存在着很多的缺陷。是一种不完全的框架,我将在会下一篇中博客中具体对比分析自定义的springmvc框架和真正的springmvc。







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值