JAVAWEB(五)过滤器,事务管理

文章目录

  • 一. Filter
    • 简介
    • 过滤器的结构
    • 创建过滤器的步骤
    • FilterChain接口
      • 如何建立过滤器链
    • 应用
      • 强转???????????
  • 二. 事务管理-操作失败进行回滚
    • 前言
    • 前置知识
      • 难点-同一个数据库连接
      • OpenSessionInViewFilter类
      • TransactionManager类
      • ThreadLocal类
        • 共用数据库连接的机制
        • 在TransactionManager中引入ThreadLocal类
      • ConnUtil类
    • 开始融合
      • 进一步封装ConnUtil类
      • 事务的传播机制
      • BaseDAO
      • 关闭资源
      • 关闭连接
      • 使用OpenSessionInViewFilter进行事务管理
      • 异常体系的搭建
        • 自定义运行时异常
    • ThreadLocal类源码
      • set方法
      • get方法
  • 三. 监听器
      • ServletContextListener
        • 应用-在监听器中提前实例化IOC容器
        • 优化IOC容器
  • 四. 水果系统项目的回顾

继续后端

一. Filter

简介

发送到服务器端的请求要先经过过滤器
JAVAWEB(五)过滤器,事务管理_第1张图片
①拦截
过滤器之所以能够对请求进行预处理,关键是对请求进行拦截,把请求拦截下来才能够做后续的操作。而且对于一个具体的过滤器,它必须明确它要拦截的请求,而不是所有请求都拦截。

②过滤
根据业务功能实际的需求,看看在把请求拦截到之后,需要做什么检查或什么操作,写对应的代码即可。

③放行
过滤器完成自己的任务或者是检测到当前请求符合过滤规则,那么可以将请求放行。所谓放行,就是让请求继续去访问它原本要访问的资源。

JAVAWEB(五)过滤器,事务管理_第2张图片
不只是请求,响应也要经过filter

过滤器的结构

从头分析一遍
(1)注解@webFilter,servlet的注解是@webServlet,看起来差不多。后面这个标签要和关联的servlet的标签一样

在上面那张图里,filter是放在servlet之前的,可以说filter和servlet成对出现。只有标签一样,才能说这俩成对有关系

(2)一个类要成为filter,他得去实现javax.servlet.Filter这个接口
(3)实现接口之后,有三个方法要重写,init方法,doFilter方法,destroy方法。这个结构和servlet很像,init,service,destroy
(4)拦截,过滤和放行都是在doFilter方法中实现,所谓的拦截,其实就是在调用servlet之前,先调用filter
(5)doFilter在参数上和service有区别
doFilter用的是servletRequest和servletResponse
service用的是HttpServletRequest和HttpServletResponse

servletRequest接口是HttpServletRequest接口的父接口
servletResponse接口是HttpServletResponse接口的父接口
(6)FilterChain也是接口,其实现类的实例调用doFilter方法实现放行,这个doFilter方法是FilterChain自己定义的

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
//@WebFilter("/demo01.do")
@WebFilter("*.do")
public class Demo01Filter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("helloA");
        //放行
        filterChain.doFilter(servletRequest,servletResponse);
        System.out.println("helloA2");
    }

    @Override
    public void destroy() {

    }
}

前面也说了,不只是请求,服务器端的响应也要经过过滤器,也就是说,请求先被拦截,打印helloA,然后放行,执行servlet的service方法,响应传回给客户端之前,响应被过滤器拦截,打印helloA2,最后,响应传到客户端

创建过滤器的步骤

  1. Filter也属于Servlet规范

  2. Filter开发步骤:新建类实现Filter接口,然后实现其中的三个方法:init、doFilter、destroy
    配置Filter,可以用注解@WebFilter,也可以使用xml文件

  3. Filter在配置时,和servlet一样,也可以配置通配符,例如 @WebFilter(“*.do”)表示拦截所有以.do结尾的请求

  4. 过滤器链
    1)执行的顺序依次是: A B C demo03 C2 B2 A2
    2)如果采取的是注解的方式进行配置,那么过滤器链的拦截顺序是按照全类名的先后顺序排序的
    3)如果采取的是xml的方式进行配置,那么按照配置的先后顺序进行排序

FilterChain接口

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

FilterChain有非常实际的应用场景,在调用控制器之前,需要对请求进行多层过滤

如何建立过滤器链

每个过滤器的设置都是一样的,标签都和配对的servlet一样

过滤器的顺序由全类名的先后顺序决定,也就是过滤器的名字按照字典序排列。这有点好笑,我估计底层是获取了所有的过滤器名,然后排了序

JAVAWEB(五)过滤器,事务管理_第3张图片
如果采取的是注解的方式进行配置,那么过滤器链的拦截顺序是按照全类名的先后顺序排序的

如果采取的是xml的方式进行配置,那么按照配置的先后顺序进行排序

应用

JAVAWEB(五)过滤器,事务管理_第4张图片
之前中央控制器要负责请求的转码工作,现在转码可以交给过滤器去做

过滤器里边也可以写配置文件,设置一些初始化参数。之前在servlet中做过,有init-param,context-param

初始化参数存在FilterConfig实例中,妈的,这个FilterConfig也是一个接口

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

    private String encoding = "UTF-8";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String encodingStr = filterConfig.getInitParameter("encoding");
        if(StringUtil.isNotEmpty(encodingStr)){
            encoding = encodingStr ;
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ((HttpServletRequest)servletRequest).setCharacterEncoding(encoding);
        filterChain.doFilter(servletRequest,servletResponse);

    }

    @Override
    public void destroy() {

    }
}

强转???????????

在doFilter方法中,我们看到这么一句(HttpServletRequest)servletRequest,将一个ServletRequest实例强转为HttpServletRequest实例,但是ServletRequest是HttpServletRequest的父接口啊,这里我就觉得很别扭

但是,这里用了多态,我们看不到接口的实现类,于是我去tomcat提供的servlet API里面去找,还真让我找着了

ServletRequest接口的实现类是ServletRequestWrapper
HttpServletRequest接口的实现类是HttpServletRequestWrapper

ServletRequestWrapper是HttpServletRequestWrapper的父类

public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest{}

即便是找到了实现类,父类对象强转为子类对象,我印象中应该是不行的,我模仿这个结构试了一下,报错了com.atguigu.java.Father cannot be cast to com.atguigu.java.Son,父类不能强转为子类

不清楚这里为啥可以

我突然想到是为啥可以强转了

在这里,形参+实参是
ServletRequest servletRequest = HttpServletRequestWrapper req
(HttpServletRequest)servletRequest

但这里的关系是真的复杂,强转为子接口类型还真是没听说过
父接口的引用 = 子接口的实现类的实例
将父接口的引用强转为子接口类型

二. 事务管理-操作失败进行回滚

前言

DML,数据操作语言,也就是增删改查
定义:一组逻辑操作单元,也就是一个或多个DML操作

原则:保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式
(1)要么所有的事务都被提交(commit),那么这些修改就永久地保存下来
(2)要么数据库管理系统将放弃所作的所有修改,整个事务回滚(rollback)到最初状态(回滚默认情况下是回滚到最近的一次DML操作,每进行一次DML都会默认提交一次,这在之前批量插入查询语句时碰到过)

DDL数据定义语言:
create alter(质变,而非change所指的表象上的变化) drop rename truncate(清空)

不可回滚的操作
(1)DDL操作一旦执行,都会自动提交。
set autocommit = false 对DDL操作失效
(2)DML默认情况下,一旦执行,就会自动提交。
我们可以通过set autocommit = false的方式取消DML操作的自动提交。
(3)默认在关闭连接时,会自动的提交数据

前置知识

JAVAWEB(五)过滤器,事务管理_第5张图片
在JDBC的课程中,讲到事务管理的时候,还没有引入DAO的概念。当时只是实现了增删改查的通用操作,然后在一个test方法中实现了事务管理。当时举的例子是,将两个改操作合并为一个事务

如果引入DAO的概念,那么这个事务就可以作为DAOImpl中的一个方法

由此,我们再看上面的图,所谓的DAO1, DAO2, DAO3可以看做是DAOImpl中的三个方法,这三个方法相当于是3个事务

但是,在引入业务层之后,上面的结构就不再适用。因为事务的范围改变了,扩大了,业务层的一个方法才能称之为一个事务,业务层方法调用的任何一个DAO方法执行失败,则该事务失败,全部都要回滚

具体该怎么做呢???业务层和DAO层都加上事务管理吗??

JAVAWEB(五)过滤器,事务管理_第6张图片
最终解决方案
JAVAWEB(五)过滤器,事务管理_第7张图片
将事务管理放到了过滤器??

前面说了,会话层的一个方法就是一个事务,而会话层的一个方法其实就是早期的servlet,比如说AddServlet,EditServlet,DelServlet,UpdateServlet等,一个请求不就对应了一个servlet吗,不就对应了会话层的一个方法了,那么一个请求不就对应了一个事务吗!!!!

离请求最近的不就是过滤器吗,那我用一个过滤器来做事务管理不就很合理了吗

难点-同一个数据库连接

会话层的一个业务方法可能调用DAOImpl中的多个方法,而DAOImpl中的每个方法独立使用数据库连接,现在我们希望调用的多个DAOImpl方法共用一个数据库连接

OpenSessionInViewFilter类

前面已经说了使用一个过滤器来负责事务管理

OpenSessionInView这个词直译过来是在视图中打开会话,很抽象,我查了一下,还有一种说法是 open session也就是open request, 这个session存在于一个请求-响应周期内,是否可以理解为延长了生命周期啥的,不是很清楚

@WebFilter("*.do")
public class OpenSessionInViewFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {}

    @Override
    public void destroy() {}
}

在doFilter方法中进行事务管理的相关操作

TransactionManager类

我们将事务管理的具体细节封装到TransactionManager类中,其结构如下

JAVAWEB(五)过滤器,事务管理_第8张图片

ThreadLocal类

这个类来自于java.lang包

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只能在指定的线程中获取存储的数据,对于其他线程来说则是无法获取到的

感觉上也可以把她当做容器,但是他和线程应该有关系,更靠近线程

引入ThredLocal类的目的是为了实现一个事务共用一个数据库连接的需求

共用数据库连接的机制

我们获取连接时只从ThreadLocal实例中获取,如果没有拿到,就造一个连接放到ThreadLocal实例中,然后再从ThreadLocal实例中取出数据库连接

这样就保证了事务执行过程中只有一个连接

在TransactionManager中引入ThreadLocal类

JAVAWEB(五)过滤器,事务管理_第9张图片

ConnUtil类

JAVAWEB(五)过滤器,事务管理_第10张图片
上面的代码else部分就需要获取数据库连接了

我们将获取数据库连接的方法封装到了BaseDAO中,要想获取连接,就必须实例化XxxDAOImpl类,因此我们将获取数据库连接的操作封装到另外一个工具类ConnUtil类

ConnUtil类负责和连接相关的操作
JAVAWEB(五)过滤器,事务管理_第11张图片
于是TransactionManager就可以调用ConnUtil中获取数据库连接的方法来开启事务
JAVAWEB(五)过滤器,事务管理_第12张图片
同样的,提交事务和回滚事务也可完成
JAVAWEB(五)过滤器,事务管理_第13张图片

开始融合

进一步封装ConnUtil类

回顾上面TransactionManager中的代码,我们可以看到获取数据库连接的部分还是有重复代码,虽然已经封装了通过DriverManager获取连接的操作

因此,将重复代码封装到ConnUtil类中

(1)将通过DriverManager获取连接封装为一个方法
(2)从ThreadLocal实例获取连接封装为一个方法

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

public static final String DRIVER
public static final String URL
public static final String USER
public static final String PWD

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(){
    Connection conn = threadLocal.get();
    if(conn==null){
        conn =createConn();
        threadLocal.set(conn);
    }
    return threadLocal.get() ;
}

于是TransactionManager中的结构变成

public class TransactionManager {

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

    //提交事务
    public static void commit() throws SQLException {
         ConnUtil.getConn().commit();
    }

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

事务的传播机制

前面说了,一个业务就是一个事务,那如果两个业务组合呢,他们算一个事务还是两个事务,还是怎么着??

但是先不考虑这么复杂

BaseDAO

请注意,在引入ConnUtil类之后,BaseDAO里面的结构变化

我们的目的是保证一个事务执行过程中只有一个数据库连接,此时BaseDAO中获取数据库连接的操作已经被封装到了ConnUtil类,但是BaseDAO中通用的增删改查方法仍然是调用BaseDAO中获取数据库连接的方法,因此BaseDAO中获取数据库连接的方法要修改,他直接调用ConnUtil类中获取数据库连接的方法,实质上就是要从ThreadLocal实例中取出数据库连接

protected Connection getConn(){return ConnUtil.getConn();}

这一套改下来,实质上已经实现同一个事务,同一个连接

关闭资源

再次回顾BaseDAO中的代码,通用的增删改查方法内部调用了BaseDAO中关闭资源的方法,关键在于他把数据库连接给关掉了,假如我的事务包含两个查询,第一个查询结束后就把连接关了,这样就破坏了同一个事务,同一个连接这一条件

因此,将BaseDAO中关闭资源的方法的方法体置空

protected void close(ResultSet rs , PreparedStatement psmt , Connection conn){}

关闭连接

我们还是需要关闭连接的,但是前面说了我们获取连接时只从ThreadLocal实例中获取,因此,在引入ThreadLocal背景之下,关闭连接实际上是要处理掉存储在ThreadLocal实例里面的数据库连接

跟连接相关的操作还是封装到ConnUtil类中

从程序来看,还是先从ThreadLocal实例中取出连接,再看这个连接是否关闭,没关闭就关闭,然后将ThreadLocal实例中的Connection类型的属性设为null

public static void closeConn() throws SQLException {
    Connection conn = threadLocal.get();
    if(conn==null){
        return ;
    }
    if(!conn.isClosed()){
        conn.close();
        threadLocal.set(null);
    }
}

之后就是在TransactionManager中关闭连接

//提交事务
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();
}

使用OpenSessionInViewFilter进行事务管理

完成TransactionManager的编写之后,就可以完成过滤器的编写了

@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() {

    }
}

异常体系的搭建

到目前为止,过滤器,中央控制器,控制器,业务层,DAO,我们大量使用try catch,带来的直接问题就是一旦出现异常,try catch在异常出现的位置就把他处理了,我们不清楚到底是哪一层的那个位置出了问题

我们希望建立这样一种异常体系,如果DAO层出现异常,那么应该有提示说DAO层出现了异常

尝试着在XxxDAOImpl里边抛出DAO层的异常
JAVAWEB(五)过滤器,事务管理_第14张图片
自定义DAO层异常

public class DAOException extends RuntimeException{
    public DAOException(String msg){
        super(msg);
    }
}

自定义运行时异常

值得注意的是,他是一个运行时异常

运行时异常有特定用途 - 它们表示编程问题只能通过更改代码来解决,而不是更改程序运行的环境

一般来说,需要抛出运行时异常的情况有两类:

(1)传递无效参数值 - 这是运行时异常的最常见原因。大多数参数验证异常应该是运行时异常。 Java 提供了几个子类来表示这些特定问题。
(2)以错误的顺序调用方法 - 这是另一个常见原因。当某些方法在类完成初始化或其他一些准备步骤之前无法调用时,在错误的时间调用会导致运行时异常。

但是,这种做法的问题在于有很多重复代码,XxxDAOImpl中的每一个方法都需要去抛DAO异常

反正XxxDAOImpl也是调用父类BaseDAO中的增删改查,那么就在父类里边抛DAO异常呗,BaseDAO中的方法数量是固定的

//执行更新,返回影响行数
protected int executeUpdate(String sql , Object... params) {
    boolean insertFlag = false ;
    insertFlag = sql.trim().toUpperCase().startsWith("INSERT");

    conn = getConn();
    try{
        
    }catch (SQLException e){
        e.printStackTrace();
        throw new DAOException("BaseDAO executeUpdate出错了");
    }
}

控制器里边都是调用业务层的方法,没有try catch异常

中央控制器里边倒是有try catch

ThreadLocal类源码

前面说了,这个类来自于java.lang

直译过来是本地线程,他也确实和线程有关。如果你在线程1中向ThreadLocal实例存了一个变量,那么在线程2中你是无法从同一个ThreadLocal实例获取到这个变量

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只能在指定的线程中获取存储的数据,对于其他线程来说则是无法获取到的

set方法

只能存一个变量,所以说不需要setXxx,直接set就行了

源码

public void set(T value) {
    Thread t = Thread.currentThread(); //获取当前的线程
    ThreadLocalMap map = getMap(t);    //每一个线程都维护各自的一个容器(ThreadLocalMap)
    if (map != null)
        map.set(this, value);          //这里的key对应的是ThreadLocal,因为我们的组件中需要传输(共享)的对象可能会有多个(不止Connection)
    else
        createMap(t, value);           //默认情况下map是没有初始化的,那么第一次往其中添加数据时,会去初始化
}

ThreadLocalMap类是ThreadLocal类的静态内部类

static class ThreadLocalMap{}

如果说同一个线程内有多个ThreadLocal,他们共用同一个ThreadLocalMap容器

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就可以获取到工具箱了
            return result;
        }
    }
    return setInitialValue();
}

entry在集合中出现过,在计算机领域可以译为项,一项,两项的项,好像就是表示键值对

三. 监听器

监听器的种类很多,其实就跟咱们的任务管理器一样,对相关组件都要进行监听

tomcat容器中有3个map容器(或者叫域对象)。HttpServletRequest,HttpSession,ServletContext

ServletRequest是HttpServletRequest的父接口,他们的实现类由tomcat提供,都加上了后缀Wrapper

1) ServletContextListener - 监听ServletContext对象的创建和销毁的过程(initialize,destroy)
2) HttpSessionListener - 监听HttpSession对象的创建和销毁的过程
3) ServletRequestListener - 监听ServletRequest对象的创建和销毁的过程

4) ServletContextAttributeListener - 监听ServletContext的保存作用域的改动(add,remove,replace)
5) HttpSessionAttributeListener - 监听HttpSession的保存作用域的改动(add,remove,replace)
6) ServletRequestAttributeListener - 监听ServletRequest的保存作用域的改动(add,remove,replace)

7) HttpSessionBindingListener - 监听某个对象在Session域中的创建与移除
8) HttpSessionActivationListener - 监听某个对象在Session域中的序列化和反序列化

ServletContextListener

和之前自定义过滤器一样,自定义的监听器需要去实现上面的接口

以 ServletContextListener 为例,它是javax.servlet下的一个接口,继承了java.util下的接口EventListener

public interface EventListener {
}

public interface ServletContextListener extends EventListener {
    void contextInitialized(ServletContextEvent var1);

    void contextDestroyed(ServletContextEvent var1);
}

//@WebListener
public class MyServletContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("Servlet上下文对象初始化动作被我监听到了....");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("Servlet上下文对象销毁动作被我监听到了.....");
    }
}

配置也相对简单,有两种方法:
(1)注解配置@WebListener
(2)在web.xm中配置,不需要像过滤器,serlet那样配置mapping,直接给出自定义监听器的位置即可

<listener>
    <listener-class>com.atguigu.listener.MyServletContextListenerlistener-class>
listener>

应用-在监听器中提前实例化IOC容器

我们在中央控制器重写的init方法中实例化了IOC层,但是我们希望将实例化的时间提前,提前到ServletContext初始化的时候,ServletContext容器是随着tomcat的启动而创建。这一做法的目的是提高系统的相应速度

前面已经看了监听器的结构

public interface ServletContextListener extends EventListener {
    void contextInitialized(ServletContextEvent var1);

    void contextDestroyed(ServletContextEvent var1);
}

到这儿,我们才知道这东西应该怎么用。我们自定义的监听器需要重写上面这两个方法,就从方法的角度讲,就是说当我们监听到ServletContext初始化的时候,可以执行我们重写的contextInitialized方法中的逻辑,比如说我们就可以实例化IOC层

但是,这样一来,中央控制器是没有办法拿到IOC实例的。难道说我们还要实例化自定义的监听器吗???没有这么麻烦,请注意ServletContext也是一个容器,而且他的作用域是当前的web应用,只要tomcat服务器开着,他就一直存在。所以,我们将IOC实例存到ServletContext容器里边,然后在中央控制器里把IOC实例从ServletContext容器中取出来

于是,我们先自定义一个监听器

注意看contextInitialized方法的参数,表示当ServletContext容器初始化的时候会捕获到一个ServletContextEvent实例传到contextInitialized方法

ServletContextEvent也是一个类,来自javax.servlet,继承EventObject类public class ServletContextEvent extends EventObject

EventObject类,来自java.util,实现了java.io.Serializable接口

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

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

然后中央控制器再从application作用域中去获取IOC容器

private BeanFactory beanFactory ;

public 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容器获取失败!");
    }
}

优化IOC容器

在原版的IOC容器中,我们在程序中硬编码了.xml配置文件的文件名,一般来说,我们希望这些第三方API都要尽可能隐藏起来

于是我们修改了IOC层的ClassPathXMLApplicationContext类,他增加了一个空参构造器,其内部调用的还是那个带参构造器

但奇怪的是为什么要有一个String类型的属性path,感觉它没什么作用. 无参构造器里边也不能使用这个path,因为使用无参构造器的时候,path还没有产生

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

IOC加了带参构造器后,在监听器里边实例化IOC容器的时候就可以往里边传入.xml配置文件的位置

//监听上下文启动,在上下文启动的时候去创建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) {

    }
}

之前已经学过,三大容器,ServletContext,HttpServletRequest,HttpSession都可以配置初始化参数,于是我们可以在ServletContext容器的初始化参数中加上IOC配置文件的位置
在web.xml文件中配置

<context-param>
    <param-name>contextConfigLocationparam-name>
    <param-value>applicationContext.xmlparam-value>
context-param>

四. 水果系统项目的回顾

这是我目前写过最复杂的项目了,我之前的贪吃蛇也不过5,600行,而且逻辑很简单

JAVAWEB(五)过滤器,事务管理_第15张图片
JAVAWEB(五)过滤器,事务管理_第16张图片
还有一堆依赖:
(1)tomcat
(2)用于渲染的thymeleaf
(3)用于DAO的mysql
(4)用于数据库连接池的druid(德鲁伊)

JAVAWEB(五)过滤器,事务管理_第17张图片

你可能感兴趣的:(编程语言,servlet,java,开发语言)