可能很多人看到标题都不会点进来,因为 JSP 这种老掉牙的技术很多人根本不学,所以我有些感想写在下面。

这是在学校选课老师让做的实验报告,可能大家会觉得这些东西毫无意义,因为 JSP 早就没人使用了,原因是因为写页面太繁琐,执行速度慢,消耗内存,响应速度慢不能处理高并发等原因;但是我想觉得不能因为他现在被淘汰了就不去学他,更不能抱着轻蔑的态度去学习这门技术,我自己在学习的过程中一直在惊叹 JSP 太强了,真的简化了很多后台开发,还有那些标签技术,真的是独具匠心,发明出这项技术的人真的非常厉害。现在越来越多的 Java 开发相关人员上来直接学习 SpringBoot 等框架,然后快速开发出一个网页,看起来很厉害,但这是不对的,也是错误的,我也学过 Spring 和 SpringBoot 等流行框架,但是现在还是在老老实实的跟着学校的进度学习 JSP,因为我觉得经典的东西一定有学习的价值,只有搞懂基础才能提高境界,这些高大上的框架确实极大的简化了我们的开发,但是有没有想过,如果你一直学习这些别人封装好的框架,其实你根本没有一点核心竞争力,这些东西你会,别人一样也可以学习,而且也是速成,所以我们要掌握基础,学习底层,这样就算别人想超过你也要付出很长时间,久而久之你就有你的核心竞争力了,所以,不要看不起任何一项技术,每一项技术都有他存在的意义。其实仔细想想,我们到底会什么?全都是用的别人封装好的框架,我们只会调用接口,我们写 Java 程序调用 JDK 的接口,然后 Servlet 封装了 JDK,接着 Spring 封装了 Servlet ,简化我们的开发,后来 SpringBoot 进而封装了 Spring 框架,让我们开发网页触手可得······我相信将来还会有一层一层的封装,到最后我们写网页可能是几行代码就搞定了,那个时候可能有的 Java 程序员看似写了一个网页,他可能都不知道 JDK 是什么(夸张的比喻一下),因为封装太多太多层了。换个视角,我甚至觉得人类本来就是调用接口生活,比如我们想吃饭,不想自己做,怎么办?好办,调用美团外卖的接口,传入参数20元,返回一份盖浇饭。说了那么多,就想表明一个观点,就是调用接口虽然方便,但是谁都会调,请问你的核心竞争力是什么?当然我也是个巨菜,没有核心竞争力可言,目前在阅读 JDK 源码和学习算法,感兴趣的朋友可以一起阅读 源码 和算法 交流

下面开始正文。


实验一 Servlet基础操作

先来看一下最终效果:

一、基础功能

1、项目结构

首先来看一下项目的整体结构:

2、数据初始化

首先是数据的初始化,这里为了使 Servlet 容器能在一开始就加载数据,我选择在注解中进行了如下配置:

@WebServlet(name = "ShoppingCartServlet", 
        urlPatterns = {
            "/shop/products",
            "/shop/details",
            "/shop/addCart",
            "/shop/deleteItem",
            "/shop/clearCart"}, 
        loadOnStartup = 1)

其中 urlPatterns 为匹配的路径。
这样在一开始就可以加载在 init 方法中的数据了。

/** * 数据的初始化 * * @param config 配置参数,可以获取应用作用域 * @throws ServletException 抛出异常 */
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        // 装载数据
        Product p1 = new Product
                (1, "单反相机", "最没有性价比的单反相机", 3306f);
        Product p2 = new Product
                (2, "双反相机", "最不值的双反相机", 3307f);
        Product p3 = new Product
                (3, "三反相机", "最难看的三反相机", 3308f);
        Product p4 = new Product
                (4, "四反相机", "最花里胡哨的四反相机", 3309f);
        List<Product> list = new ArrayList<>();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        list.add(p4);
        // 保存到应用作用域中
        config
                .getServletContext()
                .setAttribute("products", list);
    }

因为这些数据是所有的应用都会用到的,所以把它放在 context上下文域中,代替了从数据库查询数据。

3、主页

然后我们访问页面,会自动跳转到 productList页面,来看一下 index页面的代码:

<body>
<c:redirect url="shop/productList.jsp"/>
</body>

就只有一行代码,是一个重定向。

访问之后的页面如图:

我们可以选择喜欢的商品,也可以查看购物车。

4、商品详情

这里我们先点进去商品详情:

请求地址为:

http://localhost:8080/WsShoppingCart/shop/details?id=1

这里拼接了一个 id ,我们可以在 Servlet中取出来,里看代码:

首先是根据请求路径,我们把不同的请求交给不同的方法来处理:

/** * 根据请求参数后缀来分别处理请求 * * @param request 请求 * @param response 响应 * @throws ServletException Servlet异常 * @throws IOException IO异常 */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String uri = request.getRequestURI();
        if (uri.endsWith("products")) displayProducts(request, response);
        else if (uri.endsWith("details")) displayGoods(request, response);
        else if (uri.endsWith("addCart")) addCart(request, response);
        else if (uri.endsWith("deleteItem")) deleteCard(request, response);
        else if (uri.endsWith("clearCart")) clearCart(request, response);
    }

比如我们这次请求就会转发到 displayGoods方法中去处理,下面来看一下该方法:

/** * 根据 ID 查询用户点击的是哪一个商品,然后跳转到商品详情页面 * 响应请求: /shop/details * * 注意: 只接受同级目录下的页面请求,所以 ./ 或者不写都可以 * * @param request 请求 * @param response 响应 * @throws ServletException Servlet 异常 * @throws IOException IO 异常 */
    private void displayGoods(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Integer id = Integer.valueOf(request.getParameter("id"));
        List<Product> products = (List<Product>) getServletContext()
                .getAttribute("products");
        Product product = findById(id, products);
        if (product != null) request.setAttribute("product", product);
        request.getRequestDispatcher("./productDetails.jsp")
                .forward(request, response);
    }

我们首先从请求路径中获取到了请求参数 id,然后从 context中获取到了之前存进去的商品,这里调用了一个 findById 方法,来看一下这个方法:

/** * 根据 id 查询相关内容是否在作用域中 * */
    private Product findById(Integer id, List<Product> products) {
        for (Product product : products) {
            if (id.equals(product.getId())) {
                return product;
            }
        }
        return null;
    }

它的作用就是根据 id 查询商品,如果查到了就返回,否则返回为空。

这样我们获取这个对象之后就再把它存放到请求作用域中,然后将请求转发到 productDetails 页面。也就是之前我们看到的页面。

5、添加商品到购物车

然后我们可以在文本框中输入加入购物车的商品的数量:


如果我们点击按钮,他会发送一个请求,我们使用这个方法来处理这个请求:

/** * 添加到购物车 * 响应请求: /shop/addCart * * @param request 请求 * @param response 响应 * @throws IOException 异常 */
    private void addCart(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletContext context = getServletContext();
        HttpSession session = request.getSession();
        Integer id = Integer.parseInt(request.getParameter("id"));
        Integer quality = Integer.parseInt(request.getParameter("quality"));
        List<Product> products = (List<Product>) context
                .getAttribute("products");
        // 初始化购物车
        Map<Product, Integer> cart = (Map<Product, Integer>) session
                .getAttribute("cart");
        if (cart == null) {
            cart = new HashMap<>();
        }
        Product product = findById(id, products);
        if (product != null) {
            cart.put(product, quality);
            session.setAttribute("cart", cart);
        }
        displayProducts(request, response);
    }

那他会发送什么请求呢?

<form action="addCart" method="post">
    <input type="hidden" name="id" value="${requestScope.product.id}">

我们可以查看 form 表单,而且我们加了一个隐藏域,这样在 Servlet 中才可以获取商品的 id 然后放入 session 域中。并且重定向到 productList 页面中。


我们可以点击查看购物车:

那么 cart 页面是怎么获取数据的呢?

这里用到了 jstl 的标签库以及 el 表达式:

<c:forEach items="${sessionScope.cart}" var="cart">
                <tr>
                    <td>📌${cart.value}</td>
                    <td>${cart.key.name}</td>
                    <td><span style="color: darkorange">${cart.key.price}</span>💰</td>
                    <td><span style="color: darkorange">${cart.key.price * cart.value}</span>💰</td>
                    <c:if test="${sessionScope.cart.size() >= 2}">
                        <td>
<%--                            <a href="deleteItem?id=${cart.key.id}">删除💨</a>--%>
                            <input type="button" value="删除" onclick="confirmDel(${cart.key.id})">
                        </td>
                    </c:if>
                </tr>
                <c:set var="total" value="${cart.value * cart.key.price + total}"/>
                <c:set var="sum" value="${sum + cart.value}"/>
            </c:forEach>

这样就可以输出到页面上了,而且我们还可以删除商品。

6、从购物车中删除商品

从购物车中删除商品需要 cart 页面发送一个请求,然后在 Servlet 页面中处理请求。

/** * 删除商品 * /shop/deleteItem * * @param request 请求 * @param response 响应 */
    private void deleteCard(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Integer id = Integer.parseInt(request.getParameter("id"));
        ServletContext context = getServletContext();
        HttpSession session = request.getSession();
        List<Product> products = (List<Product>) context
                .getAttribute("products");
        Map<Product, Integer> cart = (HashMap) session
                .getAttribute("cart");

        Product product = findById(id, products);
        if (product != null) {
            cart.remove(product);
            session.setAttribute("cart", cart);
        }
        response.sendRedirect("./cart.jsp");
    }

我们从 session 域中获取对象之后再删除该对象,因为它本身是一个 Map 集合,最后重新存到 session 域中,然后重定向到他自己实现刷新效果。

删除之后:

7、总体的流程演示

下面通过 GIF 演示一下:

二、扩展功能

下面实现拓展功能:

1、清空购物车

cart 页面:

<a href="./clearCart">清空购物车❗</a>

Servlet 中的代码:

/** * 清空购物车 * 响应请求: /shop/clearCart * * @param request * @param response */
    private void clearCart(HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.getSession().removeAttribute("cart");
        response.sendRedirect("./cart.jsp");
    }

就是从会话作用域中移除掉所有数据。

2、显示购物车中的商品种类数量和商品总数量

我们先在 forEach 循环中设置一个值 sum 和 total,用于记录商品数量与总数量。

	<c:set var="total" value="${cart.value * cart.key.price + total}"/>
	<c:set var="sum" value="${sum + cart.value}"/>
</c:forEach>


<tr>
                <td>✍总计 <span style="color: darkorange">${total}</span>💰</td>
                <td>总数量 ${sum}</td>
                <td></td>
                <td></td>
                <td><a href="./clearCart">清空购物车❗</a></td>
</tr>

3、购物车为空的提示

使用 jstl 实现,使用相关标签:

<c:choose>
    <c:when test="${sessionScope.cart != null}">
	</c:when>
    	<c:otherwise>
    	    <h2>购物车中没有商品 🙁</h2>
	</c:otherwise
</c:choose>

这样一旦购物车为空就会进入 otherwise 语句。

4、删除时提示是否确认删认

使用 JavaScript 实现:

<script>
        function confirmDel(param) {
            if (window.confirm("您确定要删除这件美丽的商品吗?")) {
                document.location = "deleteItem?id=" + param;
            }
        }
</script>

<input type="button" value="删除" onclick="confirmDel(${cart.key.id})">

5、在输入数量时,如果不是数字,要提示

使用 JavaScript 的正则表达式实现:

<script>
        test = function () {
            var quality = document.getElementById("quality");
            var reg = /^[0-9]*$/g;
            if (!reg.test(quality.value)) {
                alert("请输入数字");
            }
        };
</script>

<input type="text" name="quality" id="quality" onkeyup="test()">

这样就可以了。

三、关于数据源

由于我们没有使用数据库,所以自己造了数据,但是不太真实也很麻烦,所以我后来使用爬虫爬了京东的数据,然后模仿他的页面写了一个 jsp:

首先来看一下如何爬取数据? 我这里使用的是 jsoup 包,代码如下:

private void initProduct(String keyWords) {
        String url = "https://search.jd.com/Search?keyword=" + keyWords + "&enc=utf-8";
        List<Product> list = new ArrayList<>();
        try {
            Document parse = Jsoup.parse(new URL(url), 30000);
            Element element = parse.getElementById("J_goodsList");
            Elements li = element.getElementsByTag("li");
            for (Element el : li) {
                count++;
                String img = el.getElementsByTag("img").eq(0).attr("source-data-lazy-img");
                String price = el.getElementsByClass("p-price").eq(0).text();
                String title = el.getElementsByClass("p-name").eq(0).text();
                Product product =
                        new Product(count, title, img, Float.valueOf(price.split("¥")[1]));
                list.add(product);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        this
                .getServletContext()
                .setAttribute("products", list);
    }

然后写一个接口,当有请求过来的时候就可以从请求中获取关键字,然后查询,再重定向到首页,实现展示商品的功能:

/** * 搜索功能 * /shop/search * */
    private void doSearch(
            HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String keyWords = request.getParameter("keyword");
        System.out.println("数据初始化");
        this.getServletContext().removeAttribute("products");
        initProduct(keyWords);
        System.out.println("转发到商品首页");
        displayProducts(request, response);
    }

感兴趣的同学可以去 Github
上面查看相关代码。

四、总结

  • 通过这次实验,巩固了 Servlet 的基本操作,以及 JSP 的操作,体会了 JSP 的页面强大之处,JSP 太强了!!!(逃
  • 还使用了 jstl 的表达式,用起来很方便,就算不会 java 的人也能轻松实现 java 服务端和客户端代码的编写,太强了!
  • 各个域之间的存储数据,让我更清楚的明白了域的区别的与联系,适合什么样的场景就用什么样的域。

相关源码已上传至 Github 地址