前言:
对于后端常用框架的技术整理,其实框架在平时就是会用就行,但面试时多半需要描述实现原理,这个要靠自己理解,不推荐死记硬背。
这篇和另外几篇文章区分开,主要用于规整Java后端各种框架,面试使用及原理相关问题。
一:Spring
Spring是一个轻量级开源的后端框架,是为Java应用程序提供基础性服务的一套框架。核心功能为IOC容器管理和AOP面向切面编程。
1:Spring IOC的理解
总:控制反转,即原来的对象是由使用者控制(手动new创建),有了Spring之后,可以把整个对象交给Spring来帮我们管理。降低代码耦合性。
容器:存储Bean对象,底层使用map结构来存储。singletonObjects存放完整的Bean对象。
(可引出Bean的生命周期,从创建到销毁的过程都是容器管理。生命周期可引出循环依赖问题,进而可引出三级缓存处理及原理。)
BeanFactory:基础容器,提供基础DI功能,延迟加载(使用时才实例化Bean)
ApplicationContext(更常用):扩展自BeanFactory,支持更多企业级功能(AOP、事件、国际化等)预加载单例Bean(容器启动时实例化所有非懒加载的Bean)。通常说的容器就指这个。
如果直接使用BeanFactory创建Bean,需手动提供IOC加载过程的所需资源,如BeanDifinition。
而ApplicationContext则封装了整个IOC的加载过程,更加自动。
1:实例化容器对象
- ClassPathXmlApplicationContext:基于 XML 配置的应用程序上下文实现。它从类路径中加载 XML 配置文件,这些文件包含了 Spring Bean 的定义和配置信息。开发人员需要在 XML 文件中使用特定的标签(如 <bean>)来定义和配置 Spring Bean。
- AnnotationConfigApplicationContext:基于注解配置的应用程序上下文实现。它不需要 XML 配置文件,而是使用配置类(例如通过 @Configuration 注解标记的类)来定义和配置 Spring Bean。这种方式将配置信息直接嵌入到 Java 代码中。
通过构造函数初始化容器,触发核心方法 refresh() 创建并启动容器。
//Spring容器创建流程
//1: 使用AnnotationConfigApplicationContext(类.class) ClassPathXmlApplicationContext(管理Bean的XML文件) 创建
AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext(Empvo.class);
ClassPathXmlApplicationContext classPathContext = new ClassPathXmlApplicationContext("XXX.xml");
二者都会调用核心方法refresh,区别于配置方式不同(xml,注解),前置准备有所不同,但都是
读取和扫描文件或配置类,处理一些初始化和环境的设置。之后Spring会统一进行处理。
2:加载配置与注册Bean定义
不论通过何种方式(解析XML、Java Config、注解如@ComponentScan等),解析获取的Bean对象,Spring会把bean的配置信息(如BeanCLass、scope等)统一存储在:
BeanDefinition对象中,需注意该对象是一个接口,封装了所有Bean的定义信息,后续创建Bean就需要根据这些定义信息来创建,即想要创建Bean,先要注册BeanDefinition。
AbstractBeanDefinition是BeanDefinition接口的一个抽象实现类,该抽象类及其子类用于在Spring容器内部管理和操作Bean的元数据,如Bean的实例化、属性注入和初始化等。
所以,一个Bean会对应一个BeanDefinition,之后所有的BeanDefinition会存储到BeanFactory的beanDefinitionMap中,使用ConcurrentHashMap进行存储,用bean的名称作为键,对象作为值。
3:创建Bean
在循环BeanDefinition创建Bean之前,Spring容器会判断这个Bean:
是否懒加载(xml设置:lazy-init="true",注解设置:@Lazy,可加在配置类或@Bean方法上)。
是否多例(xml设置:scope="prototype",注解设置:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)即可)。
且判断不为抽象类。
懒加载表示只有在使用时才创建。
Spring容器Bean默认是单例的,如果设置多例,则只在使用时创建,且每次都重新创建实例。
创建Bean:
创建Bean实例的过程包括实例化Bean对象、注入依赖、调用生命周期回调方法等。
首先循环beanDefinitionMap,根据名称调用getBean(beanName) ,doGetBean(beanName)
之后会调用 getSingleton(beanName) 判断单例池是否实例化过。如果没有则创建Bean实例。
1:实例化Bean
创建Bean的关键方法,createBean(beanName,mbd,args),doCreateBean()实例化。
通过createBeanInstance()创建Bean实例,实例化时使用构造方法或者工厂方法,底层是通过反射获取对象。
先根据BeanName获取到BeanDefinition实例,然后通过 getBeanClass() 获取到Class<>实例,随后使用Class实例的无参构造创建Bean对象实例:getConstractor().newInstance();
此时创建的Bean称为纯净Bean(未完成)。
2:属性注入
创建Bean实例后,Spring容器会根据BeanDefinition中的信息来配置Bean,例如设置属性值、注入依赖等。尤其是处理@Autowired,此时会再走到 getBean 方法处,尝试从单例池获取,获取不到就创建。因为之前创建的纯净Bean依赖这个实例。(引出循环依赖问题)。
3:初始化Bean
随后,Spring容器会调用Bean的初始化方法来完成Bean的初始化。共有三种不同的方式。
注意如果同时配置,执行有先后顺序(销毁时顺序相反):
——》在指定方法上使用注解:@PostConstruct
——》实现 InitializingBean 接口的:afterPropertiesSet () 方法
——》xml方式在Bean标签处添加:init-method指定的方法 或 @Bean()添加init-method属性
4:最后,会将Bean放在单例池 singletonObjects 下。
以下是使用ApplicationContext.getBean(),体现创建Bean的过程。
public Object getBean(String name) throws BeansException {
this.assertBeanFactoryActive();
//底层也是调用BeanFactory的方法
//这里使用的是简单工厂模式,根据标识返回不同的对象。最后使用多态接收。
return this.getBeanFactory().getBean(name);
}
创建后的Bean会放在单例池中:DefaultSingletonBeanRegistry类下的 singletonObjects 集合。
存储结构为ConcurrentHashMap<beanName, Object>,其中键是Bean的名称(ID),值是对应的Bean实例对象。
Spring容器在启动时会创建好所有单例Bean的实例,并将其存储在单例池中。当客户端请求获取单例Bean时,Spring容器会直接从单例池中获取已经创建好的Bean实例,而不需要每次都重新创建。
2:DI 依赖注入
在需要使用Spring容器对象时,可将对应的属性值注入到具体的对象中,依赖注入有三种方式:
构造器注入:通过类的构造方法传递依赖对象
将一个配置类的方法添加@Bean注解,在返回的对象中设置一个final对象,通过构造器赋值。
官方推荐使用构造器注入,因为它强制依赖不可变,并且保证完全初始化的对象。符合不可变对象设计原则。好处是有助于保持代码的清晰和可测试性。缺点是不够灵活,不够简洁。
Setter注入:通过类的 Setter 方法设置依赖对象
在XML中配置需注入的属性位置及信息,在指定类下创建set方法,并添加 @Autowired 注解。
Setter注入方式,相比构造器更加灵活,依赖对象可以在对象创建后动态设置。但需谨慎使用,仅在依赖可能变化的场景使用。对象可能在未完全初始化时被使用(需注意空指针问题)。
字段注入:使用注解,直接通过字段或成员变量注入依赖,无需构造方法或 Setter。
使用@Autowired(Spring注解) 或 @Resourse(JSR-250规范,Java EE注解),注入对象。
目前使用较多的一种方式,代码简介,适合快速开发。通过反射进行注入,有两种注入方式:
@Autowired 默认按类型注入,可配合 @Qualifier 按名称注入。
@Resource 默认按名称注入,找不到名称时按类型注入。
该方式缺点是依赖关系不明确,需避免重复依赖。单元测试不方便。
3:Bean的生命周期
总:总体分为以下几个环节,其中可以通过 BeanPostProcessor 插入扩展逻辑(贯穿全流程)
实例化 → 属性注入 → Aware → 前置处理 → 初始化 → 后置处理 → 使用 → 销毁
下面是各个环节详细描述:
1:Bean加载定义
容器解析、读取、扫描配置,生成BeanDifinition实例并注册到BeanFactory的map中。
2:BeanFactoryPostProcessor处理
该对象是Spring容器的一个扩展点,允许在Bean实例化之前修改Bean的定义(BeanDefinition),
通常用于在应用程序上下文加载时调整配置,比如修改属性值、添加属性等。
常见场景有:
1:解析配置文件占位符
解析配置文件(如 application.properties)中的 ${...} 占位符
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="location" value="classpath:config.properties"/>
</bean>
2:动态覆盖 Bean 定义
根据环境变量或条件修改 Bean 的元数据(如类名、作用域、属性值)。
public class DynamicBeanProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
BeanDefinition beanDef = beanFactory.getBeanDefinition("dataSource");
beanDef.getPropertyValues().add("url", "jdbc:new-url");
}
}
3:根据条件注册或移除Bean
根据运行时条件(如系统参数)动态注册或移除 Bean。
public class ConditionalBeanProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
if (!System.getProperty("env").equals("prod")) {
((BeanDefinitionRegistry) beanFactory).removeBeanDefinition("prodOnlyBean");
}
}
}
其他还有:Profile 激活扩展、配置类增强、自定义注解解析等场景。
3:Bean实例化
通过反射原理,从构造方法或工厂方法实例化Bean,优先尝试从单例池获取Bean实例,获取不到则进行创建。
4:属性注入
通过populateBean() 方法,注入Bean的属性和依赖:
调用 postProcessProperties() 方法注入依赖,例如@Autowired;
调用 applyPropertyValues() 方法设置属性值,例如@Value;
这个节点可以引出循环依赖问题,进而可以引出三级缓存处理。
// 简化版源码逻辑
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
// 1. 处理 @Autowired/@Value 注解
if (hasInstAwareBpps) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
// 触发注解注入
((InstantiationAwareBeanPostProcessor) bp).postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
}
}
}
// 2. 处理 XML/Java Config 显式属性定义
applyPropertyValues(beanName, mbd, bw, pvs);
}
5:Aware接口回调
属性注入完成后,调用invokeAwareMethod()方法,实现接口可设置Bean的上下文信息,完成对象的属性设置。常见的例如有:
BeanNameAware => setBeanName()
BeanClassLoaderAware => setBeanClassLoader()
BeanFactoryAware => setBeanFactory()
ApplicationContextAware => setApplicationContextAware() 需注意此时容器未初始化。
6:BeanPostProcessor前置处理
执行所有,实现了BeanPostProcessor下的 postProcessBeforeInitialization() 方法。
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
System.out.println("🟡 前置处理 Bean: " + beanName + " | 类型: " + bean.getClass());
// 可在此修改 Bean 属性或返回代理对象
return bean;
}
}
场景运用场景,日志监控(耗时),属性校验(是否非空)等。
以及做其他个性化操作,但是要注意,避免在其中执行耗时操作(影响启动速度)。
7:初始化方法
前置处理完成后,调用初始化方法,并判断是否实现了InitializingBean,如果有则调用afterPropertiesSet() 方法,如果没有则不调用。
AbstractAutowireCapableBeanFactory#initializeBean() {
// 1️⃣ BeanPostProcessor 前置处理
applyBeanPostProcessorsBeforeInitialization();
// 2️⃣ 执行 InitializingBean 逻辑(核心阶段)
invokeInitMethods() {
if (bean instanceof InitializingBean) {
((InitializingBean) bean).afterPropertiesSet();
}
// 3️⃣ 执行自定义 init-method(XML/Java Config 定义)
invokeCustomInitMethod();
}
// 4️⃣ BeanPostProcessor 后置处理
applyBeanPostProcessorsAfterInitialization();
}
调用初始化有三种方式,且有先后顺序,推荐使用 @PostConstruct 注解方式
@Service
public class PaymentService implements InitializingBean {
// 组合使用三种初始化方式
@PostConstruct
public void validateConfig() {
System.out.println("🟢 @PostConstruct 优先执行");
}
@Override
public void afterPropertiesSet() {
System.out.println("🟠 InitializingBean 次之执行");
}
@Bean(initMethod = "initMethod")
public void initMethod() {
System.out.println("🔵 init-method 最后执行");
}
}
避免在 afterPropertiesSet() 执行数据库连接等阻塞操作,可结合 @Lazy 注解优化启动速度。
8:BeanPostProcessor后置处理
执行所有,实现了BeanPostProcessor下的 postProcessAfterInitialization() 方法。
Spring的AOP动态代理,就是在此处实现的。
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("🟡 后置处理 Bean: " + beanName + " | 类型: " + bean.getClass());
// 可在此生成代理对象,例如AOP动态代理
return bean;
}
}
9:Bean就绪
完成的Bean对象,被放入容器中,可通过依赖注入,或直接 getBean() 的方式来进行对象的获取及使用。
10:销毁阶段
容器关闭或手动调用close方法时,执行销毁流程。如果提供了多个销毁方法,按照顺序执行。
注意在执行销毁方法前,会优先执行:
DestructionAwareBeanPostProcessor(接口)下的 postProcessBeforeDestruction 方法。
@Component
public class DemoApplicationTests implements DestructionAwareBeanPostProcessor {
@Override
public void postProcessBeforeDestruction(Object o, String s) throws BeansException {
//销毁前置处理,优先执行
}
}
之后,按照顺序执行提供的销毁方法。三种方式分别为:
@PreDestroy 注解方法,优先执行
实现DisposableBean接口,重写方法destroy(),次之执行
自定义销毁方法,如XML:destroy-method="customDestroy",或@Bean(destroyMethod = "customDestroy"),最后执行。
// 实现销毁逻辑的三种方式
public class DemoBean implements DisposableBean {
@PreDestroy
public void preDestroy() {
System.out.println("1注解方式优先执行,@PreDestroy 方法");
}
@Override
public void destroy() {
System.out.println("2接口实现次之执行,DisposableBean#destroy()");
}
//注解或XML配置的自定义方法,最后执行
public void xmlDestroy() {
System.out.println("XML 配置的 destroy-method");
}
@Bean(destroyMethod = "customDestroy")
public ExampleBean exampleBean() {
return new ExampleBean();
}
}
// 测试销毁流程
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(App.class);
ctx.close(); // 触发销毁
}
}
Spring 不触发Prototype作用域(多例)的 Bean,需手动触发多例Bean的销毁。
例如:BeanFactory.destroyBean(beanInstance) 。
若某个销毁方法抛出异常,Spring 会记录错误(WARN 级别)但继续执行后续销毁逻辑。
若未显式指定 destroyMethod,Spring 会自动检测 close() 或 shutdown() 方法(可通过 @Bean(destroyMethod = "") 禁用)。
4:循环依赖及三级缓存
前文提过,在Bean实例化之后,属性注入阶段,可能发生循环依赖问题。
总:循环依赖问题是:A引用B,B引用A,造成一直实例化并注入依赖的过程。
处理方式:三级缓存,提前暴露对象,AOP
三级缓存其实就是不同的三个Map集合,用于存放不同Bean的相关对象。
缓存级别 | 存储内容 | 作用 |
---|---|---|
一级缓存(singletonObjects) | 完整的单例Bean | 对外提供可用的Bean |
二级缓存(earlySingletonObjects) | 提前暴露的未初始化Bean(半成品) | 临时存储,解决循环依赖的核心 |
三级缓存(singletonFactories) | Bean工厂(ObjectFactory) | 早期引用,处理动态代理逻辑 |
三级缓存:Map<BeanName, () -> getEarlyBeanReference()>
二级缓存:Map<BeanName, 原始对象/代理对象>
一级缓存:Map<BeanName, 完全体Bean>
解决思路:在B引用A时候,此时其实A已经实例化了,但未初始化,所以可以设法先拿到A的实例化对象,将B初始化完成之后,再将A的属性补全。
所以,循环依赖的核心处理逻辑就是:将Bean的实例化与初始化分离,三级缓存作为中间态解决依赖注入时序问题。
为什么要三级缓存:三级缓存的设计是为了效率和正确性,避免重复创建和保证单例。三级缓存催在的意义是保证在整个容器的运行过程中同名的Bean对象只能有一个。
若Bean需要AOP代理,三级缓存的ObjectFactory会提前生成代理对象,确保依赖注入的正确性。
Spring 之所以需要三级缓存而不是简单的二级缓存,主要原因在于AOP代理和Bean的早期引用问题。二级缓存虽然可以解决循环依赖的问题,但在涉及到动态代理(AOP)时,直接使用二级缓存不做任何处理会导致我们拿到的Bean是未代理的原始对象。如果二级缓存内存放的都是代理对象,则违反了Bean的生命周期。(正常代理对象的生成是在后置处理器)
所以,针对循环依赖问题,所有创建的Bean都要优先放到三级缓存中,后续向一二级缓存提升。
针对上述问题,调整后的实例化和属性注入流程为:
1:实例化A
A实例化完毕后,但未进行属性注入时,将创建A的ObjectFactory并存入三级缓存。
// 源码位置:AbstractAutowireCapableBeanFactory
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, bean, beanDefinition));
Lambda表达式即为ObjectFactory,该对象只负责生成Bean的早期引用(非BeanFactory)。
注意需优先判断对象是否需要被代理,如果是代理对象,则覆盖原来对象,重新创建。
// 伪代码:AbstractAutoProxyCreator
protected Object getEarlyBeanReference(String beanName, Object bean) {
//...
return wrapIfNecessary(bean, beanName);
}
随后进行三级缓存的操作,注意有其他的后续操作。
// DefaultSingletonBeanRegistry
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) { //加锁,确保三级缓存-二级缓存的原子性
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory); // 存入三级缓存
this.earlySingletonObjects.remove(beanName); //清除二级缓存
this.registeredSingletons.add(beanName); //记录所有单例Bean的注册状态
}
}
}
上述涉及到几个知识点:
1:加锁,为了保证三级缓存到二级缓存的唯一性,避免并发问题。
2:earlySingletonObjects.remove(beanName),清除二级缓存。是为了保证获取的是最新的早期对象。比如AOP代理对象的唯一性、多个线程的并发安全。
3:registeredSingletons.add(beanName),将当前正在创建的 Bean 名称(beanName
)添加到一个有序集合中,记录所有已注册的单例 Bean。使用LinkedHashSet有序存储,为了保证销毁时逆序执行。
// 源码参考:DefaultSingletonBeanRegistry
public void destroySingletons() {
String[] singletonNames = this.registeredSingletons.toArray(new String);
for (int i = singletonNames.length - 1; i >= 0; i--) {
destroySingleton(singletonNames[i]); // 逆序销毁
}
}
2:初始化B
A依赖于B,此时按照顺序从1、2、3级缓存查找,如果没有则进入B的创建流程(实例化并放入三级缓存中)
之后进行属性注入,发现依赖于A,此时从1、2、3级缓存查找A,发现此时A在三级缓存。
从三级缓存中获取A的早期对象,getSingleton("a")。
// 源码位置:DefaultSingletonBeanRegistry#getSingleton()
public Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName); // ① 查一级缓存
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName); // ② 查二级缓存
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); // ③ 查三级缓存
if (singletonFactory != null) {
// 命中三级缓存,触发后续操作...
}
}
}
}
return singletonObject;
}
注意在这之后,还会调用 getEarlyBeanReference() 判断是否代理,确保代理对象的一致性。
随后,将 A 的早期引用存入二级缓存,清空三级缓存。后续都从二级缓存获取A的引用。
// 源码位置:DefaultSingletonBeanRegistry#getSingleton()
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject(); // 生成早期引用
this.earlySingletonObjects.put(beanName, singletonObject); // ⑤ 存入二级缓存
this.singletonFactories.remove(beanName); // ⑥ 清空三级缓存
}
最后,B 完成属性注入和初始化,并存入一级缓存。添加锁,避免出现后续A获取不到B的情况。
// 源码位置:DefaultSingletonBeanRegistry#addSingleton()
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject); // ⑦ 写入一级缓存
this.singletonFactories.remove(beanName); //清除该Bean三级缓存
this.earlySingletonObjects.remove(beanName); //清除该Bean二级缓存
this.registeredSingletons.add(beanName); //实例化Bean记录
}
}
3:初始化A
当 B 通过 addSingleton 完成初始化后,返回到 A 的 populateBean 方法继续执行。
B已初始化成功并存入一级缓存,还需补充A的属性注入及初始化。(注意此时A的引用在二级缓存中,在上述B的属性注入过程中,从三级缓存提升)
之后,A注入B,完成自身初始化。并存入一级缓存,清空二三级缓存。添加锁。
// DefaultSingletonBeanRegistry#getSingleton
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 标记Bean正在创建(防止重复进入)
beforeSingletonCreation(beanName);
try {
// 关键恢复点:B完成初始化后返回此处
singletonObject = singletonFactory.getObject();
}
finally {
afterSingletonCreation(beanName);
}
// 存入一级缓存
addSingleton(beanName, singletonObject);
}
return singletonObject;
}
}
至此,A和B都完成了初始化,且都存入一级缓存中。后续其他Bean注入AB也会直接查询获取。
补充:构造器注入无法解决循环依赖(需使用Setter/字段注入),或添加@Lazy注解延迟加载。
非单例Bean(如prototype)无法解决循环依赖。
注意清空二级、三级缓存,都指的是清空当前Bean的实例,并非整个缓存数据。
缓存操作的代码,获取实例,或缓存创建删除,全部都会加同步锁,避免不一致情况。
二:Spring MVC
三:MyBatis
MyBatis是一个开源的,半自动的ORM持久层框架,主要用于简化数据库操作,将 Java 对象与数据库表中的记录进行映射。支持普通sql,关联查询,嵌套查询等。
它的核心思想是让开发者通过更简单的方式操作数据库,同时保留对 SQL 语句的完全控制权,避免了传统 JDBC 代码的复杂性。
1:#和$符的区别
总:两者均为Mybatis框架的,用于动态 SQL 参数替换的实现。
设计初衷:
#{} 用于处理数据值(如字符串、数字)
${} 用于处理SQL 结构(如动态表名、列名、排序字段)
#符是预编译占位符,在执行时,会将sql中的#{}替换为 ? 号,之后调用 PreparedStatement 的set方法进行赋值。可以有效防止sql注入。操作数据优先使用该方式。
#是参数占位符,会替换为?号,并加双引号。
它是在编译期替换为?号,变量的替换是在DBMS数据库中。
适用场景:普通参数操作,如where id = #{id}
$符是字符串替换,在处理时,就只是把 $() 替换为变量的值,不防sql注入,语句不加双引号。
sql语句变量的替换是在动态sql解析阶段。
存在sql注入问题,在预编译之前变量可被替换。
适用场景:动态表名、列名、排序字段(如 ORDER BY ${sortField})
<!-- 高危操作:用户输入直接拼接 -->
<select id="login" resultType="User">
SELECT * FROM user
WHERE username = '${username}' AND password = '${password}'
</select>
<!-- 若 username = "admin' --",会绕过密码验证! -->
四:SpringBoot
SpringBoot的原则是:约定大于配置
Spring框架需要大量的配置,而SpringBoot引入自动配置的概念,使项目搭建运行更容易快捷。SpringBoot本身并不提供Spring框架的核心特性及扩展功能,只是用于快速,敏捷的开发新一代基于Spring框架的应用程序。
对使用者来说,项目初始化方式变了,配置文件变了,以及不需要单独安装tomcat这类服务器了,使用maven或gradle项目管理工具,打出jar包即可快速运行。但核心的业务逻辑和业务流程实现没有变化。
持续整理中