javaweb BaseServlet 自动封装数据并调用service方法
这篇笔记是学习web开发时基于反射和泛型的产物,实际开发时不需要去造这种轮子,仅供参考
引入
之前写了一篇随笔,说明了javaweb中如何自动封装请求头中的数据到指定实体类中
javaweb 自动封装请求头中的数据到指定实体类中
在随笔中,前端请求的接口是确定的,就是添加数据,现在我们希望这个封装能跟贴近实际使用一点,或者说跟自动化一点
整个项目要以数据库的表为基准,有多少张表,就要有多少个实体类,在之前的开发中,我们都是单表开发,根据功能去拆分出多个servlet、service和dao。
如图就是典型的单表开发,根据功能书写多个servlet、service和dao。
但是现在有很多表,如果每个表都要根据功能去写servlet,整个项目就会很冗余,也很不直观。
所以我们不应该根据功能去写dao\service\servlet,而是应该根据表写这些东西。
一张表对应一个实体类,对应一个dao和daoImpl,对应一个service和serviceImpl,对应一个servlet。
在dao中写五个基本的方法(增删改查、查所有)
然后在这个servlet中的doPost/doGet下用switch列出所有功能(增删改查、查所有)
如果这样做,我们就要在servlet中对浏览器的请求进行判断,是执行增删改查,还是查所有
浏览器端表单提交请求类型:
服务器端servlet接收请求:
1 Servlet封装目标说明
封装目标:
-
前端各个模块发送的请求会指向后端不同的servlet,并且通过请求参数type的值说明需要执行的操作。
后端每个servlet上都通过使用switch-case调用对应的方法。
以上的操作存在许多重复代码,而且每次新增功能后,还得去维护switch-case,因此要降低代码重复性,使用反射实现switch-case要实现的功能。 -
每个servlet中都直接将请求头和响应头传给方法,在方法中完成请求头的解析,一方面造成代码重复,另一方面也与“servlet只允许给方法传递对象”的代码规范相违背
因为方法一般都放在service里,而servlet调用service的方法时,应该只传递对象,而不能传递请求头、响应头。
因此我们要降低重复性,消灭传递给方法的请求头和响应头,做到“只给方法传递对象” -
如果”只给方法传递对象“,那么就要求所有的方法在执行完毕后都要将结果返回给servlet,由servlet进行响应——这也会造成代码重复,这也要解决。
2 封装请求类型type 自动执行请求的操作 基础版 便于理解
之前写servle时,前端每个模块对应一个servlet,前端发送的请求被各模块指定的servlet的doGet和doPost接收,从中取出type,使用switch-case进行判断。
比如前端的模块一需要执行查询操作,那么就需要后端的servlet_1取出请求参数type=show,然后进入case=show的分支,去调用指定的方法。
然后前端的模块二需要执行添加操作,那么就需要后端的servlet_2取出请求参数type=add,然后进入case=add的分支,去调用指定的方法。
这样后期维护起来太麻烦,每加入一个新模块就要重写一次doGet、doPost,并且各个模块里还需要维护switch-case
例图:ajax中实现级联菜单,取出请求参数type,然后进入不同的分支
因此我们现在要转变整个项目的编写思路。
首先前端保持不变,依然是根据不同的模块指向后端不同的servlet,请求参数依然是根据不同的需求给出不同的type值
而后端则设立一个BaseServlet去实现doGet和doPost以及编码设置,然后基于反射和泛型取出请求参数type的值,令servlet下的对应方法自动执行。
所有模块的servlet通过继承BaseServlet,实现代码的降重,而servlet中根据模块需求写各自的方法。
核心方法——通过反射获取类中的同名方法
语法 | 说明 |
---|---|
Method 变量名 = Class对象 . getMethod ("方法名", 指向参数类型的Class对象) | 获取类中某个带参的方法,由于存在方法重载的可能,因此需要给定方法的参数表 |
2.1 BaseServlet代码
①代码写在父类上,这是为了代码降重,但实际上是在子类中被执行,此时this代指的是各种BaseServlet类的子类。
②下图代码中没有设置编码格式,是因为这部分代码被放在了过滤器Filter中
③作为基础类的BaseServlet继承HttpServlet,别的Servlet则继承该BaseServlet
④虽然目前没见过,但是有时候doGet和doPost需要执行不同的代码,到时候就在BaseServlet中修改一下doGet中的代码即可
2.2 子类代码
①子类直接继承BaseServlet,简化了doGet和doPost的操作
②子类中根据模块需求编写响应的方法,从父类继承的反射方法会自动解析前端发来的请求调用对应的方法
③子类中要对请求头进行数据解析,转换为对象,然后使用
3 封装请求类型type 自动执行请求的操作 正式版 实际使用
在上面的代码中,已经实现了自动根据type类型去执行同名的操作,并且写在了父类servlet上,令所有子类servlet都具备该功能。
可以说已经实现了整个项目的代码降重以及使用反射替代switch-case的目的
3.1 基础版存在的问题
但是实际上存在三个问题:
①如果找不到同名的type,会报错,因为上面的代码中并没有进行同名判断以及结果为空时的处理
②使用invoke调用方法时传入的参数顺序是固定的,现在这种写法要求子类中的方法参数表只能是 "请求头,响应头",但实际上可能会不同
③每个子servlet都需要对请求头进行解析,将其中存储的json对象转为实体对象,再传给各自的方法。对请求头进行解析的代码也应该进行降重,让每个子servlet只需要编写方法。
3.2 解决type找不到方法的问题
在执行完下图代码后,我们可以获得一个指向确定的方法的Method对象,如果没有取到,则返回null,不会由于空指针而报错,解决了问题①
注意:代码在子类中执行,获取到的是子类Servlet的方法,建议子类servlet中的方法都设置为公开方法,避免这里还得暴力破解
3.3 解决使用Invoke执行方法时参数表的问题
此时如果想使用invoke调用该Method对象,需要给出该对象需要的参数,即解决Method对象.invoke(this,?)中的?
3.3.1 基础版
通过自动识别方法的参数表需要的参数类型以及参数类型的顺序,就能解决问题②,即?
通过自动封装请求头中的数据,就能解决问题③,即代码降重,关于图中的第五步操作——自动解析与封装请求头数据的进一步说明 点击这里了解
跳转链接中能说明清楚图中第五步对应实际开发中的哪个场景需求,又是如何实现的
3.3.2 基础版存在的问题
现状分析
上图解决了问题②和③,但是里面存在一个问题:
对方法参数表中参数类型的判断只局限于请求头、响应头、指定路径下的实体类,如果方法的参数表里存在字符串或者需求的是其他路径下的实体类就会出问题
具体说明
其他路径:第四步判断是否为实体类时借助了常量 "com.javasm.entity",如果我们传递的实体类对象是vo类或者包名叫bean而不是entity,那就会出问题
存在字符串或者其他数据类型:方法需求直接传入一个或多个字符串数据或者其他数据类型,由于这种情况没有写在循环判断中,就会导致整个object数组元素与参数表对不上
解决办法
针对存在字符串或者其他数据类型:目前没办法把这部分完美解决,需要spring的四个包进行辅助,这些包的底层机制目前也还不清楚
针对路径的情况,需要JAVA动态识别方法中参数表需要的实体对象的全类名,获取的具体实现原理 点击这里了解
3.3.3 正式版
下面给出正式版相比基础版的改动,其他没给出的与基础版一致,这些改动解决了实体类的路径问题。
父类servlet
①设置父类为泛型类
②声明一个Class对象 用于储存当前servlet类声明的泛型类型
具体到应该声明一个对象还是一个数组得看父类声明泛型时设置了几个泛型,通常设置为1个,最多设置2个。
如果只设置了一个泛型,那么此时子类的泛型个数就是一个,Class对象设置为一个对象即可
如果设置了多个泛型,那么此时子类的泛型个数就是多个,此时Class对象就要设置为数组,并且下方对Class数组的使用就要逐个取出分别判断
③创建一个构造函数,在里面编写获取servlet声明的泛型的代码
也可以创建一个非静态代码块,总之要求获取servlet声明的泛型的代码要在整个servlet类执行时就执行
④上图代码执行完毕后就能获得servlet在声明泛型时的具体类型,此时就能对Class对的全类名进行一个判定,如果相同,说明此时Class对指向的就是一个实体类而不是别的东西,此时就能实现条件都不满足时提示“你这方法需要的参数既不是请求头又不是响应头还不是实体类,我这个servlet封装搞不定这种复杂情况,方法停止”
子类Servlet
子类继承父类时要声明泛型的实际类型,可以声明多个,但一般最多声明2个,就比如继承Map集合时声明K和V的数据类型,总之要包含子类中各种方法使用时的所有数据类型
4 封装响应代码 实现代码降重
上面的代码实现了自动读取请求参数type,然后执行对应的方法,并且在方法的参数表需要传入实体对象时,自动创建一个实体对象给到方法。
现在需要做的就是当servlet调用的方法执行完毕时,对前端的响应。
之前对前端的响应都是直接在各个子servlet下进行,各个子Servlet的响应方法都一样,这无疑是重复代码,因此可以放在父类servlet下统一编写。
子类Servlet
首先在子类Servlet的方法中,执行完毕后根据需求可以分别请求转发、重定向、异步请求,根据数据类型跳转可以分为页面、servlet、json数据,根据方法需求返回
返回时根据"响应类型:响应的数据"手动拼接
父类Servlet
在父类Servlet编写的是所有servlet都能拿到的代码,代码的目的简单来说就是判断。
使用method对象.invoke之后获得的是一个Object对象
此时执行响应结果时,需要先把传入的object强制转换为字符串,再进行后面的判断
5 封装响应代码 封装方式 - 注解
上面给出了响应代码的封装,现在更进一步,基于注解去封装响应代码
5.1 之前的版本存在的问题
之前的版本在使用时要求方法返回时要在原数据前加上响应类型,约定请求转发加 f 、重定向加 r 、AJAX加 a
如果编写方法时不按照这个要求来做,就会产生问题,而实际开发中,这种对于返回类型的要求很不友好,很容易被忽略
5.2 解决办法
①创建注解类。
- 设置注解要加在serlvet类中的方法上
- 设置注解生成器为全周期
- 根据业务情况设置默认响应方式,如果前端基于VUE框架,那么基本都是AJAX
②注解所能选择的值则放在枚举类中,这样数据的安全性就更高了。
- 设置枚举的值以区分响应的方式(转发、重定向、out)
- 注意枚举的值全部都要大写
通过使用注解,能将响应方法的添加转移到注解上,这样方法的响应方式与方法的返回值实现了解耦
但是现在的问题是编写方法时忘记加上注解了,就会导致无法响应。
这种情况目前没办法解决,因为当前的封装手段只能尽量保证数据的完整性,不能检测是否添加了注解,或者添加的是否是需要添加的注解
5.3 子类Servlet上的变化
原来:
现在: