1. 准备和回顾
本篇基于上一篇JavaWeb《后端内容:1. Tomcat - Servlet - Thymeleaf》 继续使用mvc进行优化,复制上面模块的代码,并新建工件和项目和配置服务器
这里可以再好好复习揣摩一下这里index页面的逻辑部分,尤其是关键字的两个if和else,并思考之后的覆盖session作用域的操作
//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
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
HttpSession session = req.getSession();
//通过地址栏访问默认页码为1
Integer pageNum = 1;
String oper = req.getParameter("oper");
//如果oper不为空,说明通过表单的查询按钮点击过来的
//如果oper为空,说明通过其他的操作进入
String keyword = null;
if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
//说明是点击表单查询发送过来的请求
//此时,pageNum应该还原为1 , keyword应该从请求参数获取
pageNum = 1;
keyword = req.getParameter("keyword");
if(StringUtil.isEmpty(keyword)){
keyword = "";
}
//将我们表单提交所写的keyword覆盖session作用域的keyword
session.setAttribute("keyword", keyword);
} else {
//说明此处是通过页面按钮跳转的,或者从网页直接输入网址
String pageNumStr = req.getParameter("pageNum");
//我们在最初设置了默认值1,能进入if判断说明session已经有pageNum了
if(StringUtil.isNotEmpty(pageNumStr)){
pageNum = Integer.parseInt(pageNumStr);
}
//keyword应该从session作用域获取
//如果不是点击的查询按钮,那么查询的基于session中保存的现有keyword进行查询
Object keywordObj = session.getAttribute("keyword");
if(keywordObj!= null){
keyword = keywordObj.toString();
}else {
keyword = "";
}
}
//保存或覆盖页码到session作用域
session.setAttribute("pageNum", pageNum);
//保存或覆盖水果库存到session作用域
FruitDao fruitDao = new FruitDao();
List<Fruit> fruitList = fruitDao.getFruitList(keyword, pageNum);
session.setAttribute("fruitList", fruitList);
//保存或覆盖总页码到session作用域
int fruitCount = fruitDao.getFruitCount(keyword);
int pageCount = (fruitCount + 5 - 1) / 5;
session.setAttribute("pageCount", pageCount);
//此处的视图名称是 index
//那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上
//逻辑视图名称 : index
//物理视图名称 : view-prefix + 逻辑视图名称 + view-suffix
//真实的视图名称是: / index .html
super.processTemplate("index", req, resp);
}
}
这是之前水果管理系统的架构,省略了Thymeleaf渲染后发给客户端的过程(为了整洁),我们使用Servlet的方式,实际上会使得架构非常混乱。比如购物车,消费等记录,都需要添加类似的Servlet,这么做会使得项目非常难以维护
我们现在想要实现单纯基于水果数据库的后端,我们只用一个FruitServlet进行响应,而不同的操作,调用内部不同的方法,把之前的Servlet的doPost或者doGet方法复制过来封装为方法即可,同时也不需要在意是否是doPost或者doGet,都是由我们FruitServlet的service转发过来请求和响应,我们这时需要考虑的就是如何把获取这个operate字符串的值的逻辑改写我们之前的项目
@WebServlet("/fruit.do")
public class FruitServlet extends ViewBaseServlet {
private FruitDao fruitDao = new FruitDao();
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
String operate = req.getParameter("operate");
if (StringUtil.isEmpty(operate)) {
operate = "index";
}
switch (operate) {
case "index":
index(req, resp);
break;
case "add":
add(req, resp);
break;
case "del":
del(req, resp);
break;
case "edit":
edit(req, resp);
break;
case "update":
update(req, resp);
break;
default:
throw new RuntimeException("operate值非法!");
}
}
index(HttpServletRequest req, HttpServletResponse resp){...}
add(HttpServletRequest req, HttpServletResponse resp){...}
del(HttpServletRequest req, HttpServletResponse resp){...}
edit(HttpServletRequest req, HttpServletResponse resp){...}
update(HttpServletRequest req, HttpServletResponse resp){...}
}
这个时候我们就需要去改写对应html和js文件所有调用以前的Servlet的地方,都改写为响应给FruitServlet并且加入一个对应字符串的参数代表调用哪个方法
edit.do改为如下
del.do原本是点击图标的超链接,然后调用js方法传入delServlet,所以我们改写js方法
顺带把page的js方法页改写一下
同时我们重定向之前是del.do现在改写为fruit.do
同时添加库存的index.html部分我们当时跳转到了add.html页面,我们先把action改写,再改写add.html
add.html只需要把action改成fruit.do并传入参数,这里我们为了传入FruitServlet也有operate值,就加一个隐藏域,写入它即可(为什么加隐藏域:因为我们form表单使用的是post方法,post方法没法传入链接的字符串常量,因此使用隐藏域)
同时add方法重定向也从index改为fruit.do
现在我们改写edit的html,和上面相同,也是改为响应fruit.do,同时需要加入隐藏域的operate
update方法我们也重定向改为fruit.do
现在我们就完成了合并的操作
2. MVC-reflect
我们如果采用升级后的方式,确实很大程度上优化了项目结构,但是如果方法多了switch case会很长很多,完全不利于项目梳理,我们可以利用反射去优化上面的结构
switch (operate) {
case "index":
index(req, resp);
break;
case "add":
add(req, resp);
break;
case "del":
del(req, resp);
break;
case "edit":
edit(req, resp);
break;
case "update":
update(req, resp);
break;
default:
throw new RuntimeException("operate值非法!");
}
}
把上面的switch换成下面的反射方式
//获取当前类声明的方法
Method[] methods = this.getClass().getDeclaredMethods();
for (Method m : methods) {
//获取方法名称
String methodName = m.getName();
if(operate.equals(methodName)) {
//找到和operate同名的方法,那么通过反射技术调用它
try {
m.invoke(this, req, resp);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
break;
}
}
throw new RuntimeException("operate值非法!");
3. MVC-dispatcherServlet
3.1 思路部分
未来可能会多出很多不同物件的Servlet,但是他们可能都有类似的方法,我们难不成都需要给他们每个都写一个反射?
我们可以加一层,中央控制器,让它调度传入哪个Controller(Servlet)
我们在myspringmvc创建一个Servlet类:DispatcherServlet,它的Servlet注解为WebServlet(“*.do”)继承我们的ViewBaseServlet,同时实现service方法(父类的父类的HttpServlet的方法),目的是能通过req的getServletPath()方法,获取到当前拦截的请求路径名称
我们下一步是为了满足能通过对应获得的servletPath对应相应的Controller,所以把之前的servlets包名换成controllers,FruitServlet换成FruitController,同时删掉它WebServlet(“fruit.do”)的注解
补充xml的概念
HTML : 超文本标记语言
XML : 可扩展的标记语言
HTML是XML的一个子集
XML包含三个部分:
1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
2) DTD 文档类型定义
3) XML正文
为了做到fruit能对应到FruitController,我们在当前模块下的src下写一个配置文件applicationContext.xml
这里已经可以看到我们的全类名路径已经写错了,后续debug部分改正
<?xml version="1.0" encoding="utf-8"?>
<beans>
<!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
<bean id="fruit" class="com.fanxy.fruit.controllers.FruitController"/>
</beans>
既然DispatcherServlet是个Servlet,我们根据Servlet的生命周期(上一章有笔记),可以考虑在它构造阶段就读取这个配置文件,当然还是用熟悉的类加载器,使用流的方法读取
这个技术被称为DOM技术
然后使用DocumentBuilderFactory.newInstance()创造一个DocumentBuilderFactory对象
然后使用DocumentBuilderFactory.newDocumentBuilder()创造一个DocumentBuilder对象
DocumentBuilder可以把xml文件解析成一个Document对象,然后就可以用这个对象获取数据了
然后读取里面的所有节点,并读取到bean的id和全类名,并通过一个HashMap存储,方便后面调用,我们希望直接存储类的对象,所以我们直接通过反射把className对应的全类名通过反射创造实例,存储到HashMap
为什么要获取类的对象?因为我们之前版本是通过反射获取类的方法,然后循环找和operate的值相同的方法然后通过反射调用,这里我们获取到这个对象,我们就可以通过之前完全相同的代码,把this换成从hashmap提取到的对象即在Dispatcher中可让它来调用getClass方法,然后还按之前反射的循环找寻和operate相同字段的方法进行调用了
@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet{
private Map<String, Object> beanMap = new HashMap<>();
public DispatcherServlet() {
try {
InputStream ips = getClass().getClassLoader().getResourceAsStream("applicationContext");
// 1 创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
// 2 创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
// 3 创建Document对象
Document document = documentBuilder.parse(ips);
// 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");
Object beanObj = Class.forName(className).newInstance();
beanMap.put(beanId, beanObj);
}
}
} catch (ParserConfigurationException | SAXException | IOException |
ClassNotFoundException | InstantiationException |
IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {.........}
}
根据上面所说,我们可以直接把之前FruitServlet(现在是FruitController)的反射读取operate的那段部分拿过来进行使用(这里直接剪切,然后放入DispatcherServlet的service方法中,只需要把this换成从beanMap中get到的提取的servletPath
但是这里通过for循环还是感觉太繁琐,我们既然即得到了operate的值也就是方法名称,自然可以直接通过反射找寻到指定方法的名称,而不是通过循环遍历,同时如果找不到这个方法再抛出异常即可,同时我们这里把DispatcherServlet继承HttpServlet,不需要让它经过Thymeleaf
@WebServlet("*.do")
public class DispatcherServlet extends HttpServlet {
private Map<String, Object> beanMap = new HashMap<>();
public DispatcherServlet() {
try {
InputStream ips = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
// 1 创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
// 2 创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
// 3 创建Document对象
Document document = documentBuilder.parse(ips);
// 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");
Object beanObj = Class.forName(className).newInstance();
beanMap.put(beanId, beanObj);
}
}
} catch (ParserConfigurationException | SAXException | IOException | ClassNotFoundException |
InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置编码
req.setCharacterEncoding("UTF-8");
/*
假设url是http://localhost:8080/module7/hello.do
那么ServletPath是: /hello.do
我们需要:
1. /hello.do -> hello
2. hello ---对应上---> HelloController
*/
String servletPath = req.getServletPath();
servletPath = servletPath.substring(1);
int lastDotIndex = servletPath.lastIndexOf(".do");
servletPath = servletPath.substring(0, lastDotIndex);
Object controllerBeanObj = beanMap.get(servletPath);
String operate = req.getParameter("operate");
if (StringUtil.isEmpty(operate)) {
operate = "index";
}
//获取当前类中同名的单个方法
try {
Method method = controllerBeanObj.getClass().getDeclaredMethod(operate, HttpServletRequest.class, HttpServletResponse.class);
if (method != null) {
method.invoke(controllerBeanObj, req, resp);
}else {
throw new RuntimeException("operate值非法!");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
3.2 Debug部分
首先运行时发现自己当时把获取流的部分的application.xml忘记加xml后缀,导致出现流都没有读取到,同时xml文件放到了fanxy下,没有放到src目录下,然后同时前面的全类名多写了一个fruit的包,这是冗余的
然后运行tomcat发现显示为空白页面,看控制台发现我们service方法出现非法通入异常,定位到98行发现是因为我们调用方法的时候,把FruitController的方法设置的都是私有方法,这里使用反射调用方法的时候应该提前设置 method.setAccessible(true);
接着运行发现报错空指针异常,而且层层导致ViewBaseServlet出现空指针,是因为我们的FruitController已经不是Servlet了,就没法调用ViewBaseServlet的init()方法,之前init()方法内部会出现一句话:super.init(),现在不调用,这个init()方法的ServletContext就不会被赋值获取,老师这里调试了很久找问题, 其实本质就是因为FruitContrller不是Servlet,没有ServletContext,但是它的父类的init()方法还需要获取到它,所以我们就想办法把DispatcherServlet的ServletContext想办法传给它
其实下次迭代过程就不会出现这种错误,但是这里为了完成当前版本的框架,我们就强行把这里改造,给ViewBaseServlet的空参init()方法直接传入一个ServletContext
3.3 基于controller进行优化
我们现在就把上一节改动的一些代码回滚到上上代的ViewBaseSevlet,把FruitController里面一些重复的方法代码封装成一个通用的
我们的类似update的方法,都有资源跳转的操作,我们把方法修改为返回值类型为String,结尾的资源跳转改为 return "redirect:fruit.do";到时候传给中央控制器,让它完成资源的转发工作
那么中央控制器这里我们就要把之前的直接调用方法改为接受为一个字符串,但是这个方法本身返回的是Object类型,所以从代码规范的角度,我们应该接受为一个Object类型,不为空再转化为String类型
中央控制器的service方法优化如下,完成重定向的拦截和客户端重定向,让客户端查询到最新的数据
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置编码
req.setCharacterEncoding("UTF-8");
/*
假设url是http://localhost:8080/module7/hello.do
那么ServletPath是: /hello.do
我们需要:
1. /hello.do -> hello
2. hello ---对应上---> HelloController
*/
String servletPath = req.getServletPath();
servletPath = servletPath.substring(1);
int lastDotIndex = servletPath.lastIndexOf(".do") ;
servletPath = servletPath.substring(0,lastDotIndex);
Object controllerBeanObj = beanMap.get(servletPath);
String operate = req.getParameter("operate");
if(StringUtil.isEmpty(operate)){
operate = "index" ;
}
try {
Method method =
controllerBeanObj.getClass().getDeclaredMethod(operate,HttpServletRequest.class,
HttpServletResponse.class);
if(method!=null){
// 2. controller组件中的方法调用
method.setAccessible(true);
Object returnObj = method.invoke(controllerBeanObj,req,resp);
// 3. 视图处理
String methodReturnStr = (String) returnObj;
if (methodReturnStr.startsWith("redirect:")){ //比如 redirect:fruit.do
String redirectStr = methodReturnStr.substring("redirect:".length());
resp.sendRedirect(redirectStr);
}
throw new RuntimeException("operate值非法!");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
相应的FruitController中的del方法,update方法,都需要进行重定向的部分,我们都不需要IO异常,同时也把重定向部分改成最新的,以返回值为刚刚规范的 return "redirect:fruit.do";
而其他的方法,如edit,没有重定向,但是有thymeleaf的视图渲染,我们现在只想让Conntroller进行控制,而不作为Conntroller,也把它返回值设定为String,return "edit";
改为如下,如果字符串为空即传过来的fid为空,我们返回error,到时候给它做个error页面
此时我们就应该去改造DispatcherServlet了, 现在它可以不继承HttpServlet了,继承ViewBaseServlet,进行渲染工作了
并且在判断方法返回字符串的情况,字符串开头没有 "redirect:"的情况下,就是默认情况,我们进行渲染
此时再回到FruitController中,我们完全可以把方法中的HttpServletResponse resp参数去掉,然后把所有的方法都改成返回值为String,跳转的返回direct:fruit.do 字符串返回fruit.do,需要渲染的直接返回父类对应的方法名字符串,也没有必要继承任何Servlet类了
但是参数变了一定不要忘了反射的时候,就不用写它的那个reponse的参数了
改为
有了上一节Debug的经验,我们知道DispatcherServlet的现在的父类ViewBaseServlet需要调用init()方法获取Context(),我们直接在它的init()方法中调用父类的init()方法就行
此时运行即可完成基本的改造了,现在可以基本改造了一半的中央转发器功能
private String update(HttpServletRequest req) {
// 获取参数
String fidStr = req.getParameter("fid");
int fid = Integer.parseInt(fidStr);
String fname = req.getParameter("fname");
String priceStr = req.getParameter("price");
int price = Integer.parseInt(priceStr);
String fcountStr = req.getParameter("fcount");
int fcount = Integer.parseInt(fcountStr);
String remark = req.getParameter("remark");
// 更新数据库
fruitDao.updateFruit(new Fruit(fid, fname, price, fcount, remark));
//资源跳转
//resp.sendRedirect("fruit.do");
return "redirect:fruit.do";
}
但是目前的FruitController还需要从reqest中获取参数,这么还是没有完全从Servlet转变成Controller,以update方法为例,我们删除从request中获取参数的过程,直接方法获取参数,然后改写DispatcherServlet中的方法,按参数列表去反射获取参数
private String update(Integer fid, String fname, Integer price, Integer fcount, String remark) {
// 更新数据库
fruitDao.updateFruit(new Fruit(fid, fname, price, fcount, remark));
return "redirect:fruit.do";
}
edit方法也一样进行改造
private String edit(HttpServletRequest req) {
String fidStr = req.getParameter("fid");
if (StringUtil.isNotEmpty(fidStr)) {
int fid = Integer.parseInt(fidStr);
Fruit fruit = null;
fruit = fruitDao.getFruitByFid(fid);
req.setAttribute("fruit", fruit);
//super.processTemplate("edit", req, resp);
return "edit";
}
return "error";
}
但我们需要调用request作用域,所以还需要获取参数
private String edit(Integer fid, HttpServletRequest req) {
if (fid != null) {
Fruit fruit = null;
fruit = fruitDao.getFruitByFid(fid);
req.setAttribute("fruit", fruit);
//super.processTemplate("edit", req, resp);
return "edit";
}
return "error";
}
del方法也一样进行修改
private String del(HttpServletRequest req) {
String fidStr = req.getParameter("fid");
if (StringUtil.isNotEmpty(fidStr)) {
int fid = Integer.parseInt(fidStr);
fruitDao.delFruitByFid(fid);
//有了前面Edit的经验,我们这里这么调用没有查询到最新
//super.processTemplate("index", req, resp);
return "redirect:fruit.do";
}
return "error";
}
从上面获取参数,不需要获取request
private String del(Integer fid) {
if (fid != null) {
fruitDao.delFruitByFid(fid);
return "redirect:fruit.do";
}
return "error";
}
add方法一样进行改造
private String add(HttpServletRequest req) {
String fname = req.getParameter("fname");
Integer fcount = Integer.parseInt(req.getParameter("fcount"));
Integer price = Integer.parseInt(req.getParameter("price"));
String remark = req.getParameter("remark");
Fruit fruit = new Fruit(0, fname, price, fcount, remark);
fruitDao.addFruit(fruit);
//resp.sendRedirect("fruit.do");
return "redirect:fruit.do";
}
从上面获取参数,不需要获取request
private String add(String fname, Integer fcount, Integer price, String remark) {
Fruit fruit = new Fruit(0, fname, price, fcount, remark);
fruitDao.addFruit(fruit);
return "redirect:fruit.do";
}
index方法最长,需要认真思考如何改造
private String index(HttpServletRequest req) {
HttpSession session = req.getSession();
//通过地址栏访问默认页码为1
Integer pageNum = 1;
String oper = req.getParameter("oper");
//如果oper不为空,说明通过表单的查询按钮点击过来的
//如果oper为空,说明通过其他的操作进入
String keyword = null;
if (StringUtil.isNotEmpty(oper) && "search".equals(oper)) {
//说明是点击表单查询发送过来的请求
//此时,pageNum应该还原为1 , keyword应该从请求参数获取
pageNum = 1;
keyword = req.getParameter("keyword");
if (StringUtil.isEmpty(keyword)) {
keyword = "";
}
//将我们表单提交所写的keyword覆盖session作用域的keyword
session.setAttribute("keyword", keyword);
} else {
//说明此处是通过页面按钮跳转的,或者从网页直接输入网址
String pageNumStr = req.getParameter("pageNum");
//我们在最初设置了默认值1,能进入if判断说明session已经有pageNum了
if (StringUtil.isNotEmpty(pageNumStr)) {
pageNum = Integer.parseInt(pageNumStr);
}
//keyword应该从session作用域获取
//如果不是点击的查询按钮,那么查询的基于session中保存的现有keyword进行查询
Object keywordObj = session.getAttribute("keyword");
if (keywordObj != null) {
keyword = keywordObj.toString();
} else {
keyword = "";
}
}
//保存或覆盖页码到session作用域
session.setAttribute("pageNum", pageNum);
//保存或覆盖水果库存到session作用域
FruitDao fruitDao = new FruitDao();
List<Fruit> fruitList = fruitDao.getFruitList(keyword, pageNum);
session.setAttribute("fruitList", fruitList);
//保存或覆盖总页码到session作用域
int fruitCount = fruitDao.getFruitCount(keyword);
int pageCount = (fruitCount + 5 - 1) / 5;
session.setAttribute("pageCount", pageCount);
//此处的视图名称是 index
//那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上
//逻辑视图名称 : index
//物理视图名称 : view-prefix + 逻辑视图名称 + view-suffix
//真实的视图名称是: / index .html
//super.processTemplate("index", req, resp);
return "index";
}
需要获取session作用域,所以不删除request参数,同时我们的oper值可以放入参数列表,删除在内部获取,同时keyword是从request中获取的参数,也可以内部删除,在参数列表获取,同理pageNum也一样
我们现在应该明白了,所有从request获取的参数,都可以在之前的DispatcherServlet提前获取到并赋值然后再传递给Controller
private String index(String oper, String keyword, Integer pageNum, HttpServletRequest req) {
HttpSession session = req.getSession();
if (StringUtil.isNotEmpty(oper) && "search".equals(oper)) {
pageNum = 1;
if (StringUtil.isEmpty(keyword)) {
keyword = "";
}
session.setAttribute("keyword", keyword);
} else {
Object keywordObj = session.getAttribute("keyword");
if (keywordObj != null) {
keyword = keywordObj.toString();
} else {
keyword = "";
}
}
//保存或覆盖页码到session作用域
session.setAttribute("pageNum", pageNum);
//保存或覆盖水果库存到session作用域
FruitDao fruitDao = new FruitDao();
List<Fruit> fruitList = fruitDao.getFruitList(keyword, pageNum);
session.setAttribute("fruitList", fruitList);
//保存或覆盖总页码到session作用域
int fruitCount = fruitDao.getFruitCount(keyword);
int pageCount = (fruitCount + 5 - 1) / 5;
session.setAttribute("pageCount", pageCount);
return "index";
}
删除固然很爽,但是我们现在改完方法后需要回到中央控制器给它增加获取参数的方法了,之前为什么从第二部开始写,就是埋得伏笔,现在可以给它加入第一步,获取参数的步骤了
仔细思考,我们改写之后,Controller中的方法都有了不同的参数列表,给他们传参数是什么,需要我们去思考,这里显然我们利用反射去获取参数,再给它赋值即可
这里打断点发现根本没有进入断点,分析发现上面的这个通过反射获取指定方法的代码不对了,现在他们每个方法的参数列表都不太相同,根本无法这么去获取方法,所以我们还按以前的增强for循环去一个个找方法
再次通过断点试着看看能获取怎么样的参数列表,当前显然应该是index的方法,我们修改后的参数名和类型应该是
但是反射数组没有获得准确的名称,只是根据arg+元素下标位置的参数
从jdk8开始有一个新特性,我们打开Idae的设置, 让java编译器带有一个-parameters的参数,这样java虚拟机在编译class文件的时候就会把参数名称等信息也带过来,这样虽然文件会大一些,但是参数名称会带过来
我们删除out下的编译文件目录,然后重新构建当前的项目和工件
现在调试发现就带有了名称和全类名
代码改造为如下,我们打断点看看能否获取对应的名称和值
然后我们调试发现已经可以获取到了,初始状态是没有oper的,也没有keyword和pageNum
但是我们改写index方法的时候其实没有出现当出现空值的情况下进行转化为1的操作,这里去补充一下
补充上这句话就对了
逐步调试观察可以进入index页面,获取值,并且通过Dao查询到数据库数据,能正常渲染,并展示渲染后的html页面
但是我们点下一页显示500错误,参数类型不匹配
因为我们url其实当前pageNum是传入参数获取的,本身是2
但是我们倒数第二行赋值前获取参数值其实获取了字符串值
和我们index方法的形参类型不匹配了
我们可以试试看如果通过参数获取类型然后获取类型的名称看看值是什么
验证发现返回的是当前参数的全类名的字符串形式
那么我们只需要进行一个if判断,把相应类型进行转化即可,这里因为我们的数据库的数据类型和参数类型也就只有String和Integer,所以只需要写一个Integer的转化就行了
至此,我们的手搭版SpringMVC结构的Controller完全搭建完毕,功能得以实现
4. Servlet-api
4.1 回顾
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.....
4.2 Init方法
新建一个module10-servlet-api,新建一个继承于HttpServlet的类,我们在HttpServlet类中按ctrl + F12可以搜索init()方法,具有一个带参数,一个不带参数的
我们发现不带参数的init方法它是空的实现
带参数的需要传入一个ServletConfig
如果我们想在Servlet初始化时做一些准备工作,我们可以在子类重写它,我们可以通过如下步骤获取初始化设置的数据
1. 获取ServletConfig config = getServletConfig();
2. 获取初始化参数值: config.getInitParameter(key);
3. 在web.xml文件中配置Servlet
<servlet>
<servlet-name>Demo01Servlet</servlet-name>
<servlet-class>com.atguigu.servlet.Demo01Servlet</servlet-class>
<init-param>
<param-name>hello</param-name>
<param-value>world</param-value>
</init-param>
<init-param>
<param-name>uname</param-name>
<param-value>jim</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Demo01Servlet</servlet-name>
<url-pattern>/demo01</url-pattern>
</servlet-mapping>
演示: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>Demo01Servlet</servlet-name>
<servlet-class>com.fanxy.servlet.Demo01Servlet</servlet-class>
<init-param>
<param-name>hello</param-name>
<param-value>world</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Demo01Servlet</servlet-name>
<url-pattern>/demo01</url-pattern>
</servlet-mapping>
</web-app>
public class Demo01Servlet extends HttpServlet {
@Override
public void init() throws ServletException {
ServletConfig config = getServletConfig();
String initValue = config.getInitParameter("hello");
System.out.println("initValue = " + initValue);
}
}
我们没写service方法,可以进父类去看,我们运行tomcat服务器,肯定是通过Get请求去request的,会跳转405页面,但我们根据Servlet生命周期知道,init()早于service,会先在控制台打印数据
4. 也可以通过注解的方式进行配置:
@WebServlet(urlPatterns = {"/demo01"} ,
initParams = {
@WebInitParam(name="hello",value="world"),
@WebInitParam(name="uname",value="jim")
})
public class Demo01Servlet extends HttpServlet {
@Override
public void init() throws ServletException {
ServletConfig config = getServletConfig();
String initValue = config.getInitParameter("hello");
System.out.println("initValue = " + initValue);
}
}
4.3 ServletContext和<context-param>
1. 获取ServletContext,有很多方法
在初始化方法中: ServletContxt servletContext = getServletContext();
在服务方法中也可以通过request对象获取,也可以通过session获取:
request.getServletContext();
session.getServletContext();
2. 获取初始化值
servletContext.getInitParameter();
3. 在web.xml文件中配置ServletContext
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<servlet>
<servlet-name>Demo01Servlet</servlet-name>
<servlet-class>com.atguigu.servlet.Demo01Servlet</servlet-class>
<init-param>
<param-name>hello</param-name>
<param-value>world</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Demo01Servlet</servlet-name>
<url-pattern>/demo01</url-pattern>
</servlet-mapping>
public class Demo01Servlet extends HttpServlet {
@Override
public void init() throws ServletException {
ServletConfig config = getServletConfig();
String initValue = config.getInitParameter("hello");
System.out.println("initValue = " + initValue);
ServletContext servletContext = getServletContext();
String contextConfigLocation = servletContext.getInitParameter("contextConfigLocation");
System.out.println("contextConfigLocation = " + contextConfigLocation);
}
}
可以看到控制台输出了我们定义的<context-param>