问题分析

servlet规范与我们的业务无关,但现在Request、Response、视图渲染工具类、Redirect等等这些api“打扰”了程序员。

程序员的精力是很宝贵的,我们希望在开发项目时,程序员更专注于业务,而不要看到这些与业务无关的servlet规范api。

我们的目标就是,我们写的代码类就是普通的pojo类。这样的pojo类就是常用Web框架SpringMVC中创建的Controller类。

在使用SpringMVC前,我们在之前的代码基础上,实现一个简易版的SpringMVC框架,加深对框架底层原理和功能的理解。

1 利用switch语句-解决servlet类爆炸问题

现在一个方法就定义一个servlet类,类数量过多,不好维护。

01.mvc01

我们把Fruit相关的方法都放到一个servlet类中,该类中根据前端传的operate参数,调用对应的方法。

02.mvc02

2 引入反射-解决switch判断语句过长问题

通过反射来匹配前端operate参数和对应的处理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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" ;
}
//获取当前类中所有的方法
Method[] methods =this.getClass().getDeclaredMethods();
for(Method m:methods){
//获取方法名称
String methodName =m.getName();
if(operate.equals(methodName)){
try {
//找到和operate同名的方法,那么通过反射技术调用它
m.invoke( obj:this,request,response);
return;
}
catch(IllegalAccessExceptione){
e.printstackTrace();
}
catch(InvocationTargetExceptione){
e.printstackTrace();
}
}
}
throw new RuntimeException("operate值非法!");

}

3 引入中央处理器-解决多个业务的servlet需要重复编写反射相关代码的问题

03.MVC03

4 消除控制器类与视图渲染、重定向、servlet规范的耦合

之前的控制器方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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");
}

新的控制器方法【消除servlet规范api相关代码后】,一下子清爽了很多。

1
2
3
4
5
6
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";
}

servlet规范api相关代码都放在中央控制器处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@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();
}
}

5 review

  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…..