第十五章项目1——水果管理系统

Source

目的

  1. 巩固之前学习的基础知识

    • 前端知识JS,CSS,HTML,我们自己写前端代码
    • 使用TomCat,了解TcoCat的用处和熟悉IDEA中如何去布置Web项目,和Web项目相关的知识
    • 运用Servlet,理解Servlet的用法和原理
  2. 关于JDBC方面

    • 巩固JDBC的基础语法
    • 在JDBC的基础语法之上,进行封装JDBC的功能,帮助理解关于以后使用MyBatis或者MyBatis等封装框架的原理
  3. 关于MVC方面

    • 基于ViewBaseServlet对模板引擎渲染的理解,Thymeleaf是什么和它的作用,学习基本的Thymeleaf的基本语法
    • 了解dispatcherServlet的大概原理和流程
  4. 关于Ioc方面

    • 理解Ioc是什么,为什么我们需要Ioc这种模式
  5. 关于事务方面

    • 了解事务是什么,和事务的应用
    • 利用Fliter实现我们的事务管理

从需求开始出发

  • 我们的水果管理系统,主要就是在我们的浏览器端进行对数据库中的数据进行展示
    • 按分页进行展示,每页5条数据,提供上一页,下一页,首页,尾页跳转的功能
    • 提供搜索功能,可以根据关键词来展示,提供首页的搜索框进行搜索展示
    • 提供修改信息功能,通过点击首页的水果名超链接进入修改页面进行修改
    • 提供添加水果信息,通过点击首页的添加新库存记录跳转到添加页面添加
    • 删除水果信息,点击操纵列中的❌进行删除

在这里插入图片描述

从数据库和数据库连接开始——DAO层

对应数据库和数据表的创建

CREATE DATABASE fruitdb charset utf8;
USE fruitdb ;
CREATE TABLE `t_fruit` (
  `fid` int(11) NOT NULL AUTO_INCREMENT,
  `fname` varchar(20) NOT NULL,
  `price` int(11) DEFAULT NULL,
  `fcount` int(11) DEFAULT NULL,
  `remark` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`fid`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8;

insert  into `t_fruit`(`fid`,`fname`,`price`,`fcount`,`remark`) 
values 
(1,'红富士',5,16,'红富士也是苹果!'),
(2,'大瓜',5,100,'王校长的瓜真香'),
(3,'南瓜',4,456,'水果真好吃'),
(4,'苦瓜',5,55,'苦瓜很好吃'),
(5,'莲雾',9,99,'莲雾是一种神奇的水果'),
(6,'羊角蜜',4,30,'羊角蜜是一种神奇的瓜'),
(7,'啃大瓜',13,123,'孤瓜');

我们使用的技术是JDBC,用JDBC本身连接数据库并操作数据库没有什么可说的,但是我们的BaseDAO的编写,可以帮助我们以后更好的理解Mybatis等框架的理解

看一下普通的JDBC操作数据库的编写

 //删除对应fname的数据记录
public boolean delFruit(String fname) {
    
      
        try {
    
      
            Class.forName(DRIVER);
            connection = DriverManager.getConnection(URL,USER,PWD);
            String sql = "delete from t_fruit where fname like ? " ;
            ps = connection.prepareStatement(sql);
            ps.setString(1,fname);
            return ps.executeUpdate() > 0 ;
        } catch (ClassNotFoundException | SQLException e) {
    
      
            e.printStackTrace();
        }finally {
    
      
            try {
    
      
                if (rs != null) {
    
      
                    rs.close();
                }
                if(ps!=null){
    
      
                    ps.close();
                }
                if(connection!=null && !connection.isClosed()){
    
      
                    connection.close();
                }
            } catch (SQLException e) {
    
      
                e.printStackTrace();
            }
        }
        return false;
}
//根据对应fid来修改数据
public boolean updateFruit(Fruit fruit) {
    
      
        try {
    
      
            Class.forName(DRIVER);
            connection = DriverManager.getConnection(URL,USER,PWD);
            String sql = "update t_fruit set fcount = ? where fid = ? " ;
            ps = connection.prepareStatement(sql);
            ps.setInt(1,fruit.getFcount());
            ps.setInt(2,fruit.getFid());
            return ps.executeUpdate() > 0  ;
        } catch (ClassNotFoundException | SQLException e) {
    
      
            e.printStackTrace();
        }finally {
    
      
            try {
    
      
                if(ps!=null){
    
      
                    ps.close();
                }
                if(connection!=null && !connection.isClosed()){
    
      
                    connection.close();
                }
            } catch (SQLException e) {
    
      
                e.printStackTrace();
            }
        }
        return false;
}
  • 我们看到对于我们的这两个方法,虽然可以实现功能,但是会有大量的冗余代码,比如加载JDBC的驱动,和获得数据源和连接,获取预处理对象和对获得数据的处理,还有资源的关闭 这些都要我们自己去编写,还是非常的麻烦

项目优化——Mybatis等框架思路引入

对应的类中类信息

public abstract class BaseDAO<T> {
    
      
    public final String DRIVER = "com.mysql.jdbc.Driver" ;
    public final String URL = "jdbc:mysql://localhost:3306/fruitdb?useUnicode=true&characterEncoding=utf-8&useSSL=false";
    public final String USER = "root";
    public final String PWD = "******" ;

    protected Connection conn ;
    protected PreparedStatement psmt ;
    protected ResultSet rs ;

    //T的Class对象
    private Class entityClass ;
	
}    
  • 这个类写成泛型的形式,这样我们以后可以实现不仅仅是对水果的数据进行操作,也可以是订单这种数据进行操作,实现复用
  • 将我们要连接的数据库的需要的数据都写成静态常量

构造函数

理解getGenericSuperclass()

import java.lang.reflect.*;

public class Student extends Person<Integer, Boolean> {
    
      

	@SuppressWarnings("rawtypes")
	public static void main(String[] args) {
    
      
		
		Student student = new Student();
		Class clazz = student.getClass();
		System.out.println("获取父类对象:" + clazz.getSuperclass());
		/**
		 * getGenericSuperclass()获得带有泛型的父类
         * Type是 Java 编程语言中所有类型的公共高级接口。它们包括原始类型、参数化类型、数组类型、类型变量和基本类型。
		 */
		Type type = clazz.getGenericSuperclass();
		System.out.println(type);
		
		//ParameterizedType参数化类型,即泛型
		ParameterizedType p = (ParameterizedType)type;
		//getActualTypeArguments获取参数化类型的数组,泛型可能有多个
		Class c1 = (Class)p.getActualTypeArguments()[0];
		System.out.println(c1);
		Class c2 = (Class)p.getActualTypeArguments()[1];
		System.out.println(c2);
	}

class Person<T,T>{
    
      
}

	/**
	 * 运行结果:
	 * 获取父类对象:class com.mycode.test.Person
	 * com.mycode.test.Person<java.lang.Integer, java.lang.Boolean>
	 * class java.lang.Integer
	 * class java.lang.Boolean
	 */

    public BaseDAO(){
    
      
        //FruitDAO是一个接口  FruitDAOImpl是实现了这个接口的一个类  FruitDAOImpl 并且继承 BaseDAO<Fruit>
        //getClass() 获取Class对象,当前我们执行的是FruitDAO fruitDAO=new FruitDAOImpl() , 创建的是FruitDAOImpl的实例
        //那么子类构造方法内部首先会调用父类(BaseDAO)的无参构造方法
        //因此此处的getClass()会被执行,但是getClass获取的是FruitDAOImpl的Class
        //所以getGenericSuperclass()获取到的是BaseDAO带有泛型的Class BaseDAO<Fruit>
        Type genericType = getClass().getGenericSuperclass();
        //ParameterizedType 参数化类型
        Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
        //获取到的<T>中的T的真实的类型
        Type actualType = actualTypeArguments[0];//这里获取的就是Fruit
        try {
    
      
            entityClass = Class.forName(actualType.getTypeName());//创建了一个Fruit对象
        } catch (ClassNotFoundException e) {
    
      
            e.printStackTrace();
        }
}
  • 我们的构造方法非常好的运用了反射的功能
  • 为什么要写这么麻烦,不直接写Fruit呢?就是为了实现复用,如果以后不是Fruit对象,而是关于订单的数据库相关操纵,我们也可以通过这个进行创建成Order对象,只需要extends BaseDAO就可以实现,而不需要重新写方法

获取数据库的连接

protected Connection getConn(){
    
      
        try {
    
      
            //1.加载驱动
            Class.forName(DRIVER);
            //2.通过驱动管理器获取连接对象
            return DriverManager.getConnection(URL, USER, PWD);
        } catch (ClassNotFoundException | SQLException e) {
    
      
            e.printStackTrace();
        }
        return null ;
}
  • 这个是通过调用这个方法就可以实现连接到我们类中指定信息的数据库,让这个类的内部方法调用

关闭资源的方法

 protected void close(ResultSet rs , PreparedStatement psmt , Connection conn){
    
      
        try {
    
      
            if (rs != null) {
    
      
                rs.close();
            }
            if(psmt!=null){
    
      
                psmt.close();
            }
            if(conn!=null && !conn.isClosed()){
    
      
                conn.close();
            }
        } catch (SQLException e) {
    
      
            e.printStackTrace();
        }
    }
  • 对这三个资源进行判断,如果不为空就进行关闭,也是实现代码的复用,让内部类进行调用

给预处理命令对象设置参数

//给预处理命令对象设置参数
    private void setParams(PreparedStatement psmt , Object... params) throws SQLException {
    
      
        if(params!=null && params.length>0){
    
      
            for (int i = 0; i < params.length; i++) {
    
      
                psmt.setObject(i+1,params[i]);
            }
        }
    }
  • Object … 表示Java的多参数,因为要给不同的预处理对象进行设置参数(也就是给sql语句中的?进行设置值),所以参数的个数是不确定的
  • 我们PapreStatement设置参数的下标从1开始
  • 这个方法也是给这个类的方法使用

通用更新方法——核心方法

 //执行更新,返回影响行数
    protected int executeUpdate(String sql , Object... params){
    
      
        boolean insertFlag = false ;
        insertFlag = sql.trim().toUpperCase().startsWith("INSERT");//这个参数用来判断我们的SQL语句是不是
        try {
    
      
            conn = getConn();
            if(insertFlag){
    
      
                psmt = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
           
            }else {
    
      
                psmt = conn.prepareStatement(sql);
            }
            setParams(psmt,params);
            int count = psmt.executeUpdate() ;
            if(insertFlag){
    
      
                rs = psmt.getGeneratedKeys();
                if(rs.next()){
    
      
                    return ((Long)rs.getLong(1)).intValue();
                    //对于我们的insert方法,我们需要它返回我们的fid
                }
            }
            return count ;//对于其他更新方法update delete,返回影响的行数
        } catch (SQLException e) {
    
      
            e.printStackTrace();
        }finally {
    
      
            close(rs,psmt,conn);
        }
        return 0;
 }
  • 关于参数,对应执行更新方法,肯定需要SQL语句,和对应的sql里面问号的参数
public void updateFruitByFid(Fruit fruit) {
    
      
    String sql="update t_fruit set fname= ? ,price= ? , fcount= ? , remark= ? where fid = ?";
    super.executeUpdate(sql,fruit.getFname(),fruit.getPrice(),fruit.getFcount(),fruit.getRemark(),fruit.getFid());
}
public void deleteFruitByFid(int fid) {
    
      
     String sql="delete from t_fruit where fid = ?";
     super.executeUpdate(sql,fid);
}
public void addFruit(Fruit fruit) {
    
      
     String sql="insert into t_fruit values(0,?,?,?,?)";
     super.executeUpdate(sql,fruit.getFname(),fruit.getPrice(),fruit.getFcount(),fruit.getRemark());
}
  • 有了executeUpdate,我们实现数据库的操纵就更方便了,只需要写对应的sql语句和参数就可以

给一个对象进行设置属性值

  //通过反射技术给obj对象的property属性赋propertyValue值
    private void setValue(Object obj ,  String property , Object propertyValue){
    
      
        Class clazz = obj.getClass();
        try {
    
      
            //获取property这个字符串对应的属性名 , 比如 "fid"  去找 obj对象中的 fid 属性
            Field field = clazz.getDeclaredField(property);
            if(field!=null){
    
      
                field.setAccessible(true);
                field.set(obj,propertyValue);
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
    
      
            e.printStackTrace();
        }
    }
  • 利用反射技术,给一个对象设置属性,还是给我们的内部方法使用,主要也是为了实现复用,因为可以给不同的对象使用,设置不同的属性

通用查询方法1——核心方法——返回List

  //执行查询,返回List
protected List<T> executeQuery(String sql , Object... params){
    
      
        List<T> list = new ArrayList<>();
        try {
    
      
            conn = getConn() ;
            psmt = conn.prepareStatement(sql);
            setParams(psmt,params);
            rs = psmt.executeQuery();
            //元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,这些列分别叫什么是什么类型等等
            ResultSetMetaData rsmd = rs.getMetaData();//通过rs可以获取结果集的元数据
            //获取结果集的列数 也就是数据库中有多少个属性
            int columnCount = rsmd.getColumnCount();
            //6.解析rs
            while(rs.next()){
    
      
                T entity = (T)entityClass.newInstance();//创建了一个Fruit对象
                for(int i = 0 ; i<columnCount;i++){
    
      
                    String columnName = rsmd.getColumnName(i+1);
                    Object columnValue = rs.getObject(i+1);
                     //fid   fname   price  fcount  remark
                     //33    苹果      5
                    setValue(entity,columnName,columnValue);
                }
                list.add(entity);
            }
        } catch (SQLException e) {
    
      
            e.printStackTrace();
        } catch (IllegalAccessException e) {
    
      
            e.printStackTrace();
        } catch (InstantiationException e) {
    
      
            e.printStackTrace();
        } finally {
    
      
            close(rs,psmt,conn);
        }
        return list ;
    }
}

public List<Fruit> getFruitList(String keyword, Integer pageNO) {
    
      
        String sql="select * from t_fruit where fname like ? or remark like ? limit ?, 5";
        return super.executeQuery(sql,"%"+keyword+"%","%"+keyword+"%", (pageNO-1)*5);
}
  • 有了executeQuery,我们实现数据库的查询返回一个列表就更方便了,只需要写对应的sql语句和参数就可以

进行复杂查询——核心方法——返回Object[]

 //执行复杂查询,返回例如统计结果
    protected Object[] executeComplexQuery(String sql , Object... params){
    
      
        try {
    
      
            conn = getConn() ;
            psmt = conn.prepareStatement(sql);
            setParams(psmt,params);
            rs = psmt.executeQuery();

            //通过rs可以获取结果集的元数据
            //元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,什么类型等等
            ResultSetMetaData rsmd = rs.getMetaData();
            //获取结果集的列数
            int columnCount = rsmd.getColumnCount();
            Object[] columnValueArr = new Object[columnCount];
            //6.解析rs
            //因为我们执行的是复杂查询,比如select count(*) from t_fruit where fname like ? or remark like ?"类似
            //所以只会有一行数据
            if(rs.next()){
    
      
                for(int i = 0 ; i<columnCount;i++){
    
      
                    Object columnValue = rs.getObject(i+1);//获得对应的复杂查询结果的值 
                    columnValueArr[i]=columnValue;
                }
                return columnValueArr ;
            }
        } catch (SQLException e) {
    
      
            e.printStackTrace();
        } finally {
    
      
            close(rs,psmt,conn);
        }
        return null ;
    }
 @Override
    public int getFruitCount(String keyWord) {
    
      
        String sql="select count(*) from t_fruit where fname like ? or remark like ?";
//        Integer count= (Integer) super.executeComplexQuery(sql)[0];
        //43行报错    java.lang.Long cannot be cast to java.lang.Integer
        int count = ((Long) super.executeComplexQuery(sql,"%"+keyWord+"%","%"+keyWord+"%")[0]).intValue();
        return count;
    }

执行查询操纵——核心方法——只返回一个对象的信息

 //执行查询,返回单个实体对象
    protected T load(String sql , Object... params){
    
      
        try {
    
      
            conn = getConn() ;
            psmt = conn.prepareStatement(sql);
            setParams(psmt,params);
            rs = psmt.executeQuery();

            //通过rs可以获取结果集的元数据
            //元数据:描述结果集数据的数据 , 简单讲,就是这个结果集有哪些列,什么类型等等
            ResultSetMetaData rsmd = rs.getMetaData();
            //获取结果集的列数
            int columnCount = rsmd.getColumnCount();
            //6.解析rs
            if(rs.next()){
    
      
                T entity = (T)entityClass.newInstance();
                for(int i = 0 ; i<columnCount;i++){
    
      
                    String columnName = rsmd.getColumnName(i+1);  
                    Object columnValue = rs.getObject(i+1);     
                    setValue(entity,columnName,columnValue);
                }
                return entity ;
            }
        } catch (SQLException e) {
    
      
            e.printStackTrace();
        } catch (IllegalAccessException e) {
    
      
            e.printStackTrace();
        } catch (InstantiationException e) {
    
      
            e.printStackTrace();
        } finally {
    
      
            close(rs,psmt,conn);
        }
        return null ;
    }
public Fruit getFruitByFid(Integer fid) {
    
      
        String sql="select * from t_fruit where fid= ? ";
        return super.load(sql,fid);
}
  • 通过上面的编写,我们也大概知道为什么我们的框架中使用数据库,只需要写sql语句,而不需要我们写JDBC写大量的代码,因为这些框架在底层为我们实现了类似这些的封装,方便我们使用

关于Servlet的编写

项目优化——MVC思路引入

项目优化——ViewBaseServlet模板引擎的引入

public class ViewBaseServlet extends HttpServlet {
    
      

    private TemplateEngine templateEngine;

    @Override
    public void init() throws ServletException {
    
      

        // 1.获取ServletContext对象
        ServletContext servletContext = this.getServletContext();

        // 2.创建Thymeleaf解析器对象
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);

        // 3.给解析器对象设置参数
        // ①HTML是默认模式,明确设置是为了代码更容易理解
        templateResolver.setTemplateMode(TemplateMode.HTML);

        // ②设置前缀
        String viewPrefix = servletContext.getInitParameter("view-prefix");

        templateResolver.setPrefix(viewPrefix);

        // ③设置后缀
        String viewSuffix = servletContext.getInitParameter("view-suffix");

        templateResolver.setSuffix(viewSuffix);

        // ④设置缓存过期时间(毫秒)
        templateResolver.setCacheTTLMs(60000L);

        // ⑤设置是否缓存
        templateResolver.setCacheable(true);

        // ⑥设置服务器端编码方式
        templateResolver.setCharacterEncoding("utf-8");

        // 4.创建模板引擎对象
        templateEngine = new TemplateEngine();

        // 5.给模板引擎对象设置模板解析器
        templateEngine.setTemplateResolver(templateResolver);

    }

    protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
    
      
        // 1.设置响应体内容类型和字符集
        resp.setContentType("text/html;charset=UTF-8");

        // 2.创建WebContext对象
        WebContext webContext = new WebContext(req, resp, getServletContext());

        // 3.处理模板数据
        templateEngine.process(templateName, webContext, resp.getWriter());
    }
}
  • 有了这个类,就可以实现模板和业务数据的渲染功能

就举一个比较核心的例子index

  • index->indexServlet和index.html

这是我们的HTML页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="css/index.css">
    <script language="JavaScript" src="js/index.js"></script>
</head>
<body>
    <div id="div_container">
        <div id="div_fruit_list">
            <p class="center f30">欢迎使用水果库存后台管理系统</p>
            <div id="fc">
           		 <form th:action="@{/index}" method="post" id="search">
               		 <input type="hidden" name="oper" value="search"/>
               		 请输入查询关键字:<input type="text" name="keyword" th:value="${session.keyword}" style="height:24px;"/>
               		 <input type="submit" value="查询" class="btn">
            	</form>
                <div  id="add" >
                    <a th:href="@{/add.html}">添加新库存记录</a>
                </div>
            </div>
            <table id="tbl_fruit">
                <tr>
                    <th class="w20">名称</th>
                    <th class="w20">单价</th>
                    <th class="w20">库存</th>
                    <th>操作</th>
                </tr>
                <tr th:if="${#lists.isEmpty(session.FruitList)}">
                    <td colspan="4">对不起,库存为空!</td>
                </tr>
                <tr th:unless="${#lists.isEmpty(session.FruitList)}" th:each="fruit : ${session.FruitList}">
<!--                    <td ><a th:href="@{'/edit.do?fid='+${fruit.fid}}" th:text="${fruit.fname}">无</a></td>-->
                    <td><a th:text="${fruit.fname}" th:href="@{/edit.do(fid=${fruit.fid})}"></a></td>
                    <td th:text="${fruit.price}"></td>
                    <td th:text="${fruit.fcount}"></td>
<!--                    <td><img src="imgs/del.jpg" class="delImg" th:onlick="'delFruit('+${fruit.fid}+')' "/></td>-->
                    <td><img src="imgs/del.jpg" class="delImg" th:onclick="|delFruit(${fruit.fid})|"/></td>
                </tr>
            </table>
            <div style="padding-top: 4px">
                <input type="button" value="首 页"  class="btn" th:onclick="page(1)"                     				th:disabled="${session.pageNo==1}"/>
                <input type="button" value="上一页" class="btn" th:onclick="|page(${session.pageNo-1})|" th:disabled="${session.pageNo==1}" />

                <input type="button" value="下一页" class="btn" th:onclick="|page(${session.pageNo+1})|" th:disabled="${session.pageNo==session.pageCount} "/>
                <input type="button" value="尾 页"  class="btn" th:onclick="|page(${session.pageCount})|"th:disabled="${session.pageNo==session.pageCount} "/>

            </div>
        </div>
     </div>
</body>
</html>

首页的Servlet

//Servlet从3.0版本开始支持注解方式的注册
@WebServlet("/index")
public class IndexServlet extends ViewBaseServlet {
    
      
    private FruitDAO fruitDAO=new FruitDAOImpl();
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
        request.setCharacterEncoding("utf-8");
        HttpSession session=request.getSession();
        String oper = request.getParameter("oper");
            //如果oper不等于空,说明通过表单的查询按钮点击过来的 说明走的是查询
            //如果oper是空的,说明不是通过表单的查询按钮点击过来的
        String keyword=null;
        Integer pageNo=1;
        if(StringUtil.isNotEmpty(oper)&&"search".equals(oper)){
    
      
            //说明点击表单的查询发送过来的请求
            //此时pageNo应该还原为1,keyWord应该从请求参数中获取
            pageNo=1;
            keyword=request.getParameter("keyword");
            if(StringUtil.isEmpty(keyword)){
    
      
                keyword="";
                //因为我们的默认keyword是null,我们要拼接到sql语句中
                //如果是null,则会变成%null%,而""会变成%%,就是查出所有的数据
            }
            session.setAttribute("keyword",keyword);//将数据存储到session中,在一次会话范围内是有效的
        }else {
    
      
            //说明这次不是点击表单查询发送过来的请求(比如点击下面的上一页,下一页,或者直接访问/index)
            String pageNoStr= request.getParameter("pageNo");
            if(StringUtil.isNotEmpty(pageNoStr)){
    
      
                pageNo=Integer.parseInt(pageNoStr);
            }
            Object keywordObj = session.getAttribute("keyword");
            if(keywordObj!=null){
    
      
                keyword=(String)keywordObj;
            }else {
    
      
                keyword="";
                //如果是null,则会变成%null%,而""会变成%%,就是查出所有的数据
            }
        }
        //获取pageNo这一页的数据,并且是含有关键字keyword的
        List<Fruit> list=fruitDAO.getFruitList(keyword, pageNo);
        //获取库存中水果的记录条数
        Integer count=fruitDAO.getFruitCount(keyword);
        //总页数
        int pageCount=(count+5-1)/5;
         /*
        总记录条数       总页数
        1               1
        5               1
        6               2
        10              2
        11              3
        fruitCount      (fruitCount+5-1)/5
         */

        //保存到Session作用域中
        session.setAttribute("pageNo",pageNo);
        session.setAttribute("FruitList",list);
        session.setAttribute("pageCount",pageCount);
        //此处的视图名称是 index
        //那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
        //逻辑视图名称 :   index
        //物理视图名称 :   view-prefix + 逻辑视图名称 + view-suffix
        //所以真实的视图名称是:      /       index       .html
        super.processTemplate("index",request,response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
        doGet(request,response);
    }
}
  • @WebServlet(“/index”),说明在浏览器访问/web应用名/index,会访问这个类
  • doGet()方法,如果是用get方式访问,会定位到这个方法 doPost()方法, 如果用post访问,会定位到这个方法

在这里插入图片描述

其他的Servlet大概也是这种思路和页面结合起来,只是内在的逻辑是不一样的

image-20221215155455437

@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");
        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 "delete":
                delete(request,response);
                break;
            case "update":
                update(request,response);
                break;
            case "edit":
                edit(request,response);
                break;
            default:
                throw new RuntimeException("operate值非法");
        }
        */
        //反射形式
          //获取当前类中所有的方法
        Method methods[] = this.getClass().getDeclaredMethods();
        for (Method m:methods) {
    
      
            //获取方法的名称
            String methodName=m.getName();
            if(operate.equals(methodName)){
    
      
                try {
    
      
                    m.invoke(this,request,response);
                    return;
                } catch (IllegalAccessException e) {
    
      
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
    
      
                    e.printStackTrace();
                }
            }
        }
        throw new RuntimeException("operate值非法");
    }
    private void index(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
        HttpSession session=request.getSession();
        String oper = request.getParameter("oper");
        //如果oper不等于空,说明通过表单的查询按钮点击过来的
        //如果oper是空的,说明不是通过表单的查询按钮点击过来的
        String keyword=null;
        Integer pageNo=1;
        if(StringUtil.isNotEmpty(oper)&&"search".equals(oper)){
    
      
            //说明点击表单的查询发送过来的请求
            //此时pageNo应该还原为1,keyWord应该从请求参数中获取
            pageNo=1;
            keyword=request.getParameter("keyword");
            if(StringUtil.isEmpty(keyword)){
    
      
                keyword="";
                //因为我们的默认keyword是null,我们要拼接到sql语句中
                //如果是null,则会变成%null%,而""会变成%%,就是查出所有的数据
            }
            session.setAttribute("keyword",keyword);
        }else {
    
      
            //说明次数不是点击表单查询发送过来的请求(比如点击下面的上一页,下一页)
            String pageNoStr= request.getParameter("pageNo");
            if(StringUtil.isNotEmpty(pageNoStr)){
    
      
                pageNo=Integer.parseInt(pageNoStr);
            }
            Object keywordObj = session.getAttribute("keyword");
            if(keywordObj!=null){
    
      
                keyword=(String)keywordObj;
            }else {
    
      
                keyword="";
                //如果是null,则会变成%null%,而""会变成%%,就是查出所有的数据
            }
        }
        //获取pageNo这一页的数据
        List<Fruit> list=fruitDAO.getFruitList(keyword, pageNo);
        //获取库存中水果的记录条数
        Integer count=fruitDAO.getFruitCount(keyword);
        //总页数
        int pageCount=(count+5-1)/5;
         /*
        总记录条数       总页数
        1               1
        5               1
        6               2
        10              2
        11              3
        fruitCount      (fruitCount+5-1)/5
         */

        //保存到Session作用域中
        session.setAttribute("pageNo",pageNo);
        session.setAttribute("FruitList",list);
        session.setAttribute("pageCount",pageCount);
        //此处的视图名称是 index
        //那么thymeleaf会将这个 逻辑视图名称 对应到 物理视图 名称上去
        //逻辑视图名称 :   index
        //物理视图名称 :   view-prefix + 逻辑视图名称 + view-suffix
        //所以真实的视图名称是:      /       index       .html
        super.processTemplate("index",request,response);
    }
    private void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
       
    }
    private void delete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
      

    }
    private void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
      

    }
    private void edit(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
      
    }
  • 我们只写了一个Servlet,把不同的请求我们根据String operate = request.getParameter(“operate”)来从请求域中获得对应操纵类型,然后根据iperate的类型对应到不同的方法上

项目优化——dispatcherServlet引入

在这里插入图片描述

  • 虽然对于我们的Fruit相关的资源访问可以只定位到我们的一个Servlet上,我们的不同业务还是需要对应到不同的Servlet,比如Fruit和Order订单还是会定位到不同的Servlet上,如果业务多了,还是很复杂,所以可不可以所有的资源我们都定位到一个Servlet上,所以引入了dispatcherServelt,中文为中央控制器

applicationContext.xml——为了实现对象的id和对应class对象的连接起来

<?xml version="1.0" encoding="utf-8"?>
<beans>
    <!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
    <bean id="fruit" class="com.lsc.fruit.controller.FruitController"/>
</beans>
        <!--
        1.概念
        HTML : 超文本标记语言
        XML : 可扩展的标记语言
        HTML是XML的一个子集

        2.XML包含三个部分:
        1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
        2) DTD 文档类型定义
        3) XML正文
         -->

自己写的DispatcherServlet——提取视图资源处理通用代码

@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet {
    
      
    private Map<String,Object> beanMap=new HashMap<>();
    public void init() throws ServletException {
    
      
        super.init();
        try {
    
      
            // 读取xml文件中的数据
            
            InputStream inputStream=getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
            //创建DocumentBuilderFactory对象
            DocumentBuilderFactory documentBuilderFactory= DocumentBuilderFactory.newInstance();
            //创建DocumentBuilder对象
            DocumentBuilder documentBuilder=documentBuilderFactory.newDocumentBuilder();
            //创建Document对象
            Document document=documentBuilder.parse(inputStream);
            //获取所有的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=Class.forName(className).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();
        }
    }

    public DispatcherServlet() {
    
      

    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
        //设置编码
        request.setCharacterEncoding("utf-8");
        //1对获得的路径进行处理,以至于可以找到对应的类
        
        //假如我们的url是http://localhost:8080/JavaWebFruitSystemMVC/fruit.do
        //那么servletPath是 /hello.do
        //第一步将/hello.do->hello
        //第二步 hello->HelloController
        String servletPath=request.getServletPath();
        servletPath=servletPath.substring(1);
        int lastDonIndex=servletPath.lastIndexOf(".do");
        servletPath=servletPath.substring(0,lastDonIndex);
        Object controllerBeanObj = beanMap.get(servletPath);//对应到FruitController类的实例(也可以是其他类的对象,这只是个例子)
        String operate = request.getParameter("operate");//对应到FruitController的某个方法的方法名
        if(StringUtil.isEmpty(operate)){
    
      
            operate="index";
        }
        try {
    
      
            //利用反射获得对应的方法
            Method method=controllerBeanObj.getClass().getDeclaredMethod(operate,HttpServletRequest.class);
           
            if(method!=null){
    
      
                //2controller组件中的方法调用
                method.setAccessible(true);
               Object methodReturnObj= method.invoke(controllerBeanObj,request);//利用反射调用对应的方法,然后接收方法的返回值
               String methodReturnStr=(String) methodReturnObj;//通过返回值,判断是需要重定向还是进行视图处理
               //-------------------------------------------------
               //3视图处理
                if(methodReturnStr.startsWith("redirect:")){
    
      
                    //比如"redirect:fruit.do"
                     String redirectStr= methodReturnStr.substring("redirect:".length());
                     response.sendRedirect(redirectStr);//重定向
                }else {
    
      
                    super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
                }
                //---------------------------------------------------
            }else {
    
      
                throw new RuntimeException("operate值非法");
            }
        } catch (NoSuchMethodException e) {
    
      
            e.printStackTrace();
        } catch (IllegalAccessException e) {
    
      
            e.printStackTrace();
        } catch (InvocationTargetException e) {
    
      
            e.printStackTrace();
        }
//
    }
}

  • 首先我们的DispatcherServlet也是一个Servlet组件
    • @WebServlet(“*.do”)可以将所以以.do结尾资源拦截到这个Servlet
    • 我们的init()方法中主要是是读取对应的xml文件中的内容,为后续运行提供条件, 比如用beanMap<String,Object>就是将一个id字符串和一个实例对象建立联系
      • 第一次接收请求时,这个Servlet会进行实例化(调用构造方法)、初始化(调用init())、然后服务(调用service())
    • 我的Service方法中利用三步来实现HTTP请求来对应到一个Controller类的某个方法
      1. 对获得的路径进行处理,以至于可以利用beanMap找到对应的类的实例对象
      2. 使用反射对controller组件中的方法调用
      3. 利用方法的返回值进行视图处理
public class FruitController {
    
      
    private FruitDAO fruitDAO=new FruitDAOImpl();
    private String update(HttpServletRequest request) throws  IOException {
    
      
        //1设置编码
        request.setCharacterEncoding("utf-8");
        //2获取参数
        String fname=request.getParameter("fname");

        String priceStr=request.getParameter("price");
        Integer price=0;
        if(StringUtil.isNotEmpty(priceStr)){
    
      
            price=Integer.parseInt(priceStr);
        }

        String fcountStr=request.getParameter("fcount");
        Integer fcount=0;
        if(StringUtil.isNotEmpty(fcountStr)){
    
      
            fcount=Integer.parseInt(fcountStr);
        }

        String fidStr=request.getParameter("fid");
        int fid=0;
        if(StringUtil.isNotEmpty(fidStr)){
    
      
            fid=Integer.parseInt(fidStr);
        }
        String remark=request.getParameter("remark");
        //3执行更新
        fruitDAO.updateFruitByFid(new Fruit(fid,fname,price,fcount,remark));
        //资源跳转
        // response.sendRedirect("fruit.do");
        return "redirect:fruit.do";
    }
    private String edit(HttpServletRequest request) throws  IOException {
    
      
        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);
            return "edit";
        }
        return "error";
    }
}

  • 我们的FruitController不需要继承ViewBaseServlet,因为视图渲染交给我们的DispatcherServlet了

自己写的DispatcherServlet优化——统一获取参数以及视图处理

 protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
      
        //设置编码
        request.setCharacterEncoding("utf-8");
        //1对获得的路径进行处理,以至于可以找到对应的类
     
        //假如我们的url是http://localhost:8080/JavaWebFruitSystemMVC/hello.do
        //那么servletPath是 /hello.do
        //第一步将/hello.do->hello
        //第二步 hello->HelloController
        String servletPath=request.getServletPath();
        servletPath=servletPath.substring(1);
        int lastDonIndex=servletPath.lastIndexOf(".do");
        servletPath=servletPath.substring(0,lastDonIndex);
        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())){
    
      
                    //2统一获取请求参数
                    //2.1获取当前方法的参数,返回参数数组
                    Parameter []parameters=method.getParameters();
                    //2.2parameterValues用来承载参数的值
                    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 ;
                        }
                    }
                    //3controller组件中的方法调用
                    method.setAccessible(true);
                    Object methodReturnObj= method.invoke(controllerBeanObj,parameterValues);
                    //4视图处理
                    String methodReturnStr=(String) methodReturnObj;
                    if(methodReturnStr.startsWith("redirect:")){
    
      
                        //比如"redirect:fruit.do"
                        String redirectStr= methodReturnStr.substring("redirect:".length());
                        response.sendRedirect(redirectStr);//重定向
                    }else {
    
      
                        super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
                    }
                }
            }

//            else {
    
      
//                throw new RuntimeException("operate值非法");
//            }
        }catch (IllegalAccessException e) {
    
      
            e.printStackTrace();
        } catch (InvocationTargetException e) {
    
      
            e.printStackTrace();
        }
//
    }
  • 我的Service方法中利用四步来实现HTTP请求来对应到一个Controller类的某个方法——不需要我们方法Controller类中的方法去获取参数

    • 1对获得的路径进行处理,以至于可以利用beanMap找到对应的类的实例对象
      • 假如我们的url是http://localhost:8080/JavaWebFruitSystemMVC/fruit.do
      • 那么servletPath是 /fruit.do
      • 第一步将/fruit.do->fruit
      • 第二步 hello->FruitController
      • 第三步beanMap.get(FruitController)来获取FruitController的一个实例对象
      • 第四步通过request来获得对应operate的值,是为了知道对应FruitController的那个方法,比如update对应update方法
    • 2利用反射统一获取参数,因为参数的个数和类型不统一,所以设置参数需要判断逻辑
      • 获取当前方法的参数,返回参数数组parameters
      • parameterValues数组用来承载参数的值

    image-20221216021424611

    • 3使用反射对controller组件中的方法调用
      • 因为参数不确定,所以传入我们的parameterValues[]数组
    • 4利用方法的返回值进行视图处理
public class FruitController {
    
      

    private FruitDAO fruitDAO=new FruitDAOImpl();
    private String update(Integer fid,String fname,Integer price,Integer fcount,String remark)  {
    
      
        //3执行更新
        fruitDAO.updateFruitByFid(new Fruit(fid,fname,price,fcount,remark));
        //资源跳转
        // response.sendRedirect("fruit.do");
        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";
    }
}

  • 我们不需要再Controller方法中去获取参数,我们的业务方法只需要考虑业务功能如何实现,到底如何获取参数,如何进行资源的跳转都不需要管

小结

  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…

项目优化——Service层引入

什么是业务层

Model1和Model2

MVC : Model(模型)、View(视图)、Controller(控制器)

  • ​ 视图层:用于做数据展示以及和用户交互的一个界面

  • ​ 控制层:能够接受客户端的请求,具体的业务功能还是需要借助于模型组件来完成

  • ​ 模型层:模型分为很多种:有比较简单的pojo/vo(value object),有业务模型组件,有数据访问层组件

    • pojo/vo : 值对象

    • DAO : 数据访问对象

    • BO : 业务对象

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

  • DAO中的方法都是单精度方法或者称之为细粒度方法。什么叫单精度?一个方法只考虑一个操作,比如添加,那就是insert操作、查询那就是select操作…
  • BO中的方法属于业务方法,也实际的业务是比较复杂的,因此业务方法的粒度是比较粗的

注册这个功能属于业务功能,也就是说注册这个方法属于业务方法。

那么这个业务方法中包含了多个DAO方法。也就是说注册这个业务功能需要通过多个DAO方法的组合调用,从而完成注册功能的开发。

  • 注册:

    • 检查用户名是否已经被注册 - DAO中的select操作

    • 向用户表新增一条新用户记录 - DAO中的insert操作

    • 向用户积分表新增一条记录(新用户默认初始化积分100分) - DAO中的insert操作

    • 向系统消息表新增一条记录(某某某新用户注册了,需要根据通讯录信息向他的联系人推送消息) - DAO中的insert操作

    • 向系统日志表新增一条记录(某用户在某IP在某年某月某日某时某分某秒某毫秒注册) - DAO中的insert操作

image-20221216180114550

  • 以前我们的Contoller是直接向DAO层请求服务,现在是通过FruitService来请求服务

FruitDAO接口

public interface FruitDAO {
    
      
    //获取指定页码所有的库存列表信息 每页显示5条
    List<Fruit> getFruitList(String keyword,Integer pageNo);
  
}

FruitDAOImpl

public class FruitDAOImpl extends BaseDAO<Fruit> implements FruitDAO {
    
      
    @Override
    public List<Fruit> getFruitList(String keyword, Integer pageNO) {
    
      
        String sql="select * from t_fruit where fname like ? or remark like ? limit ?, 5";
        return super.executeQuery(sql,"%"+keyword+"%","%"+keyword+"%", (pageNO-1)*5);
    } 
}

FruitService接口

public interface FruitService {
    
      
    //获取指定页码所有的库存列表信息 每页显示5条
    List<Fruit> getFruitList(String keyword, Integer pageNo);
   
}

FruitServiceImpl

public class FruitServiceImpl implements FruitService {
    
      

    private FruitDAO fruitDAO=new FruitDAOImpl();//调用FruitDAO
    @Override
    public List<Fruit> getFruitList(String keyword, Integer pageNo) {
    
      
        return fruitDAO.getFruitList(keyword,pageNo);
    }
}

FruitController

public class FruitController {
    
      

    private FruitService fruitService=new FruitServiceImpl();//调用FruitService
    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="";
                //如果是null,则会变成%null%,而""会变成%%,就是查出所有的数据
            }
        }
        List<Fruit> list=fruitService.getFruitList(keyword, pageNo);
        Integer pageCount=fruitService.getFruitCount(keyword);
        session.setAttribute("pageNo",pageNo);
        session.setAttribute("FruitList",list);
        session.setAttribute("pageCount",pageCount);
        return "index";
    }
 
}

  • 虽然我们这里的代码,可以用Controller直接调用DAO层,因为Servcie层也没做什么工作,这是因为我们现在的业务逻辑还是很简单,但是我们必须要培养这样的思维

项目优化——Ioc思想引入

认识什么叫依赖/耦合

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

    private FruitDAO fruitDAO=new FruitDAOImpl();//调用FruitDAO
    @Override
    public List<Fruit> getFruitList(String keyword, Integer pageNo) {
    
      
        return fruitDAO.getFruitList(keyword,pageNo);
    }
}
  • 比如这串代码中,如果这个 FruitServiceImpl的实例对象能够正常被使用,那么必须也要有一个 FruitDAOImpl对象,也就是说 FruitServiceImpl的实例对象是依赖于 FruitDAOImpl对象的

代码改造

对应的xml配置文件

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

<beans>
    <!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
    <bean id="fruit" class="com.lsc.fruit.controller.FruitController">
        <!-- property标签用来表示属性;name表示属性名;ref表示引用其他bean的id值-->
        <property name="fruitService" ref="fruitService"></property>
    </bean>
    <bean id="fruitService" class="com.lsc.fruit.service.impl.FruitServiceImpl">
        <property name="fruitDAO" ref="fruitDAO"></property>
    </bean>
    <bean id="fruitDAO" class="com.lsc.fruit.dao.impl.FruitDAOImpl"></bean>

</beans>
<!--
    Node 节点
    Element 元素节点
    Text 文本节点
<sname>jim</sname>
-->
<!--
1.概念
HTML : 超文本标记语言
XML : 可扩展的标记语言
HTML是XML的一个子集

2.XML包含三个部分:
1) XML声明 , 而且声明这一行代码必须在XML文件的第一行
2) DTD 文档类型定义
3) XML正文
 -->
  • bean表示是一个对象,property,其实这么标签的名称可以随便设置,因为在后面自己IoC容器自己来识别,但是这样写是为迎合我们以后的Spring

自己写的IoC容器

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

package com.lsc.myssm.Ioc;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class ClassPathXmlApplicationContext implements BeanFactory {
    
      
    private HashMap<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的类
                    //创建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");
                    //String beanClass=beanElement.getAttribute("class");
                    NodeList beanChildNodeList=beanNode.getChildNodes();
                    for(int j=0;j<beanChildNodeList.getLength();j++){
    
      
                        Node beanChildNode=beanChildNodeList.item(j);
                        //确定是property属性
                        if(beanChildNode.getNodeType()==Node.ELEMENT_NODE&&"property".equals(beanChildNode.getNodeName())){
    
      
                            Element propertyElement=(Element) beanChildNode;
                            String propertyName=propertyElement.getAttribute("name");
                            String propertyRef=propertyElement.getAttribute("ref");
                            //找到ref对应的实例,也就是用id去我们的beanMap中寻找
                            Object refObj=beanMap.get(propertyRef);
                            //将我们的refObj设置到当前bean对应的实例的property属性上
                            Object beanObj=beanMap.get(beanId);//获得bean对应的实例对象
                            Class beanClazz=beanObj.getClass();//获得对应实例对象的类对象
                            Field propertyFiled= beanClazz.getDeclaredField(propertyName);
                            propertyFiled.setAccessible(true);
                            propertyFiled.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 getObject(String id){
    
      
        return beanMap.get(id);
    }
}

  • 其实就是把获取对象的步骤从DispatcherServlet交给了我们的IoC容器,而且把对象之间的依赖也交给了我们的IoC容器

看看引入Ioc容器之后我们的代码

public class FruitServiceImpl implements FruitService {
    
      
    private FruitDAO fruitDAO=null;
    @Override
    public int getFruitCount(String keyWord) {
    
      
        int count = fruitDAO.getFruitCount(keyWord);
        int pageCount = (count+5-1)/5 ;
        return pageCount;
    }
}

public class FruitController {
    
      
    private FruitService fruitService=null;
    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 FruitDAO fruitDAO=null; private FruitService fruitService=null;我们代码不需要自己去new一个对象来使用,而是在我们 写的ClassPathXmlApplicationContext利用反射进行设置

理解IOC - 控制反转 和 DI - 依赖注入

控制反转:

之前在Servlet中,我们创建service对象 , FruitService fruitService = new FruitServiceImpl();

  • 这句话如果出现在servlet中的某个方法内部,那么这个fruitService的作用域(生命周期)应该就是这个方法级别;
  • 如果这句话出现在servlet的类中,也就是说fruitService是一个成员变量,那么这个fruitService的作用域(生命周期)应该就是这个servlet实例级别

之后我们在applicationContext.xml中定义了这个fruitService。然后通过解析XML,产生fruitService实例,存放在beanMap中,这个beanMap在一个BeanFactory中

  • 因此,我们转移(改变)了之前的service实例、dao实例等等他们的生命周期。控制权从程序员转移到BeanFactory。这个现象我们称之为控制反转

依赖注入:

之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();

  • 那么,控制层和service层存在耦合。

之后,我们将代码修改成FruitService fruitService = null ;
然后,在配置文件中配置:

<bean id="fruit" class="FruitController">
     <property name="fruitService" ref="fruitService"/>
</bean>

为什么需要这样

  • 从我们现在的代码来看,确定好像没啥必要,因为虽然我们解决了fruitController和fruitService层之间存在的耦合,fruitService和FruitDAO之间的耦合,但是在我们的DispatcherServlet中出现了beanFactory=new ClassPathXmlApplicationContext(),因为我们把获取对象的功能从DispatcherServlet交给了我们的IoC容器,我们写这么多好像不还是出现了耦合
    • 这是因为我们的代码量还是小了,拿我们的fruitController和fruitService来说,现在我们的代码可能fruitController只是依赖fruitService,但是在实际开发中,我们的一个控制器可能会依赖上百个service层的功能,如果不让我们的IoC容器来负责依赖注入,那么逻辑非常复杂,我们程序员只是人,可能就会被绕晕,而且不使用Ioc容器,依赖程度也是很大的,其中一个依赖出问题,就会导致这个Controller是不能使用的

拿生活中例子

img

  • IoC(控制反转)思想依赖DI(依赖注入)形式,两者只是从不同的角度去描述同一个事情,随着软件规模的发展,我们的对象经济也不可避免的从小农经济发展到商品经济
  • IoC Container(Ioc容器):它是一个容器,放的是对象——就是一个对象市场,里面放置的就是待出售的对象
  • 作为对象的买方,我们只在代码的中声明我们的依赖(需要购买)哪些类型的对象,最终的对象就是通过IoC这套流程注入到我们的代码里(类似对象送货上门)

齿轮的例子来突出Ioc的重要

img

  • 描述的就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转。
  • 齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。所以提出了IoC思想

img

  • 由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

为什么要叫IoC(控制反转)

  • 软件系统在没有引入IOC容器之前,如图1所示,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。
  • 软件系统在引入IOC容器之后,这种情形就完全改变了,如图2所示,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。

IOC也叫依赖注入(DI)

  • 既然IOC是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入。于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现IOC的方法:注入。所谓依赖注入,就是由IOC容器在运行期间,动态地将某种依赖关系注入到对象之中。

项目优化——事务的引入

什么是事务

img

事务的概念

  • 事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部失败。
  • 在不同的环境中,都可以有事务。对应在数据库中,就是数据库事务。

事务的四大性质

  • 原子性,最核心的性质,一个事务中所有操作,要么全部执行成功,要么全部执行失败(执行失败就会通过rollbock回滚到执行这个事务操作之前的情况)
  • 持久性,一个事务执行完之后,这个事务对数据库的所有修改都有永久的效果,不会丢失
  • 一致性 ,一个事务在执行前后的数据都是一种合法性的状态(事务永远是从一个一致性状态到另一个一致性状态)
  • 隔离性,多个并发的事务访问数据库的时候,事务之间都是相互隔离的,一个事务不应该让其他事务干扰,不同事务之间相互隔离

事务引起的问题

  • 脏读,事务A在修改数据,事务B读取了事务A修改后的数据,但是事务A进行了回滚,前面修改的数据不算了,那么事务B读到的数据就是脏数据,这种情况就称脏读
  • 不可重复读,同一个事务在多次相同操作查询到数据不同,(在不同的查询时,其他事务的修改对本事务是可见的),比如select name from stu,我查询学生表所有学生的姓名,如果这时候有别的事务添加了几个学生的姓名,那么再select name from stu,得到的数据跟上次查询就不一样了
  • 幻读:就是每次查询得到的值是相同的,和其他事务隔离,其他事务的修改数据库对本事务都是不可见的,我们插入一个主键id,在别的事务中插入了信息,我们在本事务中是查询不到的,但是我们在本事务就是插入不进去这个主键

事务的三个操作

  • 开启事务 start transaction——>后面的多个sql语句就是一个整体

  • 回滚操作 rollback,回滚了上次对数据库做的修改

  • 提交事务 commit,就是把开启事务之后的所有SQL语句统一再数据库上进行持久化

JDBC开启事务的注意点:

  • 为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个

  • 线程并发情况下, 每个线程只能操作各自的 connection

关于确定事务的粒度

如果事务的粒度是DAO层

image-20221218143429437

  • 如果我们考虑是我们的事务开启的粒度是DAO层,那么这种方式带来的问题是如果我们的service中包含了三个DAO操作,如果DAO1执行成功—提交,DAO执行失败—回滚,DAO3执行成功—提交,那么我们此时的service操作是成功还是失败,我们的service的方法应该是业务方法,所以serivice对应的操作应该是整体,不能出现这种部分成功部分失败的问题,也就是我们上面提出的例子,比如两个人完成转账这种操作,转账这件事是一个业务操作,而张三扣款和李四到账这两件事是我们的DAO层操作,是一个单精度的操作,对应着张三扣款成功,李四账户增加金额失败,这种事情是不允许出现的
  • 因此得出结论是service是一个整体,要么都成功,要么都失败,事务管理不能以DAO层的单精度方法为单位,而是应该以业务层的方法为单位

image-20221218144721101

  • 如果我们在service层的每个方法都用try-catch这种方法去开启事务,那么会很麻烦,因为每个麻烦都要去写,我们可以利用过滤器Fliter去实现

image-20221218145636554

  • 我们用过滤器,因为我们的过滤器会拦截请求,会有一个放行操作,我们的service自然也被包含进去了,我们在放行操作之前进行开启事务,在放行操作之后提交事务,如果发生了异常就catch住,在catch里面进行回滚操作
  • 关于事务的单位解决了,现在就是解决我们必须要保证我们不同的DAO操作的connection必须是同一个,这样才能保证三个操作是处于同一个事务

代码改造——保证是操作都是同一个Connection

数据库连接的工具类

public class ConnUtil {
    
      
    private static ThreadLocal<Connection> threadLocal=new InheritableThreadLocal<>();
    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 = "245281" ;
    public static Connection getConnection(){
    
      
        Connection connection=threadLocal.get();
        if(connection==null){
    
      
          connection=createConn();
          threadLocal.set(connection);
        }
        return threadLocal.get();
    }

    private static Connection createConn() {
    
      
        try {
    
      
            Class.forName(DRIVER);
            return DriverManager.getConnection(URL,USER,PWD);
        } catch (ClassNotFoundException | SQLException e) {
    
      
            e.printStackTrace();
        }
        return null;
    }
    public static  void closeConnection() throws SQLException {
    
      
        Connection connection=threadLocal.get();
        if(connection==null){
    
      
            return;
        }
        if(!connection.isClosed()){
    
      
            connection.close();;
            threadLocal.set(null);
        }
    }
}

  • 这个工具类提供了获得数据库连接和关闭连接的功能,这里的获得连接保证获得的是同一个连接(对于同一次请求)
    • 以前我们的操作,在进行一个DAO操作之前获得一个连接,是用完一个DAO层次的操作,就会关闭这个连接,但是我们现在要保证同一次请求使用的Connection是同一个,所以一次DAO操作用完,不能像以前一样直接关闭,关于如何关闭,看下面

事务管理的工具类

public class TransactionManger {
    
      
    //开启事务
    public static void beginTrans() throws SQLException {
    
      
        ConnUtil.getConnection().setAutoCommit(false);//关闭事务的自动提交
    }
    //提交事务
    public static void commitTrans() throws SQLException {
    
      
        Connection connection = ConnUtil.getConnection();
        connection.commit();
        ConnUtil.closeConnection();
    }
    //回滚事务
    public static void rollback() throws SQLException {
    
      
        Connection connection = ConnUtil.getConnection();
        connection.rollback();
        ConnUtil.closeConnection();

    }
}

  • 当提交事务或者是回滚事务的时候,进行关闭连接

对应的Filter

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 {
    
      
            TransactionManger.beginTrans();
            System.out.println("开启事务");
            filterChain.doFilter(servletRequest,servletResponse);//放行
            TransactionManger.commitTrans();
            System.out.println("提交事务");
        } catch (SQLException throwables) {
    
      
            throwables.printStackTrace();
            try {
    
      
                TransactionManger.rollback();
                System.out.println("回滚事务");
            } catch (SQLException e) {
    
      
                e.printStackTrace();
            }
        }
    }

    @Override
    public void destroy() {
    
      

    }
}
  • 通过这个Filter就实现了包围service,进行开启和提交或者回滚事务的操作

解决关于如何让异常可以被Filter接收

之前DAO层的操作

 //执行更新,返回影响行数
    protected int executeUpdate(String sql , Object... params){
    
      
        boolean insertFlag = false ;
        insertFlag = sql.trim().toUpperCase().startsWith("INSERT");
        try {
    
      
            conn = getConn();
            if(insertFlag){
    
      
                psmt = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
            }else {
    
      
                psmt = conn.prepareStatement(sql);
            }
            setParams(psmt,params);
            int count = psmt.executeUpdate() ;
            if(insertFlag){
    
      
                rs = psmt.getGeneratedKeys();
                if(rs.next()){
    
      
                    return ((Long)rs.getLong(1)).intValue();
                }
            }

            return count ;
        } catch (SQLException e) {
    
      
            e.printStackTrace();
        }finally {
    
      
            close(rs,psmt,conn);
        }
        return 0;
    }
  • 我们发现我们在DAO层对抛出异常是直接捕获,那么我们的Filter的catch肯定就捕获不到异常,那么也就不能正常的进行事务的回滚,所以我们必须想办法把异常抛到我们的OpenSessionInViewFilter

改进后的操作

 //执行更新,返回影响行数
    protected int executeUpdate(String sql , Object... params){
    
      
        boolean insertFlag = false ;
        insertFlag = sql.trim().toUpperCase().startsWith("INSERT");
        try {
    
      
            conn = getConn();
            if(insertFlag){
    
      
                psmt = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
            }else {
    
      
                psmt = conn.prepareStatement(sql);
            }
            setParams(psmt,params);
            int count = psmt.executeUpdate() ;
            if(insertFlag){
    
      
                rs = psmt.getGeneratedKeys();
                if(rs.next()){
    
      
                    return ((Long)rs.getLong(1)).intValue();
                }
            }
            return count ;
        } catch (SQLException e) {
    
      
            e.printStackTrace();
            throw new DAOException("BaseDAO executeUpdate出错了");
        }
    }
//定义了一个运行时异常
public class DAOException extends RuntimeException{
    
      
    public DAOException(String msg){
    
      
        super(msg);
    }
}
  • 我们定义了一个运行时异常,如果发生异常,我们对异常进行catch,在catch我们再抛出一个运行时异常,那么上层就可以收到我们的异常,也就可以进行回滚
  • DAO——>Service——>Controller——>DispatherServlet——>Filter,所以在这条路径上,如果像捕获异常,我们就用这样抛出运行时异常的方法,一步步抛到我们的Filter上,让其捕获

项目优化——监听器的引入

之前的代码

@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet  {
    
      
    private BeanFactory beanFactory ;
    public void init() throws ServletException {
    
      
        super.init();
        beanFactory=new ClassPathXmlApplicationContext();
 }
  • 在我们之前DispatcherServlet中,在DispatcherServlet初始化init的时候,才会初始化Ioc容器
  • 我们应该在项目启动的时候就将Ioc容器初始化

对应的上下文监听器

//监听上下文启动,在上下文启动的时候去创建IOC容器,将其保存application的作用域中
//在后面我们的DispatcherServlet需要的时候,直接从application作用域中取出
@WebListener
public class ContextLoaderListener implements ServletContextListener {
    
      
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
    
      
        //1获取ServletContext对象
        ServletContext application=servletContextEvent.getServletContext();
        //2获取上下文的初始化参数
        String path=application.getInitParameter("contextConfigLocation");
        //从我们的web.xml文件中找出我们配置bean对象信息的xml文件位置
        //3创建Ioc容器
        BeanFactory beanFactor=new ClassPathXmlApplicationContext(path);
        //4将Ioc容器保存到application作用域
        application.setAttribute("beanFactory",beanFactor);

    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    
      

    }
}

对应的xml文件

 <context-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>applicationContext.xml</param-value>
   </context-param>
  • 我们在web.xml文件中去配置我们bean对象信息的xml配置文件

总结

在这里插入图片描述