基于SpringFramework 5.3.9 的core部分,一些常用方法、类、注解说明
1 IOC容器
1.1 Bean
1.1.1 实例化
- 构造器实例化
- 静态工厂实例化
- 实例工厂实例化
1.1.2 依赖
- 构造函数注入
- setter方法注入
1.1.3 作用域
| 作用域 | 描述 |
|---|---|
| singleton | (默认) 每一Spring IOC容器都拥有唯一的实例对象。 |
| prototype | 一个Bean定义可以创建任意多个实例对象. |
| request | 将单个bean定义范围限定为单个HTTP请求的生命周期。 也就是说,每个HTTP请求都有自己的bean实例,它是在单个bean定义的后面创建的。 只有基于Web的Spring ApplicationContext的才可用。 |
| session | 将单个bean定义范围限定为HTTP Session的生命周期。 只有基于Web的Spring ApplicationContext的才可用。 |
| application | 将单个bean定义范围限定为ServletContext的生命周期。 只有基于Web的Spring ApplicationContext的才可用。 |
| websocket | 将单个bean定义范围限定为 WebSocket的生命周期。 只有基于Web的Spring ApplicationContext的才可用。 |
自定义作用域需实现org.springframework.beans.factory.config.Scope,然后注册到ConfigurableBeanFactory.registerScope(..)容器中
1.1.4 自定义Bean特性
Bean生命周期
1.1.4.1初始化回调
org.springframework.beans.factory.InitializingBean
所有属性设置后进行实例化,实现**afterPropertiesSet()**方法
@PostConstruct
@Bean指定**initMethod()**方法
销毁回调
org.springframework.beans.factory.DisposableBean
实现destroy()方法
@PreDestroy
@Bean指定**destroyMethod()**方法
Lifecycle
org.springframework.context.Lifecycle
Lifecycle接口的start和stop方法需要显示的调用,若想在容器容器时调用需要实现SmartLifecycle
spring启动刷新容器时调用refreshContext 方法,在AbstractApplicationContext类的refresh()方法的finishRefresh()方法中
java
protected void finishRefresh() {
// Clear context-level resource caches (such as ASM metadata from scanning).
clearResourceCaches();
// 初始化容器的生命周期处理接口,默认为DefaultLifecycleProcessor实现
initLifecycleProcessor();
// DefaultLifecycleProcessor刷新并找到容器中实现了Lifecycle接口的Bean并调用start()方法
getLifecycleProcessor().onRefresh();
// Publish the final event.
publishEvent(new ContextRefreshedEvent(this));
// Participate in LiveBeansView MBean, if active.
if (!NativeDetector.inNativeImage()) {
LiveBeansView.registerApplicationContext(this);
}
}
DefaultLifecycleProcessor先找到实现了SmartLifecycle接口并且isAutoStartup()方法为true的Bean,Phased接口可实现执行顺序,值越小,越先执行
java
private void startBeans(boolean autoStartupOnly) {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
Map<Integer, LifecycleGroup> phases = new TreeMap<>();
lifecycleBeans.forEach((beanName, bean) -> {
if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) {
int phase = getPhase(bean);
phases.computeIfAbsent(
phase,
p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
).add(beanName, bean);
}
});
if (!phases.isEmpty()) {
phases.values().forEach(LifecycleGroup::start);
}
}
ApplicationContextAware
org.springframework.context.ApplicationContextAware
可获取ApplicationContext,以获取容器的Bean等,方法setApplicationContext(ApplicationContext applicationContext)由ApplicationContextAwareProcessor实现
BeanNameAware
org.springframework.beans.factory.BeanNameAware
获取Bean的名字,方法setBeanName(String name)在设置属性之后InitializingBean.afterPropertiesSet()和初始化方法之前
1.1.5 容器扩展
BeanPostProcessor
org.springframework.beans.factory.config.BeanPostProcessor用于容器完成实例化,配置和初始化bean之后可实现实例化逻辑,依赖关系解析逻辑等。在配置类上使用 @Bean 工厂方法声明BeanPostProcessor时,工厂方法返回的类型应该是实现类自身或至少也是org.springframework.beans.factory.config.BeanPostProcessor接口, 要清楚地表明这个bean是后置处理器。否则,在它完全创建之前,ApplicationContext将不能通过类型自动探测它
可实现org.springframework.core.Ordered排序,容器中的每个Bean都会执行实现的后置处理器,以addBeanPostProcessor方式注册的处理器的Ordered无效,AbstractAutoProxyCreator实现了创建AOP自动代理,也实现了BeanPostProcessor,@Autowired注解由AutowiredAnnotationBeanPostProcessor实现。
自定义BeanPostProcessor实现毁掉接口或注解是容器扩展的一种常用手段
java
public interface BeanPostProcessor {
/**
* 在InitializingBean's afterPropertiesSet和初始化方法之前
*/
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
/**
* 在InitializingBean's afterPropertiesSet和初始化方法之后
*/
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
BeanFactoryPostProcessor
org.springframework.beans.factory.config.BeanFactoryPostProcessor,Bean实例化前,可修改BeanDefinition,占位符解析设置属性值等
与BeanPostProcessor类似,可以获取Bean的元数据并操作,若通过BeanFactory.getBean()获取Bean实例,会提前实例化Bean,不推荐
java
@FunctionalInterface
public interface BeanFactoryPostProcessor {
/**
* 已加载所有的Bean定义,未实例化Bean
*/
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}
PropertySourcesPlaceholderConfigurer用于替换占位符
FactoryBean
org.springframework.beans.factory.FactoryBean
继承该接口,注册为Bean,通过id获取FactoryBean实例时,需在前面加上**&**,否则获取的是getObject中的实例
java
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
1.1.6 基于注解的容器配置
@Autowired
用于set方法、构造函数、属性
按照类型注入,required = false依赖为非必需,未找到不会报错。由BeanPostProcessor处理
在构造器上 只有一个构造器时可不使用;参数为数组、集合、map(键为Bean名称)时至少有一个在容器中。注入的Bean可实现org.springframework.core.Ordered或使用@Order注解指定注入顺序(默认为容器中注册顺序);多个构造函数使用注解,只能有一个required=true,若都为false,则尝试使用依赖最多的构造函数
@Primary
当按照类型注入有多个Bean时使用
@Qualifier
结合@Autowired注入指定名称的Bean
@Resource
属性、set方法
按照名称注入,默认为属性名或方法对应的Bean名。先按照名称,再按照类型注入
@Value
注入外部属性(@Value("${catalog.name:defaultCatalog}"),可设置默认值
可自定义
PropertySourcesPlaceholderConfigurer的Bean(@Bean方法需用static修饰),可设置占位符前缀后缀等
BeanPostProcessor使用ConversionService将string转为目标类型
#{}获取Bean属性
@Component、@Service、@Repository、@Controller
注册Bean,后三个视为和@Component一样
@ComponetScan
自动扫描包下的Bean并注册,在@Configuration类上添加该注解,容器会扫描basePackages属性的包路径。includeFilters或excludeFilters属性可自定义包括或排除的行为,每个filter元素都需要包含type和expression属性
| 过滤类型 | 表达式例子 | 描述 |
|---|---|---|
| annotation (default) | org.example.SomeAnnotation |
要在目标组件中的类级别出现的注解。 |
| assignable | org.example.SomeClass |
目标组件可分配给(继承或实现)的类(或接口)。 |
| aspectj | org.example..*Service+ |
要由目标组件匹配的AspectJ类型表达式。 |
| regex | org\.example\.Default.* |
要由目标组件类名匹配的正则表达式。 |
| custom | org.example.MyTypeFilter |
org.springframework.core.type .TypeFilter接口的自定义实现。 |
@Configuration、@Bean
可以在@Configuration类或@Component类中使用@Bean,bean的类型是方法的返回值类型。默认情况下, bean名称将与方法名称相同,@Bean方法中的参数会自动注入。@Configuration类允许通过调用同一个类中的其他@Bean方法来定义bean间依赖关系,多次调用同一个@Bean方法返回的是同一个被CGLB代理的对象
@Scope
指定Bean范围
@Import
允许在一个配置类中显式引入其他类(如 @Configuration 类、@Component 类、ImportSelector 或 ImportBeanDefinitionRegistrar 的实现类)并注入为容器的Bean
@Profiles
当组件满足环境条件时,可以注册该组件,类或方法注解
java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
@PropertySource
用于增加PropertySource到Spring的 Environment中,可使用占位符${}
Spring的StandardEnvironment配置有两个PropertySource对象 ,一个表示JVM系统属性(System.getProperties()),一个表示系统环境变量(System.getenv())。系统属性优先于环境变量,属性值为合并。
@EnableLoadTimeWeaving
LoadTimeWeaver被Spring用来在将类加载到Java虚拟机(JVM)中时动态地转换类
1.1.7 扩展
1.1.7.1 国际化
ApplicationContext 接口扩展了一个名为MessageSource的接口,提供了国际化("i18n")功能,ApplicationContext被加载时,它会自动搜索在上下文中定义的一个MessageSource,bean名称必须为messageSource
1.1.7.2 事件
内置事件
| 事件 | 说明 |
|---|---|
| ContextRefreshedEvent | 初始化或刷新ApplicationContext时发布(例如,通过使用ConfigurableApplicationContext 接口上的refresh() 方法)。 这里,"initialized"意味着加载所有bean,检测并激活bean的后置处理器,预先实例化单例,并且可以使用ApplicationContext对象。 只要上下文尚未关闭,只要所选的ApplicationContext实际支持这种"热"刷新,就可以多次触发刷新。 例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext 不支持。 |
| ContextStartedEvent | 启动 ApplicationContext 时发布。通过使用ConfigurableApplicationContext接口上的start()方法启动ApplicationContext 时发布。 通常,此信号用于在显式停止后重新启动Bean,但它也可用于启动尚未为自动启动配置的组件(例如,在初始化时尚未启动的组件)。 |
| ContextStoppedEvent | 当 ApplicationContext 停止时发布。通过使用ConfigurableApplicationContext接口上的close() 方法停止ApplicationContext时发布。 这里,"已停止"表示所有生命周期bean都会收到明确的停止信号。 可以通过start()调用重新启动已停止的上下文。 |
| ContextClosedEvent | 当 ApplicationContext 关闭时发布。通过使用ConfigurableApplicationContext接口上的close()方法关闭ApplicationContext时发布。 这里, "关闭" 意味着所有单例bean都被销毁。 封闭的环境达到其寿命终结。 它无法刷新或重新启动。 |
| RequestHandledEvent | 一个特定于Web的事件,告诉所有bean已经为HTTP请求提供服务。 请求完成后发布此事件。 此事件仅适用于使用Spring的DispatcherServlet的Web应用程序。 |
| ServletRequestHandledEvent | RequestHandledEvent的一个子类,添加了Servlet的特定上下文信息 |
自定义事件
继承ApplicationEvent以自定义事件,通过调用ApplicationEventPublisher的publishEvent()方法发布事件,通常,通过创建一个实现 ApplicationEventPublisherAware 接口的类并将其注册为 bean 来完成的
java
public class BlackListEvent extends ApplicationEvent {
private final String address;
private final String content;
public BlackListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// accessor and other methods...
}
java
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blackList;
private ApplicationEventPublisher publisher;
public void setBlackList(List<String> blackList) {
this.blackList = blackList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blackList.contains(address)) {
publisher.publishEvent(new BlackListEvent(this, address, content));
return;
}
// send email...
}
}
接收自定义事件,事件侦听器会同步接收事件。publishEvent()`方法将阻塞,直到所有侦听器都已完成对事件的处理
java
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
基于注解的事件监听@EventListener,若返回一个事件则相当于发布了一个事件(异步监听器@Async不支持),可通过@Order指定监听顺序
java
public class BlackListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
2 资源
Resource资源抽象接口封装资源的操作,ApplicationContext的部分子类构造函数上接收一个参数用于创建Resource,如ClassPathXmlApplicationContext
2.1 内置实现
2.1.1 UrlResource
UrlResource包装了 java.net.URL,访问任何通常可以通过 URL 访问的对象,如:
- file:用于访问文件系统路径
- https:用于通过 HTTPS 协议访问资源
- ftp:用于通过 FTP 访问资源
- 以及其他类型
java
public class UrlResourceTest {
public static void main(String[] args) throws Exception {
UrlResource urlResource = new UrlResource("file:D:\\学习\\study-code\\spring\\Spring-5.3.9\\src\\main\\resources\\微信图片_20240902182123.jpg");
System.out.printf("description: %s \n", urlResource.getDescription());
System.out.printf("filename: %s \n", urlResource.getFilename());
System.out.println("available:" + urlResource.getInputStream().available());
}
cmd
description: URL [file:D:/学习/study-code/spring/Spring-5.3.9/src/main/resources/微信图片_20240902182123.jpg]
filename: 微信图片_20240902182123.jpg
available:55754
2.1.2 ClassPathResource
ClassPathResource从类路径(classpath)中获取资源,它使用线程上下文类加载器、指定的类加载器或指定的类来加载资源,打包后资源位于JAR内,只能通过流读取,不可用 File 操作
java
public class ClassPathResourceTest {
public static void main(String[] args) throws Exception {
//加载类路径下的资源
ClassPathResource classPathResource1 = new ClassPathResource("微信图片_20240902182123.jpg");
System.out.println("available:" + classPathResource1.getInputStream().available());
}
}
cmd
available:55754
2.1.3 FileSystemResource
封装对java.io.File的处理
java
public class FileSystemResourceTest {
public static void main(String[] args) throws Exception {
//以/开头表示绝对路径,否则表示相对路径
FileSystemResource resource = new FileSystemResource("D:\\学习\\study-code\\spring\\Spring-5.3.9\\src\\main\\resources\\微信图片_20240902182123.jpg");
System.out.printf("description: %s \n", resource.getDescription());
System.out.printf("filename: %s \n", resource.getFilename());
System.out.println("available:" + resource.getInputStream().available());
}
}
cmd
description: file [D:\学习\study-code\spring\Spring-5.3.9\src\main\resources\微信图片_20240902182123.jpg]
filename: 微信图片_20240902182123.jpg
available:55754
2.1.4 PathResource
封装对java.nio.file.Path的处理
java
public class PathResourceTest {
public static void main(String[] args) throws Exception {
PathResource resource = new PathResource("D:\\学习\\study-code\\spring\\Spring-5.3.9\\src\\main\\resources\\微信图片_20240902182123.jpg");
System.out.printf("description: %s \n", resource.getDescription());
System.out.printf("filename: %s \n", resource.getFilename());
System.out.println("available:" + resource.getInputStream().available());
}
}
cmd
description: path [D:\学习\study-code\spring\Spring-5.3.9\src\main\resources\微信图片_20240902182123.jpg]
filename: 微信图片_20240902182123.jpg
available:55754
2.1.5 ServletContextResource
2.1.6 InputStreamResource
封装一打开的InputStream,只能读取一次
java
public InputStream getInputStream() throws IOException, IllegalStateException {
if (this.read) {
throw new IllegalStateException("InputStream has already been read - " +
"do not use InputStreamResource if a stream needs to be read multiple times");
}
this.read = true;
return this.inputStream;
}
2.1.7 ByteArrayResource
封装字节数组,每次读取时创建InputStream
2.2 ResourceLoader
加载Resource的策略接口,ApplicationContext接口扩展了此接口的功能
java
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");
ClassPathXmlApplicationContext返回ClassPathResource
FileSystemXmlApplicationContext返回FileSystemResource
WebApplicationContext返回ServletContextResource
可使用前缀如:classpath:、https:、file:等强制使用对应Resource
ResourceLoader标准实现类为DefaultResourceLoader可在ApplicationContext之外使用
java
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContex
java
public class ResourceLoaderTest {
public static void main(String[] args) throws Exception {
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource classPathResource = resourceLoader.getResource("classpath:微信图片_20240902182123.jpg");
System.out.println("classpath-available:" + classPathResource.getInputStream().available());
System.out.println("classPathResource的类名:" + classPathResource.getClass().getName());
Resource fileResource = resourceLoader.getResource("file:D:\\学习\\study-code\\spring\\Spring-5.3.9\\src\\main\\resources\\微信图片_20240902182123.jpg");
System.out.println("file-available:" + fileResource.getInputStream().available());
System.out.println("fileResource的类型:" + fileResource.getClass().getName());
}
}
cmd
classpath-available:55754
classPathResource的类名:org.springframework.core.io.ClassPathResource
file-available:55754
fileResource的类型:org.springframework.core.io.FileUrlResource
2.3 ResourcePatternResolver
ResourceLoader的扩展接口,解析Resource位置模式的策略接口(如ant路径模式),可解析类路径下的多个资源(classpath*:)
任何标准 ApplicationContext 中的默认 ResourceLoader 实际上都是实现 ResourcePatternResolver 的 PathMatchingResourcePatternResolver 的实例接口
java
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,MessageSource, ApplicationEventPublisher, ResourcePatternResolver
java
public AbstractApplicationContext() {
this.resourcePatternResolver = getResourcePatternResolver();
}
PathMatchingResourcePatternResolver 是一个独立的实现,可在 ApplicationContext 外部使用
2.4 Resource作为依赖
spring会自动将字符串转为对应的Resource
3 验证、数据绑定和类型转换
3.1 Spring中的Validator
ValidationUtils工具类可用于校验,错误信息传入Error中
java
public static class Person {
private String name;
private int age;
//...getter setter
}
public static class PersonValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.equals(clazz);
}
@Override
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
3.2 MessageCodesResolver
解析Error中的错误消息,默认实现为DefaultMessageCodesResolver
3.3 BeanWrapper
封装对Bean的操作,不直接使用,由DataBinder(在BeanPropertyBindingResult中,Errors和BingingResult的默认实现)和BeanFactory使用,提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性是否可读或可写的功能。支持属性的无限嵌套
java
public interface BeanWrapper extends ConfigurablePropertyAccessor
java
public interface ConfigurablePropertyAccessor extends PropertyAccessor, PropertyEditorRegistry, TypeConverter
PropertyEditorRegistry用于注册PropertyEditor
3.3.1 设置和获取基本及嵌套属性
name:与getName()或isName()和setName(..)方法对应的属性nameaccount.name:getAccount().setName()或getAccount().getName()方法对应的属性account中的嵌套属性nameaccont[2]:索引属性account的第三个元素。索引属性可以是array类型、list类型,或其他自然排序的集合account[COMPANYNAME]:accountMap属性中的COMPANYNAME关键索引的映射条目的值
java
public class BeanWrapperTest {
public static void main(String[] args) {
BeanWrapper company = new BeanWrapperImpl(new Company());
//设置company name
company.setPropertyValue("name", "Some Company Inc.");
//等同于
PropertyValue value = new PropertyValue("name", "Some Company Inc.");
company.setPropertyValue(value);
//创建managingDirector并绑定到company
BeanWrapper jim = new BeanWrapperImpl(new Employee());
jim.setPropertyValue("name", "Jim Stravinsky");
company.setPropertyValue("managingDirector", jim.getWrappedInstance());
//设置managingDirector的salary属性
company.setPropertyValue("managingDirector.salary", 100000f);
//通过company获取managingDirector的salary属性
Float salary = (Float) company.getPropertyValue("managingDirector.salary");
}
private static class Company {
private String name;
private Employee managingDirector;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Employee getManagingDirector() {
return this.managingDirector;
}
public void setManagingDirector(Employee managingDirector) {
this.managingDirector = managingDirector;
}
}
private static class Employee {
private String name;
private float salary;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public float getSalary() {
return salary;
}
public void setSalary(float salary) {
this.salary = salary;
}
}
}
3.3.2 内置PropertyEditor实现
Spring 使用 PropertyEditor 的概念来实现 Object 和 String 之间的转换,可以自定义 java.beans.PropertyEditor 的编辑器,在 BeanWrapper 上注册自定义编辑器,或者在特定的 IoC 容器中注册。
在XML的属性值或MVC框架解析HTTP请求的参数是由PropertyEditor完成的
PropertyEditorRegistrySupport内置了默认的PropertyEditor
注册自定义PropertyEditor到ApplicationContext
- 使用
ConfigurableBeanFactory接口的registerCustomEditor()方法 - 使用
CustomEditorConfigurer - 使用
PropertyEditorRegistrar,将PropertyEditor注册到PropertyEditorRegistry
java
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
public void registerCustomEditors(PropertyEditorRegistry registry) {
registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());
//可以注册多个PropertyEditor...
}
}
3.4 类型转换
通用类型转换系统,作为 PropertyEditor 实现的替代方案,Spring容器用来绑定Bean属性值,SpEL和DataBinder使用
3.4.1 Converter
java
@FunctionalInterface
public interface Converter<S, T> {
T convert(S source);
}
s为源类型,T为目标类型
3.4.2 ConverterFactory
java
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
Converter的工厂类,T为R的子类,根据传入的targetType获取对应的Converter
3.4.3 GenericConverter
java
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
更复杂更灵活的转换接口,可以转换多个源、目标类型,源、目标类型成对在getConvertibleTypes()方法中(源、目标类型封装为ConvertiblePair)
ConditionalGenericConverter 在满足条件时运行
java
public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}
3.4.4 ConversionService
封装类型转换逻辑统一的API,大多数 ConversionService 实现也实现了 ConverterRegistry ,它提供了用于注册转换器,应用程序启动时实例化,可以注入到Bean中使用
GenericConversionService 是一个通用实现,适用于大多数环境,内部有两个适配器类ConverterAdapter、ConverterFactoryAdapter,用于将Converter、ConverterFactory转为GenericConverter
spring中的默认ConversionService,实例化类型为GenericConversionService,可以调用setConverters()方法补充自定义转换器
java
public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean
3.5 spring字段格式化
3.5.1 Formatter
格式化日期和数字
java
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
3.5.2 注解形式的格式化
实现AnnotationFormatterFactory接口,将注解与Formatter绑定
java
public interface AnnotationFormatterFactory<A extends Annotation> {
//返回注解可使用的字段类型
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
例:@NumberFormat格式化Number字段,如Double 和 Long,@DateTimeFormat格式化 java.util.Date 、 java.util.Calendar 、 Long (用于毫秒时间戳)以及 JSR-310 java.time
3.5.3 FormatterRegistry
注册格式化和转换器
3.5.4 FormatterRegistrar
向FormatterRegistry注册Formatter
3.6 java的Bean的校验
更多信息见Bean Validation Hibernate Validator
LocalValidatorFactoryBean同时实现 javax.validation.ValidatorFactory 和 Validator 以及 Spring 的 org.springframework.validation.Validator,Spring在LocalValidatorFactoryBean中配置了默认的SpringConstraintValidatorFactory(ConstraintValidatorFactory的子类)创建ConstraintValidator实例
可注入javax.validation.Validator进行手动校验
自定义
- 注解加上
@Constraint - 实现接口
ConstraintValidator
4 SpEL
4.1 评估
常规使用是提供一个表达式,再将该表达式根据特定的根对象进行评估
基础使用,getValue()指定了转换类型时,原理是通过Converter接口转换
java
public class SpElTest {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
//表达式为字符串字面量
Expression expression = parser.parseExpression("'Hello World'");
String msg = (String) expression.getValue();
System.out.println("打印字符:" + msg);
//调用字符串字面量的方法
Expression upperCasexpression = parser.parseExpression("'Hello World'.toUpperCase()");
String upperCaseMsg = (String) upperCasexpression.getValue();
System.out.println("转为大写字符:" + upperCaseMsg);
//调用字符串字面量的属性并获取长度
Expression lenExpression = parser.parseExpression("'Hello World'.bytes.length");
Integer len = (Integer) lenExpression.getValue();
System.out.println("获取字符长度:" + len);
//通过String的构造函数创建字符串并调用转大写方法,getValue()指定了转换类型,原理是通过Converter接口转换
String constructMsg = parser
.parseExpression("new String('Hello World').toUpperCase()")
.getValue(String.class);
System.out.println("String构造函数的创建的字符串并转为大写:" + constructMsg);
}
}
cmd
打印字符:Hello World
转为大写字符:HELLO WORLD
获取字符长度:11
String构造函数的创建的字符串并转为大写:HELLO WORLD
4.1.1 EvaluationContext
评估表达式时的上下文,Spring提供的实现:
SimpleEvaluationContext仅支持 SpEL 语言语法的子集。不包括 Java 类型引用、构造函数和 bean 引用StandardEvaluationContext
4.1.2 SpelParserConfiguration
配置ExpressionParser表达式解析器,SpelCompilerMode指定编译器模式,评估过程中编译器生成一个java类,模式如下:
OFF(默认):编译器关闭IMMEDIATE:通常在第一次评估滞后MIXED:随着时间推移在解释和编译模式之间切换
4.2 Bean定义中的表达式
#{}设置Bean的属性,可以通过Bean的名称获取其属性
4.3 参考
更多信息参考docs.spring.io/spring-fram...
数组和列表可通过下标获取,map根据键名获取
java
// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
context, tesla, String.class);
// evaluates to "Idvor"
String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue(
societyContext, String.class);
可以使用特殊的 T 操作符来指定 java.lang.Class (类型)的实例。静态方法也可以通过使用此操作符来调用, java.lang 包内对类型的 T() 引用不需要完全限定
在EvaluationContext通过setVariable方法设置变量,在表达式中通过#{variableName}引用
5 Spring的面向切面编程
由于 Spring 的 AOP 框架基于代理,目标对象内的调用不会被拦截。对于 JDK 代理,只有代理上的公共接口方法调用可以被拦截。使用 CGLIB 时,代理上的公共和受保护方法调用会被拦截,通过 this 调用时,而 this 指向的是目标对象本身,而非代理对象
5.1 概念
Spring AOP仅支持在方法或类上执行连接点,更多的拦截功能使用AspectJ
切面 跨越多个类的关注点的模块化,例如实现切面的类
连接点 程序执行过程中的一个点,例如方法执行或异常处理
切点 匹配连接点的断言,用于确定哪些连接点会被织入通知
通知 切面在连接点采取的操作,类似拦截器
- 前置通知(Before):连接点之前运行
- 后置通知(After):连接点之后运行
- 返回通知(After Returning):连接点正常完成之后运行
- 异常通知(After Throwing):连接点抛出异常时运行
- 环绕通知(Around):连接点之前之后都会运行,可以控制连接点是否执行
引入 允许在不修改目标类代码的情况下,为现有类添加新的方法或字段
目标对象 被一个或多个切面通知的对象,该对象为代理对象
AOP代理 JDK动态代理或CGLB代理的对象
织入 将切面和目标对象连接起来,创建代理对象的过程
5.2 @Aspect
该注解在aspectjweaver包中
xml
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
或使用
xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.9</version>
</dependency>
启用注解需要添加@EnableAspectJAutoProxy,proxyTargetClass属性表示使用CGLB代理,exposeProxy为true时,将代理对象暴露到AOP 上下文,可在目标对象内通过AopContext.currentProxy()获取代理对象
java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
或使用XML的元素
xml
<aop:aspectj-autoproxy/>
自定义一个切面,添加注解@Aspect,并将该Bean注入到Spring容器中
切点,在返回类型为void的方法上使用@Pointcut注解,定义的切点方法应为空方法
java
@Pointcut("execution(* transfer(..))") //切点表达式
private void anyOldTransfer() {} //切点的签名方法
常用切点指示符
execution匹配方法执行的连接点within匹配指定类的内部方法this匹配实现了指定接口的代理对象target匹配实现了指定接口的目标对象args匹配参数实现了指定类的方法@target匹配有指定注解的类@args匹配有指定注解的方法@within匹配带有指定注解的类的方法@annotation匹配带有指定注解的方法
基本语法,支持正则,其中?表示可选
plaintext
execution(修饰符? 返回类型 类限定名.方法名(参数) 异常?)
- 修饰符 :可选,如
public、protect。 - 返回类型 :必填,如
void、String。 - 参数 :使用
..表示任意个参数,*表示单个任意参数。
可以使用 &&, || 和 ! 组合切入点表达式
java
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")//通过方法名称引用
private void tradingOperation() {}
-
@Before -
@AfterReturningreturning属性名称必须与方法中的参数名对应 -
@AfterThrowingthrowing属性名称必须与方法中的参数名对应 -
@After -
@Around方法第一个参数必须是ProceedingJoinPoint,返回的值为为方法的返回值,若为void则对应null上述注解中的
argNames若编译时带有 -parameters 参数,则编译时参数名会保存在字节码中,argNames可以省略,JoinPoint内置方法如下 -
getArgs(): 返回方法参数 -
getThis(): 返回代理对象 -
getTarget(): 返回目标对象 -
getSignature(): 返回正在被通知的方法的描述 -
toString(): 打印正在被建议的方法的有用描述。
第一个参数可以声明 org.aspectj.lang.JoinPoint类型,如下的示例中的args,绑定值按照方法顺序绑定,也可替换为@annotation获取注解(也可换为this、target、@within、@target、@args)
java
public class AOPTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.pht.aop");
TestBean bean = context.getBean(TestBean.class);
bean.testMethod("test 参数");
bean.testMethod("参数1", "参数2");
context.close();
}
}
@EnableAspectJAutoProxy
@Component
@Aspect
class AOPAspect {
// 定义一个切点,匹配TestBean类中返回值为任意类型、参数为任意数量的testMethod方法
@Pointcut("execution(* com.pht.aop.TestBean.testMethod(..))")
public void pointcut() {
}
// 前置通知,匹配pointcut定义的切点
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("前置通知,方法:" + joinPoint.getSignature().getName());
}
//返回任意类型,参数类型为String的testMethod方法
@Before("execution(* com.pht.aop.TestBean.testMethod(String))")
public void before2(JoinPoint joinPoint) {
System.out.println("前置通知2,方法:" + joinPoint.getSignature().getName());
}
/*
*返回任意类型,参数类型为任意数量的testMethod方法,
*如果使用参数名代替args中参数类型,则会传递值到通知方法中的参数中,
*匹配参数类型与方法中参数类型一致的方法
*/
@Before("execution(* com.pht.aop.TestBean.testMethod(..)) && args(name)")
public void before3(JoinPoint joinPoint, String name) {
System.out.println("前置通知3,参数:" + name);
}
/*
*如果使用参数名代替args中参数类型,则会传递值到通知方法中的参数中,
*args必须指定且与方法中的参数一致
*匹配参数类型与方法中参数类型一致的方法
* 若编译时带有 -parameters 参数,则编译时参数名会保存在字节码中,argNames可以省略
*/
@Pointcut(value = "execution(* com.pht.aop.TestBean.testMethod(..))&&args(name,value)", argNames = "name,value")
public void pointcut1(String name, String value) {
}
/*
*如果使用参数名代替args中参数类型,则会传递值到通知方法中的参数中,
*argNames必须指定且与方法中的参数一致,JoinPoint可省略
* 若编译时带有 -parameters 参数,则编译时参数名会保存在字节码中,argNames可以省略
*/
@Before(value = "pointcut1(name,value)", argNames = "joinPoint,name,value")
public void before4(JoinPoint joinPoint, String name, String value) {
System.out.println("前置通知4,参数:" + name);
System.out.println("前置通知4,参数2:" + value);
}
@AfterReturning(pointcut = "pointcut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("返回通知,方法:" + joinPoint.getSignature().getName());
System.out.println("返回通知,结果:" + result);
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知开始,方法:" + joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
System.out.println("环绕通知结束,方法:" + joinPoint.getSignature().getName());
return result;
}
}
@Component
class TestBean {
public String testMethod(String name) {
System.out.println("原方法被调用");
return "test result";
}
public String testMethod(String name, String value) {
System.out.println("两个参数的原方法被调用");
return "two parameters method";
}
cmd
环绕通知开始,方法:testMethod
前置通知,方法:testMethod
前置通知2,方法:testMethod
前置通知3,参数:test 参数
原方法被调用
返回通知,方法:testMethod
返回通知,结果:test result
环绕通知结束,方法:testMethod
环绕通知开始,方法:testMethod
前置通知,方法:testMethod
前置通知4,参数:参数1
前置通知4,参数2:参数2
两个参数的原方法被调用
返回通知,方法:testMethod
返回通知,结果:two parameters method
环绕通知结束,方法:testMethod
对同一个连接点,两个切面的通知顺序可由org.springframework.core.Ordered或@Ordered指定,值越低,优先级越高
接口ParameterNameDiscoverer用于解析方法参数
5.3 引入
允许切面为被通知的对象实现指定接口,并为这些对象提供具体的实现
@DeclareParents注解
value类型匹配模式defaultImpl指定默认的实现
java
public class DeclareParentsTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.pht.aop");
UserService userService = context.getBean(UserService.class);
userService.createUser("张三");
if (userService instanceof Loggable) {
Loggable loggable = (Loggable) userService;
loggable.log("UserService 实现了 Loggable.");
} else {
System.out.println("UserService 未实现 Loggable.");
}
}
}
interface Loggable {
void log(String message);
}
class LoggableImpl implements Loggable {
@Override
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
//被通知的类
@Service
class UserService {
public void createUser(String username) {
System.out.println("创建用户: " + username);
}
}
@EnableAspectJAutoProxy
@Aspect
@Component
class LoggableIntroductionAspect {
// @DeclareParents 注解用于引入接口
// value:指定目标类的匹配模式(UserService及其子类)
// defaultImpl:指定接口的默认实现类
@DeclareParents(value = "com.pht.aop.UserService+", defaultImpl = LoggableImpl.class)
public Loggable loggable; //引入的接口实例
}
也可以使用 org.springframework.aop.aspectj.annotation.AspectJProxyFactory 类为目标对象创建代理
5.4 基于XML的AOP
见官网
5.5 AOP的APIS
5.5.1 Pointcut
org.springframework.aop.Pointcut ,子类AspectJExpressionPointcut用于解析Aspecth切点表达式
java
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
ClassFilter用于判断类是否匹配,MethodMatcher用于判断方法是否匹配,MethodMatcher的两个参数的方法matches()在创建AOP代理时执行,若返回true且isRuntime()也为true,则方法在每次调用时都会执行三个参数的matches()方法动态判断。
在AbstractAutoProxyCreator类中的方法postProcessBeforeInstantiation()中,调用的getAdvicesAndAdvisorsForBean()方法会调用Pointcut的getClassFilter()方法和getMethodMatcher()方法判断切点是否匹配类上的方法
java
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
...
if (targetSource != null) {
if (StringUtils.hasLength(beanName)) {
this.targetSourcedBeans.add(beanName);
}
//判断切点是否匹配类上的方法
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
//创建代理对象,以及需要通知的方法
Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
return null;
}
具体的使用在AopUtils.canApply()方法中
java
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
return true;
}
IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
}
Set<Class<?>> classes = new LinkedHashSet<>();
if (!Proxy.isProxyClass(targetClass)) {
classes.add(ClassUtils.getUserClass(targetClass));
}
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
if (introductionAwareMethodMatcher != null ?
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}
5.5.2 通知
环绕通知 org.aopalliance.intercept.MethodInterceptor
前置通知 BeforeAdvice
异常通知 ThrowsAdvice
返回通知 AfterReturningAdvice
通知的具体调用都是通过实现MethodInterceptor调用对应Advice
ThrowsAdvice必须实现public void afterThrowing(Method method, Object[] args, Object target, Exception ex)方法,该方法通过反射调用,前三个参数是可选的,可以为一个或四个参数,具体调用由ThrowsAdviceInterceptor实现
5.5.3 Advisor
将切点表达式与通知关联,以实现对要拦截的方法进行通知
java
public class MethodInterceptorTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
TargetClass target = context.getBean(TargetClass.class);
target.targetMethod();
context.close();
}
}
class CustomMethodInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("前置拦截: " + invocation.getMethod().getName());
Object result = invocation.proceed();
System.out.println("后置拦截: " + invocation.getMethod().getName());
return result;
}
}
class TargetClass {
public void targetMethod() {
System.out.println("执行目标方法");
}
}
@EnableAspectJAutoProxy
@Configuration
class TestConfiguration {
@Bean
public CustomMethodInterceptor customMethodInterceptor() {
return new CustomMethodInterceptor();
}
@Bean
public TargetClass targetClass() {
return new TargetClass();
}
@Bean
public AspectJExpressionPointcutAdvisor aspectJExpressionPointcutAdvisor() {
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression("execution(* com.pht.aop.TargetClass.targetMethod(..))");
advisor.setAdvice(customMethodInterceptor());
return advisor;
}
}
cmd
前置拦截: targetMethod
执行目标方法
后置拦截: targetMethod
5.5.4 ProxyFactoryBean创建代理对象
目标类若实现了接口,则创建JDK代理,否则创建CGLB代理
org.springframework.aop.framework.ProxyFactoryBean为目标bean手动设置代理对象
| 属性名 | 作用 |
|---|---|
target |
指定被代理的目标对象。 |
proxyInterfaces |
指定代理需要实现的接口(仅 JDK 代理需要)。 |
interceptorNames |
指定拦截器 Bean 的名称(如org.aopalliance.intercept.MethodInterceptor, Advisor, Advice, SingletonTargetSource)。若为*则匹配所有拦截器,或使用global*匹配前缀相同的拦截器 |
proxyTargetClass |
是否使用 CGLIB 代理目标类(true 时),默认为 false(JDK 代理)。 |
optimize |
是否优化代理,启用 CGLIB 代理的优化策略。 |
exposeProxy |
是否将代理暴露到 ThreadLocal 中,允许通过 AopContext.currentProxy() 访问。 |
java
public class ProxyFactoryBeanTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProxyFactoryBeanConfig.class);
TargetProxyClass target = context.getBean(TargetProxyClass.class);
target.myMethod();
context.close();
}
}
@Configuration
class ProxyFactoryBeanConfig {
@Bean
public ProxyFactoryBean targetProxyClass() {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
//设置拦截对象
proxyFactoryBean.setTarget(new TargetProxyClass());
//设置拦截器,拦截器为容器的Bean的id
proxyFactoryBean.setInterceptorNames("myMethodInterceptor", "beforeAdvice");
return proxyFactoryBean;
}
@Bean
public MethodInterceptor myMethodInterceptor() {
return invocation -> {
System.out.println("前置拦截: " + invocation.getMethod().getName());
Object result = invocation.proceed();
System.out.println("后置拦截: " + invocation.getMethod().getName());
return result;
};
}
@Bean
public MethodBeforeAdvice beforeAdvice() {
return (method, args, target) -> {
System.out.println("前置拦截1: " + method.getName());
};
}
}
class TargetProxyClass {
public void myMethod() {
System.out.println("执行目标方法");
}
}
cmd
前置拦截: myMethod
前置拦截1: myMethod
执行目标方法
后置拦截: myMethod
5.5.5 ProxyFactory创建代理对象
org.springframework.aop.framework.ProxyFactory
java
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();
5.5.6 自动创建代理对象
BeanNameAutoProxyCreator
代理匹配属性beanNames的Bean,通过指定interceptorNames属性设置拦截器或通知,
DefaultAdvisorAutoProxyCreator