学习笔记-spring core

159 阅读24分钟

基于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()方法中

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接口可实现执行顺序,值越小,越先执行

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实现毁掉接口或注解是容器扩展的一种常用手段

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,不推荐

@FunctionalInterface
public interface BeanFactoryPostProcessor {

    /**
     * 已加载所有的Bean定义,未实例化Bean
     */
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;

}

PropertySourcesPlaceholderConfigurer用于替换占位符

FactoryBean

org.springframework.beans.factory.FactoryBean

继承该接口,注册为Bean,通过id获取FactoryBean实例时,需在前面加上**&**,否则获取的是getObject中的实例

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要在目标组件中的类级别出现的注解。
assignableorg.example.SomeClass目标组件可分配给(继承或实现)的类(或接口)。
aspectjorg.example..*Service+要由目标组件匹配的AspectJ类型表达式。
regexorg\.example\.Default.*要由目标组件类名匹配的正则表达式。
customorg.example.MyTypeFilterorg.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

当组件满足环境条件时,可以注册该组件,类或方法注解

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应用程序。
ServletRequestHandledEventRequestHandledEvent的一个子类,添加了Servlet的特定上下文信息

自定义事件

继承ApplicationEvent以自定义事件,通过调用ApplicationEventPublisherpublishEvent()方法发布事件,通常,通过创建一个实现 ApplicationEventPublisherAware 接口的类并将其注册为 bean 来完成的

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...
}
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()`方法将阻塞,直到所有侦听器都已完成对事件的处理

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指定监听顺序

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 访问资源
  • 以及其他类型
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());
}
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 操作

public class ClassPathResourceTest {
    public static void main(String[] args) throws Exception {
        //加载类路径下的资源
        ClassPathResource classPathResource1 = new ClassPathResource("微信图片_20240902182123.jpg");
        System.out.println("available:" + classPathResource1.getInputStream().available());
    }
}
available:55754

2.1.3 FileSystemResource

封装对java.io.File的处理

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());
    }
}
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的处理

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());
    }
}
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,只能读取一次

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接口扩展了此接口的功能

Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

ClassPathXmlApplicationContext返回ClassPathResource

FileSystemXmlApplicationContext返回FileSystemResource

WebApplicationContext返回ServletContextResource

可使用前缀如:classpath:https:file:等强制使用对应Resource

ResourceLoader标准实现类为DefaultResourceLoader可在ApplicationContext之外使用

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContex
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());
    }
}
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 的实例接口

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,MessageSource, ApplicationEventPublisher, ResourcePatternResolver
public AbstractApplicationContext() {
		this.resourcePatternResolver = getResourcePatternResolver();
}

PathMatchingResourcePatternResolver 是一个独立的实现,可在 ApplicationContext 外部使用

2.4 Resource作为依赖

spring会自动将字符串转为对应的Resource

3 验证、数据绑定和类型转换

3.1 Spring中的Validator

ValidationUtils工具类可用于校验,错误信息传入Error

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使用,提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性是否可读或可写的功能。支持属性的无限嵌套

public interface BeanWrapper extends ConfigurablePropertyAccessor
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 关键索引的映射条目的值
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
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

@FunctionalInterface
public interface Converter<S, T> {
    T convert(S source);
}

s为源类型,T为目标类型

3.4.2 ConverterFactory

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

package org.springframework.core.convert.converter;
public interface GenericConverter {
    public Set<ConvertiblePair> getConvertibleTypes();
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

更复杂更灵活的转换接口,可以转换多个源、目标类型,源、目标类型成对在getConvertibleTypes()方法中(源、目标类型封装为ConvertiblePair

ConditionalGenericConverter 在满足条件时运行

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()方法补充自定义转换器

public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean 

3.5 spring字段格式化

3.5.1 Formatter

格式化日期和数字

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

3.5.2 注解形式的格式化

实现AnnotationFormatterFactory接口,将注解与Formatter绑定

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接口转换

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);
    }
}
打印字符: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根据键名获取

// 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包中

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

或使用

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-aspects</artifactId>
	<version>5.3.9</version>
</dependency>

启用注解需要添加@EnableAspectJAutoProxyproxyTargetClass属性表示使用CGLB代理,exposeProxy为true时,将代理对象暴露到AOP 上下文,可在目标对象内通过AopContext.currentProxy()获取代理对象

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

或使用XML的元素

<aop:aspectj-autoproxy/>

自定义一个切面,添加注解@Aspect,并将该Bean注入到Spring容器中

切点,在返回类型为void的方法上使用@Pointcut注解,定义的切点方法应为空方法

@Pointcut("execution(* transfer(..))") //切点表达式
private void anyOldTransfer() {} //切点的签名方法

常用切点指示符

  • execution 匹配方法执行的连接点
  • within 匹配指定类的内部方法
  • this 匹配实现了指定接口的代理对象
  • target 匹配实现了指定接口的目标对象
  • args 匹配参数实现了指定类的方法
  • @target 匹配有指定注解的类
  • @args 匹配有指定注解的方法
  • @within 匹配带有指定注解的类的方法
  • @annotation 匹配带有指定注解的方法

基本语法,支持正则,其中表示可选

execution(修饰符? 返回类型 类限定名.方法名(参数) 异常?)
  • 修饰符:可选,如publicprotect
  • 返回类型:必填,如voidString
  • 参数:使用..表示任意个参数,*表示单个任意参数。

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

@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

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";
    }
环绕通知开始,方法: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 指定默认的实现
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切点表达式

public interface Pointcut {
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

ClassFilter用于判断类是否匹配,MethodMatcher用于判断方法是否匹配,MethodMatcher的两个参数的方法matches()在创建AOP代理时执行,若返回trueisRuntime()也为true,则方法在每次调用时都会执行三个参数的matches()方法动态判断。

AbstractAutoProxyCreator类中的方法postProcessBeforeInstantiation()中,调用的getAdvicesAndAdvisorsForBean()方法会调用PointcutgetClassFilter()方法和getMethodMatcher()方法判断切点是否匹配类上的方法

@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()方法中

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

将切点表达式与通知关联,以实现对要拦截的方法进行通知

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;
    }
}

前置拦截: 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() 访问。
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("执行目标方法");
    }
}
前置拦截: myMethod
前置拦截1: myMethod
执行目标方法
后置拦截: myMethod

5.5.5 ProxyFactory创建代理对象

org.springframework.aop.framework.ProxyFactory

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

5.5.6 自动创建代理对象

BeanNameAutoProxyCreator

代理匹配属性beanNames的Bean,通过指定interceptorNames属性设置拦截器或通知,

DefaultAdvisorAutoProxyCreator