MVC框架一

一、servlet的简化

在使用servlet处理前端发来的请求时,往往需要根据不同的请求创建不同的servlet类。可不可以通过一个servlet类处理所有请求呢?答案是可以的:

package com.fan.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//处理所有路由为f的请求
@WebServlet("/f/*")
public class MyServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}

这个servlet类不进行任何请求的处理与响应,而是将请求按照分类并分发给其它对应的类进行处理。创建一个包com.fan.controller,在这个包中添加各种Controller类处理请求,为了举例,这里加入三个Controller类,分别为OrderController、ProductController和UserController。

OrderController

package com.fan.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class OrderController {
    public void deleteOrder(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("删除订单!");
    }
}

ProductController

package com.fan.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ProductController {
    public void onSale(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("It is onSale!");
    }
    
    public void offSale(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("It is offSale!");
    }
}

UserController

package com.fan.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@FApi("user")
public class UserController {
    public void setVip(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("设置VIP等级!");
    }

    public void disableUser(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("冻结用户!");
    }
}

这三个类可以处理订单、商品和用户的操作。比如此时在利用传统请求的方式发送/f/UserController/setVip。这个请求会被MyServlet接收到,此时可以做如下处理:

package com.fan.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;

@WebServlet("/f/*")
public class MyServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String prefix = "com.fan.controller";
        //获取地址栏的内容
        String uri = req.getRequestURI();
        try {
            //根据地址栏内容获得应该调用的类和方法
            //这里能够获取到类名UserController
            String apiP = uri.split("/")[3];
            //这里能够获取到方法名setVip
            String apiS = uri.split("/")[4];
            //根据包名加类名反射,实例化对象
             Object obj = Class.forName(prefix + "." + apiP).newInstance();
            //继续反射,获取对应的方法
            Method[] m = obj.getClass().getDeclaredMethod(apiS);
            //调用这个方法,处理请求
            m.invoke(obj);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这样在前端发出请求后,控制栏应该输出

设置VIP等级!

这样通过一个总路由的分发请求的方式确实可行,相应的controller内类的方法参数列表加上HttpServletRequest req, HttpServletResponse resp,就可以和普通的servlet类一样和前端进行交互了。但是,这种方法也有缺点,那就是类名和方法名有时候太长,也会直接暴露出来,可不可以给方法起个简单的“名字”,前端输入通过这些指定处理请求的方法呢?答案是可以的,可以通过注解来完成。

二、通过注解进一步简化servlet

1、注解的简介

在java中可以通过创建注解类自定义注解,在这里我自定义FApi注解

package com.fan.anno;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

//加入这个注解,可以让我的注解在运行时能够被反射
@Retention(RetentionPolicy.RUNTIME)
public @interface FApi {
    //只定义一个字符串变量value
    String value();
}

这样之前的三个类和各自的方法都可以使用这个注解起名字,以OrderController为例

package com.fan.controller;

import com.fan.anno.FApi;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@FApi("order")
public class OrderController {
    @FApi("del")
    public void deleteOrder(HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("删除订单!");
         try {
            resp.getWriter().print("okok");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2、反射注解找到对应方法

注解已经有了,并且每个注解都是独一无二的,那么该如何进行处理前端发过来的注解呢?例如在地址栏输入/order/del,通过反射所有类的所有注解,首先根据order找到对应类,动态创建类,然后反射遍历这个类里面方法的注解,找到与del对应的方法,通过执行这个方法来处理请求:

package com.fan.servlet;

import com.fan.anno.FApi;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;

@WebServlet("/f/*")
public class MyServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String prefix = "com.fan.controller";
        //获取地址栏的内容
        String uri = req.getRequestURI();
        try {
            String apiP = uri.split("/")[3];
            String apiS = uri.split("/")[4];
            //获取部署编译后class文件的路径
            String ctrlPath = req.getServletContext().getRealPath("") + "WEB-INF\\classes\\" + prefix.replace(".", "\\");
            //遍历该路径获取.class文件
            File ctrlDir = new File(ctrlPath);
            File[] fs = ctrlDir.listFiles();
            for (File f : fs) {
                //利用字符串分割获得class的文件名,就是类名。注意不能直接用.分割 而要用\\.分割
                String className = f.getName().split("\\.")[0];
                //利用类名进行反射,得到类上的注解
                Object obj = Class.forName(prefix + "." + className).newInstance();
                //获取指定注解及其属性值
                FApi fApiP = obj.getClass().getAnnotation(FApi.class);
                //匹配一级路径
                if (fApiP.value().equals(apiP)) {
                    //获取所有方法并根据注解进行匹配
                    Method[] ms = obj.getClass().getDeclaredMethods();
                    for (Method m : ms) {
                        FApi fApiS = m.getAnnotation(FApi.class);
                        if (fApiS.value().equals(apiS)) {
                            //匹配到方法就可以执行
                            m.invoke(obj, req, resp);
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

因为java文件在编译后文件名不变,所以通过编译路径的.class文件来获得所有类的类名。这样最后在控制台可以输出

删除订单!

在网页上会显示

okok

三、引入mvc

一个总路由通过注解分发请求的方式,其实已经有前人做了总结,并且写成了框架方便我们使用,这里介绍mvc。本次介绍mvc是使用最简单的配置先将mvc运行起来。首先导入需要的jar包,在web项目的WEB-INF文件夹里创建web.xml文件,在文件内写入如下配置:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

因为url-pattern设置为了/所以能够处理整个网站的所有请求。除了这个配置文件,还要在同目录下创建一个名为spring-servlet.xml的配置文件。注意这里的文件名一定要和我写的一样,因为mvc框架在运行的时候,会默认在WEB-INF文件夹内读取spring-servlet.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--controller包扫描-->
    <context:component-scan base-package="com.fan.controller"/>
</beans>

这个配置是最简单的配置,有了这个就可以做一些基本的实验了,完整的配置远不止这些。所谓包扫描与之前在编译路径中遍历注解一样。做完简单的配置后,如何处理请求呢?此时已经不需要我们自己写总路由了,只需要写好具体的类和方法就可以了:

package com.fan.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/vip")
    public void setVip() {
        System.out.println("设置会员!");
    }

    @RequestMapping("/login")
    public ModelAndView login() {
        System.out.println("okok");
    }
}

解释一下这里注解@RequestMapping("/user"),这个注解就相当于我之前自定义的FApi,里面放入对应“别名”。@Controller表示这个类需要实例化才可以使用里面的方法,所以框架在处理对应请求时会动态创建类,不写这个注解,就会用类名直接调用方法。此时在地址栏输入/user/login,控制栏就会显示如下语句

okok

那么后端如何接受前端传过来的数据呢?比如我在地址栏这样输入/user/login?account=fan&password=123,后端想要接到这些参数有三种方法:

1、参数列表设置对应参数

这里需要注意,参数名一定要和地址栏内传参使用的名称相同

@RequestMapping("/login")
    public ModelAndView login(String account,String password) {
        System.out.println(String account + "---" + String password);
    }

2、创建实体类接受数据

创建一个com.fan.entity,专门存放实体类。在这里,我创建了一个User类

package com.fan.entity;

public class User {
    private int id;
    private String account;
    private String password;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getAccount() {
        return account;
    }

    public void setAccount(String account) {
        this.account = account;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

这样在方法中可以将实体类作为参数

@RequestMapping("/login")
    public void login(User user) {
        String account = user.getAccount();
        String password = user.getPassword();
        System.out.println(account + "---" + password);
    }

这样做的好处是:当传入的参数过多时,可以用实体类一一获取,参数列表也可以精简

3、通过HttpServletRequest 获取

类似于我们自己反射的时候,这里也可以将前端的请求和回应作为参数传入方法中,所以类似servlet类,可以接到前端传入的值

@RequestMapping("/login")
      public void login(HttpServletRequest req, HttpServletResponse resp) {
        String account = req.getParameter("account");
        String password = req.getParameter("password");
        System.out.println(account + "---" + password);
    }

这三种方法都可以获得前端传入的值,所以它们的都会在控制台打印输出

fan---123

后端向前端传输数据,除了可以用(HttpServletRequest req, HttpServletResponse resp,还可以用mvc自带的ModelAndView。下面将利用这个类来处理传统请求,抓取视图返回数据。

1、完善spring-servlet.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--controller包扫描-->
    <context:component-scan base-package="com.fan.controller"/>
    <!--视图解析器-->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/page"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

在这里property name="prefix" value="/page"/ 表示文件路径在page文件夹下,property name="suffix" value=".jsp"表示文件类型为jsp。

2、ModelAndView的使用

@RequestMapping("/login")
    public ModelAndView login(User user) {
        //构造参数是指抓取的视图的文件名
        ModelAndView mav = new ModelAndView("/resA");
        System.out.println("okok");
        //这个方法相当于在请求域内传入参数
        mav.addObject("resA", "登录成功!!!");
        return mav;
    }

3、在page文件夹下创建视图

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<% String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
            + path + "/";%>
<html>
    <head>
        <base href="<%=basePath%>">
        <title>标题</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
              integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
              crossorigin="anonymous">
        <script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.js"></script>
    </head>
    <body>
        <!--获取请求域内的参数-->
        <h1>${resA}</h1>
    </body>
</html

经过这三步后,在地址栏输入/user/login,后端就会抓取视图,浏览器会跳转页面,并且显示:登录成功!!!

4、对配置文件中bean的详解

注意在后端new ModelAndView("/resA");时,并没有写全视图文件的路径,这是因为有以下配置

<!--class后面是一个带包名的完整类名,当框架读取到这里就会实例化一个InternalResourceViewResolver类-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <!--property里面是类的属性,其中name是属性名,value是属性值-->
        <property name="prefix" value="/page"/>
        <property name="suffix" value=".jsp"/>
    </bean>

由此我们可以推测,这个InternalResourceViewResolver类里应该有如下代码

public class InternalResourceViewResolver {
    private String prefix;
    private String suffix;

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}

四、spring-servlet.xml文件的自定义

一般会将这个配置文件移动到src下一个config文件夹中,如果这样,web.xml文件的配置就需要更改

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--在servlet标签里添加init-param标签-->
        <init-param>
            <!--这里一定要是contextConfigLocation-->
            <param-name>contextConfigLocation</param-name>
            <!--这里是spring-servlet文件路径,classpath指的是编译路径,在部署后java会被编译
            此时就会指向编译路径,而文件名也可以在此处自定义,比如改为mvc.xml-->
            <param-value>
                classpath:/config/spring-servlet.xml
            </param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
    <!--这个标签搭配前面的标签使用,单独配置没有用,改变spring-servlet.xml路径还是会报错-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:/config/spring-servlet.xml</param-value>
    </context-param>

</web-app>

五、总结

所谓MVC就是指:数据、视图和控制,代表着整个前后端交互的数据传输、视图转换和逻辑控制。其中C是核心,它负责接受传
递处理数据M,控制视图C的的渲染。这是一种界面层交互设计方式,也是一种思想,并不是只有在java中存在,也可以扩展到
其它语言其他领域。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容