JavaWeb知识点汇总(Tomcat、Servlet、MVC架构、模拟IOC、Filter、Listener)

CS、BS架构模式

创建Web项目

介绍

Tomcat是Web容器 。可以将一个或多个Web项目 部署在tomcat服务器中。

Tomcat目录:

创建web项目步骤

  1. 新建一个project项目 pro-web

    项目project和模块module的区别:

    project 通常指一个完整的软件开发实体,它可以包含多个模块、库、配置文件以及其他相关的资源。Project是一个不具备任何编码设置、构建等开发功能的概念 ,其主要作用就是起到一个项目定义、范围约束、规范类型的效果。我们也可以简单地理解Project就是一个单纯的目录 ,只是这个目录在命名上必须有其代表性的意义。如果新建了一个项目(project),默认会有一个同名的模块(module)。

    • 当为单Module项目的时候,这个单独的Module实际上就是一个Project
    • 当为多Module项目的时候,多个模块处于同一个Project之中,此时彼此之间可具有互相依赖的关联关系。

    模块是项目的一部分,可以独立编译、运行、测试和调试。

  2. 新建一个pro07-javaweb-begin模块,给该模块设置为web项目。

  3. 为pro07-javaweb-begin模块创建artifact部署包。

    **创建 artifact可以将整个 Web 项目及其所需的文件、资源以及配置打包成一个单独的部署单元。**这简化了部署流程,只需将生成的 artifact(WAR 文件)上传至 Tomcat 的 Deployment 即可完成部署

    artifact(war文件)在模块运行后生成在out文件下:

    out文件通常指的是程序或编译器输出的文件。

创建tomcat服务器

配置tomcat相关信息

新建一个Tomcat服务器:

将该web项目的部署包 部署到tomcat容器中

导入Tomcat的lib

servlet等jar包在/tomcat/lib里面,没有导入Tomcat的依赖,无法使用Servlet

lib里面包括多个jar包

启动tomcat

目录结构:

启动tomcat会访问默认配置的url: http://localhsot:8080,并打开Web目录下的index.html页面。

在/Tomcat8/conf/web.xml有相关配置 欢迎页

开始使用servlet进行交互

add.html:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="add" method="post">
        名称:<input type="text" name="fname"/><br/>
        价格:<input type="text" name="price"/><br/>
        库存:<input type="text" name="fcount"/><br/>
        备注:<input type="text" name="remark"/><br/>
        <input type="submit" value="添加" />
    </form>
</body>
</html>

配置web.xml

配置web.xml使之能接收并处理前端发来的请求

  1. 用户发请求,action=add
  2. 项目中,web.xml找到url-pattern = /add
  3. 找servlet-name = AddServlet
  4. 找和servlet-mapping中servlet-name 一致的servlet
  5. 找到servlet-class 中的com.czk.servlets.AddServlet
  6. 用户发送的post请求(method = post),因此tomcat会执行AddServlet中的doPost方法
xml 复制代码
<servlet>
    <servlet-name>AddServlet</servlet-name>
    <servlet-class>com.czk.servlets.AddServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>AddServlet</servlet-name>
    <url-pattern>/add</url-pattern>
</servlet-mapping>

AddServlet.class

java 复制代码
public class AddServlet extends HttpServlet {
    @Override
    // doPost:处理post请求
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String fname = req.getParameter("fname");
        String priceStr = req.getParameter("price");
        Integer price = Integer.parseInt(priceStr);
        String countStr = req.getParameter("fcount");
        Integer fcount = Integer.parseInt(countStr);
        String remark = req.getParameter("remark");

        System.out.println(fname);
        System.out.println(price);
        System.out.println(fcount);
        System.out.println(remark);

    }
}

404问题。意味着找不到指定的资源。

405问题。当前请求的方法不支持。比如,我们表单method=post , 那么Servlet必须对应doPost。否则报405错误。

设置编码

java 复制代码
//get方式目前不需要设置编码(基于tomcat8),post请求需要设置编码,不然传过来的中文会乱码
req.setCharacterEncoding("UTF-8");
String fname = req.getParameter("fname");
System.out.println(fname);

servlet继承关系

继承关系

javax.servlet.Servlet接口
javax.servlet.GenericServlet抽象类
javax.servlet.http.HttpServlet抽象子类

相关方法

javax.servlet.Servlet接口:
java 复制代码
void init(config) - 初始化方法
void service(request,response) - 服务方法
void destory() - 销毁方法
javax.servlet.http.HttpServlet 抽象子类:
java 复制代码
void service(request,response) - 不是抽象的
java 复制代码
void service(request, response){
    // 1.获取请求的方式
    String method = req.getMethod(); 
    //2. 各种if判断,根据请求方式不同,决定去调用不同的do方法
    if (method.equals("GET")) {
        this.doGet(req,resp);
    } else if (method.equals("HEAD")) {
        this.doHead(req, resp);
    } else if (method.equals("POST")) {
        this.doPost(req, resp);
    } else if (method.equals("PUT")) {
    	//3. 在HttpServlet这个抽象类中,do方法都差不多:
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String protocol = req.getProtocol();
            String msg = lStrings.getString("http.method_get_not_supported");
            if (protocol.endsWith("1.1")) {
                resp.sendError(405, msg);
            } else {
                resp.sendError(400, msg);
            }
        }
    }
}

案例

java 复制代码
public class Demo01Servlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}
xml 复制代码
<servlet>
    <servlet-name>Demo01Servlet</servlet-name>
    <servlet-class>com.czk.servlets.Demo01Servlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Demo01Servlet</servlet-name>
    <url-pattern>/demo01</url-pattern>
</servlet-mapping>

当在浏览器输入,http://localhost:8080/demo01:

通常情况下,当你在浏览器中输入一个 URL 地址并按下回车时,浏览器发送一个 GET 请求。但是,如果这个服务器端的接口只允许 POST 请求(即重写了doPost方法),就会导致 405 错误。

小结

  1. 继承关系: HttpServlet -> GenericServlet -> Servlet

  2. Servlet中的核心方法: init() , service() , destroy()

  3. 服务方法: 当有请求过来时,service方法会自动响应 (其实是tomcat容器调用的)在HttpServlet中我们会去分析请求的方式 :到底是get、post、head还是delete等等然后再决定调用的是哪个do开头的方法 ,在HttpServlet中这些do方法默认都是405的实现风格所以要继承HttpServlet的子类去实现对应的方法,否则默认会报405错误。

    java 复制代码
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String msg = lStrings.getString("http.method_get_not_supported");
        this.sendMethodNotAllowed(req, resp, msg);
    }
  4. 因此,我们在新建Servlet时,我们才会去考虑请求方法,从而决定重写哪个do方法

servlet生命周期

  1. 生命周期 :从出生到死亡的过程就是生命周期。对应Servlet中的三个方法:init(),service(),destroy()

  2. 默认情况下:第一次接收请求 时,这个Servlet会进行实例化 (tomcat反射机制调用构造方法)、初始化(调用init()) 、然后服务(调用service())从第二次请求开始,每一次都是服务 。 当容器关闭 时,其中的所有的servlet实例会被销毁,调用销毁方法。

  3. Servlet实例tomcat只会创建一个,所有的请求都是这个实例去响应

    ​ 默认情况下,第一次请求时,tomcat才会去实例化,初始化,然后再服务.这样的好处是什么? 提高系统的启动速度 。 这样的缺点是什么? 第一次请求时,耗时较长。

    ​ 因此得出结论: 如果需要提高系统的启动速度,当前默认情况就是这样。如果需要提高响应速度,我们应该设置Servlet的初始化时机。

  4. Servlet的初始化时机:

    • 默认是第一次接收请求时,实例化,初始化

    • 我们可以通过<load-on-startup>设置servlet启动的先后顺序,数字越小,启动越靠前,最小值0

      xml 复制代码
      <servlet>
          <servlet-name>Demo01Servlet</servlet-name>
          <servlet-class>com.czk.servlets.Demo01Servlet</servlet-class>
          <load-on-startup>1</load-on-startup>
      </servlet>
  5. Servlet在容器中是:单例的、线程不安全的

  • 单例:所有的请求都是同一个实例去响应
  • 线程不安全:一个线程需要根据这个实例中的某个成员变量值去做逻辑判断。但是在中间某个时机,另一个线程改变了这个成员变量的值,从而导致第一个线程的执行路径发生了变化
  • 我们已经知道了servlet是线程不安全的,给我们的启发是: 尽量的不要在servlet中定义成员变量。如果不得不定义成员变量,那么不要去:①不要去修改成员变量的值 ②不要去根据成员变量的值做一些逻辑判断

Http协议

复制代码
1) Http 称之为 超文本传输协议
2) Http是无状态的
3) Http请求响应包含两个部分:请求和响应
  - 请求:
    请求包含三个部分: 1.请求行 ; 2.请求消息头 ; 3.请求主体
    1)请求行包含是三个信息: 1. 请求的方式 ; 2.请求的URL ; 3.请求的协议(一般都是HTTP1.1)
    2)请求消息头中包含了很多客户端需要告诉服务器的信息,比如:我的浏览器型号、版本、我能接收的内容的类型、我给你发的内容的类型、内容的长度等等
    3)请求体,三种情况
      get方式,没有请求体,但是有一个queryString
      post方式,有请求体,form data
      json格式,有请求体,request payload
  - 响应:
    响应也包含三本: 1. 响应行 ; 2.响应头 ; 3.响应体
    1)响应行包含三个信息:1.协议 2.响应状态码(200) 3.响应状态(ok)
    2)响应头:包含了服务器的信息;服务器发送给浏览器的信息(内容的媒体类型、编码、内容长度等)
    3)响应体:响应的实际内容(比如请求add.html页面时,响应的内容就是<html><head><body><form....)

session(会话)

java 复制代码
	1) Http是无状态的
    - HTTP 无状态 :服务器无法判断这两次请求是同一个客户端发过来的,还是不同的客户端发过来的
    - 无状态带来的现实问题:第一次请求是添加商品到购物车,第二次请求是结账;如果这两次请求服务器无法区分是同一个用户的,那么就会导致混乱
    - 通过会话跟踪技术来解决无状态的问题。

    2) 会话跟踪技术
    - 客户端第一次发请求给服务器,服务器获取session,获取不到,则创建新的,然后响应给客户端
    - 下次客户端给服务器发请求时,会把sessionID带给服务器,那么服务器就能获取到了,那么服务器就判断这一次请求和上次某次请求是同一个客户端,从而能够区分开客户端
    - 常用的API:
  	HttpSession session = req.getSession();
    req.getSession() -> 获取当前的会话,没有则创建一个新的会话
    req.getSession(true) -> 效果和不带参数相同
    req.getSession(false) -> 获取当前会话,没有则返回null,不会创建新的

    session.getId() -> 获取sessionID
    session.isNew() -> 判断当前session是否是新的
    session.
    session.getMaxInactiveInterval() -> session的非激活间隔时长,默认1800秒
    session.setMaxInactiveInterval()
    session.invalidate() -> 强制性让会话立即失效
    ....

    3) session保存作用域
    - session保存作用域是和具体的某一个session对应的
    - 常用的API:
    void session.setAttribute(k,v)
    Object session.getAttribute(k)
    void removeAttribute(k)

图示:

客户端第一次发送请求,服务端会创建和响应一个session。

客户端不是第一次发送请求,则客户端会携带sessionID。

会话跟踪图示:

服务器内部转发和客户端重定向

java 复制代码
	1) 服务器内部转发 : request.getRequestDispatcher("...").forward(request,response);
- 一次请求响应的过程,对于客户端而言,内部经过了多少次转发,客户端是不知道的
    - 地址栏没有变化
    2) 客户端重定向: response.sendRedirect("....");
- 两次请求响应的过程。客户端肯定知道请求URL有变化
    - 地址栏有变化

图示:

服务器内部转发:

客户端重定向:

java 复制代码
public class Demo06Servlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("demo06...");
        //        req.getRequestDispatcher("demo07").forward(req, resp); //内部服务器转发
        resp.sendRedirect("/demo07"); // 客户端重定向
    }
}

public class Demo07Servlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("demo07..");
    }
}

内部服务器转发:

客户端重定向:

保存作用域

原始情况下,保存作用域我们可以认为有四个: page(页面级别,现在几乎不用) , request(一次请求响应范围) , session(一次会话范围) , application(整个应用程序范围)

1) request:一次请求响应范围

2) session:一次会话范围有效。

3) application: 一次应用程序范围有效

request保存作用域:仅仅对当前request有效。服务器内部转发有效,重定向(改变了request)无效。

session作用域:对同一个session有效

application作用域:(整个应用程序范围)

java 复制代码
ServletContext application = request.getServletContext(); //ServletContext servlet上下文
application.setAttribute();

@WebServlet

@WebServlet("/url") 是 Java Servlet 3.0+ 提供的注解,用于将特定的 Servlet 映射到指定的 URL 路径上。

举个例子,@WebServlet("/del") 的作用是将对应的 Servlet 映射到 /del 这个 URL 路径上。

无需再web.xml配置servlet

案例:

java 复制代码
@WebServlet("/add.do")
public class AddServlet extends ViewBaseServlet {

    private FruitDAO fruitDAO = new FruitDAOImpl();

    @Override
    protected void doPost(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);

    }
}

mvc-servlet

  1. 最初的做法是: 一个请求对应一个Servlet,这样存在的问题是servlet太多了

    java 复制代码
    // 增加水果servlet
    @WebServlet("/add.do")
    public class AddServlet {
    
        private FruitDAO fruitDAO = new FruitDAOImpl();
    
        @Override
        protected void doPost(HttpServletRequest request, HttpServletResponse response){
            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("index"); //跳回到主页
            /*
                <!-- 配置上下文参数 -->
            <context-param>
                <param-name>view-prefix</param-name>
                <param-value>/</param-value>
            </context-param>
            <context-param>
                <param-name>view-suffix</param-name>
                <param-value>.html</param-value>
        </context-param>
            */
    
        }
    }
    
    // 删除水果servlet
    @WebServlet("/del.do")
    public class DelServlet {
        private FruitDAO fruitDAO = new FruitDAOImpl();
        @Override
        public void doGet(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
            String fidStr = request.getParameter("fid");
            if(StringUtil.isNotEmpty(fidStr)){
                int fid = Integer.parseInt(fidStr);
                fruitDAO.delFruit(fid);
    
                response.sendRedirect("index");
            }
        }
    }
    ...
  2. 改进:把一些列的请求都对应一个Servlet, IndexServlet/AddServlet/EditServlet/DelServlet/UpdateServlet -> 合并成FruitServlet

    通过一个operate的值来决定调用FruitServlet中的哪一个方法

    使用的是switch-case

    java 复制代码
    @WebServlet("/fruit.do")
    public class FruitServlet{
        private FruitDAO fruitDAO = new FruitDAOImpl();
    
        @Override
        protected void service(HttpServletRequest request, HttpServletResponse response) throws Exception {
            //设置编码
            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 "del":
                    del(request,response);
                    break;
                case "edit":
                    edit(request,response);
                    break;
                case "update":
                    update(request,response);
                    break;
                default:
                    throw new RuntimeException("operate值非法!");
            }
        }
    	
    	private void update{
            ...
        }
        ...
    }
  3. 在上一个版本中,Servlet中充斥着大量的switch-case ,试想一下,随着我们的项目的业务规模扩大,那么会有很多的Servlet ,也就意味着会有很多的switch-case,这是一种代码冗余。因此,我们在servlet中使用了反射技术 ,我们规定operate的值和方法名一致 ,那么接收到operate的值是什么就表明我们需要调用对应的方法进行响应,如果找不到对应的方法,则抛异常。

  4. 在上一个版本中我们使用了反射技术,但是其实还是存在一定的问题:每一个servlet中都有类似的反射技术的代码。因此继续抽取,设计了中央控制器类DispatcherServlet

    步骤:

    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中需要调用的方法

applicationContext.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>

<beans> <!--模拟ioc中的bean-->
    <!-- 这个bean标签的作用是 将来servletpath中涉及的名字对应的是fruit,那么就要FruitController这个类来处理 -->
    <bean id="fruit" class="com.atguigu.fruit.controllers.FruitController"/>
</beans>

<!--
    1.概念
    HTML : 超文本标记语言
    XML : 可扩展的标记语言
    HTML是XML的一个子集

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

DispatcherServlet.java

java 复制代码
@WebServlet("*.do") //捕获所有的请求
public class DispatcherServlet extends HttpServlet {

    private Map<String,Object> beanMap = new HashMap<>(); //定义一个容器,用于存放不同的Controller

    public DispatcherServlet(){
    }

    public void init(){
        // 获取applicationContext.xml文件,这中调用比普通io流new File友好
        InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");

        // 从配置文件xml中拿到所有的controller类对象,并保存在beanMap中
        //1.创建DocumentBuilderFactory
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        //2.创建DocumentBuilder对象
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
        //3.创建Document对象
        Document document = documentBuilder.parse(inputStream);
        //4.获取所有的bean节点
        NodeList beanNodeList = document.getElementsByTagName("bean");
        for(int i = 0; i < beanNodeList.getLength() ; i++){
            Node beanNode = beanNodeList.item(i);
            if(beanNode.getNodeType() == Node.ELEMENT_NODE){
                Element beanElement = (Element)beanNode ;
                String beanId =  beanElement.getAttribute("id"); 
                String className = beanElement.getAttribute("class");
                Class controllerBeanClass = Class.forName(className);
                Object beanObj = controllerBeanClass.newInstance() ;
                beanMap.put(beanId , beanObj) ; //<fruit, fruitController实例>
            }
        }
    }


    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");

        // 从url中提取servletPath
        //假设url是:  http://localhost:8080/fruit.do
        //那么servletPath是:    /fruit.do
        // 我的思路是:
        // 第1步:字符串处理: /fruit.do ->fruit
        // 第2步:拿到beanMap容器中bean:fruit -> fruitController
        String servletPath = request.getServletPath();
        servletPath = servletPath.substring(1);
        int lastDotIndex = servletPath.lastIndexOf(".do") ;
        servletPath = servletPath.substring(0,lastDotIndex); //fruit.do ->fruit

        Object controllerBeanObj = beanMap.get(servletPath); //根据fruit 获取 fruitController实例  (fruit -> fruitController)

        String operate = request.getParameter("operate"); // 获取operate参数
        
        if(StringUtil.isEmpty(operate)){ 
            operate = "index" ;
        }

        // 根据反射机制,根据对应的operator,调用fruitController对应的add、del等方法
        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值非法!");
        }
    }
}

FruitController.java

java 复制代码
public class FruitController {
    private FruitDAO fruitDAO = new FruitDAOImpl();

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

        fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
        
		//资源跳转
        response.sendRedirect("fruit.do");
    }
    
    private void edit(HttpServletRequest request , HttpServletResponse response)throws IOException, ServletException {
        
        ...
            
    }
    
    ...
}
  1. 在上个版本中,FruitController类中的每个add、del等方法的参数都带有request、response,即把request的参数带到方法才来处理 。继续改进,只用DispatcherServlet处理request传递带来的参数 ,再将这些参数一个个传递给FruitController中具体的add、del等方法

DispatcherServlet.class

java 复制代码
// 修改部分
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    try {
        Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
        for(Method method : methods){
            if(operate.equals(method.getName())){ //判断需要调用哪个method方法

                // 获取method其参数信息
                Parameter[] parameters = method.getParameters();
                Object[] parameterValues = new Object[parameters.length];
                for (int i = 0; i < parameters.length; i++) {

                    Parameter parameter = parameters[i];

                    String parameterName = parameter.getName(); // 比如name、price等参数名称
                    // 除了add、update等方法之外不带request/response,index方法带了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); // 根据参数名称获取到request的参数值
                        String typeName = parameter.getType().getName(); // 获取参数类型,比如Integer,String等

                        Object parameterObj = parameterValue ;
                        // 接受的所有参数为String类型,若要用到int类型参数,需要强转类型
                        if(parameterObj != null) {
                            if ("java.lang.Integer".equals(typeName)) {
                                parameterObj = Integer.parseInt(parameterValue);  
                            }
                        }

                        parameterValues[i] = parameterObj ;
                    }
                }
                method.setAccessible(true);
                Object returnObj = method.invoke(controllerBeanObj,parameterValues);
                
                //视图处理(前后端未分离,略看)
                String methodReturnStr = (String)returnObj ;
                if(methodReturnStr.startsWith("redirect:")){        //比如:  redirect:fruit.do
                    String redirectStr = methodReturnStr.substring("redirect:".length());
                    response.sendRedirect(redirectStr); // 跳转页面
                }else{
                    super.processTemplate(methodReturnStr,request,response);    // 比如:  "edit"
                }
            }
        }
    }

FruitController.java

java 复制代码
public class FruitController {
    private FruitDAO fruitDAO = new FruitDAOImpl();

    private String update(Integer fid , String fname , Integer price , Integer fcount , String remark ){
        fruitDAO.updateFruit(new Fruit(fid,fname, price ,fcount ,remark ));
    }

    private String del(Integer fid  ){
        if(fid!=null){
            fruitDAO.delFruit(fid);
            return "redirect:fruit.do";
        }
        return "error";
    }

    private String add(String fname , Integer price , Integer fcount , String remark ) {
        Fruit fruit = new Fruit(0,fname , price , fcount , remark ) ;
        fruitDAO.addFruit(fruit);
        return "redirect:fruit.do";
    }
    ...
}

再次学习Servlet的初始化方法

  1. Servlet生命周期:实例化、初始化、服务、销毁

  2. Servlet中的初始化方法有两个:init() , init(config)

其中带参数的方法代码如下:

java 复制代码
public void init(ServletConfig config) throws ServletException {
  this.config = config ;
  init()
}

另外一个无参的init方法如下:

java 复制代码
public void init() throws ServletException{
}

如果我们想要在Servlet初始化时做一些准备工作,那么我们可以重写init方法

我们可以通过如下步骤去获取初始化设置的数据:

  • 获取config对象:ServletConfig config = getServletConfig();

  • 获取初始化参数值: config.getInitParameter(key);

  1. 在web.xml文件中配置Servlet

    xml 复制代码

Demo01Servlet com.atguigu.servlet.Demo01Servlet hello world uname jim Demo01Servlet /demo01 ```

  1. 也可以通过注解的方式进行配置:
java 复制代码
@WebServlet(urlPatterns = {"/demo01"} , // 可设置多个serlvet映射
initParams = {
   @WebInitParam(name="hello",value="world"),
   @WebInitParam(name="uname",value="jim")
})

举例:

java 复制代码
@WebServlet(urlPatterns = {"/demo01"},
            initParams = {
                @WebInitParam(name = "hello", value = "world"),
                @WebInitParam(name = "uname", value = "jim")
            })
public class Demo01Servlet extends HttpServlet {
    @Override
    public void init() throws ServletException {
        ServletConfig config = getServletConfig();
        String name = config.getInitParameter("uname");
        System.out.println("initValue: " + name);
    }
}

@WebServlet注解

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
    ...
    String[] urlPatterns() default {};
    ...
}

@WebInitParam注解

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebInitParam {
    String name();

    String value();

    String description() default "";
}

Servlet中的ServletContext和<context-param> 配置上下文参数

  1. 获取ServletContext,有很多方法
    在初始化方法中: ServletContxt servletContext = getServletContext();
    在服务方法中也可以通过request对象获取,也可以通过session获取:
    request.getServletContext(); session.getServletContext()
  2. 获取初始化值:
    servletContext.getInitParameter();

MVC

复制代码
1) Model1和Model2
    MVC : Model(模型)、View(视图)、Controller(控制器)
    视图层:用于做数据展示以及和用户交互的一个界面
    控制层:能够接受客户端的请求,具体的业务功能还是需要借助于模型组件来完成
    模型层:模型分为很多种:有比较简单的pojo/vo(value object),有业务模型组件,有数据访问层组件
        1) pojo/vo : 值对象
        2) DAO : 数据访问对象
        3) BO : 业务对象

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

Ioc控制反转&DI依赖注入

耦合/依赖

依赖指的是某某某离不开某某某

在软件系统中,层与层之间是存在依赖的。我们也称之为耦合。

我们系统架构或者是设计的一个原则是: 高内聚低耦合

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

控制反转:

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

    • 这句话如果出现在servlet中的某个方法内部 ,那么这个fruitService的作用域(生命周期)应该就是这个方法级别 ; (调用方法一次创建和销毁实例一次
    • 如果这句话出现在servlet的类中 ,也就是说fruitService是一个成员变量 ,那么这个fruitService的作用域(生命周期)应该就是这个servlet实例级别程序员需要控制管理它的生命周期
  2. 之后我们在applicationContext.xml中定义了这个fruitService。然后通过解析XML,产生fruitService实例,存放在beanMap中 ,这个beanMap放在一个BeanFactory中

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

依赖注入:

  1. 之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();那么,控制层和service层存在耦合

  2. 之后,我们将代码修改成FruitService fruitService = null;

    然后,在配置文件中配置,然后将fruitService实例注入到FruitController实例中,这就是依赖注入。

    xml 复制代码
    <bean id="fruit" class="FruitController">
        <property name="fruitService" ref="fruitService"/>
    </bean>
  • IOC (Inversion of Control):控制反转,指的是在软件工程中一种设计原则。它将传统的程序控制流程反转过来,将一些控制权交给框架或容器,以实现松耦合、模块化的设计。在 IOC 中,对象的创建、管理以及它们之间的关系由容器负责,而不是在应用程序代码中直接控制。
  • DI (Dependency Injection):依赖注入,是 IOC 的一种实现方式。它指的是将一个对象所需的依赖关系通过构造函数、方法参数或者直接在对象内部设定,由外部容器在创建对象的时候注入进去。这样做的好处是增强了对象之间的松耦合性,使得代码更易于维护和测试。

代码

BeanFactory.java接口

java 复制代码
public interface BeanFactory {
    Object getBean(String id);
}

applicationContext.xml

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>


<!--
	xml节点结构:
	<bean>aa<property></property>bb</bean>
	这里bean是个元素(Element)节点,是父节点
	有三个子节点:
	第一个:"aa",是文本节点
	第二个:<<property>元素(Element)节点
	第三个:"bb",是文本节点
-->

ClassPathXmlApplicationContext实现类:

java 复制代码
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) ;
                }
            }
            //5.组装bean之间的依赖关系
            for(int i = 0 ; i<beanNodeList.getLength() ; i++){
                Node beanNode = beanNodeList.item(i);
                if(beanNode.getNodeType() == Node.ELEMENT_NODE) { // 获取元素节点<bean>
                    Element beanElement = (Element) beanNode;
                    String beanId = beanElement.getAttribute("id");
                    NodeList beanChildNodeList = beanElement.getChildNodes();// 获取<bean>元素节点下的所有子节点
                    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");
                            //1) 找到propertyRef对应的实例
                            Object refObj = beanMap.get(propertyRef);
                            //2) 将refObj设置到当前bean对应的实例的property属性上去
                            Object beanObj = beanMap.get(beanId);
                            Class beanClazz = beanObj.getClass();
                            Field propertyField = beanClazz.getDeclaredField(propertyName);
                            propertyField.setAccessible(true);
                            propertyField.set(beanObj,refObj); //注入
                        }
                    }
                }
            }
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }


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

DispatcherServlet.java

java 复制代码
// 修改部分
@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet{

    private BeanFactory beanFactory ; // 定义一个bean工厂

    public DispatcherServlet(){
    }

    public void init() throws ServletException {
        super.init();
        beanFactory = new ClassPathXmlApplicationContext();
    }
    Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       
		...
        Object controllerBeanObj = beanFactory.getBean(servletPath); //从beanFactory获取实例
        ...
            
    }
}

FruitController.java

java 复制代码
public class FruitController {
    private FruitService fruitService = null ;
    ...
}

Filter过滤器

  1. Filter也属于Servlet规范

  2. Filter开发步骤:新建类实现Filter接口,然后实现其中的三个方法:init、doFilter、destroy

    配置Filter,可以用注解**@WebFilter**,也可以使用xml文件 <filter> <filter-mapping>

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

  4. 过滤器链

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

xml配置方式的filter:

xml 复制代码
<filter>
    <filter-name>MyFilter</filter-name>
    <filter-class>com.example.MyFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>MyFilter</filter-name>
    <url-pattern>/demo02</url-pattern>
</filter-mapping>

注解方式的filter

java 复制代码
@WebFilter("/demo02.do")

设置一个编码配置过滤器:

代码

Demo02Servlet.java

java 复制代码
@WebServlet("/demo02.do")
public class Demo02Servlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("demo02...");
    }
}

CharacterEncodingFilter.java

java 复制代码
@WebFilter(urlPatterns = {"*.do"}, initParams = {@WebInitParam(name = "encoding", value = "GBK")})
public class CharacterEncodingFilter implements Filter {
    private String encoding = "UTF-8";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String encodingStr = filterConfig.getInitParameter("encoding");
        if(!encodingStr.isEmpty()) { // 如果初始参数不为空,则赋值编码
            encoding = encodingStr;
        }
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("helloA"); // 过滤前执行的代码
        servletRequest.setCharacterEncoding(encoding);
        filterChain.doFilter(servletRequest, servletResponse); // 放行
        System.out.println("helloB"); // 过滤后执行的代码
    }

    @Override
    public void destroy() { // 销毁

    }
}

事务管理transaction

为了实现service的事务管理,可以加一个过滤器。可对service所有操作进行事务管理,如果出错,则全部回滚。

难点:DAO1、DAO2、DAO3三个组件需要同一个Connection。

这时候需要用到ThreadLocal,来保存和获取同一个conn对象

代码:

ConnUtil.java

java 复制代码
public class ConnUtil {

    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    //    private static ThreadLocal<Fruit> threadLocal2 = new ThreadLocal<>();
    //private static ThreadLocal<Object> threadLocal3 = 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 = "123456" ;

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

    // 通过get方法获取当前线程的 ThreadLocal中的conn
    public static Connection getConn(){ 
        Connection conn = threadLocal.get();
        if(conn == null){
            conn = createConn();
            threadLocal.set(conn); // 通过set方法将conn加入到当前线程的threadLocal中
        }
        return threadLocal.get() ;
    }

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

TransactionManager.java: 数据库事务管理的封装对象

java 复制代码
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();
    }
}

OpenSessionInViewFilter.java

实现事务管理的过滤器

java 复制代码
@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){ // 出错则执行catch 回滚操作
            e.printStackTrace();
            try {
                TransactionManager.rollback();
                System.out.println("回滚事务....");
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        }
    }

    @Override
    public void destroy() {

    }
}

ThreadLocal

ThreadLocal 是 Java 中的一个类,它提供了线程局部变量的支持 。这意味着,通过 ThreadLocal 可以在多线程环境下为每个线程创建独立的变量,每个线程可以独立访问自己的变量副本,互不干扰。

ThreadLocal 实例通常被用作线程范围内的私有变量存储器 。通过 ThreadLocal,每个线程可以拥有自己的私有副本,线程之间相互隔离,不会相互影响。

要使用 ThreadLocal,您可以创建一个 ThreadLocal 的实例,并通过 set()get() 方法来设置和获取变量的值。每个使用 ThreadLocal 的线程都可以独立设置和获取其对应的变量值。

  1. set源码分析:
java 复制代码
public void set(T value) {
    Thread t = Thread.currentThread(); // 获取当前的线程
    ThreadLocalMap map = getMap(t); //每一个线程都维护各自的一个容器(ThreadLocalMap)
    if (map != null) 
        map.set(this, value);
    else
        createMap(t, value); //默认情况下map是没有初始化的,那么第一次往其中添加数据时,会去初始化
}
  1. get源码分析
java 复制代码
public T get() {
    Thread t = Thread.currentThread(); // 获取当前的线程
    ThreadLocalMap map = getMap(t); // 获取和这个线程相关的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 获取map中的实体对象
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value; // 获取该实体对象的值
            return result;
        }
    }
    return setInitialValue();
}

Listener监听器

复制代码
1) ServletContextListener - 监听ServletContext对象的创建和销毁的过程
2) HttpSessionListener - 监听HttpSession对象的创建和销毁的过程
3) ServletRequestListener - 监听ServletRequest对象的创建和销毁的过程

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

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

xml配置:

xml 复制代码
<listener>
    <listener-class>com.atguigu.myssm.listeners.ContextLoaderListener</listener-class>
</listener>

注解配置:

java 复制代码
@WebListener

将中央控制器来创建容器改为在上下文启动的时候取创建IOC容器,保存在作用域。(中央控制器只用来接受请求并响应)

代码:

web.xml

xml 复制代码
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>applicationContext.xml</param-value>
</context-param>

applicationContext.xml

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>

ContextLoaderListener.java

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

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

DispatcherServlet.java

java 复制代码
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容器获取失败!");
    }
}

小结

相关推荐
ejinxian25 分钟前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之30 分钟前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring
爱的叹息1 小时前
Java 连接 Redis 的驱动(Jedis、Lettuce、Redisson、Spring Data Redis)分类及对比
java·redis·spring
松韬2 小时前
Spring + Redisson:从 0 到 1 搭建高可用分布式缓存系统
java·redis·分布式·spring·缓存
天上掉下来个程小白2 小时前
Redis-14.在Java中操作Redis-Spring Data Redis使用方式-操作列表类型的数据
java·redis·spring·springboot·苍穹外卖
汤姆大聪明3 小时前
Redisson 操作 Redis Stream 消息队列详解及实战案例
redis·spring·缓存·maven
正经摸鱼5 小时前
classpath与classpath*实现逻辑
后端·spring
良枫5 小时前
Spring Security认证授权深度解析
spring boot·spring
BeerBear6 小时前
记一次Kill <Pid> Java进程无法退出的问题处理
java·后端·spring