水果库存管理系统-进阶版2- disPatcherServlet版
做下面的操作之前,先设置一下这个,可以让接下来的反射可以获取类中方法的参数。
如果说,当前的项目中不止有一个FruitServlet,而是还有UserServlet,ProductServlet等等不同的Servlet,且这些Servlet中的代码几乎都是相同的,我们就没必要写那么多个Servlet,我们可以只写一个Dispatcher Servlet,然后再根据请求判断该请求要发给原本的哪个Servlet(现在叫做Controller,哪个Controller)。
我们现在需要做的是什么呢?就是比如说//假设我们的url是: http://localhost:8080/pro15/Fruit.do,(或者说我们请求的Fruit.do)那么我们就能找到FruitController,如果我们请求(User.do),那么我们就能找到UserController。
而且我们可以用DispatcherServlet的统一处理,这样可以更好的配合反射(虽然我还没没学框架,但是已经能隐约感受出这大概是框架的灵魂)。
讲后面的案例就会大概有个思路。
首先是DispatcherServlet的service方法,也就是当DispatcherServlet中央过滤器收到请求时,它要做的第一件事是根据不同请求,让它对应到不同的Servlet。
思路是:
// 第1步: /hello.do -> hello 或者 /fruit.do -> fruit
String servletPath = request.getServletPath();
servletPath = servletPath.substring(1);
int lastDotIndex = servletPath.lastIndexOf(".do") ;
servletPath = servletPath.substring(0,lastDotIndex);
// 第一步:hello.do -> hello 或者 /fruit.do -> fruit
这一步的实际作用就是将hello.do截取到只剩下hello
然后再下一步再根据hello去寻找与hello对应的Servlet,这些在那对应呢?
这就要讲到xml文件了:
xml文件
首先,我们讲一下xml文件。
1.概念
HTML : 超文本标记语言
XML : 可扩展的标记语言
HTML是XML的一个子集
2.XML包含三个部分:
1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
2) DTD 文档类型定义
3) XML正文
<beans>
<!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
<bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
</beans>
xml文件是配置文件,里面是标签语言。
你可以写不同的标签,然后我们就可以在代码中取到这些标签中的内容,就比如上面的bean,就是我们直接定义的标签,而里面的id,class这些也是我们可以定义的。我们要先取到bean,再去找到id和class的对应,都是可以的。
比如我们上面写了一个id=“fruit” class=“com.atguigu.fruit.controllers.FruitController”/意思就是将它们对应在一起。待会讲下面的例子就懂了。
所以我们的第二部就是得通过 hello -> HelloController 或者 fruit -> FruitController。
不过在这之前,我们得先加载一下xml文件:
我们写在DispatcherServlet的init函数中:
public void init() throws javax.servlet.ServletException {
super.init();
/*
这个初始化就是解析xml文件,将其中对应的名称和类传进map中
*/
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节点
//可以理解这个document可以解析xml文件内的所有标签,所以只需要给出标签名!
//就可以获取以该标签名的所有的行,获取后返回一个NodeList
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 ;
// 取出每一个bean标签行后面的值,因为我们在里面写的是id=?class=?
//所以下面就根据id,class来获取。
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
//获取到对应的类之后,就获取该类的实例,然后将名称和实例名传进map
Class controllerBeanClass = Class.forName(className);
//创建该类的实例
Object beanObj = controllerBeanClass.newInstance() ;
//将名称和实例传进map
beanMap.put(beanId , beanObj) ;
//比如说xml文件中写的是:
<bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
那么取出来后就是map中创建一个映射
fruit 对应 FruitController的实例
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
就是我们要取得hello到helloServlet的对应还是需要做一系列操作的,这些操作就可以写在初始化函数init中,这些操作简而言之,就是新建了一个map,然后存入了hello和helloServlet的映射。
所以在这之后,service方法中就可以通过刚刚截取到的hello然后在init初始化函数时创建的map中取到相应对应的实例
//创建具体对应的实例对象
Object controllerBeanObj = beanMap.get(servletPath);
//进行不同请求时都会传一个方法名过来
那么获取到一个实例后,接下来要做什么呢?
接下来要做的事就重要了。
Dispatcher真正重要的事才刚刚开始,它真正的目的只有一个,就是反射!
其实我们是需要Dispatcher帮我们过滤吗?并不是,我们原本就可以在不同的Servlet中写注射,它们自身就可以被区分开来。我们真正要实现的事,任何的请求都到我DispatcherServlet中来,然后需要 FruitController干什么事,或者需要UserController干什么事,DispatcherServlet再调用它们的方法就行了。
那么如果有很多个类似FruitController的类,且其中的方法都不尽相同,难道我们要一个个实例化,然后一个个创建其中的函数吗?这样显然比原本还耦合复杂,如何将其变得更加简单,那么就用反射机制。
这就是反射的灵魂,而反射就是框架的灵魂。理解了这些,也就渐渐能懂得了框架。
我们要做的事,就是让Controller尽可能的简单,最好让其变为一个普通的类,有关于任何Http,Servlet的代码实现都交由DisPatcherServlet去完成,有没有可能?有,利用好反射机制,都有可能。
好,所以我们接下来的目标就是将将FruitController改为不含http的类。
首先,我们看原本的FruitController。
第一部分:
首先使原本的service方法
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" ;
}
//可以获取到该类的方法的数组
Method[] methods=this.getClass().getDeclaredMethods();
for(Method m:methods){
String methodName=m.getName();
if(operate.equals(methodName)){
try {
m.invoke(this,request,response);
return;
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
throw new RuntimeException("operate值非法")
}
这部分是可以很简单地可以搬到disPatcherServlet中去的,因为
- String operate = request.getParameter(“operate”);这里的参数是从request中获取的,我们也可以在disPatcherServlet直接获取就好。
- Method[] methods=this.getClass().getDeclaredMethods();这个methods是通过该FruitServlet的反射机制找到该方法数组的,而我们刚刚在
disPatcherServlet可以通过xml文件中获得FruitServlet的实例,所以也可以获得FruitServlet的方法数组,且有实例指针可以调用里面的方法。所以FruitServlet的service方法甚至可以不要了。
其二,看里面的函数,比如说edit,可否改成与http无关的。
private void edit(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);
}
}
仔细观察一下,与http有关的由两部分,一是通过request来获取各种参数,这里我们想,能否从disPatcherServlet获取好参数,然后再传进来,可以的。
第二是 super.processTemplate(“edit”,request,response);这里与request和resonpse都有关,但是disPatcherServlet都有request和resonpse,能否让其在disPatcherServlet中处理?也是可以的。只需要传个参数过去。
所以我们可以将其改为这样:
private String edit(Integer fid , HttpServletRequest request){
if(fid!=null){
Fruit fruit = fruitDAO.getFruitByFid(fid);
request.setAttribute("fruit",fruit);
//super.processTemplate("edit",request,response);
return "edit";
}
return "error" ;
}
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 ));
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";
}
而看原本的update,它原本最后一句是 response.sendRedirect(“fruit.do”);
我们要区分一下原本 super.processTemplate(“edit”,request,response)和response.sendRedirect(“fruit.do”),所以传不同的字符串过去让disPatcherServlet处理。
但是Index简化到最后还是得有一个request参数,因为其在代码逻辑中需要session域保存值,所以无法再删减
private String index(String oper , String keyword , Integer pageNo , HttpServletRequest request ) {
HttpSession session = request.getSession() ;
if(pageNo==null){
pageNo = 1;
}
if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
pageNo = 1 ;
if(StringUtil.isEmpty(keyword)){
keyword = "" ;
}
session.setAttribute("keyword",keyword);
}else{
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 ;
session.setAttribute("pageCount",pageCount);
return "index" ;
}
然后现在主要看disPatcherServlet那边如何实现了。
一点一点讲吧。
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)){
operate = "index" ;
}
首先,和之前一样,获取传过来的方法名,判断是要实行什么方法。
然后刚刚上面不是通过
Object controllerBeanObj = beanMap.get(servletPath);
映射获得了要请求的control的实例。
所以就获得该实例的类的所有方法
Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
(慢慢感受到xml文件搭配反射的强大之处了吧。不要写具体实现,简化了很多代码)
for(Method method : methods){
if(operate.equals(method.getName())){
//1.统一获取请求参数
//1-1.获取当前方法的参数,返回参数数组
Parameter[] parameters = method.getParameters();
//1-2.parameterValues 用来承载参数的值
Object[] parameterValues = new Object[parameters.length];
遍历这些方法,如果有一个方法和我们请求的方法名相同的话,
就获取该方法的参数名(反射有这个机制),然后返回一个数组。
private String add(String fname , Integer price , Integer fcount , String remark ) {
Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;
fruitDAO.addFruit(fruit);
return "redirect:fruit.do";
}
就比如刚刚的add函数,如上图,那么返回的数组的组成就是 fname,price,fcount,remark(参数名数组)
Object[] parameterValues = new Object[parameters.length];
然后创建一个参数值数组,来存放参数数组中各参数的值。
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String parameterName = parameter.getName() ;
//如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
if("request".equals(parameterName)){
parameterValues[i] = request ;
}else if("response".equals(parameterName)){
parameterValues[i] = response ;
}else if("session".equals(parameterName)){
parameterValues[i] = request.getSession() ;
从参数名数组中一个个获得参数的名字,比如刚刚的add函数例子,就依次获得 fname,price,fcount,remark
因为刚说了比如index方法,有些参数可能是request,response,session,所以如果是request,response,session,那么在存放参数值的数组内就直接写入它们。
而如果不是的话,比如刚刚的add例子,参数:fname,price,fcount,remark
}else{
//从请求中获取参数值
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 ;
}
那么比如说取到fname,就从request请求中去获取fname的值,然后存到参数值数组当中去
同时还要注意,从request获取值,获取的值都是String类型,所以如果判断到当前的参数的类型是int类型或其它类型的话
比如是int类型,那么得将该String转为Integer,然后再传入参数值数组中。
那么我们要这个参数值数组有何用呢?
因为在反射机制中,我们要调用一个实例的方法,需要传实例和传参数进去。所以下面:
//2.controller组件中的方法调用
method.setAccessible(true);
Object returnObj = method.invoke(controllerBeanObj,parameterValues);
让它执行方法,而且我们刚刚fruitController不是还有一些跳转页面的操作需要在外面处理吗?
下面就是了:
//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"
}
}
}
所以总的disPatcher的文件就是这样了:
@WebServlet("*.do") //只要是什么.do都找到这里来
public class DispatcherServlet extends ViewBaseServlet{
private Map<String,Object> beanMap = new HashMap<>();
public DispatcherServlet(){
}
public void init() throws javax.servlet.ServletException {
super.init();
/*
这个初始化就是解析xml文件,将其中对应的名称和类传进map中
*/
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节点
//可以理解这个document可以解析xml文件内的所有标签,所以只需要给出标签名!
//获取一系列标签名为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 ;
// 取出每一行id后面的值和class后面的值
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
//返回对应的类
Class controllerBeanClass = Class.forName(className);
//创建该类的实例
Object beanObj = controllerBeanClass.newInstance() ;
//将名称和实例传进map
beanMap.put(beanId , beanObj) ;
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置编码
request.setCharacterEncoding("UTF-8");
//假设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") ;
servletPath = servletPath.substring(0,lastDotIndex);
//创建具体对应的实例对象
Object controllerBeanObj = beanMap.get(servletPath);
//进行不同请求时都会传一个方法名过来
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)){
operate = "index" ;
}
try {
//获取一个类中的所有方法
Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
for(Method method : methods){
if(operate.equals(method.getName())){
//1.统一获取请求参数
//1-1.获取当前方法的参数,返回参数数组
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];
String parameterName = parameter.getName() ;
//如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
if("request".equals(parameterName)){
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);
String typeName = parameter.getType().getName();
Object parameterObj = parameterValue ;
if(parameterObj!=null) {
if ("java.lang.Integer".equals(typeName)) {
parameterObj = Integer.parseInt(parameterValue);
}
}
parameterValues[i] = parameterObj ;
}
}
//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值非法!");
}
*/
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
// 常见错误: IllegalArgumentException: argument type mismatch
@WebServlet(“*.do”) //注意最上面要给其一个注解,只要是什么.do都找到这里来。
然后FruitController的代码:
package com.atguigu.fruit.controllers;
import com.atguigu.fruit.dao.FruitDAO;
import com.atguigu.fruit.dao.impl.FruitDAOImpl;
import com.atguigu.fruit.pojo.Fruit;
import com.atguigu.myssm.util.StringUtil;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;
public class FruitController {
private FruitDAO fruitDAO = new FruitDAOImpl();
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";
}
private String edit(Integer fid , HttpServletRequest request){
if(fid!=null){
Fruit fruit = fruitDAO.getFruitByFid(fid);
request.setAttribute("fruit",fruit);
//super.processTemplate("edit",request,response);
return "edit";
}
return "error" ;
}
private String del(Integer fid ){
if(fid!=null){
fruitDAO.delFruit(fid);
return "redirect:fruit.do";
}
return "error";
}
private String add(String fname , Integer price , Integer fcount , String remark ) {
Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;
fruitDAO.addFruit(fruit);
return "redirect:fruit.do";
}
private String index(String oper , String keyword , Integer pageNo , HttpServletRequest request ) {
HttpSession session = request.getSession() ;
if(pageNo==null){
pageNo = 1;
}
if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
pageNo = 1 ;
if(StringUtil.isEmpty(keyword)){
keyword = "" ;
}
session.setAttribute("keyword",keyword);
}else{
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 ;
session.setAttribute("pageCount",pageCount);
return "index" ;
}
}
总结一下:
-
首先,在xml文件中写下不同的名称和controller的对应
-
无论什么请求,都找到disPatcherServlet(disPatcher的初始化函数中将xml文件中的名称和对应的实例写在map中)
-
当请求到来时,dispatcher先是截取其来源,找到其想要请求的Controller。然后找到该controller的实例。
-
通过该实例获得该类的方法数组,然后从请求中获取请求的方法,将方法与方法数组一一对应,知道找到要执行的方法。
-
通过反射查询该方法的参数名,然后通过这些参数名到请求中获取参数,保存下来,然后再通过反射,将实例和这些参数的值一起传进去执行。
-
所以就是实现了,先找到对应的controller,再找到对应的方法然后执行的过程。