J2EE 电商实战之 Filter 配合 Servlet

一、问题

在电商网站开发中,前端页面和后端页面都对应不同的访问路径,如果判断路径是后台的,则指向后台服务Servlet,如果是前端服务,则指向前端的Servlet,但是这里有一个问题,往往一个业务包含多个动作如CURD操作,需要为每个动作配置web.xml,这样很不方面后边管理维护,而且也很容易出错,这样的问题怎么解决呢?

二、改进

但是观察已经实现了分类管理的可运行项目下载包里的代码,却发现Servlet只有一个即CategoryServlet,web.xml里,也只有一个CategoryServlet的映射,并没有5个映射。

如图是对于最后完工了的项目的servlet包里类的截图,可以发现,每种实体类,对应了一个Servlet,而不是对应了5个,这样首先从Servlet数量上来讲,就大大的减少了

三、原理流程图

那么是如何做到一个CategoryServlet类,就能完成本来需要5个Servlet类才能完成的功能的呢?

让我们借助流程图来分析,为什么访问admin_category_list的时候,CategoryServlet的list()方法会被调用

  1. 假设访问路径是 http://127.0.0.1:8080/tmall/admin_category_list
  2. 过滤器BackServletFilter进行拦截,判断访问的地址是否以/admin_开头
  3. 如果是,那么做如下操作
    3.1 取出两个下划线之间的值 category
    3.2 取出最后一个下划线之后的值 list
    3.3 然后根据这个值,服务端跳转到categoryServlet,并且把list这个值传递过去
  4. categoryServlet 继承了BaseBackServlet,其service方法会被调用。 在service中,借助反射技术,根据传递过来的值 list,调用对应categoryServlet 中的方法list()
  5. 这样就实现了当访问的路径是 admin_category_list的时候,就会调用categoryServlet.list()方法这样一个效果

换句话说:
如果访问的路径是admin_category_add,就会调用categoryServlet.add()方法
如果访问的路径是admin_category_delete,就会调用categoryServlet.delete()方法
如果访问的路径是admin_category_edit,就会调用categoryServlet.edit()方法
如果访问的路径是admin_category_update,就会调用categoryServlet.update()方法
如此这般,一个categoryServlet类,就完成了本来需要5个Servlet类才能完成的功能。

file

四、代码实战

web.xml

web.xml片段

 <servlet>
        <servlet-name>CategoryServlet</servlet-name>
        <servlet-class>tmall.servlet.CategoryServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>CategoryServlet</servlet-name>
        <url-pattern>/categoryServlet</url-pattern>
    </servlet-mapping>

<filter>
    <filter-name>BackServletFilter</filter-name>
    <filter-class>tmall.filter.BackServletFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>BackServletFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

过滤器

BackServletFilter.java

package tmall.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;

public class BackServletFilter implements Filter {

    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        // 转换类型 ServletRequest -》 HttpServletRequest
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse)res;

        // 获取请求的路径
        String contextPath = request.getServletContext().getContextPath();
        String uri = request.getRequestURI();  // 浏览器发出请求的资源名部分,去掉了协议和主机名"
        uri = StringUtils.remove(uri, contextPath);

        if(uri.startsWith("/admin_")) {
            String servletPath = StringUtils.substringBetween(uri, "_", "_") + "Servlet";
            String method = StringUtils.substringAfterLast(uri, "_");
            request.setAttribute("method", method);
            req.getRequestDispatcher("/" + servletPath).forward(request, response);
            return;
        }

        chain.doFilter(request, response);
    }

    public void init(FilterConfig arg0) throws ServletException {

    }
}

那么BackServletFilter类到底是如何工作的呢? 接下来我们此类进行代码讲解。

  1. 首先在web.xml配置文件中,让所有的请求都会经过BackServletFilter

    <url-pattern>/*</url-pattern>
  2. 还是假设访问的路径是:

    http://127.0.0.1:8080/tmall/admin_category_list
  3. 在BackServletFilter 中通过request.getRequestURI()取出访问的uri: /tmall/admin_category_list

  4. 然后截掉/tmall,得到路径/admin_category_list

  5. 判断其是否以/admin开头

  6. 如果是,那么就取出两个_之间的字符串,category,并且拼接成/categoryServlet,通过服务端跳转到/categoryServlet

  7. 在跳转之前,还取出了list字符串,然后通过request.setAttribute的方式,借助服务端跳转,传递到categoryServlet里去

    Servlet控制器

    接着流程就到了categoryServlet这里。

根据web.xml中的配置

<servlet-name>CategoryServlet</servlet-name>
<url-pattern>/categoryServlet</url-pattern>

服务端跳转/categoryServlet就到了CategoryServlet这个类里

具体步骤:

  1. 首先CategoryServlet继承了BaseBackServlet,而BaseBackServlet又继承了HttpServlet
  2. 服务端跳转过来之后,会访问CategoryServlet的doGet()或者doPost()方法
  3. 在访问doGet()或者doPost()之前,会访问service()方法
  4. BaseBackServlet中重写了service() 方法,所以流程就进入到了service()中
  5. 在service()方法中有三块内容
    5.1 第一块是获取分页信息
    5.2 第二块是根据反射访问对应的方法
    5.3 第三块是根据对应方法的返回值,进行服务端跳转、客户端跳转、或者直接输出字符串。
  6. 第一块和第三块放在后面讲解,这里着重讲解第二块是根据反射访问对应的方法
    6.1 取到从BackServletFilter中request.setAttribute()传递过来的值 list
    6.2 根据这个值list,借助反射机制调用CategoryServlet类中的list()方法

这样就达到了 CategoryServlet.list() 方法被调用的效果

categoryServlet.java

 package tmall.servlet;

import java.awt.image.BufferedImage;   
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import tmall.bean.Category;
import tmall.util.ImageUtil;
import tmall.util.Page;

public class CategoryServlet extends BaseBackServlet {

    /**
     * 【Date:2020-01-18】
     * 说明:当filter 过滤器进入到CategoryServlet这个类时,首先会进入到父类BaseBackServlet service()方法中,
     * 然后根据反射机制,调用该类(CategoryServlet)的list方法,然后根据返回的信息进行服务端跳转、客户端跳转、或者直接输出字符串。
     * 
     * 父类BaseBackServlet 的 this 指代的就是子类CategoryServlet。
     * 
     * 这里有一个巧妙的设计技巧:子类继承父类,执行子类时,在父类service()中根据反射机制调用子类的方法,
     * 一般往往是子类调用父类的方法。这样做的好处是如果有多个子类,那么就可以共用通用的方法,
     * 子类单独的业务类自己实现,提高了代码的可复用和易扩展性。
     * 
     */

    public String add(HttpServletRequest request, HttpServletResponse response, Page page) {
        Map<String,String> params = new HashMap<>();
        InputStream is = super.parseUpload(request, params);

        String name = params.get("name");
        Category c = new Category();
        c.setName(name);
        categoryDAO.add(c);

        File  imageFolder= new File(request.getSession().getServletContext().getRealPath("img/category"));
        File file = new File(imageFolder,c.getId()+".jpg");

        try {
            if(null!=is && 0!=is.available()){
                try(FileOutputStream fos = new FileOutputStream(file)){
                    byte b[] = new byte[1024 * 1024];
                    int length = 0;
                    while (-1 != (length = is.read(b))) {
                        fos.write(b, 0, length);
                    }
                    fos.flush();
                    //通过如下代码,把文件保存为jpg格式
                    BufferedImage img = ImageUtil.change2jpg(file);
                    ImageIO.write(img, "jpg", file);       
                }
                catch(Exception e){
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }  

        return "@admin_category_list";

    }

    public String delete(HttpServletRequest request, HttpServletResponse response, Page page) {
        int id = Integer.parseInt(request.getParameter("id"));
        categoryDAO.delete(id);
        return "@admin_category_list";
    }

    public String edit(HttpServletRequest request, HttpServletResponse response, Page page) {
        int id = Integer.parseInt(request.getParameter("id"));
        Category c = categoryDAO.get(id);
        request.setAttribute("c", c);
        return "admin/editCategory.jsp";       
    }

    public String update(HttpServletRequest request, HttpServletResponse response, Page page) {
        Map<String,String> params = new HashMap<>();
        InputStream is = super.parseUpload(request, params);

        System.out.println(params);
        String name= params.get("name");
        int id = Integer.parseInt(params.get("id"));

        Category c = new Category();
        c.setId(id);
        c.setName(name);
        categoryDAO.update(c);

        File  imageFolder= new File(request.getSession().getServletContext().getRealPath("img/category"));
        File file = new File(imageFolder,c.getId()+".jpg");
        file.getParentFile().mkdirs();

        try {
            if(null!=is && 0!=is.available()){
                try(FileOutputStream fos = new FileOutputStream(file)){
                    byte b[] = new byte[1024 * 1024];
                    int length = 0;
                    while (-1 != (length = is.read(b))) {
                        fos.write(b, 0, length);
                    }
                    fos.flush();
                    //通过如下代码,把文件保存为jpg格式
                    BufferedImage img = ImageUtil.change2jpg(file);
                    ImageIO.write(img, "jpg", file);       
                }
                catch(Exception e){
                    e.printStackTrace();
                }
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return "@admin_category_list";

    }

    public String list(HttpServletRequest request, HttpServletResponse response, Page page) {
        List<Category> cs = categoryDAO.list(page.getStart(),page.getCount());
        int total = categoryDAO.getTotal();
        page.setTotal(total);

        request.setAttribute("thecs", cs);
        request.setAttribute("page", page);

        return "admin/listCategory.jsp";
    }

}

抽象基类

在抽象基类(父类),通过反射机制,父类里边可以调用子类的方法,这是最核心巧妙的地方。

BaseBackServlet.java

 package tmall.servlet;

import java.io.InputStream;

import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

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

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import tmall.dao.CategoryDAO;
import tmall.dao.OrderDAO;
import tmall.dao.OrderItemDAO;
import tmall.dao.ProductDAO;
import tmall.dao.ProductImageDAO;
import tmall.dao.PropertyDAO;
import tmall.dao.PropertyValueDAO;
import tmall.dao.ReviewDAO;
import tmall.dao.UserDAO;
import tmall.util.Page;

public abstract class BaseBackServlet extends HttpServlet {

    public abstract String add(HttpServletRequest request, HttpServletResponse response, Page page) ;
    public abstract String delete(HttpServletRequest request, HttpServletResponse response, Page page) ;
    public abstract String edit(HttpServletRequest request, HttpServletResponse response, Page page) ;
    public abstract String update(HttpServletRequest request, HttpServletResponse response, Page page) ;
    public abstract String list(HttpServletRequest request, HttpServletResponse response, Page page) ;

    protected CategoryDAO categoryDAO = new CategoryDAO();
    protected OrderDAO orderDAO = new OrderDAO();
    protected OrderItemDAO orderItemDAO = new OrderItemDAO();
    protected ProductDAO productDAO = new ProductDAO();
    protected ProductImageDAO productImageDAO = new ProductImageDAO();
    protected PropertyDAO propertyDAO = new PropertyDAO();
    protected PropertyValueDAO propertyValueDAO = new PropertyValueDAO();
    protected ReviewDAO reviewDAO = new ReviewDAO();
    protected UserDAO userDAO = new UserDAO();

    public void service(HttpServletRequest request, HttpServletResponse response) {
    try {
        /*获取分页信息*/
        int start= 0;
        int count = 5;
        try {
            start = Integer.parseInt(request.getParameter("page.start"));
        } catch (Exception e) {

        }
        try {
            count = Integer.parseInt(request.getParameter("page.count"));
        } catch (Exception e) {
        }
        Page page = new Page(start,count);

        System.out.println("-------输出语句测试v3 BaseBack -------------");

        /*借助反射,调用对应的方法*/
        // String method = (String)request.getParameter("method");  // 该方法不能获取method,会报错
        String method = (String) request.getAttribute("method"); // 只能通过该方法获取method参数

        // null 
        System.out.println(method);

        // 反射(三种获取类对象方法,类对象,就是用于描述这种类,都有什么属性,什么方法的,获取类对象的时候,会导致类属性被初始化)
        // 1、Class pClass1=Class.forName(className);
        // 2、Class pClass2=Hero.class;
        // 3、Class pClass3=new Hero().getClass();

        // 打印:class tmall.servlet.CategoryServlet
        // System.out.println(this.getClass());

        // 这里的this,表示哪个类调用继承父类,就是哪个子类的对象,如CategoryServlet调用,则this指代的就是该类CategoryServlet
        Method m = this.getClass().getMethod(method, javax.servlet.http.HttpServletRequest.class,
                javax.servlet.http.HttpServletResponse.class, Page.class);

        //根据反射,获取attackHero方法,并且调用hero1的这个方法,参数是hero2
        // Method attackHeroMethod = hero1Class.getMethod("attackHero", Hero.class);
        // attackHeroMethod.invoke(hero1, hero2);

        // 子类方法返回的结果(在父类里边通过反射调用子类的方法,
        // 如http://127.0.0.1:8080/tmall/admin_category_list 最终调用CategoryServlet.list方法返回"@admin_category_list")
        String redirect = m.invoke(this, request, response, page).toString();

        /*根据方法的返回值,进行相应的客户端跳转,服务端跳转,或者仅仅是输出字符串*/
        if(redirect.startsWith("@"))
            response.sendRedirect(redirect.substring(1));
        else if(redirect.startsWith("%"))
            response.getWriter().print(redirect.substring(1));
        else
            request.getRequestDispatcher(redirect).forward(request, response);

    } catch(Exception e) {
          // TODO Auto-generated catch block
        e.printStackTrace();
        throw new RuntimeException(e);

    }
    }

    public InputStream parseUpload(HttpServletRequest request, Map<String, String> params) {
            InputStream is =null;
            try {
                DiskFileItemFactory factory = new DiskFileItemFactory();
                ServletFileUpload upload = new ServletFileUpload(factory);
                // 设置上传文件的大小限制为10M
                factory.setSizeThreshold(1024 * 10240);

                List items = upload.parseRequest(request);
                Iterator iter = items.iterator();
                while (iter.hasNext()) {
                    FileItem item = (FileItem) iter.next();
                    if (!item.isFormField()) {
                        // item.getInputStream() 获取上传文件的输入流
                        is = item.getInputStream();
                    } else {
                        String paramName = item.getFieldName();
                        String paramValue = item.getString();
                        paramValue = new String(paramValue.getBytes("ISO-8859-1"), "UTF-8");
                        params.put(paramName, paramValue);
                    }
                }

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

}

一个Servlet类就能满足CRUD一系列业务要求

通过这样一种模式,一个Servlet类就能满足CRUD一系列业务要求
如果访问的路径是admin_category_list,就会调用categoryServlet.list()方法
如果访问的路径是admin_category_add,就会调用categoryServlet.add()方法
如果访问的路径是admin_category_delete,就会调用categoryServlet.delete()方法
如果访问的路径是admin_category_edit,就会调用categoryServlet.edit()方法
如果访问的路径是admin_category_update,就会调用categoryServlet.update()方法

代码疑问

问题1:下边的代码是反射部分的,但是为什么有4个参数?

Method m = this.getClass().getMethod(method, javax.servlet.http.HttpServletRequest.class,
                    javax.servlet.http.HttpServletResponse.class,Page.class);

回答:

    1. method 是方法名称,是个字符串
    1. 后面3个是参数的类型,满足这3个参数类型的方法是唯一的,这样就能定位到期望的那个方法了。

实际对应子类 CategoryServlet.add 方法的参数

public String add(HttpServletRequest request, HttpServletResponse response, Page page) 

问题2:
在url地址后面跟上action参数,比如http://127.0.0.1:8080/tmall/admin_category?action=list
然后在service中获取 request.getParameter("action"); 再根据取到的参数值list调用对应的list方法,
这样就无需使用反射技术了,不知道此方法有什么弊端

答:这样从逻辑上看似简单,但实际会变得更复杂,如果后边还有多个不同的业务,如订单、产品等等,就会很冗余。

问题3:
BaseBackServlet 66行 service 方法中 的this.getClass()为什么指向的是categoryServlet?

答①:backservletfilter对访问先进行拦截,取出对应的访问服务类,以及访问服务类中的哪个方法即method,然后跳转去要访问的服务类categoryservlet,而categoryservlet服务类继承basebackservlet,basebackservlet又继承了httpservlet,在跳转到服务类(此时已经在categoryservlet中了)之后,先执行父类(basebackservlet)中的service方法(此时已经在categoryservlet中了,所以this是categoryservlet),借助反射,调用方法。

答②:这是基础知识。this 表示当前对象,那么我问你,你的当前对象是 categoryServlet 实例? 还是 BaseBackServlet 实例

五、request.getParameter() 和request.getAttribute() 区别

在过滤器和BaseBackServlet.java类里边都有request.setAttribute() ,request.getAttribute() 方法,那么和普通的获取参数request.getParameter() 方法有什么区别呢?

getParameter 是用来接受用post个get方法传递过来的参数的,getAttribute 必须先setAttribute。

request.getParameter()

request.getParameter() 取得是通过容器的实现来取得通过类似post,get等方式传入的数据,request.setAttribute()和getAttribute()只是在web容器内部流转,仅仅是请求处理阶段

request.getParameter() 方法传递的数据,会从Web客户端传到Web服务器端,代表HTTP请求数据。request.getParameter()方法返回String类型的数据。

request.getAttribute()

request.setAttribute() 和 getAttribute() 方法传递的数据只会存在于Web容器内部

还有一点就是,HttpServletRequest 类有 setAttribute() 方法,而没有setParameter() 方法。

六、辅助类

在上边的过滤器代码中,我们使用了系统包里的字符串方法,很方便对字符串做出了截取处理,不过有时候我们并不清楚都有哪些现成可用的方法供我们使用,不过,已经有人帮我们搞好了这样一个辅助工具类包 Hutool。

工欲善其事必先利其器! Hutool 就是这么一款超级强力的工具类。

在大家日常工作中,都常常会做如下这些非常繁琐的工作:

  • 日期与字符串转换
  • 文件操作
  • 转码与反转码
  • 随机数生成
  • 压缩与解压
  • 编码与解码
  • CVS文件操作
  • 缓存处理
  • 加密解密
  • 定时任务
  • 邮件收发
  • 二维码创建
  • FTP 上传与下载
  • 图形验证码生成

等等等等

以上这些事情,要么自己动手写代码,要么从零零碎碎的各个不同的地方去找各种零零散散的代码来改造成自己需要的样子。

现在不用了,您只需要一个 hutool ! 那么这些功能都是现成滴了,而且非常好用。

功能明细:
编码工具-16进制工具
编码工具-转义工具
编码工具-Hash工具
编码工具-URL工具
编码工具-Base32-64工具
编码工具-Unicode工具

常用类辅助工具-转换工具
常用类辅助工具-日期工具
常用类辅助工具-字符串工具
常用类辅助工具-数字工具
常用类辅助工具-数组工具
常用类辅助工具-随机工具
常用类辅助工具-比较器工具
常用类辅助工具-多线程工具
常用类辅助工具-缓存工具
常用类辅助工具-定时器工具

类和对象-反射工具
类和对象-类工具
系统工具-粘贴板工具
系统工具-运行时工具
系统工具-系统属性工具

和文件有关的-文件IO工具
和文件有关的-图片工具
和文件有关的-CVS工具
和文件有关的-图形验证码工具

需要第三方的-邮件工具
需要第三方的-二维码工具
需要第三方的-FTP工具

其他-网络工具
其他-压缩工具
其他-正则工具
其他-校验工具
其他-身份证工具

字符串工具StrUtil

  • 与空判断相关的
  • 头尾处理
  • 包含与否
  • setter gettter 处理
  • 删除
  • 大小写
  • 分割
  • 截取
  • 创建字符串
  • 是否相等
  • 格式化
  • 获取字节
  • 转换为字符串
  • 格式转换
  • 包裹
  • 填充
  • 获取其他对象
  • 出现次数
  • 摘要和隐藏
  • 比较
  • 获取索引位置
  • 追加
  • 替换
  • 相似度
  • 其他

为者常成,行者常至