目录
1. 准备和回顾
2. MVC-reflect
3. MVC-dispatcherServlet
3.1 思路部分
3.2 Debug部分
3.3 基于controller进行优化
4. Servlet-api
4.1 回顾
4.2 Init方法
1. 获取ServletConfig config = getServletConfig();
2. 获取初始化参数值:config.getInitParameter(key);
3. 在web.xml文件中配置Servlet
4. 也可以通过注解的方式进行配置
4.3 ServletContext和<context-param>
1. 获取ServletContext的很多方法
2. 获取初始化值
3. 在web.xml文件中配置ServletContext
5 业务层
5.1 Model1和Model2
5.2 区分业务对象和数据访问对象
6. IOC
6.1 耦合/依赖
6.2 IOC - 控制反转 / DI - 依赖注入
1. 控制反转(Inversion of Control)
2. 依赖注入(Dependency Injection)
7 过滤器Filter (Filter也属于Servlet规范)
7.1 Filter开发步骤
7.2 创建如下模块,我们实现service方法
7.3 过滤器链
8 事务管理
8.1 涉及到的组件
8.2 ThreadLocal
8.3 实际操作
9 监听器 Listener
10 ServletContextListener的应用 - ContextLoaderListener
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>
5 业务层
5.1 Model1和Model2
MVC : Model(模型)、View(视图)、Controller(控制器)
视图层:用于做数据展示以及和用户交互的一个界面
控制层:能够接受客户端的请求,具体的业务功能还是需要借助于模型组件来完成
模型层:模型分为很多种:有比较简单的pojo/vo(value object),有业务模型组件,有数据访问层组件
1) pojo/vo(Plain Ordinary Java Object) : 值对象 简单的Java对象,实际就是普通JavaBeans,是为了避免和EJB混淆所创造的简称。使用POJO名称是为了避免和EJB混淆起来, 而且简称比较直接. 其中有一些属性及其getter setter方法的类,没有业务逻辑,有时可以作为VO(value -obj)
2) DAO(Data Access Object): 数据访问对象
3) BO (Business Object): 业务对象
4) DTO (Data Transfer Object):数据传输对象
5.2 区分业务对象和数据访问对象
1 DAO中的方法都是单精度方法或者称之为细粒度方法。什么叫单精度?一个方法只考虑一个操作,比如添加,那就是insert操作、查询那就是select操作....
2 BO中的方法属于业务方法,也实际的业务是比较复杂的,因此业务方法的粒度是比较粗的
注册这个功能属于业务功能,也就是说注册这个方法属于业务方法。
那么这个业务方法中包含了多个DAO方法。也就是说注册这个业务功能需要通过多个DAO方法的组合调用,从而完成注册功能的开发。
注册:
1. 检查用户名是否已经被注册 - DAO中的select操作
2. 向用户表新增一条新用户记录 - DAO中的insert操作
3. 向用户积分表新增一条记录(新用户默认初始化积分100分) - DAO中的insert操作
4. 向系统消息表新增一条记录(某某某新用户注册了,需要根据通讯录信息向他的联系人推送消息) - DAO中的insert操作
5. 向系统日志表新增一条记录(某用户在某IP在某年某月某日某时某分某秒某毫秒注册) - DAO中的insert操作
6. ....
3 在库存系统中添加业务层组件
业务层接口
public interface FruitService {
//获取指定页面的库存列表信息
List<Fruit> getFruitList(String keyword, Integer pageNum);
//添加库存记录信息
void addFruit(Fruit fruit);
//根据id查看指定库存记录
Fruit getFruitByFid(Integer fid);
//删除特定库存记录
void delFruitByFid(Integer fid);
//获取总页数
Integer getPageCount(String keyword);
}
业务层实现(本质是调用Dao层不同的简单的查询,组合成复杂的业务)
public class FruitServiceImpl implements FruitService {
private FruitDao fruitDao = new FruitDao();
@Override
public List<Fruit> getFruitList(String keyword, Integer pageNum) {
return fruitDao.getFruitList(keyword, pageNum);
}
@Override
public void addFruit(Fruit fruit) {
fruitDao.addFruit(fruit);
}
@Override
public Fruit getFruitByFid(Integer fid) {
return fruitDao.getFruitByFid(fid);
}
@Override
public void delFruitByFid(Integer fid) {
fruitDao.delFruitByFid(fid);
}
@Override
public Integer getPageCount(String keyword) {
int fruitCount = fruitDao.getFruitCount(keyword);
return (fruitCount + 5 - 1) / 5;
}
}
这样我们的FruitController对Dao层的操作,就应该是更改为对FruitService层的操作,而FruitService层部分,调用FruitDao层的方法
我们把FruitContrller中原本的private的FruitDao改为现在的业务层实现类,然后方法调用使用刚刚的业务层实现类来进行
public class FruitController {
private FruitService fruitService = new FruitServiceImpl();
private String index(String oper, String keyword, Integer pageNum, HttpServletRequest req) {
HttpSession session = req.getSession();
if (pageNum == null) {
pageNum = 1;
}
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作用域
List<Fruit> fruitList = fruitService.getFruitList(keyword, pageNum);
session.setAttribute("fruitList", fruitList);
//保存或覆盖总页码到session作用域
int pageCount = fruitService.getPageCount(keyword);
session.setAttribute("pageCount", pageCount);
return "index";
}
private String add(String fname, Integer fcount, Integer price, String remark) {
Fruit fruit = new Fruit(0, fname, price, fcount, remark);
fruitService.addFruit(fruit);
return "redirect:fruit.do";
}
private String del(Integer fid) {
if (fid != null) {
fruitService.delFruitByFid(fid);
return "redirect:fruit.do";
}
return "error";
}
private String edit(Integer fid, HttpServletRequest req) {
if (fid != null) {
Fruit fruit = null;
fruit = fruitService.getFruitByFid(fid);
req.setAttribute("fruit", fruit);
return "edit";
}
return "error";
}
private String update(Integer fid, String fname, Integer price, Integer fcount, String remark) {
// 更新数据库
fruitService.updateFruit(new Fruit(fid, fname, price, fcount, remark));
return "redirect:fruit.do";
}
}
此时运行方完成了我们的业务层的雏形
6. IOC
6.1 耦合/依赖
依赖指的是某某某离不开某某某
在软件系统中,层与层之间是存在依赖的。我们也称之为耦合。
我们系统架构或者是设计的一个原则是: 高内聚低耦合。
层内部的组成应该是高度聚合的,而层与层之间的关系应该是低耦合的,最理想的情况0耦合(就是没有耦合)
拿我们刚刚的水果系统为例,我们的FruitContrller依赖于FruitService,如果删掉它,整个系统会崩溃,而我们的FruitService同样也依赖于FruitDao
我们这里先把它改为null,保证删除不会报错,再去思考如何实现低耦合,乃至0耦合。
先打开applicationContext.xml我们加入fruitDao和fruitService
<?xml version="1.0" encoding="utf-8" ?>
<beans>
<!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
<bean id="fruit" class="com.fanxy.fruit.controllers.FruitController"/>
<bean id="fruitDao" class="com.fanxy.fruit.dao.FruitDao"/>
<bean id="fruitService" class="com.fanxy.fruit.service.impl.FruitServiceImpl"/>
</beans>
我们希望我们容器启动的时候,就把这三个类的实例加载
首先创建一个io包,新建一个接口名叫BeanFactory,提供一个方法,根据Bean的id获取到这个类
public interface BeanFactory {
Object getBean(String id);
}
然后写它的实现类ClassPathXmlApplicationContext
显然,这个和我们之前写过的在DispatcherServlet的过程一模一样,我们删除之前写过的DispatcherServlet中的哈希表,然后把之前给它写的init()方法中的DOM操作的部分剪切,直接把这部分放入ClassPathXmlApplicationContext的空参构造方法
此时我们获取到的类就不算是controllerBeanClass了,FruitController,FruitService,FruitDao,都属于Model范畴,也都算javabean,我们变量命名这里就叫BeanClass了
public class ClassPathXmlApplicationContext implements BeanFactory {
private Map<String, Object> beanMap = new HashMap<>();
public ClassPathXmlApplicationContext() {
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 beanClass = Class.forName(className);
Object beanObj = beanClass.newInstance();
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
public Object getBean(String id) {
return beanMap.get(id);
}
}
但是现在DispacherServlet中通过ServletPath获取对应的BeanObject这里就没这个哈希表了,我们只需要增加一个私有变量BeanFactory,然后在它初始化的时候new 一个ClassPathXmlContext通过getBean方法获取即可
@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet {
private BeanFactory beanFactory;
public DispatcherServlet(){
}
public void init() throws ServletException {
super.init();
beanFactory = new ClassPathXmlApplicationContext();
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
.............
}
service层的FruitDao的耦合也先去掉
但但他们赋值为null了,我们要怎么给它赋值?
我们其实可以在application.xml文件不止写bean的id和class,还可以写入他们的依赖关系
property标签用来表示属性:name表示类内部的属性名称; ref表示引用其他bean的id值
<?xml version="1.0" encoding="utf-8" ?>
<beans>
<!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
<bean id="fruitDao" class="com.fanxy.fruit.myssm.dao.FruitDao"/>
<bean id="fruitService" class="com.fanxy.fruit.myssm.service.impl.FruitServiceImpl">
<!-- property标签用来表示属性:name表示类内部的属性名称; ref表示引用其他bean的id值 -->
<property name="fruitDao" ref="fruitDao"/>
</bean>
<bean id="fruit" class="com.fanxy.fruit.myssm.controllers.FruitController">
<property name="fruitService" ref="fruitService"/>
</bean>
</beans>
然后我们就需要DOM操作中添加依赖关系了
首先先借助前面的for循环和一些公共部分的代码
property事实上是bean的Element的一个节点,事实上它有三个节点,property前后的空白部分也算一个节点,甚至写入其中的注释也算一个节点,写入注释当然也会再多出空白节点
1. 根据Xml文件进行书写,我们遍历BeanNodeList,遍历每个BeanNode,同时要用一个变量存储每个BeanNode的id(后面反射需要用到它,给它的私有属性赋值)
2. 然后通过if判断找寻每个BeanNode的名叫property的子节点,并把它强转为Element类型(这样就可以通过getAttribute函数根据key值获取到对应的字符串字段value),通过这个函数找寻到它的propertyname和propertyref属性对应的字符串值
3. 根据propertyref对应的字符串值,我们可以从已经存储的beanMap中获取这它依赖的beanNode对应的对象,而根据propertyname值是当前beanNode的内部私有属性的名字,我们这个时候就可以用到第1步提取到的BeanNode的Id,从BeanMap里面拿到对应的对象beanObj
4. 然后通过反射获取它的Class --> beanClazz,接着通过beanClazz获取到它名称为propertyname的DeclaredField,然后propertyField.setAccessible(true),设置能够访问它的私有属性,最后通过propertyField.set(beanObj, refObj)将refObj设置到当前beanNode对应的实例的名为propertyName的属性上
public class ClassPathXmlApplicationContext implements BeanFactory {
private Map<String, Object> beanMap = new HashMap<>();
public ClassPathXmlApplicationContext() {
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 beanClass = Class.forName(className);
//创建bean实例
Object beanObj = beanClass.newInstance();
//把bean实例对象放入beanMap
beanMap.put(beanId, beanObj);
//到目前为止,此处需要注意的是,bean和bean的依赖关系没有设置
}
}
//5. 组装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");
NodeList beanChildNodeList = beanElement.getChildNodes();
for (int j = 0; j < beanChildNodeList.getLength(); j++) {
Node beanChildNode = beanChildNodeList.item(j);
if(beanChildNode.getNodeType() == Node.ELEMENT_NODE && "property".equals(beanChildNode.getNodeName())){
Element propertyElement = (Element) beanChildNode;
String propertyName = propertyElement.getAttribute("name");
String propertyRef = propertyElement.getAttribute("ref");
// 1. 找到propertyRef对应的实例
Object refObj = beanMap.get(propertyRef);
// 2. 将refObj设置到当前bean对应的实例的property属性上去
Object beanObj = beanMap.get(beanId);
Class beanClazz = beanObj.getClass();
Field propertyField = beanClazz.getDeclaredField(propertyName);
propertyField.setAccessible(true);
propertyField.set(beanObj, refObj);
}
}
}
}
} 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();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
@Override
public Object getBean(String id) {
return beanMap.get(id);
}
}
这样,虽然我们每个Model中对应其他model的私有实例虽然初始为null,但是通过我们的 ClassPathXmlApplicationContext实现通过反射给它赋值,此时运行发现大功告成,我们完成了通过反射注入依赖
6.2 IOC - 控制反转 / DI - 依赖注入
1. 控制反转(Inversion of Control)
1) 之前在Servlet中,我们创建service对象 , FruitService fruitService = new FruitServiceImpl();
这句话如果出现在servlet中的某个方法内部,那么这个fruitService的作用域(生命周期)应该就是这个方法级别;
如果这句话出现在servlet的类中,也就是说fruitService是一个成员变量,那么这个fruitService的作用域(生命周期)应该就是这个servlet实例级别
2) 之后我们在applicationContext.xml中定义了这个fruitService。然后通过解析XML,产生fruitService实例,存放在beanMap中,这个beanMap在一个BeanFactory中
因此,我们转移(改变)了之前的service实例、dao实例等等他们的生命周期。控制权从程序员转移到BeanFactory。这个现象我们称之为控制反转
2. 依赖注入(Dependency Injection)
1) 之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();
那么,控制层和service层存在耦合。
2) 之后,我们将代码修改成FruitService fruitService = null ;
然后,在配置文件中配置:
<bean id="fruit" class="FruitController">
<property name="fruitService" ref="fruitService"/>
</bean>
7 过滤器Filter (Filter也属于Servlet规范)
过滤器笔记https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/note/note008-filter.html
7.1 Filter开发步骤
新建类实现Filter接口,然后实现其中的三个方法:init、doFilter、destroy
配置Filter,可以用注解@WebFilter,也可以使用xml文件 <filter> <filter-mapping>
创建如下模块,我们实现service方法
@WebServlet("/demo01.do")
public class Demo01Servlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("demo01 service....");
req.getRequestDispatcher("succ.html").forward(req, resp);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>succ</h1>
</body>
</html>
然后创建Filter类Demo01Filter在filters下,实现javax.servlet下的Filter接口,会要求必须重写三个类似Java生命周期的方法
我们让doFilter这里写入filterChain.doFilter(servletRequest, servletResponse)实现了放行,会优先客户端受到响应,放行前后都会实现输出语句方便我们观察
public class Dome01Filter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("helloA");
//放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("helloA2");
}
@Override
public void destroy() {
}
}
通过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">
<filter>
<filter-name>Demo01Filter</filter-name>
<filter-class>com.fanxy.filters.Dome01Filter</filter-class>
</filter>
<filter-mapping>
<filter-name>Demo01Filter</filter-name>
<url-pattern>/demo01.do</url-pattern>
</filter-mapping>
</web-app>
这里还是注释掉用注解的方式配置更方便
@WebFilter("/demo01.do")
public class Dome01Filter implements Filter {....}
也可以使用*的通配符拦截所有以.do结尾的请求
@WebFilter("*.do")
public class Dome01Filter implements Filter {....}
运行报两次是Idea的默认会测试连接
7.3 过滤器链
1)执行的顺序依次是: A B C demo03 C2 B2 A2
2)如果采取的是注解的方式进行配置,那么过滤器链的拦截顺序是按照全类名的先后顺序排序的
3)如果采取的是xml的方式进行配置,那么按照配置的先后顺序进行排序
这里我们可以利用过滤器完成一些应用,比如把我们使用IOC版本的fruit管理系统,使用过滤器设置编码
需要把ServletRequest强转为HttpServletRequest再设置编码
这么写死了可能不太好,我们也可以声明一个私有字符串型变量encoding设置为“UTF-8” ,然后在init方法里面读配置文件,如果没有设置默认值,就使用我们设置的utf-8
@WebFilter(urlPatterns = {"*.do"},
initParams = {@WebInitParam(name = "encoding", value = "GBK")})
public class CharacterEncodingFilter implements Filter {
private String encoding = "UTF-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String encodingStr = filterConfig.getInitParameter("encoding");
if (StringUtil.isNotEmpty(encodingStr)) {
encoding = encodingStr;
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
((HttpServletRequest)servletRequest).setCharacterEncoding(encoding);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
这样中央控制器就不需要单独设置编码了
8 事务管理
事务的回滚操作应该在Service层,不应该在Dao层的单精度方法为单位
在我们新版jdbc的学习中已经经历过这样的迭代过程
8.1 涉及到的组件
OpenSessionInViewFilter
TransactionManager
ThreadLocal
ConnUtil
BaseDAO
8.2 ThreadLocal
JDBC新版已经学过的线程本地变量
get() , set(obj)
ThreadLocal称之为本地线程 。 我们可以通过set方法在当前线程上存储数据、通过get方法在当前线程上获取数据
set方法源码分析:
public void set(T value) {
//获取当前的线程
Thread t = Thread.currentThread();
//每一个线程都维护各自的一个容器(ThreadLocalMap)
ThreadLocalMap map = getMap(t);
if (map != null)
/*这里的key对应的是ThreadLocal,因为我们的组件中需要传输
(共享)的对象可能会有多个(不止Connection) */
map.set(this, value);
else
//默认情况下map是没有初始化的,那么第一次往其中添加数据时,会去初始化
createMap(t, value);
}
get方法源码分析:
public T get() {
//获取当前的线程
Thread t = Thread.currentThread();
//获取和这个线程(企业)相关的ThreadLocalMap(也就是工作纽带的集合)
ThreadLocalMap map = getMap(t);
if (map != null) {
//this指的是ThreadLocal对象,通过它才能知道是哪一个工作纽带
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//entry.value就可以获取到工具箱了
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
8.3 实际操作
在com.fanxy.myssm.filters下新建一个OpenSessionInViewFilter继承Filter
@WebFilter("*.do")
public class OpenSessionInViewFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {}
@Override
public void destroy() {}
}
新建一个trans的包,新建TransactionManager类用于写事务的方法
从面向对象的角度去考虑,是传入Connection参数,保证是一个Connection,JDBC里面也说过,升级方式采用线程本地变量,这里其实就23版jdbc
23版JDBChttps://blog.csdn.net/weixin_44981126/article/details/130424791?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22130424791%22%2C%22source%22%3A%22weixin_44981126%22%7D
public class TransactionManager {
//开启事务
public static void beginTransaction() throws SQLException {
JdbcUtilsV2.getConnection().setAutoCommit(false);
}
//提交事务
public static void commit() throws SQLException {
JdbcUtilsV2.getConnection().commit();
JdbcUtilsV2.freeConnection();
}
//回滚事务
public static void rollback() throws SQLException {
JdbcUtilsV2.getConnection().rollback();
JdbcUtilsV2.freeConnection();
}
}
此时OpenSessionInViewFilter改写为如下,sout部分后期可以删除,这里为了调试
@WebFilter("*.do")
public class OpenSessionInViewFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
TransactionManager.beginTransaction();
System.out.println("starting transaction");
filterChain.doFilter(servletRequest, servletResponse);
TransactionManager.commit();
System.out.println("commit transaction");
} catch (SQLException e) {
e.printStackTrace();
try {
TransactionManager.rollback();
System.out.println("rollback transaction");
} catch (SQLException ex) {
e.printStackTrace();
}
}
}
@Override
public void destroy() {
}
}
我们这里正好可以好好体会一下到底是否完成了同一个Service是相同的Connection,这里把Service的实现类的获取水果列表和获取总页码的方法都加入输出当前线程的地址,看看是不是同一个,这两个方法在index的FruitController中的index方法同时都被使用,需要是同一个Connection
执行后,IDEA默认会连接两次,两次都是同一个地址是因为我们使用的Druid连接池,所以这里直接获取的两次的连接都是连接池里面获取的
这里也引申出一个问题,如果我们底层的Dao方法自己使用了try-catch,它就不会把异常往上抛,那么我们前面的事务管理部分就catch不到事务出错了,所以要把异常往上抛,我们继承自BaseDao的FruitDao也需要抛,而Service的实现类可以try-catch,但是我们一旦catch到可以向外面抛一个我们自定义的异常。自然我们应该先在fruit包下新建一个exceptions包下存放自定义的异常
public class FruitDaoException extends RuntimeException {
public FruitDaoException(String message) {
super(message);
}
}
public class FruitServiceImpl implements FruitService {
private FruitDao fruitDao = null;
@Override
public List<Fruit> getFruitList(String keyword, Integer pageNum) {
try {
return fruitDao.getFruitList(keyword, pageNum);
} catch (Exception e) {
throw new FruitDaoException("FruitDao层出问题了");
}
}
@Override
public void addFruit(Fruit fruit) {
try {
fruitDao.addFruit(fruit);
} catch (Exception e) {
throw new FruitDaoException("FruitDao层出问题了");
}
}
@Override
public Fruit getFruitByFid(Integer fid) {
try {
return fruitDao.getFruitByFid(fid);
} catch (Exception e) {
throw new FruitDaoException("FruitDao层出问题了");
}
}
@Override
public void delFruitByFid(Integer fid) {
try {
fruitDao.delFruitByFid(fid);
} catch (SQLException e) {
throw new FruitDaoException("FruitDao层出问题了");
}
}
@Override
public Integer getPageCount(String keyword) {
int fruitCount = 0;
try {
fruitCount = fruitDao.getFruitCount(keyword);
} catch (Exception e) {
throw new FruitDaoException("FruitDao层出问题了");
}
return (fruitCount + 5 - 1) / 5;
}
@Override
public void updateFruit(Fruit fruit) {
try {
fruitDao.updateFruit(fruit);
} catch (Exception e) {
throw new FruitDaoException("FruitDao层出问题了");
}
}
}
其实这里为了能看清FruitDao异常到底哪里出错,同时让业务层整洁,没有这么多try-catch,可以让新建的FruitDaoException在FruitDao的方法里面try-catch,然后catch到了后e.printstackTrace(),然后再抛出一个我们的自定义异常,抛出的时候把异常命名为对应方法出错了,然后上面这一坨代码的try-catch就没必要写了
public class FruitDao extends BaseDao implements FruitDAO {
@Override
public int getFruitCount(String keyword){
String sql = "SELECT COUNT(*) AS fruitCount FROM t_fruit WHERE fname LIKE ? OR remark LIKE ?;";
List<FruitData> counts = null;
try {
counts = super.executeQuery(FruitData.class, sql, "%" + keyword + "%", "%" + keyword + "%");
} catch (Exception e) {
e.printStackTrace();
throw new FruitDaoException("FruitDao.getFruitCount出现异常!");
}
return Integer.parseInt(counts.get(0).getFruitCount().toString());
}
@Override
public List<Fruit> getFruitList(String keyword, Integer pageNum){
String sql = "SELECT * FROM t_fruit WHERE fname LIKE ? OR remark LIKE ? LIMIT ? , 5;";
List<Fruit> fruits = null;
try {
fruits = executeQuery(Fruit.class, sql, "%" + keyword + "%", "%" + keyword + "%", (pageNum - 1) * 5);
} catch (Exception e) {
e.printStackTrace();
throw new FruitDaoException("FruitDao.getFruitList出现异常!");
}
return fruits;
}
.............................................其他方法类似.........................
}
Dao层的异常能够保证向上抛了,而Controller层也没有try-catch,最后我们的DispatcherServlet的try-catch也给它写一个自定义异常
public class DispatcherServletException extends RuntimeException {
public DispatcherServletException(String message) {
super(message);
}
}
@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet {
private BeanFactory beanFactory;
public DispatcherServlet() {
}
public void init() throws ServletException {
super.init();
beanFactory = new ClassPathXmlApplicationContext();
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
"代码过长省略"
try {
"代码过长省略"
} catch (Exception e) {
e.printStackTrace();
throw new DispatcherServletException("DispatcherServlet出错了");
}
}
}
至此完成了事务篇,这里建议顺便可以复习一下23的JDBC
9 监听器 Listener
监听器笔记https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/note/note009-listener.html
1 ServletContextListener - 监听ServletContext对象的创建和销毁的过程
2 HttpSessionListener - 监听HttpSession对象的创建和销毁的过程
3 ServletRequestListener - 监听ServletRequest对象的创建和销毁的过程
4 ServletContextAttributeListener - 监听ServletContext的保存作用域的改动(add,remove,replace)
5 HttpSessionAttributeListener - 监听HttpSession的保存作用域的改动(add,remove,replace)
6 ServletRequestAttributeListener - 监听ServletRequest的保存作用域的改动(add,remove,replace)
7 HttpSessionBindingListener - 监听某个对象在Session域中的创建与移除
8 HttpSessionActivationListener - 监听某个对象在Session域中的序列化和反序列化
10 ServletContextListener的应用 - ContextLoaderListener
我们创建一个ServletContextListener需要我们实现接口ServletContextListener,并且重写监听ServletContext初始化和销毁的两个方法
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
System.out.println("Servlet上下文对象初始化动作被我监听到了....");
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
System.out.println("Servlet上下文对象销毁动作被我监听到了....");
}
}
同样我们可以在注解或者是xml文件配置,但是监听器的xml配置很短,只需要写全类名,它不需要别人通过url访问,所以没有映射
<?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">
<listener>
<listener-class>com.fanxy.listeners.MyServletContextListener</listener-class>
</listener>
</web-app>
这里我们设置的Demo01Servlet会让我们访问succ.html
我们启动tomcat和关闭tomcat都会被我们的ServletContextListener监听到,因为ServletContext和tomcat容器生命周期相同
这里我们回顾之前的BeanFactory,它的一个对象被放在DispacherServlet中在它init()的时候被初始化,最好的做法其实是tomcat启动的时候就把Xml文件的BeanMap读取完
那我们现在就要把Dispatcher中的所有关于BeanFactory的操作,移动到Listener中
首先我们可以让容器被创建的时候,先把一个new的beanFactory放入上下文中
这样DispatcherServlet的beanFactory就不是现场new了,而是从application作用域获取
此前我们的xml文件是通过硬编码的方式读取,这是不太推荐的,我们可以通过传入一个字符串参数,然后通过这个参数读
然后如果使用空参构造方法,默认就使用applicationContext.xml路径
然后我们的Listener就可以改造为如下代码,从上下文初始化参数去读取path
然后在web.xml配置上下文参数即可,这里爆红是因为IDEA认为我们在配置Spring的配置文件,不需要管它
至此我们完成了整个水果后台系统的设计,图解如上。