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 beanClassNames = scanForClassNames(configClass);
3.创建BeanDefinition
导入class注解
lmport
ComponentScan
初始化bean的注解
Bean
Component
Configuration
Order
Primary
注入类注解
Autowired
Value
// 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;
protected final Map<String, BeanDefinition> beans;
4.创建Bean实例
扫描指定类上的ComponentScan和Import注解,得到class的名称组成的Set
ComponentScan scan = ClassUtils.findAnnotation(configClass, ComponentScan.class);
Import importConfig = configClass.getAnnotation(Import.class);
遍历Set集合,对有Component注解标注的class进行封装,获取Map集合,并且对标有Configuration 的class标注出工厂方法(Configuration 相当于工厂类)
Configuration 注解是包含Component注解的,所以对Configuration 注解的处理可以放在Component注解的处理代码里面
最终得到Map
Component component = ClassUtils.findAnnotation(clazz, Component.class);
Configuration configuration = ClassUtils.findAnnotation(clazz, Configuration.class);
把构造方法注入和工厂方法注入的依赖称为强依赖,不能有强依赖的循环依赖,否则只能报错
用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是工厂类,它本身的初始化是使用的构造方法(一般是无参的)
BeanPostProcessor是一个接口,一般是实现它,并标注上Component注解
BeanPostProcessor的作用是用新定义的bean替换原来的bean,有没有想到代理模式
其内一般会定义一个Map 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;
}
}
获取还没有初始化的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(后面还有后置处理方法)
因为构造方法和工厂方式是强依赖,所以大多数情况我们不会用这两种形式来注入依赖,而是用Setter方法注入和字段注入
Value 和 Autowired 都可以通过方法注入或是字段注入
其实就是前面用构造方法或是工厂方法,没有被初始化的参数bean
Value value = acc.getAnnotation(Value.class);
Autowired autowired = acc.getAnnotation(Autowired.class);
获取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容器
现在,我们已经完成了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();
}
再定义一个给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实现ConfigurableApplicationContext 接口
public class AnnotationConfigApplicationContext implements ConfigurableApplicationContext {
...
}
顺便在close()方法中把Bean的destroy方法执行了。最后加一个ApplicationUtils类,目的是能通过getRequiredApplicationContext()方法随时获取到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
1.回顾AOP原理
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是针对类实现代理的,主要对指定的类生成一个子类,并覆盖其中的方法, 因为是继承,所以不能使用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,例如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 + ".";
}
}
@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);
}
}
有了原始Bean、拦截器,就可以在IoC容器中装配AOP:
@Configuration
@ComponentScan
public class AroundApplication {
@Bean
AroundProxyBeanPostProcessor createAroundProxyBeanPostProcessor() {
return new 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.总结一下
框架需要提供的:
客户端代码(也就是我们写的代码)需要提供的包括:
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();
}
}
获取执行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();
}
}
用于执行静态SQL语句并返回其生成的结果的对象。
作用:执行SQL语句
► int executeUpdate(sql):执行DML、DDL语句
返回值:1、DML语句影响的行数 2、DDL语句执行后,执行成功也可能返回0
► ResultSet executeQuery(sql):执行DQL语句
返回值:ResultSet结果集对象
ResultSet作用:
1、封装了DQL查询语句的结果
ResultSet stmt.executeQuery(sql):执行DQL语句,返回ResultSet对象
2、获取查询结果
► boolean next():1、将光标从当前位置向前移动一行 2、判断当前行是否为有效行
返回值:
1、true:有效行,当前行有数据
2、false:无效行,当前行没有数据
► 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);
}
作用:预编译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
4、PrepareStatement原理:
(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
使用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使用的是模版方法模式,适配器模式、用到了很多回调方法
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,这里就不允许单独在方法处定义,直接在class级别启动所有public方法的事务:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface Transactional {
String value() default "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,它持有一个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);
}
}
......
}
提供一个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 ...
}
}
1.回顾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就会将其变成响应文本的格式发送给浏览器
==>找到tomcat的安装目录,在webapps目录下新建一个名为servlet的目录
==>在servlet目录下新建名为WEB-INF的目录
==>在WEB-INF目录下新建一个名为classes的目录
==>在WEB-INF目录下新建一个名为web.xml的文件
==>在classes目录下新建一个名为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());
}
}
<?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>
启动tomcat,进入到D:\Java\tomcat6\bin目录下双击运行startup.bat
打开浏览器,地址栏中输入http://localhost:8080/servlet/FirstServlet回车,若页面输出hello servlet!那么恭喜您第一个servlet程序手写成功!
2.启动loC容器
Servlet规范定义的组件有3类:
服务器为一个应用程序提供一个“容器”,即Servlet Container,一个Server可以同时跑多个Container,不同的Container可以按URL、域名等区分,Container才是用来管理Servlet、Filter、Listener这些组件的
首先,我们不能改变Servlet规范,所以,Servlet、Filter、Listener,以及IoC容器,都必须在Servlet容器内被管理
对于一个Web应用程序来说,启动时,应用程序本身只是一个war包,并没有main()方法,因此,启动时执行的是Server的main()方法。以Tomcat服务器为例:
实例化方式举例
1.通过在web.xml配置文件中定义,这也是早期Servlet规范唯一的配置方式;
2.通过注解@WebServlet、@WebFilter和@WebListener定义,
由Servlet容器自动扫描所有class后创建组件,
这和我们用Annotation配置Bean,由IoC容器自动扫描创建Bean非常类似;
3.先配置一个Listener,由Servlet容器创建Listener,然后,Listener自己调用相关接口,手动创建Servlet和Filter。
对于使用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("Hello, world!
");
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应用程序的初始化全部流程!
最后两个小问题:
<?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
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定义:
class Param {
// 参数名称:
String name;
// 参数类型:
ParamType paramType;
// 参数Class类型:
Class<?> classType;
// 参数默认值
String defaultValue;
}
这样,DispatcherServlet通过反射拿到一组Dispatcher对象,在doGet()和doPost()方法中,依次匹配URL:
这里不能用Map
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处理后返回类型包括:
不符合上述要求的返回类型则报500错误。
public interface ViewResolver {
// 初始化ViewResolver:
void init();
// 渲染:
void render(String viewName, Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp);
}
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();
}
}
@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() 方法,整个流程如下:
Servlet容器调用 DispatcherServlet 的 service() 方法处理HTTP请求
service() 根据GET或POST调用 doGet() 或 doPost() 方法;
根据URL依次匹配 Dispatcher ,匹配后调用 process() 方法,获得返回值;
根据返回值写入响应:
1.void或null返回值无需写入响应:
2.String或byte[返回值直接写入响应 (或重定向);
3.REST类型写入JSON房列化结果:
4.ModelAndView类型调用ViewResolver写入渲染结果.
未匹配到判断是否静态资源:
1.符合静态目录(默认 /static/ ) 则读取文件,写入文件内容;
2.网站图标(默认 /favicon.ico ) 则读取 ico 文件,写入文件内容;
其他情况返回404。
由于在处理的每一步都可以向HttpServletResponse写入响应,因此,后续步骤写入时,应判断前面的步骤是否已经写入并发送了HTTP Header。isCommitted()方法就是干这个用的:
if (!resp.isCommitted()) {
resp.resetBuffer();
writeTo(resp);
}
4.开发Web应用
app:
title: Hello Application
version: 1.0
summer:
datasource:
url: jdbc:sqlite:test.db
driver-class-name: org.sqlite.JDBC
username: sa
password:
@ComponentScan
@Configuration
@Import({ JdbcConfiguration.class, WebMvcConfiguration.class })
public class HelloConfiguration {
}
<?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应用程序的初始化。
最后,运行mvn clean package命令,在target目录得到最终的war包,改名为ROOT.war,复制到Tomcat的webapps目录下,启动Tomcat,可以正常访问http://localhost:8080:
1.启动嵌入式Tomcat
Spring Boot实现一个jar包直接运行的原理其实就是把Tomcat打包进去,自己再写个main()函数:
@SpringBootApplication
public class AppConfig {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
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();
}
}
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();
}
那么我们的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模块的开发,它其实就包含两个组件:
2.开发Boot应用
@ComponentScan
@Configuration
@Import({ JdbcConfiguration.class, WebMvcConfiguration.class })
public class HelloConfiguration {
}
public class Main {
public static void main(String[] args) throws Exception {
SummerApplication.run("src/main/webapp", "target/classes", HelloConfiguration.class, args);
}
}
但是,如果打一个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);
}
<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包。
我们总结一下,打包时做了哪些工作:
运行时的流程如下:
有的同学会问,我们的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包:
这样就使得编写Web应用程序时能简化打包和启动流程。代价就是编写一个自定义的Maven插件和自定义的ClassLoader工作量很大,有兴趣的同学可以试着实现Spring Boot的机制。
1.Tomcat的组成
一个Tomcat Server内部可以有多个Service(服务),通常是一个Service。Service内部包含两个组件:
在一个Engine内部,可以有一个或多个Host(主机),Host可以根据域名区分,在Host内部,又可以有一个或多个Context(上下文),每个Context对应一个Web App。Context是由路径前缀区分的,如/abc、/xyz、/分别代表3个Web App,/表示的Web App在Tomcat中表示根Web App。
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
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方法处理请求和做出响应】