2.3 第三阶段

在第二阶段中,基本的功能已经实现了。在此基础上进行优化,引入MVC 。将前面四个servlet整合在一起,放在一个FruitServlet中,如下:

<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310101924425.png" alt="image-20231010192433274" style="zoom: 50%;" />

1、合并servlet至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");
        // 通过request获取操作符的值
        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);
            default:
                throw new RuntimeException("operate值非法!");
        }
    }

    private void update(HttpServletRequest request, HttpServletResponse response) throws 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);
    }
}

2、更新service方法

/*通过反射调用匹配的方法*/
@Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("UTF-8");
        String operate = request.getParameter("operate");
        if(StringUtil.isEmpty(operate)) {
            operate = "index";
        }
        // 1. 获取当前类中声明的所有方法。
        Method[] methods = this.getClass().getDeclaredMethods();
        // 2. 遍历所有方法。
        for (Method m : methods) {
            // 3. 获取当前遍历到的方法的名称。
            String methodName = m.getName();
            // 4. 判断当前遍历到的方法的名称是否与传入的操作名称(通过"operate"参数获取)相等。
            if(operate.equals(methodName)) {
                try {
                    // 5. 如果匹配成功,则尝试调用该方法,并传入请求和响应对象。
                    m.invoke(this, request, response);
                    // 6. 如果调用成功,则返回。
                    return ;
                } catch (IllegalAccessException e) {
                    // 7. 如果没有找到匹配的方法,则抛出异常。
                    throw new RuntimeException(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            }
        }

3、引入前端控制器DispatcherServlet和controller层

<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310110956569.png" alt="image-20231011095632494" style="zoom:50%;" />

它接收所有的请求并将其分发给合适的处理器(Controller),同时新建包controller,将FruitServlet改为FruitController放入该包中。

DispatcherServlet作用:

1.根据url定位到能够处理这个请求的controller组件: 1)从url中提取servletPath : /fruit.do -> fruit 2)根据fruit找到对应的组件:FruitController , 这个对应的依据我们存储在applicationContext.xml中

      <bean id="fruit" class="com.lowell.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.....

application.xml中配置DispatcherServlet

<?xml version="1.0" encoding="utf-8" ?>

<beans>
    <bean id="fruit" class="com.lowell.fruit.controller.FruitController"/>
</beans>

DispatcherServlet代码

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

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

    public DispatcherServlet(){
    }

    @Override
    public void init(){
        try {
            /* Java的DOM方式解析XML文件 */
            // 获取XML文件的输入流
            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() ;
                    Method setServletContextMethod = controllerBeanClass.getDeclaredMethod("setServletContext", ServletContext.class);
                    setServletContextMethod.invoke(beanObj , this.getServletContext());

                    beanMap.put(beanId , beanObj) ;
                }
            }
        } catch (ParserConfigurationException | SAXException | IOException | IllegalAccessException |
                 InstantiationException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException 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 method = controllerBeanObj.getClass().getDeclaredMethod(operate,HttpServletRequest.class,HttpServletResponse.class);
            if(method!=null){
                method.setAccessible(true);
                method.invoke(controllerBeanObj,request,response);
            }else{
                throw new RuntimeException("operate值非法!");
            }
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

上述代码解释如下:

init()

  1. 通过getClass().getClassLoader().getResourceAsStream("applicationContext.xml")从类路径中获取XML文件的输入流
  2. 创建DocumentBuilderFactory实例
  3. 通过documentBuilderFactory.newDocumentBuilder()创建DocumentBuilder对象。
  4. 通过documentBuilder.parse(inputStream)方法将XML文件的输入流解析成Document对象
  5. 从解析的XML文件中获取所有的bean节点,并将每个bean节点的id和对应的class属性值存储在beanMap
    1. 遍历beanNodeList,即所有的bean节点
    2. 对于每个bean节点,首先判断其节点类型是否为元素节点
    3. 获取当前bean节点的idclass属性值
    4. 通过Class.forName(className)加载className对应的类,并将其实例化得到beanObj
    5. 通过反射获取controllerBeanClass类中声明的名为setServletContext、参数为ServletContext类型的方法,并调用该方法设置ServletContext
    6. beanIdbeanObj存储在beanMap

service()

  1. 设置请求的字符编码为UTF-8,保证正确地解析请求参数的中文字符
  2. 获取请求的Servlet路径,并去掉开头的斜杠
  3. 查找Servlet路径中最后一个".do"的位置,并截取到该位置之前的字符串,作为操作的标识(servletPath
  4. 根据beanMap中通过操作标识查找到对应的控制器实例对象(controllerBeanObj
  5. 获取请求参数operate的值,若为空,则设置默认值为index
  6. 根据操作标识调用对应的控制器方法进行处理
    1. 利用反射,通过控制器实例对象的getClass()方法获取到实际类的Class对象
    2. 利用getDeclaredMethod(operate, HttpServletRequest.class, HttpServletResponse.class)方法获取到指定名称和参数类型的方法对象(method
    3. 如果找到了该方法,则设置该方法可以访问(setAccessible(true)),并通过invoke(controllerBeanObj, request, response)方法调用该方法
    4. 如果没有找到指定的方法,则抛出运行时异常并输出错误信息。
    5. 捕获NoSuchMethodExceptionIllegalAccessExceptionInvocationTargetException异常,并打印异常堆栈信息

4、引入IOC容器和service层

实现尽可能高内聚低耦合 层内部的组成应该是高度聚合的,而层与层之间的关系应该是低耦合的,最理想的情况0耦合(就是没有耦合)

<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310111947958.png" alt="image-20231011194713796" style="zoom:50%;" />

BeanFactory

package com.atguigu.myssm.io;

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

ClassPathXmlApplicationContext

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实例对象保存到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);
                            Class beanClazz = beanObj.getClass();
                            Field propertyField = beanClazz.getDeclaredField(propertyName);
                            propertyField.setAccessible(true);
                            propertyField.set(beanObj,refObj);
                        }
                    }
                }
            }
        } catch (ParserConfigurationException | SAXException | IOException | IllegalAccessException |
                 InstantiationException | ClassNotFoundException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Object getBean(String id) {
        return beanMap.get(id);
    }
}

applicationContext.xml

<?xml version="1.0" encoding="utf-8"?>
<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>

DispatcherServletinit()方法

public void init() throws ServletException {
    super.init();
    beanFactory = new ClassPathXmlApplicationContext();
}

service层中FruitServiceFruitServiceImpl

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);
}
public class FruitServiceImpl implements FruitService {

    private FruitDAO fruitDAO = null ;

    @Override
    public List<Fruit> getFruitList(String keyword, Integer pageNo) {
        return fruitDAO.getFruitList(keyword,pageNo);
    }

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

    @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) {
        int count = fruitDAO.getFruitCount(keyword);
        int pageCount = (count+5-1)/5 ;
        return pageCount;
    }

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

第三阶段结果

<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310111959438.png" alt="image-20231011195918341" style="zoom:50%;" />

2.4 第四阶段

为了确保数据库操作的一致性和完整性,引入事务管理,引入监听器和过滤器

TransactionManager

package com.lowell.myssm.trans;

import com.lowell.myssm.basedao.ConnUtil;
import java.sql.Connection;
import java.sql.SQLException;

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

package com.lowell.myssm.basedao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnUtil {
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    public static final String DRIVER = "com.mysql.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 = "******";

    public 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() {
        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);
        }
    }
}

CharecterEncodingFilter

package com.lowell.myssm.filters;

@WebFilter(urlPatterns = {"*.do"}, initParams = {@WebInitParam(name= "encoding", value = "GBK")})
public class CharecterEncodingFilter 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("UTF-8");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

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) {
                throw new RuntimeException(ex);
            }
        }
    }

    @Override
    public void destroy() {
    }
}

<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310112034239.png" alt="image-20231011203423154" style="zoom:50%;" />

<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202310112034042.png" alt="image-20231011203454960" style="zoom:50%;" />

至此,水果后台管理系统的功能,均已实现。

源码:[github-Lowell-37](