WEB框架封装(基于SpringMVC)



import com.alibaba.fastjson.JSON;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;

/**
 * 这个类对象不是框架,是框架的入口
 */
public class DispatcherServlet extends HttpServlet {

    //缓存,存储所有请求与controller方法的映射关系
    //String key 请求名 , MappingTarget value 对应的controller对象和方法
    private Map<String,MappingTarget> mappings = new HashMap<String,MappingTarget>();

    //缓存,存储创建的所有controller对象,确保单实例管理
    //key=类路径 com.controller  value=类对象
    private Map<String,Object> controllers = new HashMap<String,Object>();

    /**
     * 所有浏览器的请求进入服务器后,都会通过DispacherServlet入口,访问当前的service
     * 将不同的请求分发给不同controller的不同方法
     *      所谓请求的分发,就是调用controller.method()
     *      框架怎们知道哪个请求调用哪个controller的哪个方法呢
     *      可以通过配置声明。
     *          就像原来通过web.xml或注解的配置告诉tomcat一样
     *          也可以通过配置告诉mvc框架
     *              * 配置的技术可以使用文件
     *                  人为规定:使用properties文件指定请求映射关系
     *                  login=com.duyi.test2.UserController.login
     *                  saveUser=com.duyi.test2.UserController.save
     *                  deleteCar=com.duyi.test2.CarController.delete
     *                  在web.xml中配置DispatcherServlet时,通过<init-param>指定配置文件的(src)位置
     *                   <init-param>
     *                      <param-name>configLocation</param-name>
     *                      <param-value>file/mvc.properties</param-value>
     *                  </init-param>
     *                  暂时不考虑方法重载情况
     *                  mvc框架读取配置文件,就可以获得映射关系
     *              * 配置的技术也可以使用注解
     *                  人为规定: 在指定请求对应的方法上使用自定义@RequestMapping注解
     *                    class UserController{
     *                      @RequestMapping("login")
     *                      public void login(){}
     *
     *                      @RequestMapping("save")
     *                      public void save(){}
     *                    }
     *                  mvc框架需要先找方法,通过反射获得方法的注解,从而获得映射关系
     *                  mvc框架需要先找到类,通过反射获得类中的所有方法,在反射获得注解
     *                      * 约定: 要求controller类必须写在controller名字的包中
     *                              并且类名必须以Controller为后缀
     *                      * 配置: 在web.xml中使用<init-param>指定扫描注解的那些类所在的包。
     *                          <init-param>
     *                              <param-name>packageScans</param-name>
     *                              <param-value>com.duyi.test2,com.duyi.test3</param-value>
     *                          </init-param>
     *              注意:每次请求到达框架,框架都需要通过上述的配置,找到此次请求的映射
     *                   这个映射关系只需要获得1次(存入缓存)
     *                      static{}
     *                      Servlet.init()
     *                   因为映射关系只读取一次,但需要使用多次
     *                   所以要(缓)存起来
     *                   每一个映射关系需要有请求,controller类,方法
     *                   自定义一个类,存储映射关系 class MappingTarget
     *                   一堆映射关系更适合存在map<请求,目标>集合中
     */
    public void init(){
        //servlet生命周期初始化操作,执行且仅执行一次
        //获得请求映射信息,装入缓存。
        //请求信息来自两个位置

        //-------------------配置文件--------------------------
        try{
            //通过dispatcherServlet初始化参数configLocation获得配置文件在src下的路径
            String configLocation = super.getInitParameter("configLocation");
            if(configLocation != null && !"".equals(configLocation)){

                //指定了配置文件
                //获得读取这个文件的输入流
                InputStream is = Thread.currentThread().getContextClassLoader()
                        .getResourceAsStream(configLocation);
                Properties p = new Properties();
                p.load(is);

                Enumeration keys =  p.propertyNames() ;
                while(keys.hasMoreElements()){
                    String key = (String) keys.nextElement(); //login
                    String value = p.getProperty(key);// com.duyi.test2.UserController.login
                    int index = value.lastIndexOf(".") ;
                    String classpath = value.substring(0,index); //com.duyi.test2.UserController 类
                    String methodname = value.substring(index+1) ; //login 方法名

                    Class clazz = Class.forName(classpath);

                    //单实例管理
                    Object controller = getSingleController(classpath);

                    //clazz.getMethod("login"); 只能获得无参的login()方法
                    //但我们未来可以需要通过login设置的方法参数,告诉mvc框架帮我们获得哪些请求参数
                    Method[] ms = clazz.getMethods() ;
                    for(Method m : ms){
                        if(m.getName().equals(methodname)){
                            //找到了对应的方法 (暂时不考虑重载)
                            MappingTarget target = new MappingTarget(key,controller,m) ;
                            mappings.put(key,target) ;
                            break ;
                        }
                    }
                }

            }
        }catch(Exception e){
            e.printStackTrace();
        }

        //--------------------注解----------------------------
        //通过一系列的编码,只要找到@RequestMapping注解中指定的那个请求
        //就等于获得了请求映射信息,就知道了这个请求对应哪个对象的哪个方法
        //要想获得注解中配置的那个请求信息就需要先获得注解对象(反射)
        //要想获得注解对象就需要先获得注解所在的那个方法(反射)
        //要想获得方法就需要先获得方法所在的controller类(反射)
        //要想获得类(Class)就需要获得 类路径,类对象,类模板
        //Class c = Class.forName("com.duyi.test3.EmpController")
        //Class c = EmpController.class
        //Class c = new EmpController().getClass()
        //后两种方式在当前框架中不可用,因为不确定,不能写死
        //只能使用第一种,但又不知道类路径
        //在web.xml中通过初始化参数指定扫描的包路径 com.duyi.test3 , com.duyi.test4
        //表示告诉框架,这些包中的类中有可能包含@RequestMapping配置的方法。
        //如何根据初始化指定的那些包,找到包中的类,从而获得这些类路径。
        //我们知道jvm中的包对应的就是os中文件夹
        //如果com.duyi.test3这个包中有类
        //那么com/duyi/test3文件夹中就有类文件。(A.java,A.class)
        //com这个文件夹在src目录下。可以使用Thread.currentThread.getXXLoader()...找到src目录
        try{
            String packageScans = super.getInitParameter("packageScans");
            if(packageScans != null && !"".equals(packageScans)){
                //指定了注解的扫描包 "com.duyi.test3 , com.duyi.test4 , com.duyi.test5"
                String[] packages = packageScans.split(",");
                for(String packageStr : packages){
                    //com.duyi.test3
                    packageStr = packageStr.trim();//去空格
                    String dirPath =packageStr.replace(".","/");//com/duyi/test3
                    dirPath = Thread.currentThread().getContextClassLoader()
                            .getResource(dirPath).getPath();//c:/xxxx/src/com/duyi/test3
                    File dir = new File(dirPath);
                    File[] files = dir.listFiles() ;//获得文件夹中的所有类文件
                    for(File file : files){
                        String filename = file.getName() ;//  EmpController.class
                        int index = filename.indexOf(".");
                        String classname = filename.substring(0,index);//EmpConroller
                        String classpath = packageStr+"."+classname ;//com.duyi.test3.EmpController


                        Class clazz = Class.forName(classpath);

                        //单实例管理
                        Object controller = getSingleController(classpath);


                        Method[] methods = clazz.getMethods();
                        for(Method method : methods){
                            RequestMapping rp = method.getAnnotation(RequestMapping.class);//获得当前方法上指定的注解
                            if(rp == null){
                                //没有这个注解,此方法不是用来映射请求,放过
                                continue ;
                            }
                            String requestStr = rp.value();//save请求
                            MappingTarget target = new MappingTarget(requestStr,controller,method);
                            mappings.put(requestStr,target);

                        }

                    }

                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }

        System.out.println(mappings);
    }


    /**
     * 浏览器发送请求至服务器
     * 服务器参考web.xml将所有的请求都交给mvc框架(调用DispatcherServlet.service)
     * 1. 框架会参考映射关系,根据请求,调用指定对象的指定方法
     *      需要获得此次的请求
     * 2. 框架获得请求,传递请求时,还需要根据controller的方法参数列表,获得请求传递参数,类型转换,并注入参数
     *      1. 获得零散参数
     *          url=login?uname=dmc&upass=123
     *          controller.login(String uname,String upass) ;
     *          表示告诉框架,根据映射关系,匹配login这个方法的请求时,还需要获得2个String类型的参数,一个是uname,一个upass
     *          框架可以根据反射获得要调用的那个login方法的参数列表,从而获得参数个数,类型和参数名
     *          方法参数列表中参数的名字,即为请求时请求参数的名字
     *          方法有一个String uname参数 -> request.getParameter("uname")
     *     2. 获得组合参数
     *          浏览器传递了一堆参数,这一堆参数在服务端恰好可以组成一个对象
     *          只需要在controller.method中定义对象类型的参数列表
     *          url=save?cno=1&cname=bmw&color=blue&price=400000
     *          controller.save(Car car) ;
     *              * mvc框架在调用方法时,通过反射获得参数列表
     *              * 获得参数后,发现参数没有@RequestParam注解,表示不对应某一个参数
     *                  1. 忘写了 传递null
     *                  2. 可能需要组成对象,通过反射,获得对象的属性名即为获取的请求参数key
     *                      对象有几个属性,就获得几个参数,为对象的属性赋值。
     *                      要求对象的属性名与请求的参数名一致。
     *                      其实我们匹配的不是属性名,而是属性对应的set方法名。(封装)
     *    3. 需要获得Servlet相关对象参数
     *          * mvc框架将之前web程序开发中许多常见操作封装了。
     *              如: 获取参数,响应
     *              即使不了解servlet的人,也可以实现请求-响应操作
     *              解耦
     *         * 但mvc框架封装的毕竟是常用的功能,不是所有的功能
     *         * 某些特殊情况下,可能依然需要使用原生对象
     *         * 就可以通过指定方法参数,让框架注入原生servlet相关对象 request,response,session
     *         * 这些参数也不需要使用RequestParam注解指定,类型就可以匹配了
     *   4. 可能是文件上传的参数
     * 目标方法处理请求(业务,程序员)
     * 目标处理请求完毕后,通过指定规则的返回值,让框架负责响应
     *      * return 普通字符串。 转发
     *          return "01.jsp"
     *          req.getRequestDispatcher("01.jsp").forward(req,resp)
     *     * return 带前缀字符串。 重定向
     *          return "redirect:01.jsp"
     *          resp.sendRedirect("01.jsp");
     *     * return 自定义ModelAndView对象 可以存储转发访问的请求信息和携带的数据信息
     *     * 所谓的直接响应,就是将方法中获得的数据,直接响应给浏览器
     *          一般情况下,响应的多为字符串
     *          也需要return 字符串
     *          此时如何判断是转发重定向,还是直接响应呢?
     *          完全可以再指定一个前缀规则 return "write:01.jsp" 但不这么做。
     *          模拟以后所学的springmvc框架,自定义一个@ResponesBody注解,作用在方法上
     *          如果controller的方法有该注解,则表示将返回的内容直接响应
     *              如果返回字符串,直接响应
     *              如果返回其他对象或集合,转换成json再响应。
     *
     *
     */
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try{
            String requestStr = req.getRequestURI() ;//获得此次请求字符串 /login , /saveCar
            requestStr = requestStr.substring(1);//去掉最前面的/ 因为映射关系中的请求名都没有/ -> login
            MappingTarget target = mappings.get(requestStr);//根据请求找到对应的映射关系 CarController.save
            if(target == null){
                //此次请求没有找到匹配的映射关系
                resp.sendError(404,"["+requestStr+"]");
                return ;
            }
            //根据请求找到了映射关系,调用对象的方法
            Object controller = target.getController() ;//UserController , CarController
            Method method = target.getMethod();// login() , save(Car car)

            Parameter[] parameters = method.getParameters();//获得方法的参数列表
            Object result = null ;
            if( parameters== null || parameters.length == 0){
                //没有参数,直接调用
                result = method.invoke(controller) ;//controller.login(无参);
            }else{
                //有参数,但此时的参数可能有2个来源
                //1个是普通请求的参数
                //  使用req.getParameter()获取
                //1个是文件上传请求的参数
                //  使用fileUpload上传组件,会将所有的参数都变成FileItem对象
                //准备一个map集合,装载两种请求传递的参数
                //Object 可能是String参数值,也可能是上传的文件参数值
                Map<String,Object> params = new HashMap<String,Object>();

                try {
                    //假设是文件上传请求
                    DiskFileItemFactory factory = new DiskFileItemFactory();
                    ServletFileUpload upload = new ServletFileUpload(factory);
                    List<FileItem> fis = upload.parseRequest(req); //当前行代码可能会出现异常,如果不是文件上传会出现异常
                    for(FileItem fi : fis){
                        //此时fi就表示一个参数,有可能是普通参数(pname),也可能是文件参数(file)
                        if(fi.isFormField()){
                            //是文件上传是的一个普通参数,如:pname
                            String key = fi.getFieldName() ;//pname
                            String value = fi.getString() ; //iphone12
                            params.put(key,value) ;
                        }else{
                            //是一个文件参数
                            String key = fi.getFieldName() ;//file
                            String fname = fi.getName() ;//文件名
                            long length = fi.getSize() ;//文件大小
                            InputStream content = fi.getInputStream() ;//一个可以获得文件内容的输入流 文件内容
                            //需要将文件参数的3个信息打包成一个对象装入map,自定义文件类MvcFile
                            MvcFile file = new MvcFile(fname,length,content) ;
                            params.put(key,file) ;
                        }
                    }
                }catch(Exception e){
                    //如果出现异常,证明不是文件上传,是普通请求
                    Enumeration<String> paramNames = req.getParameterNames() ;
                    while(paramNames.hasMoreElements()){
                        String key = paramNames.nextElement() ;//uname,upass
                        String value = req.getParameter(key);
                        params.put(key,value) ;
                    }
                }

                //上述处理完毕后,无论什么请求,最终参数都存储在map集合中。

                List<Object> paramterValues = new ArrayList<Object>();//准备装载参数列表的值
                //一个一个获得参数
                for(Parameter parameter :parameters){
                    //parameter.getName() ;//获得参数名,编码时参数名时uname,但编译后就变成了arg0,自定义RequestParam("uname")指定参数名
                    //Car car
                    RequestParam rp = parameter.getAnnotation(RequestParam.class);//获得参数列表的注解
                    if(rp == null){
                        //有参数,但没有指定注解,目前只有3种情况
                        Class parameterType = parameter.getType() ;//参数类型 Car car
                        //一种是需要原生servlet相关对象 req,resp,session
                        if(parameterType == HttpSession.class){
                            paramterValues.add( req.getSession() ) ;
                        }else if( parameterType == HttpServletRequest.class){
                            paramterValues.add( req ) ;
                        }else if(parameterType == HttpServletResponse.class){
                            paramterValues.add( resp ) ;
                        }else{
                            //另一种就是参数为对象类型的参数
                            //要根据对象的属性(set方法名)获得一系列请求参数,并组成对象,并注入
                            Object paramObj = parameterType.newInstance() ;//new Car() 要求Car必须有无参构造器
                            //car.setCno() -> cno属性赋值 -> request.getParameter("cno"); //约定优于配置
                            //car.setCname()
                            Method[] ms = parameterType.getMethods() ;
                            for(Method m : ms){
                                String mname = m.getName() ;//setCno , getCno
                                if(mname.startsWith("set")){
                                    //是一个可以赋值的set方法,是一个与请求参数对应的set方法 setCno / setCarNo-> carNo / setA
                                    String key = mname.substring(3) ;//去掉set -> Cno
                                    if(key.length() == 1){
                                        key = key.toLowerCase() ;
                                    }else{
                                        key = key.substring(0,1).toLowerCase()  + key.substring(1) ; // cno
                                    }

                                    String value = params.get(key).toString() ; //获得set方法对应的属性对应的请求参数
                                    //通过set方法,将请求参数赋值给参数对象前,还需要考虑类型问题,暂时只考虑int,String,double
                                    Class fieldType = m.getParameterTypes()[0] ;
                                    Object $value = caseType(value,fieldType);
                                    m.invoke(paramObj,$value) ;

                                }
                            }
                            //循环结束,找到了所有的set方法,获得了所有属性对应的请求参数值,并完成了赋值
                            //现在这个car对象,就是一个装有请求参数的完整的对象
                            paramterValues.add(paramObj) ;
                        }
                    }else{
                        //有参数,有注解,就可以获得参数对应的请求参数key
                        //此时参数有2种可能
                        //一种普通参数 String,int,double
                        //一种文件参数MvcFile
                        String key = rp.value(); //uname
                        Object value = params.get(key) ; // req.getParameter("uname");
                        if(value == null){
                            //此次没有传递这个参数
                            paramterValues.add(null) ;
                        }

                        Class parameterType = parameter.getType() ;//获得参数类型,暂时只处理String和Integer,Double,各种集合数组不处理
                        Object $value = caseType(value,parameterType) ;
                        paramterValues.add($value);

                    }
                }

                //循环结束,就获得了所有的参数,并装入list集合
                result = method.invoke(controller,paramterValues.toArray()); //调用方法传递参数(注入,框架主动为我们传参)
            }

            //目标方法处理完毕,根据目标方法的返回值,处理响应
            ResponseBody rb = method.getAnnotation(ResponseBody.class) ;//获得方法上的直接响应的注解
            if(rb != null){
                //设置了该注解,表示要直接响应
                String $result = "" ;
                if(result instanceof String){
                    $result = (String) result;
                }else if(result.getClass() == int.class || result.getClass() == Integer.class){
                    $result = result.toString() ;
                }else{
                    //既不是String,也不是int,double之流,我们认为就是对象或集合
                    //需要将对象或集合转换成字符串响应给浏览器
                    //响应给浏览器的字符串,还能被浏览器中的js处理
                    //要求对象或集合转换的字符串要符合json格式。
                    //   {} , [] , [{},{}] -> List<Phone>
                    //可以使用json转换工具帮我们实现json格式转换
                    //  fastjson , json-lib , gson , jackson
                    $result = JSON.toJSONString(result) ;
                }

                resp.getWriter().write($result);
            }else{
                //没有注解,间接响应
                if(result instanceof String){
                    //按照目前的分析,可能是转发,可能是重定向
                    String $result = (String) result;
                    if($result.startsWith("redirect:")){
                        //重定向 redirect:01.jsp
                        $result = $result.substring(9) ;//去掉redirect前缀
                        resp.sendRedirect($result);
                    }else{
                        //转发 01.jsp
                        req.getRequestDispatcher($result).forward(req,resp);
                    }
                }else if(result instanceof  ModelAndView){
                    //转发,并携带数据
                    ModelAndView $result = (ModelAndView) result;
                    //携带数据,就是将mv中map集合的数据,装入request
                    Map<String,Object> datas = $result.getDatas() ;
                    Set<String> keys = datas.keySet() ;
                    for(String key : keys){
                        Object value = datas.get(key) ;
                        req.setAttribute(key,value);
                    }

                    req.getRequestDispatcher($result.getViewName()).forward(req,resp); ;
                }
            }


        }catch(Exception e){
            e.printStackTrace();
        }
    }

    //类型转换
    private Object caseType(Object value,Class type){
        if(value instanceof MvcFile){
            return (MvcFile)value ;
        }
        String $value = (String) value;
        if(type == String.class){
           return $value ;
        }else if(type == int.class || type == Integer.class){
            return Integer.parseInt($value) ;
        }else if(type == double.class || type == Double.class){
            return Double.parseDouble($value);
        }
        return value ;
    }

    //单实例管理controller对象,根据controller类路径找对应的对象,有就返回,没有就新建
    private Object getSingleController(String classpath) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        Class clazz = Class.forName(classpath);
        Object controller = controllers.get(classpath);
        if(controller == null){
            synchronized (clazz){
                //还没有缓存,就new一个
                if(controller == null)
                    controller = clazz.newInstance() ;// new UserController()
            }

        }
        return controller ;
    }
}



import java.io.Serializable;
import java.lang.reflect.Method;

/**
 * 请求映射的目标类
 * 存储哪个请求对应哪个类对象的哪个方法
 */
public class MappingTarget implements Serializable {
    private String reqStr ;     //请求                 login
    private Object controller ; //目标controller对象    UserController
    private Method method ;     //请求对应的对象方法     login()

    public String getReqStr() {
        return reqStr;
    }

    public void setReqStr(String reqStr) {
        this.reqStr = reqStr;
    }

    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;
    }

    public MappingTarget(String reqStr, Object controller, Method method) {
        this.reqStr = reqStr;
        this.controller = controller;
        this.method = method;
    }

    public MappingTarget() {
    }

    @Override
    public String toString() {
        return "MappingTarget{" +
                "reqStr='" + reqStr + '\'' +
                ", controller=" + controller +
                ", method=" + method +
                '}';
    }
}



import java.util.HashMap;
import java.util.Map;

/**
 * 实现携带数据的转发响应时,装载转发的网页信息和携带的数据信息
 */
public class ModelAndView {
    private String viewName ;//01.jsp
    private Map<String,Object> datas = new HashMap<>();

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

    public Map<String, Object> getDatas() {
        return datas;
    }

    public void setDatas(Map<String, Object> datas) {
        this.datas = datas;
    }

    public void addAttribute(String key , Object value){
        datas.put(key,value);
    }
}



import java.io.InputStream;
import java.io.Serializable;

/**
 * 装载文件上传的文件参数信息
 */
public class MvcFile implements Serializable {
    private String filename ;
    private long length ;
    private InputStream content ;

    public String getFilename() {
        return filename;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public long getLength() {
        return length;
    }

    public void setLength(long length) {
        this.length = length;
    }

    public InputStream getContent() {
        return content;
    }

    public void setContent(InputStream content) {
        this.content = content;
    }

    public MvcFile(String filename, long length, InputStream content) {
        this.filename = filename;
        this.length = length;
        this.content = content;
    }

    public MvcFile() {
    }
}



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 RequestMapping {
    public String value() ; //存储注解所在方法对应的请求名。
}



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.PARAMETER)
public @interface RequestParam {
    public String value();
}



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 ResponseBody {
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值