学习笔记-spring core

基于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接口的startstop方法需要显示的调用,若想在容器容器时调用需要实现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属性的包路径。includeFiltersexcludeFilters属性可自定义包括或排除的行为,每个filter元素都需要包含typeexpression属性

过滤类型 表达式例子 描述
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 类、ImportSelectorImportBeanDefinitionRegistrar 的实现类)并注入为容器的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以自定义事件,通过调用ApplicationEventPublisherpublishEvent()方法发布事件,通常,通过创建一个实现 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中,ErrorsBingingResult的默认实现)和BeanFactory使用,提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性是否可读或可写的功能。支持属性的无限嵌套

java 复制代码
public interface BeanWrapper extends ConfigurablePropertyAccessor
java 复制代码
public interface ConfigurablePropertyAccessor extends PropertyAccessor, PropertyEditorRegistry, TypeConverter

PropertyEditorRegistry用于注册PropertyEditor

3.3.1 设置和获取基本及嵌套属性

  • name:与 getName()isName()setName(..) 方法对应的属性 name
  • account.name: getAccount().setName()getAccount().getName() 方法对应的属性 account 中的嵌套属性 name
  • accont[2]:索引属性 account 的第三个元素。索引属性可以是 array 类型、 list 类型,或其他自然排序的集合
  • account[COMPANYNAME]:account Map 属性中的 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 的概念来实现 ObjectString 之间的转换,可以自定义 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 是一个通用实现,适用于大多数环境,内部有两个适配器类ConverterAdapterConverterFactoryAdapter,用于将ConverterConverterFactory转为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字段,如DoubleLong@DateTimeFormat格式化 java.util.Datejava.util.CalendarLong (用于毫秒时间戳)以及 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.ValidatorFactoryValidator 以及 Spring 的 org.springframework.validation.Validator,Spring在LocalValidatorFactoryBean中配置了默认的SpringConstraintValidatorFactoryConstraintValidatorFactory的子类)创建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>

启用注解需要添加@EnableAspectJAutoProxyproxyTargetClass属性表示使用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(修饰符? 返回类型 类限定名.方法名(参数) 异常?)
  • 修饰符 :可选,如publicprotect
  • 返回类型 :必填,如voidString
  • 参数 :使用..表示任意个参数,*表示单个任意参数。

可以使用 &&, ||! 组合切入点表达式

java 复制代码
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {} 
@Pointcut("within(com.xyz.myapp.trading..*)")
private void inTrading() {} 
@Pointcut("anyPublicOperation() && inTrading()")//通过方法名称引用
private void tradingOperation() {}
  • @Before

  • @AfterReturning returning属性名称必须与方法中的参数名对应

  • @AfterThrowing throwing属性名称必须与方法中的参数名对应

  • @After

  • @Around 方法第一个参数必须是ProceedingJoinPoint,返回的值为为方法的返回值,若为void则对应null

    上述注解中的argNames若编译时带有 -parameters 参数,则编译时参数名会保存在字节码中,argNames可以省略,JoinPoint内置方法如下

  • getArgs(): 返回方法参数

  • getThis(): 返回代理对象

  • getTarget(): 返回目标对象

  • getSignature(): 返回正在被通知的方法的描述

  • toString(): 打印正在被建议的方法的有用描述。

第一个参数可以声明 org.aspectj.lang.JoinPoint类型,如下的示例中的args,绑定值按照方法顺序绑定,也可替换为@annotation获取注解(也可换为thistarget@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代理时执行,若返回trueisRuntime()也为true,则方法在每次调用时都会执行三个参数的matches()方法动态判断。

AbstractAutoProxyCreator类中的方法postProcessBeforeInstantiation()中,调用的getAdvicesAndAdvisorsForBean()方法会调用PointcutgetClassFilter()方法和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

相关推荐
考虑考虑5 小时前
feign异常处理
spring boot·后端·spring
知其然亦知其所以然7 小时前
Spring AI 入门实战:我用七个关键词,彻底搞懂了它的核心概念!
java·后端·spring
RexTechie8 小时前
Spring Cloud 原生中间件
spring·spring cloud·中间件
chanalbert8 小时前
Spring Boot诞生背景:从Spring的困境到设计破局
java·spring boot·spring
huisheng_qaq8 小时前
【Spring源码核心篇-08】spring中配置类底层原理和源码实现
java·spring·spring源码·spring配置类解析·spring注解与实现
LUCIAZZZ9 小时前
项目拓展-Apache对象池,对象池思想结合ThreadLocal复用日志对象
java·jvm·数据库·spring·apache·springboot
武帝为此9 小时前
【SpringMVC 入门介绍】
java·spring·mvc
dearxue10 小时前
你需要的企业级MCP开发来了-Apihug 1.4.1-RELEASE + Plugin 0.7.5 一键让你 Spring 程序拥有企业级MCP 功能!
spring·api·mcp
麦兜*10 小时前
Spring Boot 集成国内AI,包含文心一言、通义千问和讯飞星火平台实战教程
java·人工智能·spring boot·后端·spring·ai·文心一言