(尚硅谷)JavaWeb新版教程07-水果管理系统的MVC实现


1、MVC

1.1 MVC 的概念

M:Model 模型,模型分为很多种:有业务模型组件,有数据访问层组件等
V:View 视图,用于做数据展示以及和用户交互的一个界面
C:Controller 控制器,能够接受客户端的请求,具体的业务功能还是需要借助于模型组件来完成

MVC 是在表述层开发中运用的一种设计理念。主张把封装数据的『模型』显示用户界面的『视图』、**协调调度的『控制器』**分开。

好处:

  • 进一步实现各个组件之间的解耦
  • 让各个组件可以单独维护
  • 将视图分离出来以后,我们后端工程师和前端工程师的对接更方便

1.2 MVC 和三层架构之间的关系

在这里插入图片描述

1.3 Model 层

1、常见的 Model 层对象

  1. pojo/vo(Value Object): 值对象
  2. DAO(Data Access Object): 数据访问对象
  3. BO(Business Object) : 业务对象

2、区分业务对象和数据访问对象:

  1. DAO中的方法都是单精度方法或者称之为细粒度方法。什么叫单精度?一个方法只考虑一个操作,比如添加,那就是 insert 操作、查询那就是 select 操作…
  2. BO 中的方法属于业务方法,也实际的业务是比较复杂的,因此业务方法的粒度是比较粗的。
    (1)比如注册这个功能属于业务功能,也就是说注册这个方法属于业务方法。
    (2)那么这个业务方法中包含了多个 DAO 方法。也就是说注册这个业务功能需要通过多个 DAO 方法的组合调用,从而完成注册功能的开发。
  3. 注册功能的举例:
    1. 检查用户名是否已经被注册 - DAO 中的 select 操作
    2. 向用户表新增一条新用户记录 - DAO 中的 insert 操作
    3. 向用户积分表新增一条记录(新用户默认初始化积分100分) - DAO 中的 insert 操作
    4. 向系统消息表新增一条记录(某某某新用户注册了,需要根据通讯录信息向他的联系人推送消息) - DAO 中的 insert 操作
    5. 向系统日志表新增一条记录(某用户在某IP在某年某月某日某时某分某秒某毫秒注册) - DAO 中的 insert 操作
    6. …

2、IOC-控制反转/DI-依赖注入

2.1 耦合/依赖

  1. 依赖指的是某某某离不开某某某
  2. 在软件系统中,层与层之间是存在依赖的。我们也称之为耦合
  3. 我们系统架构或者是设计的一个原则是: 高内聚低耦合
  4. 层内部的组成应该是高度聚合的,而层与层之间的关系应该是低耦合的,最理想的情况0耦合(就是没有耦合)

2.2 控制反转

  1. 之前在 Servlet 中,我们创建 service 对象 , FruitService fruitService = new FruitServiceImpl();
  2. 这句话如果出现在 servlet 中的某个方法内部,那么这个 fruitService 的作用域(生命周期)应该就是这个方法级别;(这个方法结束,这个对象的生命周期就结束了)
  3. 如果这句话出现在 servlet 的类中,也就是说 fruitService 是一个成员变量,那么这个 fruitService 的作用域(生命周期)应该就是这个 servlet 实例级别
  4. 我们如果在 applicationContext.xml 中定义这个 fruitService,然后通过解析 XML,产生 fruitService 实例,存放在 beanMap 中,这个 beanMap 在一个 BeanFactory 中
  5. 因此,我们转移(改变)了之前的 service 实例、dao 实例等等他们的生命周期,控制权从程序员转移到 BeanFactory(IOC容器),这个现象我们称之为控制反转
  6. 总结: 之前各层之间耦合的时候,各个层之间某服务对象的生命周期很短暂,频繁的创建销毁,需要回收很多重复的垃圾,现在我们将每一层进行解耦,将这些实例放置到 IOC 容器中,想用某个组件就从 IOC 容器中拿,用完了再放回去,这样各个实例的生命周期就和这个 IOC 容器的生命周期一致了。

2.3 依赖注入

  1. 之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();那么,控制层和 service 层存在耦合。
  2. 之后,我们需要对各层之间进行解耦操作,将代码修改成 FruitService fruitService = null;
  3. 然后,在配置文件中配置:
<bean id="fruit" class="FruitController">
     <property name="fruitService" ref="fruitService"/>
</bean>
  1. 使用反射技术将 FruitController 需要的 fruitService 注入进去,而不是之前主动去获取的方式,这就叫依赖注入

3、事务管理

3.1 涉及到的组件

  • OpenSessionInViewFilter,负责事务的过滤器
  • TransactionManager,负责所有事务操作的开启、提交和回滚,要从ConnUtil 中获取一个唯一的连接
  • ThreadLocal,一个Thread 维护一个ThreadLocalMap,然后 map 中存放的是键值对(ThreadLocal,value),即一个 ThreadLocal 对应一个 conn 连接
  • ConnUtil,负责获取连接的工具类
  • BaseDAO,负责操作数据库的工具类

3.2 ThreadLocal

  1. ThreadLocal 称之为本地线程 。 我们可以通过 set 方法在当前线程上存储数据、通过 get 方法在当前线程上获取数据
  2. set 方法源码分析:
    在这里插入图片描述
public void set(T value) {
    Thread t = Thread.currentThread(); //获取当前的线程
    ThreadLocalMap map = getMap(t);    //每一个线程都维护各自的一个容器(ThreadLocalMap)
    if (map != null)
//一个Thread维护一个ThreadLocalMap,然后map中存放的是键值对(ThreadLocal,value),这里的一个conn相当于一个value 
//这里的key对应的是ThreadLocal,因为我们的组件中需要传输(共享)的对象可能会有多个value(不止Connection)
        map.set(this, value);         
    else
//默认情况下map是没有初始化的,那么第一次往其中添加数据时,会去初始化
        createMap(t, value);           
}
  1. get方法源码分析:
    在这里插入图片描述
 public T get() {
     Thread t = Thread.currentThread(); //获取当前的线程
     ThreadLocalMap map = getMap(t);    //获取和这个线程相关的ThreadLocalMap(也就是工作纽带的集合)
     if (map != null) {
         ThreadLocalMap.Entry e = map.getEntry(this);   //this指的是ThreadLocal对象,通过它才能知道是哪一个工作纽带
         if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;     //entry.value就可以获取到这个ThreadLocal对应的value了
             return result;
         }
     }
     return setInitialValue();
 }

4、水果管理系统 MVC 优化

4.1 FruitServlet 组件的诞生

  1. 最初的做法是: 一个请求对应一个 Servlet ,这样存在的问题是 servlet 太多了;
    在这里插入图片描述
  2. 现在我们想做的是,一个 Servlet 组件其中有多个方法,我们直接调用其中的方法,而不用发送多次请求。
    在这里插入图片描述
  3. 首先,将之前的 addServlet、updateServlet 等等封装成一个方法,放在 FruitServlet 方法中
  4. 根据获取到的不同 operate 值来选择调用不同的方法,修改 index.html 中add、del、update、index、edit 的对应按钮的 action,并且给他们添加 operate 属性,修改方法中对应的重定向页面,改为 fruit.do
  5. 我们在判断到底是哪一个 operate 的时候使用的是 switch-case来判断的

FruitServlet 组件实现:

@WebServlet("/fruit.do")
public class FruitServlet extends ViewBaseServlet {
    private FruitDAO fruitDAO = new FruitDAOImpl();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //设置编码
        request.setCharacterEncoding("UTF-8");
		//获取发送请求的operate属性,没有的话默认是index
        String operate = request.getParameter("operate");
        if (StringUtil.isEmpty(operate)) {
            operate = "index";
        }      
        switch (operate) {
            case "index":
                index(request, response);
                break;
            case "add":
                add(request, response);
                break;
            case "del":
                del(request, response);
                break;
            case "edit":
                edit(request, response);
                break;
            case "update":
                update(request, response);
                break;
            default:
                throw new RuntimeException("operate值非法!");
        }
    }

    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");
    }

    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);
        }
    }

    private void del(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
        String fidStr = request.getParameter("fid");
        if(StringUtil.isNotEmpty(fidStr)){
            int fid = Integer.parseInt(fidStr);
            fruitDAO.delFruit(fid);

            //super.processTemplate("index",request,response);
            response.sendRedirect("fruit.do");
        }
    }

    private void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");

        String fname = request.getParameter("fname");
        Integer price = Integer.parseInt(request.getParameter("price")) ;
        Integer fcount = Integer.parseInt(request.getParameter("fcount"));
        String remark = request.getParameter("remark");

        Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;

        fruitDAO.addFruit(fruit);

        response.sendRedirect("fruit.do");
    }

    private void index(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
        HttpSession session = request.getSession() ;

        // 设置当前页,默认值1
        Integer pageNo = 1 ;

        String oper = request.getParameter("oper");

        //如果oper!=null 说明 通过表单的查询按钮点击过来的
        //如果oper是空的,说明 不是通过表单的查询按钮点击过来的
        String keyword = null ;
        if(StringUtil.isNotEmpty(oper) && "search".equals(oper)){
            //说明是点击表单查询发送过来的请求
            //此时,pageNo应该还原为1 , keyword应该从请求参数中获取
            pageNo = 1 ;
            keyword = request.getParameter("keyword");
            //如果keyword为null,需要设置为空字符串"",否则查询时会拼接成 %null% , 我们期望的是 %%
            if(StringUtil.isEmpty(keyword)){
                keyword = "" ;
            }
            //将keyword保存(覆盖)到session中
            session.setAttribute("keyword",keyword);
        }else{
            //说明此处不是点击表单查询发送过来的请求(比如点击下面的上一页下一页或者直接在地址栏输入网址)
            //此时keyword应该从session作用域获取
            String pageNoStr = request.getParameter("pageNo");
            if(StringUtil.isNotEmpty(pageNoStr)){
                pageNo = Integer.parseInt(pageNoStr);   //如果从请求中读取到pageNo,则类型转换。否则,pageNo默认就是1
            }
            //如果不是点击的查询按钮,那么查询是基于session中保存的现有keyword进行查询
            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 ;
        /*
        总记录条数       总页数
        1               1
        5               1
        6               2
        10              2
        11              3
        fruitCount      (fruitCount+5-1)/5
         */
        session.setAttribute("pageCount",pageCount);

        //此处的视图名称是 index
        //那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
        //逻辑视图名称 :   index
        //物理视图名称 :   view-prefix + 逻辑视图名称 + view-suffix
        //所以真实的视图名称是:      /       index       .html
        super.processTemplate("index",request,response);
    }
}

4.2 使用反射判断 operate 优化 FruitServlet

  1. 在上一个版本中,Servlet 中充斥着大量的 switch-case,试想一下,随着我们的项目的业务规模扩大,那么会有很多的 Servlet,也就意味着会有很多的 switch-case,这是一种代码冗余
  2. 因此,我们在 servlet 中使用了反射技术,我们规定 operate 的值和方法名一致,那么接收到 operate 的值是什么就表明我们需要调用对应的方法进行响应,如果找不到对应的方法,则抛异常

将 FruitServlet 中的服务方法用反射技术进行修改:

@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 method : methods) {
        //获取方法名称
        String methodName = method.getName();
        if (operate.equals(methodName)){
            try {
                //找到和operate同名的方法,通过反射技术调用它,用当前实例来调用
                method.invoke(this,request,response);
                return;
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
    //没有找到和operate对应的方法,就报运行时错误
    throw new RuntimeException("operate值非法!");
}

4.3 DispatcherServlet 中央控制器的诞生

  1. 在上一个版本中我们使用了反射技术,但是其实还是存在一定的问题:在有很多个 servlet 的情况下,每一个 servlet 中都有类似的反射技术的代码。
  2. 因此将所有反射代码抽取,设计了中央控制器类:DispatcherServlet,将每一个 servlet 都作为一个 controller 类来实现,就不负责与浏览器进行通信了,只专注自己的业务的实现;
    在这里插入图片描述
  3. 我们需要创建一个 appliacationContext.xml 配置文件,其中包括 bean 标签中写上我们要用到的各个对象,比如 fruit 对象,它的 id 是什么,它对应哪个 controller;
  4. 在 DispatcherServlet 组件中:
    • 通过 IO 流来获取所有配置文件中定义的 bean 节点;(构造 beanMap 容器)
    • 通过 DOM 技术解析 xml 文件将 bean 节点中的 id 和 class 对应起来,下次可以直接根据解析的 servletPath 知道调用哪个类,例如 fruitController 类;(解析 url 地址)
    • 然后将以前 FruitController 中的 service 中的判断是哪个 operate 方法的反射代码也放到 DispatcherServlet 组件中的服务方法中,通过反射可以判断所有 controller 调用的是哪个方法;(统一获取参数、进行方法调用)
    • 对一个 fruitController 类中的多个方法来说(add、del等),最后都会进行资源的转发或者重定向等,将这部分提取出来由中央控制器来做,具体的 Controller 类就不关心该怎么做了。(视图处理)

FruitController 类的实现:

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" ;
    }
}

解释:

  1. 将每种方法的返回值改成一个String类型
    return “redirect:fruit.do”;//带前缀的话就是资源重定向
    return “index” ;//不带前缀就是视图渲染
    所有的方法中就不需要响应参数了;
  2. 想要获取形参的实际名称,需要在 File - Settings - Build, Execution, Deployment - Compiler - Java Compiler 中配置一句话 -parameters,才能获取到,否则获取到的参数名字就是 arg0,arg1等(JDK 5.0 之后支持);有多个参数的空格就可以了。
    在这里插入图片描述

DispatcherServlet 组件实现:

@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet{

    private Map<String,Object> beanMap = new HashMap<>();

    public DispatcherServlet(){
    }

    public void init() throws ServletException {
        super.init();//调用的是ViewBaseServlet中的init()方法
        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 controllerBeanClass = Class.forName(className);
                    Object beanObj = controllerBeanClass.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
    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"
                    }
                }
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

解释:DispatcherServlet 这个类的主要工作

  1. 解析 url 地址:根据我们请求的 url 获取 servletPath,解析出一个名字;
  2. 加载配置文件,从配置文件中读取 bean,扔到 map 容器中(初始化方法中);
  3. 根据解析的名字去 map 里面找到能够处理的那个 controller 类,然后调用 controller 类中对应的 operate 方法
  4. 判断 operate 方法所需要的参数值,从请求中获取 operate 方法所需要得参数值,在反射调用这个方法的时候将它需要的参数进行赋值;
  5. 视图处理:根据方法所返回的字符串进行重定向或者视图渲染。

4.4 FruitService 服务层的诞生

  1. 之前我们是 FruitController 直接调用 FruitDAO 的,当需要多个功能的时候要频繁的调用多个 DAO ;
  2. 现在我们要中间加一层服务层,FruitController 和 FruitService 通信,FruitService 调用 FruitDAO(实际公司中业务层体量很大)。

在这里插入图片描述

FruitService 接口实现:

public interface FruitService {
    //获取指定页面的库存列表信息
    List<Fruit> getFruitList(String keyword , Integer pageNo);
    //添加库存记录信息
    void addFruit(Fruit fruit);
    //根据id查看指定库存记录
    Fruit getFruitByFid(Integer fid);
    //删除特定库存记录
    void delFruit(Integer fid);
    //获取总页数
    Integer getPageCount(String keyword);
    //修改特定库存记录
    void updateFruit(Fruit fruit);
}

FruitServiceImpl 类实现:

public class FruitServiceImpl implements FruitService {

    private FruitDAO fruitDAO = new FruitDAOImpl();

    @Override
    public List<Fruit> getFruitList(String keyword, Integer pageNo) {
        System.out.println("getFruitList -> " + ConnUtil.getConn());
        return fruitDAO.getFruitList(keyword,pageNo);
    }

    @Override
    public void addFruit(Fruit fruit) {
        fruitDAO.addFruit(fruit);
        Fruit fruit2 = fruitDAO.getFruitByFid(2);
        fruit2.setFcount(99);
        fruitDAO.updateFruit(fruit2);
    }

    @Override
    public Fruit getFruitByFid(Integer fid) {
        return fruitDAO.getFruitByFid(fid);
    }

    @Override
    public void delFruit(Integer fid) {
        fruitDAO.delFruit(fid);
    }

    @Override
    public Integer getPageCount(String keyword) {
        System.out.println("getPageCount -> " + ConnUtil.getConn());
        int count = fruitDAO.getFruitCount(keyword);
        int pageCount = (count+5-1)/5 ;
        return pageCount;
    }

    @Override
    public void updateFruit(Fruit fruit) {
        fruitDAO.updateFruit(fruit);
    }
}

FruitController 类实现:

public class FruitController {

    private FruitService fruitService = new FruitServiceImpl();

    private String update(Integer fid , String fname , Integer price , Integer fcount , String remark ){
        //3.执行更新
        fruitService.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 = fruitService.getFruitByFid(fid);
            request.setAttribute("fruit",fruit);
            //super.processTemplate("edit",request,response);
            return "edit";
        }
        return "error" ;
    }

    private String del(Integer fid  ){
        if(fid!=null){
            fruitService.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 ) ;
        fruitService.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);

        List<Fruit> fruitList = fruitService.getFruitList(keyword , pageNo);
        session.setAttribute("fruitList",fruitList);

        //总记录条数
        int pageCount = fruitService.getPageCount(keyword);
        session.setAttribute("pageCount",pageCount);

        return "index" ;
    }
}

4.5 IOC 控制反转和DI 依赖注入的实现

  1. 之前在 Servlet 中,我们创建 service 对象 , FruitService fruitService = new FruitServiceImpl();
  2. 这句话如果出现在 servlet 中的某个方法内部,那么这个 fruitService 的作用域(生命周期)应该就是这个方法级别;(这个方法结束,这个对象的生命周期就结束了)
  3. 如果这句话出现在 servlet 的类中,也就是说 fruitService 是一个成员变量,那么这个 fruitService 的作用域(生命周期)应该就是这个 servlet 实例级别
  4. 我们如果在 applicationContext.xml 中定义这个 fruitService,然后通过解析 XML,产生 fruitService 实例,存放在 beanMap 中,这个 beanMap 在一个 BeanFactory
  5. 因此,我们转移(改变)了之前的 service 实例、dao 实例等等他们的生命周期,控制权从程序员转移到 BeanFactory(IOC容器),这个现象我们称之为控制反转
  6. 之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();那么,控制层和 service 层存在耦合。
  7. 之后,我们需要对各层之间进行解耦操作,将代码修改成 FruitService fruitService = null;
  8. 然后,在配置文件中配置:
<bean id="fruit" class="FruitController">
     <property name="fruitService" ref="fruitService"/>
</bean>
  1. 使用反射技术将 FruitController 需要的 fruitService 注入进去,而不是之前主动去获取的方式,这就叫依赖注入

applicationContext.xml 配置文件:

<beans>
    <bean id="fruitDAO" class="com.atguigu.fruit.dao.impl.FruitDAOImpl"/>
    <bean id="fruitService" class="com.atguigu.fruit.service.impl.FruitServiceImpl">
        <!-- property标签用来表示属性;name表示属性名;ref表示引用其他bean的id值-->
        <property name="fruitDAO" ref="fruitDAO"/>
    </bean>
    <bean id="fruit" class="com.atguigu.fruit.controllers.FruitController">
        <property name="fruitService" ref="fruitService"/>
    </bean>
</beans>

BeanFactory 接口实现:

public interface BeanFactory {
    Object getBean(String id);
}

ClassPathXmlApplicationContext 实现 BeanFactory 接口:

public class ClassPathXmlApplicationContext implements BeanFactory {

    private Map<String,Object> beanMap = new HashMap<>();
    private String path = "applicationContext.xml" ;
    public ClassPathXmlApplicationContext(){
        this("applicationContext.xml");
    }
    public ClassPathXmlApplicationContext(String path){
        if(StringUtil.isEmpty(path)){
            throw new RuntimeException("IOC容器的配置文件没有指定...");
        }
        try {
            InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path);
            //1.创建DocumentBuilderFactory
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            //2.创建DocumentBuilder对象
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
            //3.创建Document对象
            Document document = documentBuilder.parse(inputStream);

            //4.获取所有的bean节点把他们放进beanmap中
            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实例对象保存到map容器中
                    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);
                            //通过反射设置对象的 propertyField 属性将它需要的其他类注入进去
                            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);
    }
}

解释:

  • 创建一个 BeanFactory,该实现类负责获取所有的 bean 节点,组装 bean 之间的依赖关系
  • 将之前 DispatcherServlet 组件中初始化方法部分拿出来作为一个创建 BeanFactory 的代码,在DispatcherServlet 组件中只需要获取这个 beanfactory 就可以了, beanFactory = new ClassPathXmlApplicationContext();

DispatcherServlet 组件改动:

@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 request, HttpServletResponse response) throws ServletException, IOException {
		//2.controller组件中的方法调用
		method.setAccessible(true);
		Object returnObj = method.invoke(controllerBeanObj,parameterValues);//依赖注入
    }
}

4.6 加入字符编码集过滤器和事务管理

CharacterEncodingFilter 编码集过滤器实现:

在这里插入图片描述

@WebFilter(urlPatterns = {"*.do"},initParams = {@WebInitParam(name = "encoding",value = "UTF-8")})
public class CharacterEncodingFilter implements Filter {

    //如果配置了encoding值,使用配置的值,如果没有配置,使用这这里默认的utf-8值
    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() {}
}

解释:

  • 在 CharacterEncodingFilter 中默认编码集 “UTF-8”;
  • 可以在注解中定义初始化参数 name = "encoding",value = "UTF-8"
  • 在重写的过滤器初始化方法中,获取定义的初始化参数,如果有的话就使用注解定义的编码集,如果没有的话,就使用默认的编码集 UTF-8

4.6.2 事务管理

  1. 之前的 service 操作中比如调用了三个 DAO 操作,那么每一个 DAO 层都有一个与事务管理有关的操作,那么对服务层来说,这是同一个业务功能的操作,如果三个 DAO 操作,部分成功,部分失败,那么我们认为这个业务是成功还是失败了呢?
    在这里插入图片描述

  2. 为了完成 service 层的事务管理,我们需要将开启事务不自动提交、事务提交和自动回滚放到 service 层来实现;
    在这里插入图片描述

  3. 但是 service 层需要只注重业务功能的实现,所以我们再将事务管理操作前置,放置到过滤器里面来实现。
    在这里插入图片描述

OpenSessionInViewFilter 事务管理过滤器实现:

@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.beginTrans();
            System.out.println("开启事务....");
            filterChain.doFilter(servletRequest, servletResponse);
            TransactionManager.commit();
            System.out.println("提交事务...");
        }catch (Exception e){
            e.printStackTrace();
            try {
                TransactionManager.rollback();
                System.out.println("回滚事务....");
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

    @Override
    public void destroy() {
    }
}

解释:

  • try 中设置事务不自动提交,放行(servlet 执行),提交;
  • catch 中捕获到异常设置回滚(这里 BaseDAO 不能 try/catch,否则上层 service 无法捕获到异常);
  • 所以在 BaseDAO 和 DispatcherServlet 中我们捕获到异常之后将异常向上抛出,必须让异常被过滤器捕获到。

TransactionManager 具体事务的三步操作的实现:

public class TransactionManager {

    //开启事务
    public static void beginTrans() throws SQLException {
        ConnUtil.getConn().setAutoCommit(false);
    }

    //提交事务
    public static void commit() throws SQLException {
        Connection conn = ConnUtil.getConn();
        conn.commit();
        //事务已经提交了就可以进行关闭资源操作了
        ConnUtil.closeConn();
    }

    //回滚事务
    public static void rollback() throws SQLException {
        Connection conn = ConnUtil.getConn();
        conn.rollback();
        ConnUtil.closeConn();
    }
}

ConnUtil 获取同一个连接的工具类的实现:

public class ConnUtil {

    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();

    public static final String DRIVER = "com.mysql.cj.jdbc.Driver";
    public static final String URL = "jdbc:mysql://localhost:3306/fruitdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
    public static final String USER = "root";
    public static final String PWD = "123456";

    private static Connection createConn() {
        try {
            //1.加载驱动
            Class.forName(DRIVER);
            //2.通过驱动管理器获取连接对象
            return DriverManager.getConnection(URL, USER, PWD);
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static Connection getConn() {
        //用一个static线程(其中只放置一个连接),去获取一个数据库连接,如果为空就先创建再set再get
        Connection conn = threadLocal.get();
        if (conn == null) {
            conn = createConn();
            threadLocal.set(conn);
        }
        return threadLocal.get();
    }

    public static void closeConn() throws SQLException {
        Connection conn = threadLocal.get();
        if (conn == null) {//没有连接就没事
            return;
        }
        if (!conn.isClosed()) {
            conn.close();
            threadLocal.set(null);
        }
    }
}

网页和控制台展示

可以看出是同一个连接:
在这里插入图片描述
事务管理成功控制台输出:
在这里插入图片描述
故意写错 sql 语句,看一下事务操作:
在这里插入图片描述

4.7 上下文监听器启动时创建 IOC 容器

  1. 之前中央控制器中,中央控制器初始化中 IOC 工厂才初始化,最好是在 ServletContext 准备好的时候就初始化 IOC 容器;
    public void init() throws ServletException {
        super.init();
        beanFactory = new ClassPathXmlApplicationContext();
    }
  1. 这时我们需要用到 ServletContextListener 监听器,监听上下文启动,在上下文启动的时候去创建 IOC 容器,然后将其保存到 application 作用域,后面中央控制器再从 application 作用域中去获取 IOC 容器。

ContextLoaderListener 上下文监听器实现:

//监听上下文启动,在上下文启动的时候去创建IOC容器,然后将其保存到application作用域
//后面中央控制器再从application作用域中去获取IOC容器
@WebListener
public class ContextLoaderListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //1.获取ServletContext对象
        ServletContext application = servletContextEvent.getServletContext();
        //2.获取上下文的初始化参数
        String path = application.getInitParameter("contextConfigLocation");
        //3.创建IOC容器
        BeanFactory beanFactory = new ClassPathXmlApplicationContext(path);
        //4.将IOC容器保存到application作用域
        application.setAttribute("beanFactory",beanFactory);
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    }
}

DispatcherServlet 中央控制器初始化方法改动:

public void init() throws ServletException {
    super.init();
    //之前是在此处主动创建IOC容器的
    //现在优化为从application作用域去获取
    //beanFactory = new ClassPathXmlApplicationContext();
    ServletContext application = getServletContext();
    Object beanFactoryObj = application.getAttribute("beanFactory");
    if(beanFactoryObj!=null){
        beanFactory = (BeanFactory)beanFactoryObj ;
    }else{
        throw new RuntimeException("IOC容器获取失败!");
    }
}

4.8 整个优化流程

在这里插入图片描述

  1. DispatcherServlet 解析 URL ,解析出 fruit 路径,根据 applicationContext.xml 配置文件中配置的 bean 组件 id - class ,知道我们要找的是 FruitController;
  2. 然后根据反射,我们知道当前请求中发过来的 operate 参数是想调用 index() 方法,该方法需要调用 FruitServiceImpl 中的一些方法;
  3. FruitController 需要调用 FruitServiceImpl 中的方法的这个依赖关系在 applicationContext.xml 配置文件中的 bean 节点的 property 标签中 name - ref 已经配置好了,并且在 ClassPathXmlApplicationContext 创建 BeanFactory 的时候已经将这个依赖关系通过反射注入了;
  4. FruitController 调用 FruitService 方法, FruitService 调用 FruitDAO,因为事务操作的原因,FruitDAO 调用多个方法的时候使用的是同一个连接,这就涉及到 ThreadLocal 的使用,保证对这个事务操作来说,获取到的是同一个连接。

4.9 水果管理系统界面展示

具体效果可以看我之前的文章:(尚硅谷)JavaWeb新版教程05-水果管理系统的初步实现,实现的是一样的效果,只不过对代码进行了优化。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值