Spring - 2.手写Spring框架

👀 框架源码

👀 一、实现Ioc容器

1.实现PropertyResolver

► 作用:Spring的注入分为@Autowired和@Value两种。对于@Autowired,涉及到Bean的依赖,而对于@Value,则仅仅是将对应的配置注入,不涉及Bean的依赖,相对比较简单。为了注入配置,我们用PropertyResolver保存所有配置项,对外提供查询功能

其中定义了如下三个成员属性	
Logger logger = LoggerFactory.getLogger(getClass()); // 打印日志
Map<String, String> properties = new HashMap<>(); // 存储配置信息
Map<Class<?>, Function<String, Object>> converters = new HashMap<>(); //类型转换器

在这里插入图片描述

2.实现ResourceResolver

► 作用:在指定包下扫描所有Class,包括:在目录中搜索和在Jar包中搜索

// 扫描获取所有Bean的Class类型:
final Set<String> beanClassNames = scanForClassNames(configClass);

在这里插入图片描述

3.创建BeanDefinition

✍ 新建如下的注解

  • 导入class注解
    lmport
    ComponentScan

  • 初始化bean的注解
    Bean
    Component
    Configuration
    Order
    Primary

  • 注入类注解
    Autowired
    Value

✍ BeanDefinition的定义

	// bean名称(唯一的)
    private final String name;
    // bean class
    private final Class<?> beanClass;
    // bean实例(单例)
    private Object instance = null;
    // 构造方法,可以为空
    private final Constructor<?> constructor;
    // 工厂方法名称,可以为空
    private final String factoryName;
    // 工厂方法,可以为空
    private final Method factoryMethod;
    // 定义Bean的内部排序顺序
    private final int order;
    // 定义存在多个相同类型时返回哪个“主要”Bean
    private final boolean primary;
    // 是否自动注入且初始化
    private boolean init = false;
    //初始化和销毁
    private String initMethodName;
    private String destroyMethodName;
    private Method initMethod;
    private Method destroyMethod;

✍ 存放BeanDefinition

protected final Map<String, BeanDefinition> beans;

4.创建Bean实例

✍ 使用PropertyResolver保存所有配置项,对外提供查询功能

✍ 使用ResourceResolver扫描获取所有Bean的Class类型

扫描指定类上的ComponentScan和Import注解,得到class的名称组成的Set<String>集合

 ComponentScan scan = ClassUtils.findAnnotation(configClass, ComponentScan.class);
 Import importConfig = configClass.getAnnotation(Import.class);

✍ 创建Bean的定义

  • 遍历Set<String>集合,对有Component注解标注的class进行封装,获取Map<String, BeanDefinition>集合,并且对标有Configuration 的class标注出工厂方法(Configuration 相当于工厂类)
  • Configuration 注解是包含Component注解的,所以对Configuration 注解的处理可以放在Component注解的处理代码里面
  • 最终得到Map<String, BeanDefinition>
Component component = ClassUtils.findAnnotation(clazz, Component.class);
Configuration configuration = ClassUtils.findAnnotation(clazz, Configuration.class);

✍ 创建BeanName检测循环依赖

  • 把构造方法注入和工厂方法注入的依赖称为强依赖,不能有强依赖的循环依赖,否则只能报错
  • 用Setter方法注入和字段注入的称为弱依赖,不会报循环的错误
  • 所以,对于IoC容器来说,创建Bean的过程分两步: 创建Bean的实例,此时必须注入强依赖; 对Bean实例进行Setter方法注入和字段注入
  • 除此之外我们还要创建一个set集合保证bean的name是唯一的,也就是不重复创建,如下
private Set<String> creatingBeanNames;
if (!this.creatingBeanNames.add(def.getName())) {
  throw new UnsatisfiedDependencyException(String.format("Circular dependency detected when create bean '%s'", def.getName()));
}

✍ 创建@Configuration类型的Bean

  • @Configuration标注的bean一定要先行初始化,因为它是配置信息,下面的初始化肯定会用到
  • @Configuration标注的bean是工厂类,它本身的初始化是使用的构造方法(一般是无参的)

✍ 创建BeanPostProcessor类型的Bean

  • BeanPostProcessor是一个接口,一般是实现它,并标注上Component注解
  • BeanPostProcessor的作用是用新定义的bean替换原来的bean,有没有想到代理模式
  • 其内一般会定义一个Map<String, Object> originBeans = new HashMap<>();的数据结构保存,替换之前的bean
  • BeanPostProcessor的初始化是使用的构造方法(一般是无参的)
package com.study.notes.framework.context;

public interface BeanPostProcessor {

    /**
     * Invoked after new Bean().
     */
    default Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;
    }

    /**
     * Invoked after bean.init() called.
     */
    default Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }

    /**
     * Invoked before bean.setXyz() called.
     */
    default Object postProcessOnSetProperty(Object bean, String beanName) {
        return bean;
    }
}

✍ 创建其他普通Bean

  • 获取还没有初始化的BeanDefinition集合
// 获取BeanDefinition列表:
List<BeanDefinition> defs = this.beans.values().stream()
// filter bean definitions by not instantiation:
.filter(def -> def.getInstance() == null).sorted().collect(Collectors.toList());
  • 有两种创建方式,构造方法和工厂方法,只要工厂方法不为空,使用工厂方法(前面对标有Configuration注解的class的bean定义加入了工厂方法),否则使用构造方法
  • 如果构造方法或是工厂方法有参数,则还要对参数进行初始化,参数分为Value和Autowired注解注入的,但是构造方法和工厂方法的参数是我们自己定义的,并不是标注了Value和Autowired注解,就是构造方法或是工厂方法的参数了
  • @Configuration类型的Bean是工厂类,不允许其中的注入对象使用@Autowired创建
  • BeanPostProcessor不能依赖其他Bean,不允许其中的注入对象使用@Autowired创建
  • 使用BeanPostProcessor的前置处理方法postProcessBeforeInitialization替换创建好的实例bean(后面还有后置处理方法)

✍ 通过字段和set方法注入依赖

  • 因为构造方法和工厂方式是强依赖,所以大多数情况我们不会用这两种形式来注入依赖,而是用Setter方法注入和字段注入
  • Value 和 Autowired 都可以通过方法注入或是字段注入
  • 其实就是前面用构造方法或是工厂方法,没有被初始化的参数bean
Value value = acc.getAnnotation(Value.class);
Autowired autowired = acc.getAnnotation(Autowired.class);

✍ 调用init方法

  • 获取Bean实例,或被代理的原始实例
  • 调用BeanDefinition中定义的init方法
  • 调用BeanPostProcessor.postProcessAfterInitialization()替换之前的bean
/**
     * 调用init方法
     */
    void initBean(BeanDefinition def) {
        // 获取Bean实例,或被代理的原始实例:
        final Object beanInstance = getProxiedInstance(def);

        // 调用init方法:
        callMethod(beanInstance, def.getInitMethod(), def.getInitMethodName());

        // 调用BeanPostProcessor.postProcessAfterInitialization():
        beanPostProcessors.forEach(beanPostProcessor -> {
            Object processedInstance = beanPostProcessor.postProcessAfterInitialization(def.getInstance(), def.getName());
            if (processedInstance != def.getInstance()) {
                logger.atDebug().log("BeanPostProcessor {} return different bean from {} to {}.", beanPostProcessor.getClass().getSimpleName(),
                        def.getInstance().getClass().getName(), processedInstance.getClass().getName());
                def.setInstance(processedInstance);
            }
        });
    }

5.完成loC容器

✍ ApplicationContext接口

现在,我们已经完成了IoC容器的基本功能。最后的收尾工作主要是提取接口。先定义给用户使用的ApplicationContext接口:

public interface ApplicationContext extends AutoCloseable {

    // 是否存在指定name的Bean?
    boolean containsBean(String name);

    // 根据name返回唯一Bean,未找到抛出NoSuchBeanDefinitionException
    <T> T getBean(String name);

    // 根据name返回唯一Bean,未找到抛出NoSuchBeanDefinitionException
    <T> T getBean(String name, Class<T> requiredType);

    // 根据type返回唯一Bean,未找到抛出NoSuchBeanDefinitionException
    <T> T getBean(Class<T> requiredType);

    // 根据type返回一组Bean,未找到返回空List
    <T> List<T> getBeans(Class<T> requiredType);

    // 关闭并执行所有bean的destroy方法
    void close();
}

✍ ConfigurableApplicationContext接口

再定义一个给Framework级别的代码用的ConfigurableApplicationContext接口:

public interface ConfigurableApplicationContext extends ApplicationContext {

    List<BeanDefinition> findBeanDefinitions(Class<?> type);

    @Nullable
    BeanDefinition findBeanDefinition(Class<?> type);

    @Nullable
    BeanDefinition findBeanDefinition(String name);

    @Nullable
    BeanDefinition findBeanDefinition(String name, Class<?> requiredType);

    Object createBeanAsEarlySingleton(BeanDefinition def);
}

✍ AnnotationConfigApplicationContext接口

让AnnotationConfigApplicationContext实现ConfigurableApplicationContext 接口

public class AnnotationConfigApplicationContext implements ConfigurableApplicationContext {
    ...
}

✍ close()方法

顺便在close()方法中把Bean的destroy方法执行了。最后加一个ApplicationUtils类,目的是能通过getRequiredApplicationContext()方法随时获取到ApplicationContext实例。

✍ Spring真实的ApplicationContext的结构

Spring最早提供了BeanFactory和ApplicationContext两种容器,前者是懒加载,后者是立刻初始化所有Bean。懒加载的特性会导致依赖注入变得更加复杂,虽然BeanFactory在实际项目中并没有什么卵用。然而一旦发布了接口,处于兼容性考虑,就没法再收回去了。再考虑到Spring最早采用XML配置,后来采用Annotation配置,还允许混合配置,这样一来,早期发布的XmlApplicationContext不能动,新的Annotation配置就必须添加新的实现类,所以,代码的复杂度随着需求增加而增加,保持兼容性又会导致需要更多的代码来实现新功能。

BeanFactory
  HierarchicalBeanFactory
    ConfigurableBeanFactory
      AbstractBeanFactory
        AbstractAutowireCapableBeanFactory
          DefaultListableBeanFactory
    ApplicationContext
      ConfigurableApplicationContext
        AbstractApplicationContext
          AbstractRefreshableApplicationContext
            AbstractXmlApplicationContext
              ClassPathXmlApplicationContext
              FileSystemXmlApplicationContext
          GenericApplicationContext
            AnnotationConfigApplicationContext
            GenericXmlApplicationContext
            StaticApplicationContext

👀 二、实现Aop

1.回顾AOP原理

  • 代理模式:静态代理和动态代理。
  • 静态代理不说了,主要说说两种动态代理
  • 有两种方式可以实现aop,一种是根据利用jdk自带的proxy,另外一种是利用cglib的proxy.
  • 在外部调用代理类,在代理类的回调方法里面调用横切逻辑和原始bean的对应方法。

✍ jdk代理

JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中 InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,在并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。
1. 自定义一个接口

public interface TestInterface {
    public void insert();
}

2. 实现接口

public class TestImpl implements TestInterface{
    @Override
    public void insert() {
        System.out.println("插入数据");
    }
}

3. 创建jdk动态代理的工厂类

public class JdkDymanicProxyFactory implements InvocationHandler{
    private Object targetObject;
    public Object createProxyFactory(Object target){
        this.targetObject = target;
        return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this);
    }
    //执行方法的时候回去回调这个函数
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        //在这里做横切逻辑
        。。。。
        //这里访问原来的对象
        System.out.println("execute:"+method.getName());
        return method.invoke(targetObject, args);
    }
}

4. 测试执行结果

//利用jdk的动态代理实现aop
public class JdkProxyTest {
    public static void main(String args[]){
        JdkDymanicProxyFactory jdpf = new JdkDymanicProxyFactory();
        TestInterface ti = (TestInterface) jdpf.createProxyFactory(new TestImpl());
        ti.insert();
    }
}

5. 运行结果如下

execute:insert
插入数据

✍ cglib代理

CGLIB是针对类实现代理的,主要对指定的类生成一个子类,并覆盖其中的方法, 因为是继承,所以不能使用final来修饰类或方法。和jdk代理实现不同的是,cglib不要求类实现接口

1. 自定义一个类

public class CglibTestImpl {
    public void insert() {
        System.out.println("插入数据");
    }
}

2. 然后创建cglib代理的工厂类

import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxyFactory implements MethodInterceptor{
    
    private Object targetObject;
    public Object createProxyInstance(Object target){
        this.targetObject = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.targetObject.getClass());
        //设置回调函数
        enhancer.setCallback(this);
        return enhancer.create();
        
    }
    @Override
    public Object intercept(Object obj, Method method, Object[] args,
            MethodProxy proxy) throws Throwable {
        //在这里做横切逻辑
        System.out.println("record:"+method.getName());
        System.out.println("Object:"+obj.getClass());
        System.out.println("targetObject:"+targetObject.getClass());
        //这里访问原来的对象
        return method.invoke(targetObject, args);
    }
}

3. 最后写一个测试类

public class CglibProxyTest {
    public static void main(String[] args) {
        CglibProxyFactory cpf = new CglibProxyFactory();
        //没有实现接口
        CglibTestImpl ti = (CglibTestImpl)cpf.createProxyInstance(new CglibTestImpl());
        ti.insert();
    }
}

4. 测试结果如下

record:insert
插入数据

2.实现ProxyResolver

  • 为了实现AOP,我们先思考如何在IoC容器中实现一个动态代理。

  • 在IoC容器中,实现动态代理需要用户提供两个Bean:原始Bean,即需要被代理的Bean;

  • 拦截器,即拦截了目标Bean的方法后,会自动调用拦截器实现代理功能。拦截器需要定义接口,这里我们直接用Java标准库的InvocationHandler,免去了自定义接口。

  • 假定我们已经从IoC容器中获取了原始Bean与实现了InvocationHandler的拦截器Bean,那么就可以编写一个ProxyResolver来实现AOP代理。从ByteBuddy的官网上搜索很容易找到相关代码,我们整理为createProxy()方法。

  • 注意InvocationHandler有两层:外层的invoke()传入的Object是Proxy实例,内层的invoke()将调用转发至原始Bean。

public class ProxyResolver {
    // ByteBuddy实例:
    ByteBuddy byteBuddy = new ByteBuddy();

    // 传入原始Bean、拦截器,返回代理后的实例:
    public <T> T createProxy(T bean, InvocationHandler handler) {
        // 目标Bean的Class类型:
        Class<?> targetClass = bean.getClass();
        // 动态创建Proxy的Class:
        Class<?> proxyClass = this.byteBuddy
                // 子类用默认无参数构造方法:
                .subclass(targetClass, ConstructorStrategy.Default.DEFAULT_CONSTRUCTOR)
                // 拦截所有public方法:
                .method(ElementMatchers.isPublic()).intercept(InvocationHandlerAdapter.of(
                        // 新的拦截器实例:
                        new InvocationHandler() {
                            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                                // 将方法调用代理至原始Bean:
                                return handler.invoke(bean, method, args);
                            }
                        }))
                // 生成字节码:
                .make()
                // 加载字节码:
                .load(targetClass.getClassLoader()).getLoaded();
        // 创建Proxy实例:
        Object proxy;
        try {
            proxy = proxyClass.getConstructor().newInstance();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return (T) proxy;
    }
}

3.实现Around

✍ 定义一个原始Bean

首先,客户端需要定义一个原始Bean,例如OriginBean,用@Around注解标注:

@Component
@Around("aroundInvocationHandler")
public class OriginBean {

    @Value("${customer.name}")
    public String name;

    @Polite
    public String hello() {
        return "Hello, " + name + ".";
    }

    public String morning() {
        return "Morning, " + name + ".";
    }
}

✍ 定义AroundInvocationHandler

@Around注解的值aroundInvocationHandler指出应该按什么名字查找拦截器,因此客户端应再定义一个AroundInvocationHandler:

@Component
public class AroundInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 拦截标记了@Polite的方法返回值:
        if (method.getAnnotation(Polite.class) != null) {
            String ret = (String) method.invoke(proxy, args);
            if (ret.endsWith(".")) {
                ret = ret.substring(0, ret.length() - 1) + "!";
            }
            return ret;
        }
        return method.invoke(proxy, args);
    }
}

✍ 在IoC容器中装配AOP

有了原始Bean、拦截器,就可以在IoC容器中装配AOP:

@Configuration
@ComponentScan
public class AroundApplication {
    @Bean
    AroundProxyBeanPostProcessor createAroundProxyBeanPostProcessor() {
        return new AroundProxyBeanPostProcessor();
    }
}

✍ AroundProxyBeanPostProcessor

注意到装配AOP是通过AroundProxyBeanPostProcessor实现的,而这个类是由Framework提供,客户端并不需要自己实现。因此,我们需要开发一个AroundProxyBeanPostProcessor:

AroundProxyBeanPostProcessor的机制非常简单:检测每个Bean实例是否带有@Around注解,如果有,就根据注解的值查找Bean作为InvocationHandler,最后创建Proxy替换了原始的bean,返回前保存了原始Bean的引用,因为IoC容器在后续的注入阶段要把相关依赖和值注入到原始Bean。

public class AroundProxyBeanPostProcessor implements BeanPostProcessor {

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

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class<?> beanClass = bean.getClass();
        // 检测@Around注解:
        Around anno = beanClass.getAnnotation(Around.class);
        if (anno != null) {
            String handlerName;
            try {
                handlerName = (String) anno.annotationType().getMethod("value").invoke(anno);
            } catch (ReflectiveOperationException e) {
                throw new AopConfigException();
            }
            Object proxy = createProxy(beanClass, bean, handlerName);
            originBeans.put(beanName, bean);
            return proxy;
        } else {
            return bean;
        }
    }

    Object createProxy(Class<?> beanClass, Object bean, String handlerName) {
        ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) ApplicationContextUtils.getRequiredApplicationContext();
        BeanDefinition def = ctx.findBeanDefinition(handlerName);
        if (def == null) {
            throw new AopConfigException();
        }
        Object handlerBean = def.getInstance();
        if (handlerBean == null) {
            handlerBean = ctx.createBeanAsEarlySingleton(def);
        }
        if (handlerBean instanceof InvocationHandler handler) {
            return ProxyResolver.getInstance().createProxy(bean, handler);
        } else {
            throw new AopConfigException();
        }
    }

    @Override
    public Object postProcessOnSetProperty(Object bean, String beanName) {
        Object origin = this.originBeans.get(beanName);
        return origin != null ? origin : bean;
    }
}

4.总结一下

框架需要提供的:

  • Around注解。
  • AroundProxyBeanPostProcessor实现AOP,用代理bean替换原始bean,并写入Ioc。
  • ProxyResolver 创建jdk动态代理的工厂类。

客户端代码(也就是我们写的代码)需要提供的包括:

  • 带@Around注解的原始Bean;
  • 带@Polite注解的原始Bean的方法;
  • 实现InvocationHandler的AroundInvocationHandler,名字与@Around注解value保持一致,这里封装了横切逻辑,并调用原始bean的对应方法。

👀 三、实现JDBC和事务

1.回顾JDBC基础

✍ 快速入门

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
 
/**
 * JDBC快速入门
 */
public class JDBCDemo {
 
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
            //1、注册驱动
            Class.forName("com.mysql.jdbc.Driver");
 
            //2、获取连接:如果连接的是本机mysql并且端口是默认的3306,可以简化书写
            String url = "jdbc:mysql:///db1?useUnicode=true&characterEncoding=utf8";
            String username = "root";
            String password = "199884lj";
            Connection conn = DriverManager.getConnection(url, username, password);
 
            //3、定义sql
            String sql = "update stu set math = 90 where age = 55";
 
            //4、获取执行sql的对象
            Statement stat = conn.createStatement();
 
            //5、执行sql
            int count = stat.executeUpdate(sql);//受影响的行数
 
            //6、处理结束
            System.out.println(count);
 
            //7、释放资源
            stat.close();
            conn.close();
    }
}

✍ Connection(数据库连接对象)

  • 获取执行SQL对象
普通执行SQL对象

Statement createStatement()

预编译SQL的执行SQL对象:防止SQL注入

PreparedStatement prepareStatement(sql)

执行存储过程的对象

CallableStatement prepareCall(sql)
  • 事务管理
MySQL事务管理
开启事务:BEGIN;START  TRANSACTION;
提交事务:COMMIT;
回滚事务:ROLLBACK;
MySQL默认自动提交事务

► JDBC事务管理:Connection接口中定义了3个对应的方法
开启事务:setAutoCommit(boolean autoCommit)true为自动提交事务;false为手动提交事务,即为开启事务
提交事务:commit()
回滚事务:rollback()
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
 
/**
 * JDBC API 详解:Connection
 */
public class JDBCDemo_Connection {
 
    public static void main(String[] args) throws Exception {
        //1、注册驱动,在mysql的jar包下添加java.sql.Driver这个文件即可以省去手动注册驱动
		//            Class.forName("com.mysql.jdbc.Driver");
 
        //2、获取连接:如果连接的是本机mysql并且端口是默认的3306,可以简化书写
        String url = "jdbc:mysql:///db1?useUnicode=true&characterEncoding=utf8";
        String username = "root";
        String password = "xxxxxxxx";
        Connection conn = DriverManager.getConnection(url, username, password);
 
        //3、定义sql
        String sql1 = "update stu set math = 300 where id = 1";
        String sql2 = "update stu set math = 300 where id = 2";
        //4、获取执行sql的对象
        Statement stat = conn.createStatement();
 
        try {
            //开启事务
            conn.setAutoCommit(false);
            //5、执行sql
            int count1 = stat.executeUpdate(sql1);//受影响的行数
 
            //6、处理结束
            System.out.println(count1);
 
            //5、执行sql
            int count2 = stat.executeUpdate(sql2);//受影响的行数
 
            //6、处理结束
            System.out.println(count2);
 
            //提交事务
            conn.commit();
        } catch (Exception e) {
            //回滚事务
            conn.rollback();
            e.printStackTrace();
        }
 
        //7、释放资源
        stat.close();
        conn.close();
    }
}

✍ Statement(执行SQL语句)

用于执行静态SQL语句并返回其生成的结果的对象。
作用:执行SQL语句

► int executeUpdate(sql):执行DMLDDL语句
返回值:1DML语句影响的行数 2DDL语句执行后,执行成功也可能返回0ResultSet executeQuery(sql):执行DQL语句
返回值:ResultSet结果集对象

✍ ResultSet(结果集对象)

ResultSet作用:
1、封装了DQL查询语句的结果
ResultSet stmt.executeQuery(sql):执行DQL语句,返回ResultSet对象

2、获取查询结果
	► boolean next()1、将光标从当前位置向前移动一行 2、判断当前行是否为有效行
	返回值:
	1true:有效行,当前行有数据
	2false:无效行,当前行没有数据
	
	► xxx  getXxx(参数):获取数据
	xxx:数据类型;如:int getInt(参数);String getString(参数)
	参数
	int:列的编号,从1开始
	String:列的名称
while(rs.next()){
     Account account = new Account();

     //6.2 获取数据 getXxx()
     int id = rs.getInt("id");
     String name = rs.getString("name");
     int money = rs.getInt("money");

     //赋值
     account.setId(id);
     account.setName(name);
     account.setMoney(money);

     //存入集合
     list.add(account);
 }

✍ PreparedStatement(预编译SQL语句的对象)

作用:预编译SQL语句并执行,预防SQL注入问题
SQL注入
SQL注入是通过操作输入来修改事先定义号的SQL语句,用以达到执行代码对服务器进行攻击的方法。

1、获取PreparedStatement对象
//SQL语句中的参数值,使用?占位符替代
String sql = "select * from tb_user where username = ? and password = ?";
//通过Connection对象获取,并传入对应的sql语句
PreparedStatement pstmt = conn.prepareStatement(sql);

2、设置参数值
PreparedStatement对象:setXxx(参数1,参数2):给?赋值
Xxx:数据类型:如setInt(参数1,参数2)
参数:
参数1:?的位置编号,从1开始
参数2:?的值

3、执行SQL
executeUpdate();/executeQuery();:不需要再传递sql

4PrepareStatement原理:
(1)在获取PreparedStatement对象时,将sql语句发送给mysql服务检查,编译(这些步骤很耗时)
(2)执行时就不用再进行这些步骤,速度更快
(3)如果sql模板一样,则只需要进行一次检查、编译
//接收用户输入 用户名和密码
String name = "zhangsan";
String pwd = "'or'1'='1";
 
//定义sql
String sql = "select * from tb_user where username = ? and password = ?";
 
//获取stmt对象
PreparedStatement pstmt = conn.prepareStatement(sql);
 
//设置?的值
pstmt.setString(1,name);
pstmt.setString(2,pwd);

//执行sql
ResultSet rs = null;
rs = pstmt.executeQuery();

2.实现JdbcTemplate

✍ 配置DataSource

使用JdbcTemplate之前,我们需要配置JDBC数据源。Spring本身只提供了基础的DriverManagerDataSource,但Spring Boot有一个默认配置的数据源,并采用HikariCP作为连接池。这里我们仿照Spring Boot的方式,先定义默认的数据源配置项:

summer:
  datasource:
    url: jdbc:sqlite:test.db
    driver-class-name: org.sqlite.JDBC
    username: sa
    password: 
@Configuration
public class JdbcConfiguration {

    @Bean(destroyMethod = "close")
    DataSource dataSource(
            // properties:
            @Value("${summer.datasource.url}") String url,
            @Value("${summer.datasource.username}") String username,
            @Value("${summer.datasource.password}") String password,
            @Value("${summer.datasource.driver-class-name:}") String driver,
            @Value("${summer.datasource.maximum-pool-size:20}") int maximumPoolSize,
            @Value("${summer.datasource.minimum-pool-size:1}") int minimumPoolSize,
            @Value("${summer.datasource.connection-timeout:30000}") int connTimeout
    ) {
        var config = new HikariConfig();
        config.setAutoCommit(false);
        config.setJdbcUrl(url);
        config.setUsername(username);
        config.setPassword(password);
        if (driver != null) {
            config.setDriverClassName(driver);
        }
        config.setMaximumPoolSize(maximumPoolSize);
        config.setMinimumIdle(minimumPoolSize);
        config.setConnectionTimeout(connTimeout);
        return new HikariDataSource(config);
    }
}

这样,客户端引入JdbcConfiguration就自动获得了数据源:

@Import(JdbcConfiguration.class)
@ComponentScan
@Configuration
public class AppConfig {
}

✍ 定义JdbcTemplate

JdbcTemplate使用的是模版方法模式,适配器模式、用到了很多回调方法

1.注入DataSource

public class JdbcTemplate {
    final DataSource dataSource;

    public JdbcTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

2.定义RowMapper,读取结果集,用到了适配器模式

BooleanRowMapper
StringRowMapper
NumberRowMapper
BeanRowMapper

3.定义PreparedStatementCreator

@FunctionalInterface
public interface PreparedStatementCreator {
    PreparedStatement createPreparedStatement(Connection con) throws SQLException;
}
private PreparedStatementCreator preparedStatementCreator(String sql, Object... args) {
        return (Connection con) -> {
            var ps = con.prepareStatement(sql);
            bindArgs(ps, args);
            return ps;
        };
}
private void bindArgs(PreparedStatement ps, Object... args) throws SQLException {
    for (int i = 0; i < args.length; i++) {
        ps.setObject(i + 1, args[i]);
    }
}

3.定义了PreparedStatementCallback;调用了rowMapper.mapRow

package com.study.notes.framework.jdbc;

import java.sql.PreparedStatement;
import java.sql.SQLException;

import jakarta.annotation.Nullable;

@FunctionalInterface
public interface PreparedStatementCallback<T> {
    @Nullable
    T doInPreparedStatement(PreparedStatement ps) throws SQLException;
}

public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException {
   return execute(
   			//这里返回的是方法,它这个时候还没有触发哦
   			preparedStatementCreator(sql, args),
            // PreparedStatementCallback
            (PreparedStatement ps) -> {
                T t = null;
                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        if (t == null) {
                            t = rowMapper.mapRow(rs, rs.getRow());
                        } else {
                            throw new DataAccessException("Multiple rows found.");
                        }
                    }
                }
                if (t == null) {
                    throw new DataAccessException("Empty result set.");
                }
                return t;
            });
}

4.定义了ConnectionCallback,调用了PreparedStatementCreator、PreparedStatementCallback

public <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action) {
        return execute((Connection con) -> {
            try (PreparedStatement ps = psc.createPreparedStatement(con)) {
                return action.doInPreparedStatement(ps);
            }
        });
    }

5.获取Connection,调用了ConnectionCallback

public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
        // 获取新连接:
        try (Connection newConn = dataSource.getConnection()) {
            final boolean autoCommit = newConn.getAutoCommit();
            if (!autoCommit) {
                newConn.setAutoCommit(true);
            }
            T result = action.doInConnection(newConn);
            if (!autoCommit) {
                newConn.setAutoCommit(false);
            }
            return result;
        } catch (SQLException e) {
            throw new DataAccessException(e);
        }
    }

6.总结

  • 函数式编程,接口回调
  • 调用顺序为
dataSource(Connection )
	—>ConnectionCallback>PreparedStatementCreator>PreparedStatementCallback>RowMapper

3.实现声明式事务

✍ 定义@Transactional

  • 首先定义@Transactional,这里就不允许单独在方法处定义,直接在class级别启动所有public方法的事务:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Transactional {
    String value() default "platformTransactionManager";
}

✍ 定义接口PlatformTransactionManager

  • 默认值platformTransactionManager表示用名字为platformTransactionManager的Bean来管理事务。
public interface PlatformTransactionManager {
}

✍ 定义事务状态

  • 接着定义TransactionStatus,表示当前事务状态:
  • 一个事务对应一个连接,必须在指定的数据库连接下进行事务处理。
  • 实际上事务本身是针对连接来说的,因此一个连接可能会多次进行事务操作, 但是一个事务只连接一次数据库,无论有多少条数据库操作,也无论这些操作是不是select,insert,update等复合起来的
  • 所以实际上链接就可以代表事务的状态
public class TransactionStatus {
    final Connection connection;

    public TransactionStatus(Connection connection) {
        this.connection = connection;
    }
}

✍ 定义DataSourceTransactionManager

  • 最后写个DataSourceTransactionManager,它持有一个ThreadLocal存储的TransactionStatus,以及一个DataSource:
回顾 ThreadLocal
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
使用ThreadLocal要用try … finally结构,并在finally中清除。
  • 因为DataSourceTransactionManager是真正执行开启、提交、回归事务的地方,在哪执行呢?就在invoke()内部:
package com.study.notes.framework.jdbc.tx;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.study.notes.framework.exception.TransactionException;

public class DataSourceTransactionManager implements PlatformTransactionManager, InvocationHandler {

    static final ThreadLocal<TransactionStatus> transactionStatus = new ThreadLocal<>();

    final Logger logger = LoggerFactory.getLogger(getClass());

    final DataSource dataSource;

    public DataSourceTransactionManager(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	    TransactionStatus ts = transactionStatus.get();
	    if (ts == null) {
	        // 当前无事务,开启新事务:
	        try (Connection connection = dataSource.getConnection()) {
	            final boolean autoCommit = connection.getAutoCommit();
	            if (autoCommit) {
	                connection.setAutoCommit(false);
	            }
	            try {
	                // 设置ThreadLocal状态:
	                transactionStatus.set(new TransactionStatus(connection));
	                // 调用业务方法:
	                Object r = method.invoke(proxy, args);
	                // 提交事务:
	                connection.commit();
	                // 方法返回:
	                return r;
	            } catch (InvocationTargetException e) {
	                // 回滚事务:
	                TransactionException te = new TransactionException(e.getCause());
	                try {
	                    connection.rollback();
	                } catch (SQLException sqle) {
	                    te.addSuppressed(sqle);
	                }
	                throw te;
	            } finally {
	                // 删除ThreadLocal状态:
	                transactionStatus.remove();
	                if (autoCommit) {
	                    connection.setAutoCommit(true);
	                }
	            }
	        }
	    } else {
	        // 当前已有事务,加入当前事务执行:
	        return method.invoke(proxy, args);
	    }
	}
}

✍ 获取当前事务连接

这样,使用JdbcTemplate,如果有事务,自动加入当前事务,否则,按普通SQL执行(数据库隐含事务)。

public class TransactionalUtils {
    @Nullable
    public static Connection getCurrentConnection() {
        TransactionStatus ts = DataSourceTransactionManager.transactionStatus.get();
        return ts == null ? null : ts.connection;
    }
}
public class JdbcTemplate {
    public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
        // 尝试获取当前事务连接:
        Connection current = TransactionalUtils.getCurrentConnection();
        if (current != null) {
            try {
                return action.doInConnection(current);
            } catch (SQLException e) {
                throw new DataAccessException(e);
            }
        }
        // 无事务,从DataSource获取新连接:
        try (Connection newConn = dataSource.getConnection()) {
            return action.doInConnection(newConn);
        } catch (SQLException e) {
            throw new DataAccessException(e);
        }
    }
    ......
}

✍ 事务AOP

提供一个TransactionalBeanPostProcessor,使得AOP机制生效,才能拦截@Transactional标注的Bean的public方法

public class TransactionalBeanPostProcessor extends AnnotationProxyBeanPostProcessor<Transactional> {
}

✍ 客户端调用

@Configuration
public class JdbcConfiguration {

    @Bean(destroyMethod = "close")
    DataSource dataSource(
            // properties:
            @Value("${summer.datasource.url}") String url,
            @Value("${summer.datasource.username}") String username,
            @Value("${summer.datasource.password}") String password,
            @Value("${summer.datasource.driver-class-name:}") String driver,
            @Value("${summer.datasource.maximum-pool-size:20}") int maximumPoolSize,
            @Value("${summer.datasource.minimum-pool-size:1}") int minimumPoolSize,
            @Value("${summer.datasource.connection-timeout:30000}") int connTimeout
    ) {
        ...
        return new HikariDataSource(config);
    }

    @Bean
    JdbcTemplate jdbcTemplate(@Autowired DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    TransactionalBeanPostProcessor transactionalBeanPostProcessor() {
        return new TransactionalBeanPostProcessor();
    }

    @Bean
    PlatformTransactionManager platformTransactionManager(@Autowired DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
@Import(JdbcConfiguration.class)
@ComponentScan
@Configuration
public class AppConfig {
}
@Transactional
@Component
public class UserService {
    @Autowired
    JdbcTemplate jdbcTemplate;

    public User register(String email, String password) {
        jdbcTemplate.update("INSERT INTO ...", ...);
        return ...
    }
}

✍ 总结

  • 由JdbcConfiguration创建的DataSource,实现了连接池;
  • 由JdbcConfiguration创建的JdbcTemplate,实现基本SQL操作;
  • 由JdbcConfiguration创建的PlatformTransactionManager(InvocationHandler ),负责拦截@Transactional标识的Bean的public方法,自动管理事务;
  • 由JdbcConfiguration创建的TransactionalBeanPostProcessor,负责给@Transactional标识的Bean创建AOP代理,拦截器正是PlatformTransactionManager。

👀 四、实现Web Mvc

1.回顾Servlet

✍ tomcat和servlet的关系

Tomcat 是Web应用服务器,是一个Servlet/JSP容器. Tomcat 作为Servlet容器,负责处理客户请求,把请求传送给Servlet,并将Servlet的响应传送回给客户.而Servlet是一种运行在支持Java语言的服务器上的组件. Servlet最常见的用途是扩展Java Web服务器功能,提供非常安全的,可移植的,易于使用的CGI替代品.
①:Tomcat将http请求文本接收并解析,然后封装成HttpServletRequest类型的request对象,所有的HTTP头数据读可以通过request对象调用对应的方法查询到。
②:Tomcat同时会要响应的信息封装为HttpServletResponse类型的response对象,通过设置response属性就可以控制要输出到浏览器的内容,然后将response交给tomcat,tomcat就会将其变成响应文本的格式发送给浏览器
在这里插入图片描述

✍ 手写简单的servlet

  1. 建立程序的文件结构

==>找到tomcat的安装目录,在webapps目录下新建一个名为servlet的目录

==>在servlet目录下新建名为WEB-INF的目录

==>在WEB-INF目录下新建一个名为classes的目录

==>在WEB-INF目录下新建一个名为web.xml的文件

==>在classes目录下新建一个名为FirstServlet.java的文件

  1. 用文本编辑工具(如:EditPlus)打开FirstServlet.java,并写入一下代码:
package com.smalle;
 
import java.io.*;
import javax.servlet.*;
 
public class FirstServlet extends GenericServlet{
	public void service(ServletRequest req,ServletResponse res)throws ServletException,IOException{
		OutputStream out = res.getOutputStream();
		out.write("hello servlet!".getBytes());
	}
}
  1. 编译.java文件为.class文件。
  2. 用文本编辑工具(如:EditPlus)打开web.xml,并写入一下代码:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
   version="2.5"> 
    <display-name>servlet</display-name>
    <servlet>
      <servlet-name>FirstServlet</servlet-name>
      <servlet-class>com.smalle.FirstServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FirstServlet</servlet-name>
        <url-pattern>/FirstServlet</url-pattern>
    </servlet-mapping>
</web-app>
  1. 启动tomcat,进入到D:\Java\tomcat6\bin目录下双击运行startup.bat

  2. 打开浏览器,地址栏中输入http://localhost:8080/servlet/FirstServlet回车,若页面输出hello servlet!那么恭喜您第一个servlet程序手写成功!

2.启动loC容器

✍ Servlet规范定义

Servlet规范定义的组件有3类:

  • Servlet:处理HTTP请求,然后输出响应;
  • Filter:对HTTP请求进行过滤,可以有多个Filter形成过滤器链,实现权限检查、限流、缓存等逻辑;
  • Listener:用来监听Web应用程序产生的事件,包括启动、停止、Session有修改等。

✍ Servlet容器

服务器为一个应用程序提供一个“容器”,即Servlet Container,一个Server可以同时跑多个Container,不同的Container可以按URL、域名等区分,Container才是用来管理Servlet、Filter、Listener这些组件的

在这里插入图片描述

✍ IoC容器

  • 执行应用程序的入口方法main();
  • 在main()方法中,创建IoC容器的实例;
  • IoC容器在它的内部创建各个Bean的实例。

✍ Servlet容器和 IoC容器的关系

首先,我们不能改变Servlet规范,所以,Servlet、Filter、Listener,以及IoC容器,都必须在Servlet容器内被管理
在这里插入图片描述
对于一个Web应用程序来说,启动时,应用程序本身只是一个war包,并没有main()方法,因此,启动时执行的是Server的main()方法。以Tomcat服务器为例:

  • 启动服务器,即执行Tomcat的main()方法;
  • Tomcat根据配置或自动检测到一个xyz.war包后,为这个xyz.war应用程序创建Servlet容器;
  • Tomcat继续查找xyz.war定义的Servlet、Filter和Listener组件,按顺序实例化每个组件(Listener最先被实例化,然后是Filter,最后是Servlet);
实例化方式举例
1.通过在web.xml配置文件中定义,这也是早期Servlet规范唯一的配置方式;

2.通过注解@WebServlet、@WebFilter和@WebListener定义,
由Servlet容器自动扫描所有class后创建组件,
这和我们用Annotation配置Bean,由IoC容器自动扫描创建Bean非常类似;

3.先配置一个Listener,由Servlet容器创建Listener,然后,Listener自己调用相关接口,手动创建Servlet和Filter。
  • 用户发送HTTP请求,Tomcat收到请求后,转发给Servlet容器,容器根据应用程序定义的映射,把请求发送个若干Filter和一个Servlet处理;
  • 处理期间产生的事件则由Servlet容器自动调用Listener。

✍ Servlet容器实例化步骤

对于使用Spring框架的Web应用程序来说,Servlet、Filter和Listener数量少,而且是固定的,应用程序自身编写的Controller数量不定,但由IoC容器管理,因此,采用方式3最合适(实例化方式举例)。

具体来说,Tomcat启动一个基于Spring开发的Web应用程序时,按如下步骤初始化:
► 1. 应用程序必须配置一个 Framework提供的Listener;
► 2. Tomcat完成Servlet容器的创建后,立刻根据配置创建Listener;
(1)Listener初始化时创建IoC容器;
(2)Listener继续创建DispatcherServlet实例,并向Servlet容器注册;
(3)DispatcherServlet初始化时获取到IoC容器中的Controller实例,因此可以根据URL调用不同Controller实例的不同处理方法。另外注意到Web应用程序除了提供Controller外,并不必须与Servlet API打交道,因为被Spring提供的DispatcherServlet给隔离了。

我们先写一个只能输出Hello World的Servlet:

public class DispatcherServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Hello, world!</h1>");
        pw.flush();
    }
}

紧接着,编写一个ContextLoaderListener,它实现了ServletContextListener接口,能监听Servlet容器的启动和销毁,在监听到初始化事件时,完成创建IoC容器和注册DispatcherServlet两个工作:

public class ContextLoaderListener implements ServletContextListener {
    // Servlet容器启动时自动调用:
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // 创建IoC容器:
        var applicationContext = createApplicationContext(...);
        // 实例化DispatcherServlet:
        var dispatcherServlet = new DispatcherServlet();
        // 注册DispatcherServlet:
        var dispatcherReg = servletContext.addServlet("dispatcherServlet", dispatcherServlet);
        dispatcherReg.addMapping("/");
        dispatcherReg.setLoadOnStartup(0);
    }
}

这样,我们就完成了Web应用程序的初始化全部流程!

最后两个小问题:

  • 创建IoC容器时,需要的配置文件从哪读?这里我们采用Spring Boot的方式,默认从classpath的application.yml或application.properties读。
  • 需要的@Configuration配置类从哪获取?这是通过web.xml文件配置的:
<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
	<context-param>
        <!-- 固定名称 -->
		<param-name>configuration</param-name>
        <!-- 配置类的完整类名 -->
		<param-value>com.itranswarp.summer.webapp.WebAppConfig</param-value>
	</context-param>

	<listener>
		<listener-class>com.itranswarp.summer.web.ContextLoaderListener</listener-class>
	</listener>
</web-app>
  • 在ContextLoaderListener的contextInitialized()方法内,先获取ServletContext引用,再通过getInitParameter(“configuration”)拿到完整类名,就可以顺利创建IoC容器了。

  • 用Maven打包后,把生成的xyz.war改为ROOT.war,复制到Tomcat的webapps目录下,清除掉其他webapp,启动Tomcat,输入http://localhost:8080可看到输出Hello, world!。

3.实现MVC

✍ 自定义注解

在这里插入图片描述

✍ URL的处理器抽象

DispatcherServlet内部负责从IoC容器找出所有@Controller和@RestController定义的Bean,扫描它们的方法,找出@GetMapping和@PostMapping标识的方法,这样就有了一个处理特定URL的处理器,我们抽象为Dispatcher:

class Dispatcher {
    // 是否返回REST:
    boolean isRest;
    // 是否有@ResponseBody:
    boolean isResponseBody;
    // 是否返回void:
    boolean isVoid;
    // URL正则匹配:
    Pattern urlPattern;
    // Bean实例:
    Object controller;
    // 处理方法:
    Method handlerMethod;
    // 方法参数:
    Param[] methodParameters;
}

✍ 方法参数抽象

方法参数也需要根据@RequestParam、@RequestBody等抽象出Param类型:
一共有4种类型的参数,我们用枚举ParamType定义:

  • PATH_VARIABLE:路径参数,从URL中提取;
  • REQUEST_PARAM:URL参数,从URL Query或Form表单提取;
  • REQUEST_BODY:REST请求参数,从Post传递的JSON提取;
  • SERVLET_VARIABLE:HttpServletRequest等Servlet API提供的参数,直接从DispatcherServlet的方法参数获得。
class Param {
    // 参数名称:
    String name;
    // 参数类型:
    ParamType paramType;
    // 参数Class类型:
    Class<?> classType;
    // 参数默认值
    String defaultValue;
}

✍ DispatcherServlet定义

这样,DispatcherServlet通过反射拿到一组Dispatcher对象,在doGet()和doPost()方法中,依次匹配URL:
这里不能用Map<String, Dispatcher>的原因在于我们要处理类似/hello/{name}这样的URL,没法使用精确查找,只能使用正则匹配。

public class DispatcherServlet extends HttpServlet {

    List<Dispatcher> getDispatchers = new ArrayList<>();
    List<Dispatcher> postDispatchers = new ArrayList<>();

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String url = req.getRequestURI();
        // 依次匹配每个Dispatcher的URL:
        for (Dispatcher dispatcher : getDispatchers) {
            Result result = dispatcher.process(url, req, resp);
            // 匹配成功并处理后:
            if (result.processed()) {
                // 处理结果
                ...
                return;
            }
        }
        // 未匹配到任何Dispatcher:
        resp.sendError(404, "Not Found");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ...
    }
}

Dispatcher处理后返回类型包括:

  • void或null:表示内部已处理完毕;
  • String:如果以redirect:开头,则表示一个重定向;
  • String或byte[]:如果配合@ResponseBody,则表示返回值直接写入响应;
  • ModelAndView:表示这是一个MVC响应,包含Model和View名称,后续用模板引擎处理后写入响应;
    其它类型:如果是@RestController,则序列化为JSON后写入响应。

不符合上述要求的返回类型则报500错误。

✍ ModelAndView

  1. ViewResolver
    为了处理ModelAndView,我们需要一个模板引擎,因此,抽象出ViewResolver接口:
public interface ViewResolver {
    // 初始化ViewResolver:
    void init();

    // 渲染:
    void render(String viewName, Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp);
}
  1. FreeMarkerViewResolver
    Spring内置FreeMarker引擎,因此我们也把FreeMarker集成进来,写一个FreeMarkerViewResolver:
public class FreeMarkerViewResolver implements ViewResolver {

    final String templatePath;
    final String templateEncoding;
    final ServletContext servletContext;

    Configuration config;

    public FreeMarkerViewResolver(ServletContext servletContext, String templatePath, String templateEncoding) {
        this.servletContext = servletContext;
        this.templatePath = templatePath;
        this.templateEncoding = templateEncoding;
    }

    @Override
    public void init() {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
        cfg.setDefaultEncoding(this.templateEncoding);
        cfg.setTemplateLoader(new ServletTemplateLoader(this.servletContext, this.templatePath));
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);
        cfg.setAutoEscapingPolicy(Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY);
        cfg.setLocalizedLookup(false);
        var ow = new DefaultObjectWrapper(Configuration.VERSION_2_3_32);
        ow.setExposeFields(true);
        cfg.setObjectWrapper(ow);
        this.config = cfg;
    }

    @Override
    public void render(String viewName, Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Template templ = null;
        try {
            templ = this.config.getTemplate(viewName);
        } catch (Exception e) {
            throw new ServerErrorException("View not found: " + viewName);
        }
        PrintWriter pw = resp.getWriter();
        try {
            templ.process(model, pw);
        } catch (TemplateException e) {
            throw new ServerErrorException(e);
        }
        pw.flush();
    }
}
  1. WebMvcConfiguration
    这样我们就可以在DispatcherServlet内部,把处理ModelAndView和ViewResolver结合起来,最终向HttpServletResponse中输出HTML,完成HTTP请求的处理。为了简化Web应用程序配置,我们提供一个WebMvcConfiguration配置:
@Configuration
public class WebMvcConfiguration {
    private static ServletContext servletContext = null;
    static void setServletContext(ServletContext ctx) {
        servletContext = ctx;
    }

    @Bean(initMethod = "init")
    ViewResolver viewResolver( //
            @Autowired ServletContext servletContext, //
            @Value("${summer.web.freemarker.template-path:/WEB-INF/templates}") String templatePath, //
            @Value("${summer.web.freemarker.template-encoding:UTF-8}") String templateEncoding) {
        return new FreeMarkerViewResolver(servletContext, templatePath, templateEncoding);
    }

    @Bean
    ServletContext servletContext() {
        return Objects.requireNonNull(servletContext, "ServletContext is not set.");
    }
}

默认创建一个ViewResolver和ServletContext,注意ServletContext本身实际上是由Servlet容器提供的,但我们把它放入IoC容器,是因为许多涉及到Web的组件,如ViewResolver,需要注入ServletContext,才能从指定配置加载文件。
最后,整理代码,添加一些能方便用户开发的额外功能,例如处理静态文件等功能,我们的Web MVC模块就开发完毕!

✍ 注意事项

在整个HTTP处理流程中,入口是 DispatcherServlet 的 service() 方法,整个流程如下:

  1. Servlet容器调用 DispatcherServlet 的 service() 方法处理HTTP请求

  2. service() 根据GET或POST调用 doGet() 或 doPost() 方法;

  3. 根据URL依次匹配 Dispatcher ,匹配后调用 process() 方法,获得返回值;

  4. 根据返回值写入响应:

     1.void或null返回值无需写入响应:
     2.String或byte[返回值直接写入响应 (或重定向);
     3.REST类型写入JSON房列化结果:
     4.ModelAndView类型调用ViewResolver写入渲染结果.
    
  5. 未匹配到判断是否静态资源:

     1.符合静态目录(默认 /static/ ) 则读取文件,写入文件内容;
     2.网站图标(默认 /favicon.ico ) 则读取 ico 文件,写入文件内容;
    
  6. 其他情况返回404。

由于在处理的每一步都可以向HttpServletResponse写入响应,因此,后续步骤写入时,应判断前面的步骤是否已经写入并发送了HTTP Header。isCommitted()方法就是干这个用的:

if (!resp.isCommitted()) {
    resp.resetBuffer();
    writeTo(resp);
}

4.开发Web应用

✍ 配置文件application.yml

app:
  title: Hello Application
  version: 1.0

summer:
  datasource:
    url: jdbc:sqlite:test.db
    driver-class-name: org.sqlite.JDBC
    username: sa
    password: 

✍ 定义IoC容器的配置类

@ComponentScan
@Configuration
@Import({ JdbcConfiguration.class, WebMvcConfiguration.class })
public class HelloConfiguration {
}

✍ 创建Servlet容器所需的配置文件web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app ...>
	<display-name>Hello Webapp</display-name>

	<context-param>
		<param-name>configuration</param-name>
		<param-value>com.itranswarp.hello.HelloConfiguration</param-value>
	</context-param>

	<listener>
		<listener-class>com.itranswarp.summer.web.ContextLoaderListener</listener-class>
	</listener>
</web-app>

Servlet容器会自动读取web.xml,根据配置的Listener启动Summer Framework的web模块的ContextLoaderListener,它又会读取web.xml配置的获得配置类的全名com.itranswarp.hello.HelloConfiguration,最后用这个配置类完成IoC容器的创建。创建后自动注册Summer Framework的DispatcherServlet,以及Web应用程序定义的FilterRegistrationBean,这样就完成了整个Web应用程序的初始化。

✍ 其他用到的资源包括

  • 存储在src/main/webapp/static目录下的静态资源;
  • 存储于src/main/webapp/favicon.ico的图标文件;
  • 存储在src/main/webapp/WEB-INF/templates目录下的模板。

✍ 打包运行

最后,运行mvn clean package命令,在target目录得到最终的war包,改名为ROOT.war,复制到Tomcat的webapps目录下,启动Tomcat,可以正常访问http://localhost:8080:

👀 五、实现Boot

1.启动嵌入式Tomcat

✍ Tomcat打包

Spring Boot实现一个jar包直接运行的原理其实就是把Tomcat打包进去,自己再写个main()函数:

@SpringBootApplication
public class AppConfig {
    public static void main(String[] args) {
        SpringApplication.run(AppConfig.class, args);
    }
}

✍ 自定义SummerApplication

  • 在SpringApplication.run()方法内,Spring Boot会启动嵌入式Tomcat,然后再初始化Spring的IoC容器,实际上就是一个jar包内包含了嵌入式Tomcat、Spring IoC容器、Web MVC模块以及应用程序自己开发的Bean。
  • 因此,我们也提供一个SummerApplication,实现run()方法如下:
public class SummerApplication {
    public static void run(String webDir, String baseDir, Class<?> configClass, String... args) {
        // 读取application.yml配置:
        var propertyResolver = WebUtils.createPropertyResolver();
        // 创建Tomcat服务器:
        var server = startTomcat(webDir, baseDir, configClass, propertyResolver);
        // 等待服务器结束:
        server.await();
    }
}
  • 这里多了两个参数:webDir和baseDir,这是为启动嵌入式Tomcat准备的,启动嵌入式Tomcat的代码如下:
Server startTomcat(String webDir, String baseDir, Class<?> configClass, PropertyResolver propertyResolver) throws Exception {
    int port = propertyResolver.getProperty("${server.port:8080}", int.class);
    // 实例化Tomcat Server:
    Tomcat tomcat = new Tomcat();
    tomcat.setPort(port);
    // 设置Connector:
    tomcat.getConnector().setThrowOnFailure(true);
    // 添加一个默认的Webapp,挂载在'/':
    Context ctx = tomcat.addWebapp("", new File(webDir).getAbsolutePath());
    // 设置应用程序的目录:
    WebResourceRoot resources = new StandardRoot(ctx);
    resources.addPreResources(new DirResourceSet(resources, "/WEB-INF/classes", new File(baseDir).getAbsolutePath(), "/"));
    ctx.setResources(resources);
    // 设置ServletContainerInitializer监听器:
    ctx.addServletContainerInitializer(new ContextLoaderInitializer(configClass, propertyResolver), Set.of());
    // 启动服务器:
    tomcat.start();
    return tomcat.getServer();
}

✍ ServletContainerInitializer

那么我们的IoC容器,以及注册Servlet、Filter是在哪进行的?答案是我们在startTomcat()内注册了一个ServletContainerInitializer监听器,这个监听器负责启动IoC容器与注册Servlet、Filter:

public class ContextLoaderInitializer implements ServletContainerInitializer {
    final Class<?> configClass;
    final PropertyResolver propertyResolver;

    public ContextLoaderInitializer(Class<?> configClass, PropertyResolver propertyResolver) {
        this.configClass = configClass;
        this.propertyResolver = propertyResolver;
    }

    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        // 设置ServletContext:
        WebMvcConfiguration.setServletContext(ctx);
        // 启动IoC容器:
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(this.configClass, this.propertyResolver);
        // 注册Filter与DispatcherServlet:
        WebUtils.registerFilters(ctx);
        WebUtils.registerDispatcherServlet(ctx, this.propertyResolver);
    }
}

没有复用web模块的ContextLoaderListener是因为Tomcat不允许没有在web.xml中声明的Listener注册Filter与Servlet,而我们写boot模块原因之一也是要做到不需要web.xml。

这样我们就完成了boot模块的开发,它其实就包含两个组件:

  • SummerApplication:负责启动嵌入式Tomcat;
  • ContextLoaderInitializer:负责启动IoC容器,注册Filter与DispatcherServlet。

2.开发Boot应用

✍ 定义一个符合Maven结构的Web应用程序

  • 我们还是先定义一个符合Maven结构的Web应用程序hello-boot,先定义配置类HelloConfiguration:
@ComponentScan
@Configuration
@Import({ JdbcConfiguration.class, WebMvcConfiguration.class })
public class HelloConfiguration {
}
  • 以及UserService、MvcController等业务Bean。
  • 我们直接写个main()方法启动:
public class Main {
    public static void main(String[] args) throws Exception {
        SummerApplication.run("src/main/webapp", "target/classes", HelloConfiguration.class, args);
    }
}
  • 直接从IDE运行,是没有问题的,能顺利启动Tomcat、创建IoC容器、注册Filter和DispatcherServlet,可以直接通过浏览器访问。

✍ 打war包

  • 但是,如果打一个war包,直接运行java -jar xyz.war是不行的!会直接报错:找不到Main这个class!
    这是为什么呢?我们要从JVM的类加载机制说起。

  • 当我们用java启动一个Java程序时,需要用-cp参数设置classpath(默认为当前目录.);当我们用java -jar xyz.jar启动一个Java程序时,JVM忽略-cp参数,默认classpath为xyz.jar,这样,如果能在jar包中找到对应的class,就可以正常运行。

  • 要注意的一点是,JVM从jar包加载class,是从jar包的根目录查找的。如果它要加载com.itranswarp.hello.Main,那么,xyz.jar必须按如下目录组织:
    xyz.jar
    └── com
    └── itranswarp
    └── hello
    └── Main.class

  • 而我们在用Maven打war包时,结构是这样的:
    xyz.war
    └── WEB-INF
    └── classes
    └── com
    └── itranswarp
    └── hello
    └── Main.class
    自然无法加载Main。(注意jar包和war包仅扩展名不同,对JVM来说是完全一样的)

  • 那为什么我们把xyz.war扔到Tomcat的webapps目录下就能正常运行呢?因为Tomcat启动后,并不使用JVM的ClassLoader加载class,而是为每个webapp创建一个单独的ClassLoader,这个ClassLoader在如下位置搜索class:
    WEB-INF/classes目录;
    WEB-INF/lib目录下的所有jar包。

  • 因此,我们要运行的xyz.war包必须同时具有Web App的结构,又能在根目录下搜索到应用程序自己编写的Main:
    xyz.jar
    ├── com
    │ └── itranswarp
    │ └── hello
    │ └── Main.class
    └── WEB-INF
    ├── classes
    └── libs
    解决方案是在打包时复制所有编译的class到war包根目录,并添加启动类入口。修改pom.xml:

<project ...>
	...

	<build>
		<finalName>${project.name}</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.3.2</version>
				<configuration>
					<!-- 复制classes到war包根目录 -->
					<webResources>
						<resource>
							<directory>${project.build.directory}/classes</directory>
						</resource>
					</webResources>
					<archiveClasses>true</archiveClasses>
					<archive>
						<manifest>
							<!-- main启动类 -->
							<mainClass>com.itranswarp.hello.Main</mainClass>
						</manifest>
					</archive>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
  • 再次打包,运行,又会得到找不到Class的错误,不过这次是SummerApplication。

  • 这又是什么原因呢?很明显Main已经找到了,但是SummerApplication在哪呢?它其实在WEB-INF/lib/summer-boot-1.x.x.jar,JVM不会在WEB-INF/lib下搜索Class,也不会在一个jar包内搜索“jar包内的jar包”。

  • 怎么破?答案是Main运行时先自解压,再让JVM能搜索到WEB-INF/lib/summer-boot-1.x.x.jar即可。

  • 需要先修改main()方法代码:

public static void main(String[] args) throws Exception {
    // 判定是否从jar/war启动:
    String jarFile = Main.class.getProtectionDomain().getCodeSource().getLocation().getFile();
    boolean isJarFile = jarFile.endsWith(".war") || jarFile.endsWith(".jar");
    // 定位webapp根目录:
    String webDir = isJarFile ? "tmp-webapp" : "src/main/webapp";
    if (isJarFile) {
        // 解压到tmp-webapp:
        Path baseDir = Paths.get(webDir).normalize().toAbsolutePath();
        if (Files.isDirectory(baseDir)) {
            Files.delete(baseDir);
        }
        Files.createDirectories(baseDir);
        System.out.println("extract to: " + baseDir);
        try (JarFile jar = new JarFile(jarFile)) {
            List<JarEntry> entries = jar.stream().sorted(Comparator.comparing(JarEntry::getName)).collect(Collectors.toList());
            for (JarEntry entry : entries) {
                Path res = baseDir.resolve(entry.getName());
                if (!entry.isDirectory()) {
                    System.out.println(res);
                    Files.createDirectories(res.getParent());
                    Files.copy(jar.getInputStream(entry), res);
                }
            }
        }
        // JVM退出时自动删除tmp-webapp:
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {
                Files.walk(baseDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }));
    }
    SummerApplication.run(webDir, isJarFile ? "tmp-webapp" : "target/classes", HelloConfiguration.class, args);
}
  • 再修改pom.xml,加上Classpath:
<project ...>
	...

	<build>
		<finalName>${project.name}</finalName>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.3.2</version>
				<configuration>
					<!-- 复制classes到war包根目录 -->
					<webResources>
						<resource>
							<directory>${project.build.directory}/classes</directory>
						</resource>
					</webResources>
					<archiveClasses>true</archiveClasses>
					<archive>
						<manifest>
							<!-- 添加Class-Path -->
							<addClasspath>true</addClasspath>
							<!-- Classpath前缀 -->
							<classpathPrefix>tmp-webapp/WEB-INF/lib/</classpathPrefix>
							<!-- main启动类 -->
							<mainClass>com.itranswarp.hello.Main</mainClass>
						</manifest>
					</archive>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
  • 当我们打包后,我们来分析启动流程。我们先把war包解压到tmp-webapp,它的结构如下:
    tmp-webapp
    ├── META-INF
    │ └── MANIFEST.MF
    ├── WEB-INF
    │ ├── classes
    │ ├── lib
    │ │ ├── summer-boot-1.0.3.jar
    │ │ └── … other jars …
    │ └── templates
    │ └── … templates.html
    ├── application.yml
    ├── com
    │ └── itranswarp
    │ └── hello
    │ ├── Main.class
    │ └── … other classes …
    ├── favicon.ico
    ├── logback.xml
    └── static
    └── … static files …

  • 可见,com/itranswarp/hello/Main.class、application.yml、logback.xml都位于war包的根目录,可以被JVM的ClassLoader直接加载,而想要加载WEB-INF/lib/summer-boot-1.x.x.jar,我们需要给出Classpath。通过java -jar xyz.war启动时,虽然-cp参数无效,但JVM会自动从META-INF/MANIFEST.MF中读取Class-Path条目,我们用Maven写入后内容如下:
    在这里插入图片描述
    JVM会读取到Main-Class和Class-Path,由于已经解压,就能在tmp-webapp目录中顺利搜索到tmp-webapp/WEB-INF/lib/summer-boot-1.x.x.jar。后续Tomcat启动后,以tmp-webapp作为web目录本身就是标准的Web App,Tomcat的ClassLoader也能继续从WEB-INF/lib加载各种jar包。

✍ 总结

我们总结一下,打包时做了哪些工作:

  • 复制所有编译的class到war包根目录;
  • 修改META-INF/MANIFEST.MF:
  • 添加Main-Class条目;
  • 添加Class-Path条目。

运行时的流程如下:

  1. JVM从war包加载Main类,执行main()方法;
  2. 立刻自解压war包至tmp-webapp目录;
  3. 后续加载SummerApplication时,JVM根据Class-Path能找到tmp-webapp/WEB-INF/lib/summer-boot-1.x.x.jar,因此可顺利加载;
  4. 启动Tomcat,将tmp-webapp做为Web目录;
  5. 作为Web App使用Tomcat的ClassLoader加载其他组件。
    这样我们就实现了一个可以直接用java -jar xyz.war启动的Web应用程序!

有的同学会问,我们的boot应用,main()方法写了一堆自解压代码,而且,需要在pom.xml中配置很多额外的设置,对比Spring Boot应用,它对main()方法没有任何要求,而且,在pom.xml中也只需配置一个spring-boot-maven-plugin,没有其他额外配置,相比之下简单多了,那么,Spring Boot是如何实现的?

  • 我们找一个Spring Boot打包的jar解压后就明白了,它的jar包结构如下:
    xyz.jar
    ├── BOOT-INF
    │ ├── classes
    │ │ ├── application.yml
    │ │ ├── logback-spring.xml
    │ │ ├── static
    │ │ │ └── … static files …
    │ │ └── templates
    │ │ └── … templates …
    │ └── lib
    │ ├── spring-boot-3.0.0.jar
    │ └── … other jars …
    ├── META-INF
    │ └── MANIFEST.MF
    └── org
    └── springframework
    └── boot
    └── loader
    ├── JarLauncher.class
    └── … other classes …
    Spring Boot并不能修改JVM的ClassLoader机制,因此,Spring Boot的jar包仍然需要在META-INF/MANIFEST.MF中声明Main-Class,只不过它声明的不是应用程序自己的Main,而是Spring Boot的JarLauncher:

  • Main-Class: org.springframework.boot.loader.JarLauncher
    在jar包的根目录,JVM可以加载JarLauncher。一旦加载了JarLauncher后,Spring Boot会用自己的ClassLoader去加载其他的class和jar包,它在BOOT-INF/classes和BOOT-INF/lib下搜索。注意Spring Boot自定义的ClassLoader并不需要设置Class-Path,它可以完全自定义搜索路径,包括搜索“jar包中的jar包”。

  • 因此,Spring Boot采用了两种机制来实现可执行jar包:

    1. 提供Maven插件,自动设置Main-Class,复制相关启动Class,按BOOT-INF组织class和jar包;
    2. 提供自定义的ClassLoader,可以在jar包中搜索BOOT-INF/classes和BOOT-INF/lib。
  • 这样就使得编写Web应用程序时能简化打包和启动流程。代价就是编写一个自定义的Maven插件和自定义的ClassLoader工作量很大,有兴趣的同学可以试着实现Spring Boot的机制。

👀 六、Tomcat服务器

1.Tomcat的组成

在这里插入图片描述

一个Tomcat Server内部可以有多个Service(服务),通常是一个Service。Service内部包含两个组件:

  • Connectors:代表一组Connector(连接器),至少定义一个Connector,也允许定义多个Connector,例如,HTTP和HTTPS两个Connector;
  • Engine:代表一个引擎,所有HTTP请求经过Connector后传递给Engine。

在一个Engine内部,可以有一个或多个Host(主机),Host可以根据域名区分,在Host内部,又可以有一个或多个Context(上下文),每个Context对应一个Web App。Context是由路径前缀区分的,如/abc、/xyz、/分别代表3个Web App,/表示的Web App在Tomcat中表示根Web App。

  • 因此,一个HTTP请求:
http://www.example.com/abc/hello
  • 首先根据域名www.example.com定位到某个Host,然后,根据路径前缀/abc定位到某个Context,若路径前缀没有匹配到任何Context,则匹配/Context。在Context内部,就是开发者编写的Web App,一个Context仅包含一个Web App。

  • 可见Tomcat Server是一个全功能的Web服务器,它支持HTTP、HTTPS和AJP等多种Connector,又能同时运行多个Host,每个Host内部,还可以挂载一个或多个Context,对应一个或多个Web App。

2.Servlet

✍ Servlet规范

  • Servlet规范是Java Servlet API的规范,用于定义Web服务器如何处理HTTP请求和响应。Servlet规范有一组接口,对于Web App来说,操作的是接口,而真正对应的实现类,则由各个Web Server实现,这样一来,Java Web App实际上编译的时候仅用到了Servlet规范定义的接口,只要每个Web服务器在实现Servlet接口时严格按照规范实现,就可以保证一个Web App可以正常运行在多种Web服务器上
    在这里插入图片描述

✍ Servlet处理流程

  • 当Servlet容器接收到用户的HTTP请求后,由容器负责把请求转换为HttpServletRequest和HttpServletResponse对象,分别代表HTTP请求和响应,然后,经过若干个Filter组件后,到达最终的Servlet组件,由Servlet组件完成HTTP处理,将响应写入HttpServletResponse对象
  • 其中,ServletContext代表整个容器的信息,如果容器实现了ServletContext接口,也可以把ServletContext可以看作容器本身。ServletContext、HttpServletRequest和HttpServletResponse都是接口,具体实现由Web服务器完成。Filter、Servlet组件也是接口,但具体实现由Web App完成。此外,还有一种Listener接口,可以监听各种事件,但不直接参与处理HTTP请求,具体实现由Web App完成,何时调用则由容器决定。因此,针对Web App的三大组件:Servlet、Filter和Listener都是运行在容器中的组件,只有容器才能主动调用它们。
    在这里插入图片描述
    ★ 流程:
    ► ①发送请求
 ​http://localhost:8080/demo/hello​​

► ②DNS域名解析

□ 本地host文件进行域名解析,找不到,再通过域名解析服务器进行解析

► ③tomcat 服务器解析请求

□ 上下文路径:/demo
□ 资源名称:/hello

► ④上下文的匹配

□ 解析Tomcat/conf/server.xml文件,获取所有的<Context/>元素,遍历匹配找到path属性为/demo的元素。
□ 再读取该<Context/>元素的docBase属性值【当前访问的Web项目的根路径】

► ⑤资源名的匹配【Context的属性docBase值—>找到当前项目的根路径,读取项目根路径下的web.xml文件,在web.xml文件中匹配规则元素】,找不到(404)找到了就获取到Servlet类的全限定名

从该Web项目的根路径/WEB-INF 下找到web.xml 文件,读取该web.xml 文件,
获取所有的<url-pattern>元素,遍历匹配找到<url-pattern>的文本内容是/hello的:
□ 找不到:404
□ 找到了:获取到Servlet类的全限定名

► ⑥使用servlet对象【判断Servlet缓存池是否已经存在(全限定名为**)servlet对象】,不存在(第一次请求),创建(全限定名为**) servlet对象并存储到缓存池(使用反射创建Servlet对象并存储到缓存池中),供下一次请求使用存在,直接使用该servlet对象

► ⑦ 初始化操作【Tomcat-web容器创建servlet配置对象ServletConfig,并调用init方法】

► ⑧创建请求和响应对象,并在service方法处理请求。【web容器创建请求和响应对象ServletRequest和ServletResponse 对象调用service方法处理请求和做出响应】

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yueerba126

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值