JAVAWEB(三)FruitServlet,中央控制器,DOM,applicationContext.xml,控制器,MVC

文章目录


后端系统开发

Servlet

完善水果系统-编辑和修改特定库存信息

在这里插入图片描述

新需求:将名称,比如说山竹,改成超链接,跳转到另一个页面

在.html文件中,修改显示名称的代码
`苹果

苹果 是用来表示超链接的一对标签

再考虑edit.do所在的目录,他是上面页面的跳转页面,默认情况下他们在同一个目录下,为了保证他们在同一个目录

苹果 `

分析上面“跳转”这个新的需求,我们在点击之后,相当于客户端向服务器端发起了一个新的请求,经过前面的学习,我们已经知道请求的获取,处理,响应都是通过servlet这个结构来完成的,所以我们需要设计一个XxxServlet类来做和请求相关的事情

一个请求该调用哪一个XxxServlet类呢??怎么确定的??
一方面前端的html代码会有一个标签a,一方面后端有XxxServlet类这个类名,也可以当做是标签b。我们在两个标签之间建立映射关系

有注解以前,在web.xml中建立映射关系

    <servlet>
        <servlet-name>AddServlet</servlet-name>
        <servlet-class>com.atguigu.servlets.AddServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>AddServlet</servlet-name>
        <url-pattern>/add</url-pattern>
    </servlet-mapping>
    <!--
    1. 用户发请求,action=add
    2. 项目中,web.xml中找到url-pattern = /add   -> 第12行
    3. 找第11行的servlet-name = AddServlet
    4. 找和servlet-mapping中servlet-name一致的servlet , 找到第7行
    5. 找第8行的servlet-class -> com.atguigu.servlets.AddServlet
    6. 用户发送的是post请求(method=post) , 因此 tomcat会执行AddServlet中的doPost方法
    -->

有了注解就非常方便了,就在XxxServlet类上边加上一句@WebServlet("/标签a")
如果标签a是add,那么注解就是@WebServlet("/add")
如果标签a是edit.do,那么注解就是@WebServlet("/edit.do")

新需求:让请求携带信息发给服务器,非手动输入

后端的XxxServlet在处理请求的时候,有时候需要前端提供数据,这些数据被保存在请求当中。比如说之前的AddServlet,但是区别在于AddServlet的数据是由用户在客户端手动输入的

可以这样写<td><a th:text="${fruit.fname}" th:href="@{/edit.do?fid=fruit.fid}">苹果</a></td>?是一个连接符号,用于连接后面的参数。这在之前的数据库部分使用过

新需求:如何解析url

在上一个需求,我们写了 /edit.do?fid=fruit.fid,这是一个字符串。如果直接这样写,thymeleaf是无法识别的

<td><a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">苹果</a></td>

to be honest,前端这块我是真没看明白

新需求:服务器响应请求,返回一个新的页面

通过请求中携带的id数据,到数据库中查询相关数据,然后将数据反映到跳转的页面上

跳转的页面也是用thymeleaf来进行渲染

这一块就是后端的EditServlet,可以看到调用的processTemplate方法要求跳转到一个叫做edit的页面上

@WebServlet("/edit.do")
public class EditServlet extends ViewBaseServlet {

    private FruitDAO fruitDAO = new FruitDAOImpl();

    @Override
    public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
        String fidStr = request.getParameter("fid");
        if(StringUtil.isNotEmpty(fidStr)){
            int fid = Integer.parseInt(fidStr);
            Fruit fruit = fruitDAO.getFruitByFid(fid);
            request.setAttribute("fruit",fruit);
            super.processTemplate("edit",request,response);
        }
    }
}

这一块就是edit.html这个页面的写法

<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="utf-8">
	<link rel="stylesheet" href="css/edit.css">
</head>
<body>
<div id="div_container">
	<div id="div_fruit_list">
		<p class="center f30">编辑库存信息3</p>
		<form th:action="@{/update.do}" method="post" th:object="${fruit}">
			<!-- 隐藏域 : 功能类似于文本框 , 它的值会随着表单的发送也会发送给服务器,但是界面上用户看不到 -->
			<input type="hidden" name="fid" th:value="*{fid}"/>
			<table id="tbl_fruit">
				<tr>
					<th class="w20">名称:</th>
					<!-- <td><input type="text" name="fname" th:value="${fruit.fname}"/></td> -->
					<td><input type="text" name="fname" th:value="*{fname}"/></td>
				</tr>
				<tr>
					<th class="w20">单价:</th>
					<td><input type="text" name="price" th:value="*{price}"/></td>
				</tr>
				<tr>
					<th class="w20">库存:</th>
					<td><input type="text" name="fcount" th:value="*{fcount}"/></td>
				</tr>
				<tr>
					<th class="w20">备注:</th>
					<td><input type="text" name="remark" th:value="*{remark}"/></td>
				</tr>
				<tr>
					<th colspan="2">
						<input type="submit" value="修改" />
					</th>
				</tr>
			</table>
		</form>
	</div>
</div>
</body>
</html>

最终的效果,注意看url
在这里插入图片描述

新需求:编辑完一条记录后,提交数据

这就和之前的AddServlet那里一样了,我们自定义的数据被存到请求中,后端相应的Servlet收到以后,取出数据,写到数据库当中去

edit.html写明了action=update.do,method=post,所以说我们设计一个UpdateServlet类,重写里面的doPost方法

@WebServlet("/update.do")
public class UpdateServlet extends ViewBaseServlet {

    private FruitDAO fruitDAO = new FruitDAOImpl();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1.设置编码
        request.setCharacterEncoding("utf-8");

        //2.获取参数
        String fidStr = request.getParameter("fid");
        Integer fid = Integer.parseInt(fidStr);
        String fname = request.getParameter("fname");
        String priceStr = request.getParameter("price");
        int price = Integer.parseInt(priceStr);
        String fcountStr = request.getParameter("fcount");
        Integer fcount = Integer.parseInt(fcountStr);
        String remark = request.getParameter("remark");

        //3.执行更新
        fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));

        //4.资源跳转
        //super.processTemplate("index",request,response);
        //request.getRequestDispatcher("index.html").forward(request,response);
        //此处需要重定向,目的是重新给IndexServlet发请求,重新获取furitList,然后覆盖到session中,这样index.html页面上显示的session中的数据才是最新的
        response.sendRedirect("index");
    }
}

最后这个资源跳转值得说一下,原先在thymeleaf都回去调用父类里边的processTemplate方法,它本质上就是做一次服务器端转发。但是,这就意味着保存到HttpSession容器的查询结果fruitList还是旧的那一个,无法显示更新后的结果。因此,需要做客户端重定向,重新发起请求,这样对应的servlet需要重新处理请求,重新获取一次fruitList,覆盖掉之前的那一个

总结

在回顾一下实现这个功能的流程:
(1)index.html页面
(2)点击水果名称之后(这是超链接,不是跳转),发起请求,由EditServlet类来处理
(3)EditServlet类在最后跳转到edit.html页面(thymeleaf)
(4)在edit.html页面点击提交数据之后,相当于又发起请求
(5)这个请求由UpdateServlet类来处理
(6)UpdateServlet类最后跳转到index.html页面(thymeleaf)

完善水果系统-删除和添加

总的需求:通过点击右边的×,删除一条记录

新需求:点击×,进行弹窗

这个行为是JS的代码来控制

和 编辑和修改特定库存信息 这一需求不同,这里不是把×变成超链接,而是调用js的函数

首先在index.html中进行修改,修改表格中和×相关的部分

<head>
	<meta charset="utf-8">
	<link rel="stylesheet" href="css/index.css">
	<script language="JavaScript" src="js/index.js"></script>
</head>

<tr th:unless="${#lists.isEmpty(session.fruitList)}" th:each="fruit : ${session.fruitList}">
						<td><a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">苹果</a></td>
						<td th:text="${fruit.price}">5</td>
						<td th:text="${fruit.fcount}">20</td>
						<!-- <td><img src="imgs/del.jpg" class="delImg" th:οnclick="'delFruit('+${fruit.fid}+')'"/></td> -->
						<td><img src="imgs/del.jpg" class="delImg" th:onclick="|delFruit(${fruit.fid})|"/></td>
					</tr>

然后在js的代码里面定义好使用的函数

function delFruit(fid){
    if(confirm('是否确认删除?')){
        window.location.href='del.do?fid='+fid;
    }
}

新需求:点击确认以后,在数据库中删除这一条记录

js的代码中,这个超文本引用是del.do,同时还在请求中携带了id信息,那么跟请求相关,就需要XxxServlet类了

设计DelServlet类

@WebServlet("/del.do")
public class DelServlet extends ViewBaseServlet {
    private FruitDAO fruitDAO = new FruitDAOImpl();
    @Override
    public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
        String fidStr = request.getParameter("fid");
        if(StringUtil.isNotEmpty(fidStr)){
            int fid = Integer.parseInt(fidStr);
            fruitDAO.delFruit(fid);

            //super.processTemplate("index",request,response);
            response.sendRedirect("index");
        }
    }
}

最后做一次客户端重定向,回到index.html页面

新需求:在index.html页面加上一个添加的超链接

超链接的标签是<a></a>

<p class="center f30">欢迎使用水果库存后台管理系统</p>
				<div style="border:0px solid red;width:60%;margin-left:20%;text-align:right;">
					<a th:href="@{/add.html}" style="border:0px solid blue;margin-bottom:4px;">添加新库存记录</a>
				</div>

注意看,这里超链接是直接跳转到add.html页面,而之前

<a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}">

是调用了servlet组件,然后在重写的doPost方法里边跳转到edit.html页面。真的恶心

新需求:点击超链接,跳转到add.html页面

编写add.html的相关代码

一个bug

这个bug隐藏于无形,没有报错,却导致无法调用AddServlet类,使得无法向数据库添加新的纪录

<form action="add.do" method="post">

就是在这里是否需要给add.do加上绝对路径,如果要加,就是加thymeleaf的那个组件,加了就导致上面说的那个bug

<html xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="utf-8">
	<link rel="stylesheet" href="css/add.css">
</head>
<body>
<div id="div_container">
	<div id="div_fruit_list">
		<p class="center f30">新增库存信息2</p>
		<!--<form action="add.do" method="post">-->
		<form action="add.do" method="post">
			<table id="tbl_fruit">
				<tr>
					<th class="w20">名称:</th>
					<!-- <td><input type="text" name="fname" th:value="${fruit.fname}"/></td> -->
					<td><input type="text" name="fname" /></td>
				</tr>
				<tr>
					<th class="w20">单价:</th>
					<td><input type="text" name="price" /></td>
				</tr>
				<tr>
					<th class="w20">库存:</th>
					<td><input type="text" name="fcount" /></td>
				</tr>
				<tr>
					<th class="w20">备注:</th>
					<td><input type="text" name="remark" /></td>
				</tr>
				<tr>
					<th colspan="2">
						<input type="submit" value="添加" />
					</th>
				</tr>
			</table>
		</form>
	</div>
</div>
</body>
</html>

可以看到,在表单部分action=“add.do” method=“post”,这就说明,提交表单之后,会调用XxxServlet来处理请求

新需求:向数据库增加一条新的纪录

编写标签为add.do的AddServlet类

@WebServlet("/add.do")
public class AddServlet extends ViewBaseServlet {

    private FruitDAO fruitDAO = new FruitDAOImpl();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");

        String fname = request.getParameter("fname");
        Integer price = Integer.parseInt(request.getParameter("price")) ;
        Integer fcount = Integer.parseInt(request.getParameter("fcount"));
        String remark = request.getParameter("remark");

        Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;

        fruitDAO.addFruit(fruit);

        response.sendRedirect("index");

    }
}

最后,再重定向到index.html页面

完善水果系统-添加分页功能

新需求:每次只查询固定数量的记录

这一部分和DAO有关,在接口中定义新的方法

@Override
public List<Fruit> getFruitList(Integer pageNo) {
    return super.executeQuery("select * from t_fruit limit ? , 5" , (pageNo-1)*5);
}

每次只查询(pageNo-1)*5到(pageNo-1)*5+5的记录

新需求:在index.html页面显示翻页的按键

我们需要在页面下方设置一个区域用于显示页数,翻页等

在这里我才知道,部署的时候pro10/index访问的并不是index.html,而是调用IndexServlet类,最后再跳转到index.html页面,ciao

在index.html下加上这么一个部分

<div style="width:60%;margin-left:20%;border:0px solid red;padding-top:4px;" class="center">
	<input type="button" value="首  页1" class="btn" th:onclick="|page(1)|"/>
	<input type="button" value="上一页" class="btn" th:onclick="|page(${session.pageNo-1})|"/>
	<input type="button" value="下一页" class="btn" th:onclick="|page(${session.pageNo+1})|"/>
	<input type="button" value="尾  页" class="btn" th:onclick="|page(${session.pageCount})|"/>
</div>

服务器端需要客户端把页码传过来

这里的page呢是一个JS方法,因此在js里边定义这个page方法

function page(pageNo){
    window.location.href="index?pageNo="+pageNo;
}

然后在IndexServlet类中加上

Integer pageNo = 1 ;
String pageNoStr = request.getParameter("pageNo");
if(StringUtil.isNotEmpty(pageNoStr)){
    pageNo = Integer.parseInt(pageNoStr);
}

HttpSession session = request.getSession() ;
session.setAttribute("pageNo",pageNo);

FruitDAO fruitDAO = new FruitDAOImpl();
List<Fruit> fruitList = fruitDAO.getFruitList(pageNo);

session.setAttribute("fruitList",fruitList);

新需求:尾页在最大页数位置

需要计算最大页数,在DAO中增加一个求总记录条数的方法

然后在IndexServlet中计算总页数

//总记录条数
int fruitCount = fruitDAO.getFruitCount();
//总页数
int pageCount = (fruitCount+5-1)/5 ;
/*
总记录条数       总页数
1               1
5               1
6               2
10              2
11              3
fruitCount      (fruitCount+5-1)/5
 */
session.setAttribute("pageCount",pageCount);

新需求:将页数固定在1-最大页数之间,如果超出范围,则按钮不可用

这里就是前端额活了,有disable这么一个属性,为true可以点击,为false不可以点击

前端使用下来,感觉比javaPython这些更抽象,更接近自然语言

<div style="width:60%;margin-left:20%;border:0px solid red;padding-top:4px;" class="center">
	<input type="button" value="首  页1" class="btn" th:onclick="|page(1)|" th:disabled="${session.pageNo==1}"/>
	<input type="button" value="上一页" class="btn" th:onclick="|page(${session.pageNo-1})|" th:disabled="${session.pageNo==1}"/>
	<input type="button" value="下一页" class="btn" th:onclick="|page(${session.pageNo+1})|" th:disabled="${session.pageNo==session.pageCount}"/>
	<input type="button" value="尾  页" class="btn" th:onclick="|page(${session.pageCount})|" th:disabled="${session.pageNo==session.pageCount}"/>
</div>

完善水果系统-添加根据关键字查询

新需求:显示一个搜索框,输入搜索内容之后,可以提交搜索内容

用一个表单来充当搜索框

<form th:action="@{/index}" method="post" style="float:left;width:60%;margin-left:20%;">
	<input type="hidden" name="oper" value="search"/>
	请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
	<input type="submit" value="查询" class="btn"/>
</form>

提交表单之后,就会调用标签为index的servlet

前后端就是这么配合的。前端把数据装到请求里边传到服务器,我服务器这边的servlet捕获到请求,取出数据,然后进行一系列操作。操作的结果可以往那三个容器里边放,HTTPServletRequest,HttpSession,ServletContext,然后跳转到html,html里边有从容器中取出数据的代码

在这里插入图片描述

新需求:实现模糊匹配,比如说输入“果”,返回所有与果相关的结果,并分页

这个需求是在servlet中实现,这里的逻辑还是挺复杂的,前后端都要理顺才行

实现了前面那些需求之后,点击首页,上一页,下一页,尾页,添加库存新纪录,查询都会向服务器发起新的请求。他们有啥区别呢??

点击首页,上一页,下一页,尾页不必多说
点击添加库存新纪录,会跳转到edit.html页面
点击查询,无论当前是在哪一页,都需要在查询结果的第一页

或者说从请求中携带的数据这个角度来看,
点击首页,上一页,下一页,尾页,就会将页数传给服务器
点击查询,如果说要用同一个表示页数的变量,那么这里的逻辑必须发生变化。你就必须区分点击的是查询,还是其他位置

要做区分,在java se中,我们只要能找到一个分支条件即可,但在web项目里,如此重要的分支条件其实是由前端给出的,前端在查询相关的html代码处,增加一个隐藏域变量,这个变量会随着请求一起发给服务器。如果说servlet能获取到请求中的隐藏域变量,那就表示点击的是查询

<form th:action="@{/index}" method="post" style="float:left;width:60%;margin-left:20%;">
	//隐藏域
	<input type="hidden" name="oper" value="search"/>
	请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
	<input type="submit" value="查询" class="btn"/>
</form>

现在需要按关键字查询,那么之前那种查询可以兼容吗???当然可以,之前那种查询可以看作是关键字为“”的特殊情况
所以说,不管是点击查询,还是点击其他,都需要捕获这个keyword。因此IndexServlet里边的逻辑需要改为

//Servlet从3.0版本开始支持注解方式的注册
@WebServlet("/index")
public class IndexServlet extends ViewBaseServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }

    @Override
    public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        HttpSession session = request.getSession() ;
        Integer pageNo = 1 ;

        String oper = request.getParameter("oper");
        //如果oper!=null 说明 通过表单的查询按钮点击过来的
        //如果oper是空的,说明 不是通过表单的查询按钮点击过来的

        String keyword = null ;
        if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
            //说明是点击表单查询发送过来的请求
            //此时,pageNo应该还原为1 , keyword应该从请求参数中获取
            pageNo = 1 ;
            keyword = request.getParameter("keyword");
            if(StringUtil.isEmpty(keyword)){
                keyword = "" ;
            }
            session.setAttribute("keyword",keyword);
        }else{
            //说明此处不是点击表单查询发送过来的请求(比如点击下面的上一页下一页或者直接在地址栏输入网址)
            //此时keyword应该从session作用域获取
            String pageNoStr = request.getParameter("pageNo");
            if(StringUtil.isNotEmpty(pageNoStr)){
                pageNo = Integer.parseInt(pageNoStr);
            }
            Object keywordObj = session.getAttribute("keyword");
            if(keywordObj!=null){
                keyword = (String)keywordObj ;
            }else{
                keyword = "" ;
            }
        }

        session.setAttribute("pageNo",pageNo);

        FruitDAO fruitDAO = new FruitDAOImpl();
        List<Fruit> fruitList = fruitDAO.getFruitList(keyword , pageNo);

        session.setAttribute("fruitList",fruitList);

        //总记录条数
        int fruitCount = fruitDAO.getFruitCount(keyword);
        //总页数
        int pageCount = (fruitCount+5-1)/5 ;
        /*
        总记录条数       总页数
        1               1
        5               1
        6               2
        10              2
        11              3
        fruitCount      (fruitCount+5-1)/5
         */
        session.setAttribute("pageCount",pageCount);

        //此处的视图名称是 index
        //那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
        //逻辑视图名称 :   index
        //物理视图名称 :   view-prefix + 逻辑视图名称 + view-suffix
        //所以真实的视图名称是:      /       index       .html
        super.processTemplate("index",request,response);
    }
}

新需求:进行查询之后,即便翻页,查询框也要显示我们查询的内容

其实keyword也不一定需要存到HttpSession中,但是呢,我们希望在index.html页面的查询框一直显示我们查询的内容,这就需要把keyword的值存起来

在index.html的代码中,设置搜索框的默认值

<form th:action="@{/index}" method="post" style="float:left;width:60%;margin-left:20%;">
	<input type="hidden" name="oper" value="search"/>
	请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>
	<input type="submit" value="查询" class="btn"/>
</form>

这一句:
请输入关键字:<input type="text" name="keyword" th:value="${session.keyword}"/>

我们的逻辑是,默认先调用IndexServlet,然后keyword的默认值就被设为了“”,然后调用processTemplate方法跳转到index.html页面

二. MVC优化

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范

用一种业务逻辑、数据、界面显示分离的方法组织代码

Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,主要提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。主要使用的技术:数据模型:实体类(JavaBean),数据访问:JDBC,Hibernate等。

View(视图):负责进行模型的展示,一般就是我们见到的用户界面,比如JSP,Html等

Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。主要使用的技术:servlet,Struts中的Action类等

MVC是一个框架模式,它强制性的使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心部件:模型、视图、控制器。它们各自处理自己的任务。最典型的MVC就是JSP + servlet + javabean的模式

在这里插入图片描述
SSM:Spring+SpringMVC+MyBatis

所以说MVC是一种规范,一种思想。然后呢有一些框架是按照MVC来设计的,你也可以不遵守MVC这种规范

背景

在这里插入图片描述
这是我们当目前为止的servlet结构,如果发展下去,可以预想到,servlet的数量会非常庞大,而且很难进行管理

那么按照在java se中的经验,我们是否可以设计这样一个结构
在这里插入图片描述
在这里插入图片描述

servlet优化1

之前我们讲过servlet的继承树以及核心的几个方法

1. 继承关系
  javax.servlet.Servlet接口
      javax.servlet.GenericServlet抽象类
          javax.servlet.http.HttpServlet抽象子类
          		XxxServlet子类(对应增删改查)
2. 相关方法
    javax.servlet.Servlet接口:
      void init(config) - 初始化方法
      void service(request,response) - 服务方法
      void destory() - 销毁方法

    javax.servlet.GenericServlet抽象类:
      void service(request,response) - 仍然是抽象的

    javax.servlet.http.HttpServlet 抽象子类:
      void service(request,response) - 不是抽象的
	      1. String method = req.getMethod(); 获取请求的方式
	      2. 各种if判断,根据请求方式不同,决定去调用不同的do方法
	          if (method.equals("GET")) {
	              this.doGet(req,resp);
	          } else if (method.equals("HEAD")) {
	              this.doHead(req, resp);
	          } else if (method.equals("POST")) {
	              this.doPost(req, resp);
	          } else if (method.equals("PUT")) {
	      3.HttpServlet这个抽象类中,do方法都差不多:
	      protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
	          String protocol = req.getProtocol();
	          String msg = lStrings.getString("http.method_get_not_supported");
	          if (protocol.endsWith("1.1")) {
	              resp.sendError(405, msg);
	          } else {
	              resp.sendError(400, msg);
	          }
	      }
  3.小结:
  1) 继承关系: HttpServlet -> GenericServlet -> Servlet
  2) Servlet中的核心方法: init() , service() , destroy()
  3) 服务方法: 当有请求过来时,service方法会自动响应(其实是tomcat容器调用的)
          在HttpServlet中我们会去分析请求的方式:到底是get、post、head还是delete等等
          然后再决定调用的是哪个do开头的方法
          那么在HttpServlet中这些do方法默认都是405的实现风格-要我们子类去实现对应的方法,否则默认会报405错误
  4) 因此,我们在新建Servlet时,我们才会去考虑请求方法,从而决定重写哪个do方法

重写service方法

我一直觉得重写service方法很别扭,因为在HttpServlet这个类中已经实现了service方法,系统在用这个方法,怎么能重写呢???
实际上,之前并不是直接调用了XxxServlet类中重写的doPost方法,而是先调用service方法,然后service方法根据method的值判断应该调用XxxServlet中的哪一个方法。现在重写service方法,使用的是同一个逻辑;并且我们也不需要再重写doGet,doPost之类的方法了

核心思路是这样的:把之前的XxxServlet类都写成方法,让他们都变成FruitServlet类里边的方法,然后在FruitServlet类中重写的service方法调用

判断调用哪一个方法的依据:
和前面实现关键字查询的方法一样,前端会向服务器端回传一个隐藏域变量,通过这个变量来判断调用哪一个方法,用switch case这个结构

程序

假设现在设计一个FruitServlet类,标签为fruit.do,那么所有的.html文件的相关位置都要改为fruit.do,并且都要回传一个隐藏域变量operate

@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    request.setCharacterEncoding("UTF-8");
    String operate = request.getParameter("operate");
    if(StringUtil.isEmpty(operate)){
        operate = "index";
    }
    switch (operate){
        case "index":
            index(request,response);
            break;

        case "add":
            add(request,response);
            break;

        case "del":
            del(request,response);
            break;

        case "edit":
            edit(request,response);
            break;

        case "update":
            update(request,response);
            break;

        default:
            throw new RuntimeException("operate值非法");

    }

}

operate的默认值设为index,也就是说,启动改项目之后,请求发到服务器,先调用service方法,在这个方法中,operate被设为index,这样就调用index方法,然后跳转到index.html页面

还有就是isEmpty方法,如果字符串a = null或者a = “”,那么判定字符串a为空

servlet优化2-反射的引入

在优化1中,我们将多个XxxServlet类改写为了FruitServlet类中的多个方法,并在switch case结构中选择不同的方法

如果FruitServlet类中有100个方法呢,那么switch case不就会特别长吗!!之前有项目一个servlet中有超过5000行的代码,好几百个方法,非常难以管理

所以,引入反射机制来替代switch case这一结构。一句话总结,利用反射机制来调用和operate同名的方法

首先先获取目标类的所有方法名,每次调用service方法,都遍历方法名,找到和operate同名的方法,调用这个方法

//使用反射机制来调用和operate同名的方法
//获取当前类中的所有方法
Method[] methods = this.getClass().getDeclaredMethods();
for(Method m : methods){
    //获取方法名称
    String methodName = m.getName();
    if(operate.equals(methodName)){
        try {
            //找到和operate同名的方法,通过反射技术调用该方法
            m.invoke(this,request,response);
            return;
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        }

    }
}
throw new RuntimeException("operate值非法!");

这样一来,XxxServlet的主体部分代码量就减少了很多,额而且结构清晰

servlet优化3-dispatcherServlet的引入

再进一步增加代码量,增加和FruitServlet同级别的Servlet,为了管理这些Servlet,仿照前面的结构,就再加一层servlet用于管理和FruitServlet同级别的这些Servlet

从代码的层面来说,前面的优化也导致每一个XxxServlet类里边都有重复的反射代码用于确定调用的方法,这些重复代码是否也可以向上抽取
在这里插入图片描述
如上图所示,MVC的结构就逐渐显现出来了。引入了一个controller的概念

dispatcherServlet:作为核心控制器
FruitServlet:作为Fruit controller

核心控制器dispatcherServlet怎么知道该选择FruitServlet还是UserServlet呢???
按照以往的经验,这个判断的依据应该是由前端负责的,服务器端可以在请求中获取到这个信息。具体来讲是通过解析url得到的,客户端会向某一个url发起请求,这个url中就携带了访问哪一个控制器的信息,比如说fruit.do或者是user.do等等。dispatcherServlet的标签是*.do,对所有后缀是.do的请求都会有核心控制器dispatcherServlet先进行处理

解析url

//解析字符串url
//假设url是:  http://localhost:8080/pro15/hello.do
//那么servletPath是:    /hello.do
// 我的思路是:
// 第1步: /hello.do ->   hello   或者  /fruit.do  -> fruit
// 第2步: hello -> HelloController 或者 fruit -> FruitController
String servletPath = request.getServletPath();//得到 /hello.do
servletPath = servletPath.substring(1);//处理掉斜杠
int lastDotIndex = servletPath.lastIndexOf(".do") ;//处理掉最后的.do
servletPath = servletPath.substring(0,lastDotIndex);

xml配置文件

叫什么可标记扩展语言

解析url之后,我们就得到了一个字符串,比如说,fruit,我们的目的是去调用FruitController类,因此我们需要建立字符串fruit和FruitController类的映射关系

xml文件的标签是除自定义的,包括标签内部的属性也是自定义的

<?xml version="1.0" encoding="utf-8"?>

<beans>
    <!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
    <bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
</beans>
<!--
1.概念
HTML : 超文本标记语言
XML : 可扩展的标记语言
HTML是XML的一个子集

2.XML包含三个部分:
1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
2) DTD 文档类型定义
3) XML正文
 -->

xml配置文件的解析-DOM技术

解析XML配置文件,并最终创建一个map容器,beanMap,的技术就叫做DOM技术

DOM:Document Object Mode,文档对象模型

之前在python的DP项目中做过xml的解析,实际上就是一系列固定的步骤,挺恶心的

在java中解析xml需要用到好几个类,专门用于xml文件的解析工作。DocumentBuilderFactory,DocumentBuilder,Document,NodeList,Node,Element

DocumentBuilderFactory,DocumentBuilder都是抽象类
Document,NodeList,Node,Element都是接口
Document和Element都继承了Node接口

private Map<String,Object> beanMap = new HashMap<>();

InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);

//4.获取所有的bean节点,拿到每一组<bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
	//拿到一个bean标签
    Node beanNode = beanNodeList.item(i);
    if(beanNode.getNodeType() == Node.ELEMENT_NODE){
    	//Element有更好的API支持
        Element beanElement = (Element)beanNode ;
        String beanId =  beanElement.getAttribute("id");
        String className = beanElement.getAttribute("class");
        //最终是希望在map容器中放入字符串id以及对应的实例
        Class controllerBeanClass = Class.forName(className);
        Object beanObj = controllerBeanClass.newInstance() ;
        beanMap.put(beanId , beanObj) ;
    }
}

比如说我们想要调用FruitController,那么我们就希望从xml中解析出另个东西存到map容器中,一个是字符串id=fruit,一个是FruitController的实例

当然,从我现在的水平,我还看不出为什么非得要这么做。我听人说这是什么IOC容器的底层实现

从map容器中获取对应的Controller实例

前面我们重写service方法的时候,解析了url,就是说我们知道了前端发起的请求需要哪一个servlet,或者说controller来处理。比如说,需要FruitController来处理请求,解析完url之后,我们就得到了一个字符串fruit,然后我们就可以在beanMap容器中去找对应的Controller实例了,fruit对应的Controller实例是FruitController类的实例

Object controllerBeanObj = beanMap.get(servletPath);

调用Controller中的目标方法

核心控制器这一块的逻辑我认为还是挺复杂的, 但是他的还是很固定的几个步骤,到时候上MVC框架以后,就是一条指令的事儿

当采用了核心控制器+控制器的架构之后,控制器,或者说原来的XxxServlet就变成一个类似于工具类的存在了,里边有处理各种请求的方法

(1)所有类型的请求都汇总到核心控制器dispatcherServlet类
(2)由核心控制器重写service方法
(3)解析请求url,得到一个字符串servletPath,比如fruit,user,book,用于映射到对应的控制器
(4)核心控制器在调用init方法的时候,完成了xml配置文件的解析,得到了一个map容器,里面存了字符串beanID以及这个字符串对应的控制器实例,比如说fruit与FruitController实例,user与UserController实例
(5)以解析url的结果从map容器中取出控制器实例
(6)获取HTTPServletRequest请求中的operate属性的值,这个值用于确定调用控制器实例中的哪一个方法。operate的值比如update,del,edit,index
(7)利用反射机制捕获到控制器的某一个方法,并调用这个方法

在上面的操作步骤中,出现了好几个字符串。其中,我在练习的时候感觉有两个字符串特别容易混淆,一个是解析url的结果servletPath,一个是operate属性的值,但实际上两者的作用完全不同,两者的值天差地别

servletPath:用来充当map容器的key,来确定当前要使用的Controller
operate:用来确定调用Controller的哪一个方法

小bug

之前我们使用thymeleaf渲染的时候,设计了一个叫做ViewBaseServlet类,由ViewBaseServlet去继承HttpServlet,然后我们设计的XxxServlet去继承ViewBaseServlet

现在引入了控制器的概念之后,由谁去继承ViewBaseServlet类呢???
现在控制器已经不再是Servlet组件了,只有核心控制器才是servlet组件,核心控制器DispatcherServlet处理所有的.do请求。

核心控制器只是负责调用控制器,并不实际处理请求,所以DispatcherServlet继承的是HttpServlet,控制器才是实际处理请求的结构,控制器继承的就是ViewBaseServlet类,但是控制器又不是servlet组件

这就导致ViewBaseServlet类,这个父类中的init方法没有被调用。如果控制器还是servlet组件,那么他的父类中的init方法是会被隐式调用的。ViewBaseServlet类中的init方法没有被调用,就导致了一个空指针异常

//这里的*是一个通配符,表示前端以.do结尾的action,都要调用DispatcherServlet
@WebServlet("*.do")
public class DispatcherServlet extends HttpServlet {

    private Map<String,Object> beanMap = new HashMap<>();

    public DispatcherServlet(){
    }

    public void init(){
        try {
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
            //1.创建DocumentBuilderFactory
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            //2.创建DocumentBuilder对象
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
            //3.创建Document对象
            Document document = documentBuilder.parse(inputStream);

            //4.获取所有的bean节点
            NodeList beanNodeList = document.getElementsByTagName("bean");
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                Node beanNode = beanNodeList.item(i);
                if(beanNode.getNodeType() == Node.ELEMENT_NODE){
                    Element beanElement = (Element)beanNode ;
                    String beanId =  beanElement.getAttribute("id");
                    String className = beanElement.getAttribute("class");
                    Class controllerBeanClass = Class.forName(className);
                    Object beanObj = controllerBeanClass.newInstance() ;
                    Method setServletContextMethod = controllerBeanClass.getDeclaredMethod("setServletContext",ServletContext.class);
                    setServletContextMethod.invoke(beanObj , this.getServletContext());

                    beanMap.put(beanId , beanObj) ;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //设置编码
        request.setCharacterEncoding("UTF-8");

        //解析字符串url
        //假设url是:  http://localhost:8080/pro15/hello.do
        //那么servletPath是:    /hello.do
        // 我的思路是:
        // 第1步: /hello.do ->   hello   或者  /fruit.do  -> fruit
        // 第2步: hello -> HelloController 或者 fruit -> FruitController
        String servletPath = request.getServletPath();
        servletPath = servletPath.substring(1);//处理掉斜杠
        int lastDotIndex = servletPath.lastIndexOf(".do") ;//处理掉最后的.do
        servletPath = servletPath.substring(0,lastDotIndex);

        Object controllerBeanObj = beanMap.get(servletPath);

        String operate = request.getParameter("operate");
        if(StringUtil.isEmpty(operate)){
            operate = "index" ;
        }



        try {
            Method method = controllerBeanObj.getClass().getDeclaredMethod(operate,HttpServletRequest.class,HttpServletResponse.class);
            if(method!=null){
                method.setAccessible(true);
                method.invoke(controllerBeanObj,request,response);
            }else{
                throw new RuntimeException("operate值非法!");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

控制器里边都是些私有方法,使用反射调用私有方法,需要method.setAccessible(true);

servlet优化4-提取视图资源处理通用代码

这个视图,view,我一直觉得特别抽象。视图??

在这里插入图片描述
视图资源处理都是由各个controller的各个方法来实现的

这里的优化集中在两点:
(1)从HTTPServletRequest请求中提取参数
(2)服务器端转发或者客户端重定向

一种新的客户端重定向方式

以FruitController中的update方法为例,原来的客户端重定向方式是在update方法当中显式地调用sendRedirect方法

response.sendRedirect("fruit.do");

现在,由核心控制器来实现客户端重定向,控制器的update方法返回一个字符串

return "redirect:fruit.do";

那么核心控制器的职能其实就扩大了,不只是调度控制器了。在核心控制器中解析update方法返回的字符串,也就是拿到fruit.do这个servlet组件标签

//2.controller组件中的方法调用
 method.setAccessible(true);
 Object returnObj = method.invoke(controllerBeanObj,parameterValues);

 //3.视图处理
 String methodReturnStr = (String)returnObj ;
 if(methodReturnStr.startsWith("redirect:")){        //比如:  redirect:fruit.do
     String redirectStr = methodReturnStr.substring("redirect:".length());
     response.sendRedirect(redirectStr);
 }
 }

一种新的thymeleaf跳转

和上面一样,FruitController中有一个edit方法,原本是在方法最后调用父类ViewBaseServlet中的processTemplate方法,现在edit方法返回一个叫做edit的字符串,跳转到edit.html页面上

也就是从这里开始,核心控制器继承的不再是HttpServlet,而是ViewBaseServlet,这样核心控制器内部才可以调用processTemplate方法

//2.controller组件中的方法调用
 method.setAccessible(true);
 Object returnObj = method.invoke(controllerBeanObj,parameterValues);

 //3.视图处理
 String methodReturnStr = (String)returnObj ;
 if(methodReturnStr.startsWith("redirect:")){        //比如:  redirect:fruit.do
     String redirectStr = methodReturnStr.substring("redirect:".length());
     response.sendRedirect(redirectStr);
 }else{
     super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
 }

因为客户端重定向以及跳转的实现由核心控制器负责了,那么控制器中各个方法的形参也就不再需要HTTPServletResponse响应

thymeleaf的初始化

前面说了,核心控制器不在继承HttpServlet,而是继承ViewBaseServlet,而ViewBaseServlet需要调用init方法

因此,直接在DispatcherServlet重写的init方法中,先调用父类的init方法

servlet优化5-在核心控制器中统一获取参数以及进行视图处理

核心控制器的作用

经过前面的优化,我们发现核心控制器的职能被扩大了:
(1)负责调度控制器
(2)负责执行通用操作
(3)因为要做和thymeleaf相关的操作,继承ViewBaseServlet

private void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //1.设置编码
    request.setCharacterEncoding("utf-8");

    //2.获取参数
    String fidStr = request.getParameter("fid");
    Integer fid = Integer.parseInt(fidStr);
    String fname = request.getParameter("fname");
    String priceStr = request.getParameter("price");
    int price = Integer.parseInt(priceStr);
    String fcountStr = request.getParameter("fcount");
    Integer fcount = Integer.parseInt(fcountStr);
    String remark = request.getParameter("remark");

    //3.执行更新
    fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));

    //4.资源跳转
    //super.processTemplate("index",request,response);
    //request.getRequestDispatcher("index.html").forward(request,response);
    //此处需要重定向,目的是重新给IndexServlet发请求,重新获取furitList,然后覆盖到session中,这样index.html页面上显示的session中的数据才是最新的
    response.sendRedirect("fruit.do");
}


//修改以后
private String update(Integer fid , String fname , Integer price , Integer fcount , String remark ){
    //3.执行更新
    fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
    //4.资源跳转
    return "redirect:fruit.do";
}

可以明显地看到,不再由控制器的方法自己去获取参数,而是中央控制器在调用的时候提供实参

还是大量地在使用反射机制,这里为了获取类中方法的形参名,需要做一个设置。在file/setting/builder/compiler/java compiler/additional command line parameters处加上-parameters,这表示在编译生成的.class字节码文件中会加上每个方法的形参名

还有一点我之前都没有注意到,从HttpServletRequest实例中取到的数据全都是String类型的,和input方法一样

从代码中可以看到,HttpServletRequest实例调用getParameter方法的返回值都是String类型

String fidStr = request.getParameter("fid");
Integer fid = Integer.parseInt(fidStr);

String fname = request.getParameter("fname");

String priceStr = request.getParameter("price");
int price = Integer.parseInt(priceStr);

String fcountStr = request.getParameter("fcount");
Integer fcount = Integer.parseInt(fcountStr);

String remark = request.getParameter("remark");

在核心控制器中完成从HttpServletRequest实例中获取实参argument的代码
实际体验下来,真的需要特别熟悉反射

try {
    Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
    for(Method method : methods){
        if(operate.equals(method.getName())){
            //1.统一获取请求参数

            //1-1.获取当前方法的参数,返回参数数组,比如说fid,fname,price,fcount,remark
            Parameter[] parameters = method.getParameters();
            //1-2.parameterValues 用来承载参数的值
            Object[] parameterValues = new Object[parameters.length];
            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                //取到了一个形参名,比如说fid
                String parameterName = parameter.getName() ;
                //如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
                if("request".equals(parameterName)){
                    //我以为request是字符串,看错了,这里的request是一个对象,传到
                    //重写的service方法的一个参数HttpServletRequest request
                    parameterValues[i] = request ;
                }else if("response".equals(parameterName)){
                    parameterValues[i] = response ;
                }else if("session".equals(parameterName)){
                    parameterValues[i] = request.getSession() ;
                }else{
                    //从请求中获取参数值
                    String parameterValue = request.getParameter(parameterName);

                    parameterValues[i] = parameterValue ;
                }
            }
            //2.controller组件中的方法调用
            method.setAccessible(true);
            Object returnObj = method.invoke(controllerBeanObj,parameterValues);

            //3.视图处理
            String methodReturnStr = (String)returnObj ;
            if(methodReturnStr.startsWith("redirect:")){        //比如:  redirect:fruit.do
                String redirectStr = methodReturnStr.substring("redirect:".length());
                response.sendRedirect(redirectStr);
            }else{
                super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
            }
        }
    }

    /*
    }else{
        throw new RuntimeException("operate值非法!");
    }
    */
} 

一个bug

首页没有问题,但是点击下一页报错,说是实参类型不匹配,IllegalArgumentException: argument type mismatch

在这里插入图片描述
其原因在于,在上面的代码中,我们从HttpServletRequest实例中取到的数据都是String类型,然后将他们作为实参传给了控制器的某个方法。但是这个方法需要的实参并不都是String

//原版
//从请求中获取参数值
String parameterValue = request.getParameter(parameterName);

parameterValues[i] = parameterValue ;

还是使用反射机制,获得每个形参的类型parameter.getType().getName(),依据类型进行强转。又一次说明反射机制的重要性

//从请求中获取参数值
String parameterValue = request.getParameter(parameterName);
String typeName = parameter.getType().getName();

Object parameterObj = parameterValue ;

if(parameterObj!=null) {
    if ("java.lang.Integer".equals(typeName)) {
        parameterObj = Integer.parseInt(parameterValue);
    }
}

parameterValues[i] = parameterObj ;

总结

控制器和中央控制器的作用

经过前面的一系列优化,控制器只负责业务相关的特有操作,而那些通用的处理,比如获取参数,跳转等,都有中央控制器负责

反射机制中的invoke,有“调用、挑取、启用、引动”之意,Method实例调用invoke函数就相当于说在调用方法。invoke有个很直接的意思是援引

  1. 最初的做法是: 一个请求对应一个Servlet,这样存在的问题是servlet太多了
  2. 把一些列的请求都对应一个Servlet, IndexServlet/AddServlet/EditServlet/DelServlet/UpdateServlet -> 合并成FruitServlet
    通过一个operate的值来决定调用FruitServlet中的哪一个方法
    使用的是switch-case
  3. 在上一个版本中,Servlet中充斥着大量的switch-case,试想一下,随着我们的项目的业务规模扩大,那么会有很多的Servlet,也就意味着会有很多的switch-case,这是一种代码冗余
    因此,我们在servlet中使用了反射技术,我们规定operate的值和方法名一致,那么接收到operate的值是什么就表明我们需要调用对应的方法进行响应,如果找不到对应的方法,则抛异常
  4. 在上一个版本中我们使用了反射技术,但是其实还是存在一定的问题:每一个servlet中都有类似的反射技术的代码。因此继续抽取,设计了中央控制器类:DispatcherServlet
    DispatcherServlet这个类的工作分为两大部分:
    1.根据url定位到能够处理这个请求的controller组件:
    1)从url中提取servletPath : /fruit.do -> fruit
    2)根据fruit找到对应的组件:FruitController , 这个对应的依据我们存储在applicationContext.xml中
    <bean id=“fruit” class="com.atguigu.fruit.controllers.FruitController/>
    通过DOM技术我们去解析XML文件,在中央控制器中形成一个beanMap容器,用来存放所有的Controller组件
    3)根据获取到的operate的值定位到我们FruitController中需要调用的方法
    2.调用Controller组件中的方法:
    1. 获取参数
      获取即将要调用的方法的参数签名信息: Parameter[] parameters = method.getParameters();
      通过parameter.getName()获取参数的名称;
      准备了Object[] parameterValues 这个数组用来存放对应参数的参数值
      另外,我们需要考虑参数的类型问题,需要做类型转化的工作。通过parameter.getType()获取参数的类型
    2. 执行方法
      Object returnObj = method.invoke(controllerBean , parameterValues);
    3. 视图处理
      String returnStr = (String)returnObj;
      if(returnStr.startWith(“redirect:”)){

      }else if…
中央控制器

中央控制器DispatcherServlet重写了两个方法:
(1)重写init方法,解析applicationContext.xml配置文件,创建包含键值对id+对应类实例的beanMap容器
(2)重写service方法,解析url确定调用控制器的哪一个方法,统一获取参数,视图处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值