Spring开发笔记

Spring Framework

Spring框架被设计的目的是使开发人员更专注于应用程序的业务逻辑开发,而不必将精力分散在特定的开发环境上。

Rod Johnson

​ Spring Framework 创始人、作者,有趣的是,他的专业不是计算机,而是音乐学。

​ 2002年澳大利亚工程师 Rod Johnson 在其著作 Expert One-on-One J2EE Design and Development 中提出Spring的概念。并于2004年与其他人共同发布Spring 1.0版本。

版本时间线

版本 时间线 描述
1.0 2004 年
2.0 2006 年
3.0 2009 年
4.0 2013 年 对Java 8 的全面支持
5.0 2017 年
6.0 2022 年 最低支持Java 17

本文基于 Spring Framework 5 进行学习,5.3.x版本在 JDK 8、JDK 11、JDK 17 上提供长期支持。

功能特性

Spring 功能特性总览:

功能
核心技术 IoC、AOP、Events、Resources, i18n, validation, Data binding, Type conversion, SpEL
Web Servlet Spring MVC, WebSocket, SockJS, STOMP Messaging.
Web Reactive Spring WebFlux, WebClient, WebSocket, RSocket.
数据访问 Transactions, DAO support, JDBC, ORM, XML Marshalling
测试 Mock Objects, TestContext framework, Spring MVC Test, WebTestClient.
集成整合 REST Clients, JMS, JCA, JMX, Email, Tasks, Scheduling, Caching, Observability.
语言 Kotlin, Groovy, dynamic languages.

Spring Core

Spring Core 是Spring 框架的核心技术,包括IoC、AOP 等。

IoC 容器

​ IoC 是控制反转的缩写 (Inversion of Control),是面向对象编程中的一种设计理念,用来降低代码之间的耦合度,可以把它看作一种设计模式。

​ 那么什么控制被反转了?答案是依赖对象的获取被反转了,因为大多数应用程序都是由两个或多个类通过彼此合作来实现业务逻辑,这使得每个对象都需要获取与其合作的对象(也就是所依赖的对象),如果这个获取过程需要自己实现,那么将导致代码的耦合度越来越高,不方便维护和调试。

​ 控制反转是一种思想,实现方式有两种:

  • 依赖查找
  • 依赖注入

Spring 框架使用依赖注入的方式实现IoC 控制反转 。

BeanFactory 容器

BeanFactory 是 Spring IoC容器的顶级接口。所有的 IoC容器必须实现该接口。它主要的功能是为依赖注入提供支持。

image-20230704214821063

BeanFactory

​ BeanFactory 负责初始化这些Bean,并管理其整个生命周期。在注册Bean时,Bean的名称是全局唯一的,如果存在重复名称的Bean存在,则抛出异常。

ListableBeanFactory

​ BeanFactory 的拓展接口,可以批量查找所有的bean 实例,而不是一个一个的查找。

HierarchicalBeanFactory

​ 父子级联接口,查找Bean时,在子容器中找不到会去父容器中查找,但父容器不会去子容器中查找Bean。

ConfigurableBeanFactory

Spring的ConfigurableBeanFactory 接口是Spring Bean工厂的扩展接口之一,它允许程序员通过编程方式定制Bean工厂的行为。

ConfigurableBeanFactory接口继承了BeanFactory接口,并添加了一些额外的方法,例如:

  • registerSingleton(String name, Object singletonObject):向工厂注册一个单例bean。
  • registerAlias(String name, String alias):向工厂注册一个bean别名。
  • ignoreDependencyInterface(Class<?> ifc):告诉工厂忽略依赖检查,即忽略特定接口的依赖注入。
  • addBeanPostProcessor(BeanPostProcessor beanPostProcessor):添加一个bean后处理器,用于自定义bean实例化、初始化或销毁时的行为。
  • registerScope(String scopeName, Scope scope):注册一个新的作用域,用于创建bean实例的上下文。

通过使用ConfigurableBeanFactory接口,我们可以在应用程序中自定义Bean工厂的行为,以满足特定的需求。

AutowireCapableBeanFactory

​ AutowireCapableBeanFactory接口是Spring 的一个扩展接口,继承自BeanFactory接口。它提供了对自动装配(autowiring)功能的支持,可以用于在运行时动态创建和自动装配Bean实例。

AutowireCapableBeanFactory接口定义了一些方法,其中一些主要方法包括:

  1. autowireBean(object: Any):自动装配给定的现有对象。该方法会解析对象的依赖关系,并自动装配所需的Bean。

  2. createBean(beanClass: Class<T>):创建指定类型的新Bean实例。该方法会实例化并初始化Bean对象,并解析和装配其依赖关系。

  3. applyBeanPostProcessorsBeforeInitialization(existingBean: Any, beanName: String):在初始化之前应用所有注册的BeanPostProcessor对象对现有的Bean进行处理。

  4. applyBeanPostProcessorsAfterInitialization(existingBean: Any, beanName: String):在初始化之后应用所有注册的BeanPostProcessor对象对现有的Bean进行处理。

​ AutowireCapableBeanFactory接口的主要作用是通过编程方式控制Bean的创建和装配过程。它可以在运行时动态地创建Bean实例,并自动解析和注入它们的依赖关系。这在某些特定的场景下非常有用,例如在使用第三方库或框架时,需要将其对象纳入Spring的管理范围,或者在特定的业务逻辑中需要动态创建和配置Bean对象。

ApplicationContext 容器

​ ApplicationContext 是 BeanFactory 的子接口,也被称为 Spring 上下文。ApplicationContext 是 Spring 中较高级的容器,它包含 BeanFactory 所有的功能,一般情况下,相对于 BeanFactory,ApplicationContext 支持更多功能。当然,BeanFactory 仍可以在轻量级应用中使用,比如移动设备或者基于 applet 的应用程序。

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory,
	HierarchicalBeanFactory,MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    
}

​ BeanFactory 提供了IoC容器最基本的功能,ApplicationContext 继承自 BeanFactory ,除了拥有BeanFactory 的基本功能外,还拓展了很多高级功能:

  1. ListableBeanFactory 可列举的BeanFactory
  2. HierarchicalBeanFactory 有父子关系的BeanFactory
  3. ResourcePatternResolver 资源模式解析器
  4. Environment 环境可配置接口
  5. ApplicationEventPublisher 应用事件发布接口
  6. MessageSource 消息国际化接口
image-20230704202201607

ApplicationContext 的几种实现类:

  • AnnotationConfigServletWebServerApplicationContext
  • ClassPathXmlApplicationContext 基于 xml 配置文件的 Ioc 容器
  • AnnotationConfigApplicationContext 基于注解的 Ioc 容器
  • XmlWebApplicationContext web应用中基于 xml 文件的 Ioc 容器(外置tomcat)
  • ServletWebServerApplicationContext springboot使用内置tocmat 作为web容器时的IOC容器类

Environment接口

​ Environment接口是Spring Framework中的一个核心接口,用于表示应用程序的环境信息,包括配置属性、配置文件、系统属性、环境变量等。它提供了访问和操作应用程序环境配置的方法。

Environment接口定义了一些常用的方法,其中一些主要方法包括:

  1. getProperty(key: String):获取指定键的配置属性值。

  2. getProperty(key: String, defaultValue: String):获取指定键的配置属性值,如果不存在则返回默认值。

  3. containsProperty(key: String):判断是否存在指定键的配置属性。

  4. getRequiredProperty(key: String):获取指定键的必需配置属性值,如果不存在则抛出异常。

  5. getActiveProfiles():获取当前激活的配置文件(Profiles)名称。

  6. getDefaultProfiles():获取默认的配置文件(Profiles)名称。

  7. getSystemProperties():获取系统属性的键值对集合。

  8. getSystemEnvironment():获取环境变量的键值对集合。

​ 通过Environment接口,可以方便地获取应用程序的配置属性,例如数据库连接信息、URL地址、超时设置等。它还可以用于根据不同的环境配置文件(Profiles)加载不同的配置信息,实现应用程序在不同环境下的灵活配置和部署。Environment接口可以在Spring应用程序的各个组件中使用,包括配置类、Bean定义、注解等,以便根据不同的环境和需求进行配置和定制。

ResourcePatternResolver 接口

​ ResourcePatternResolver 资源模式解析器接口是Spring 的一个扩展接口,继承自ResourceLoader接口。它用于解析资源模式,并获取匹配的资源对象。

ResourcePatternResolver接口定义了一些方法,其中一些主要方法包括:

  1. getResources(locationPattern: String):根据指定的资源模式获取匹配的资源对象数组。资源模式可以使用通配符和Ant风格的路径匹配规则。

  2. getClassLoader():获取用于加载资源的类加载器。

  3. getResourceLoader(): 获取底层的ResourceLoader对象。

​ ResourcePatternResolver接口的主要作用是提供一种统一的方式来解析资源模式,并获取符合模式的资源对象。通过使用资源模式,可以方便地获取多个资源,例如类路径下的所有配置文件、特定目录下的所有图片文件等。在Spring应用程序中,常用的资源模式解析器是PathMatchingResourcePatternResolver,它是ResourcePatternResolver的默认实现类。该类使用Ant风格的路径模式匹配规则,可以灵活地解析资源路径,支持通配符、正则表达式等。通过使用ResourcePatternResolver接口,可以方便地在Spring应用程序中加载和管理各种资源,实现更灵活和可配置的资源管理。

MessageSource接口

​ MessageSource接口是Spring 中用于处理国际化(i18n)消息的核心接口。它提供了获取本地化消息的方法,可以根据指定的消息代码和参数,返回对应语言环境下的本地化消息文本。

MessageSource接口定义了一些方法,其中一些主要方法包括:

  1. getMessage(code: String, args: Array<Any>?, defaultMessage: String?, locale: Locale?): String:根据消息代码、参数、默认消息和语言环境获取本地化消息。如果找不到对应的消息,则返回默认消息。

  2. getMessage(code: String, args: Array<Any>?, locale: Locale?): String:根据消息代码、参数和语言环境获取本地化消息。如果找不到对应的消息,则返回null。

  3. getMessage(resolvable: MessageSourceResolvable, locale: Locale?): String:根据消息源解析器和语言环境获取本地化消息。消息源解析器可以包含消息代码、参数、默认消息等信息。

​ MessageSource接口的主要作用是实现应用程序的国际化消息支持。通过将消息文本提取到外部资源文件中,并使用MessageSource接口来获取本地化消息,可以使应用程序能够根据用户的语言环境展示相应的文本。这样可以方便地支持多语言版本的应用程序,并提供更好的用户体验。在Spring应用程序中,常用的MessageSource实现是ResourceBundleMessageSource,它基于Java的ResourceBundle机制来加载和管理消息资源。除了使用默认的资源文件外,还可以根据需求自定义MessageSource的实现,以适应不同的消息存储方式和加载策略。

获取Spring容器

Spring 中获取IoC容器的几种方式:

  1. 继承抽象类 ApplicationObjectSupport 或 WebApplicationObjectSupport
  2. 实现接口ApplicationContextAware
  3. 使用 ContextLoader.getCurrentWebApplicationContext() 获取
@Component
public class SysAdminMail implements ApplicationContextAware {

	private ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

应用启动时会自动调用 ApplicationContextAware 接口的 setApplicationContext 方法从而获取到IoC容器对象。

配置元数据

在Spring中,由IoC 容器管理的对象称之为 Bean,IoC容器根据配置的元数据来实例化,配置和组装Bean。配置元数据的方式有3种:

  • 基于XML方式配置
  • 基于注解方式配置(Spring 2.5开始支持)
  • 基于Java方式配置(Spring 3.0开始支持)

​ 基于XML是最早的配置方式,后来出现基于注解和基于Java的方式进行配置。任何一种方式都不是必须的一种,基于XML方式配置已经越来越少用,甚至可以不用,新项目应该优先使用注解或Java的方式配置元数据。

​ 基于Java方式配置元数据请参照 @Configuration, @Bean, @Import,和@DependsOn 注解。

注:IoC 容器中的Bean 名称默认以小写字母开头,使用驼峰式命名,如 userDao,loginController。也可以手动指定Bean名称。Bean名称全局不可以重复。

基于注解方式配置

使用 @Component 及其扩展注解(@Controller, @Service,@Repository, @Configuration )标注一个组件为Spring Bean,

常用声明Bean的注解如下:

注解 描述
@Component 通用组件
@Controller 控制层组件
@Service 业务层组件
@Repository DAO层组件
@Configuration 配置层组件
@Bean 注册一个Bean

示例1:

示例2:

@Configuration
public class SimpleSessionFactoryConfiguration extends ApplicationObjectSupport {
    
    	@Bean({ HibernateStoreConstants.BEAN_PRIMARY_HIBERNATE_SESSION_FACTORY,
				HibernateStoreConstants.BEAN_NP_PRIMARY_HIBERNATE_SESSION_FACTORY })
		@Primary
		@Lazy
		@ConditionalOnMissingBean(name = { HibernateStoreConstants.BEAN_PRIMARY_HIBERNATE_SESSION_FACTORY,
				HibernateStoreConstants.BEAN_NP_PRIMARY_HIBERNATE_SESSION_FACTORY })
		public HibernateSessionFactoryBean sessionFactory() throws Exception {
            //TODO
        }
}

条件装配Bean

Spring提供根据条件装配Bean的功能:(@Conditional及@Conditional的拓展注解)

注解
@Conditional 需要实现Condition 接口,自定义判断逻辑
@ConditionalOnClass 当类路径下有指定的Class,条件生效
@ConditionalOnMissingClass 当类路径下没有指定的Class,条件生效
@ConditionalOnBean 当容器里有指定Bean,条件生效
@ConditionalOnMissingBean 当容器里没有指定Bean,条件生效
@ConditionalOnProperty 当指定的配置属性有指定的值时,条件生效
@ConditionalOnExpression 基于SpEL表达式的条件判断
@ConditionalOnResource 当类路径下有指定的资源时,条件生效
@ConditionalOnSingleCandidate 如果指定类在容器中是单例,条件生效

@Conditional

使用 @Conditional 注解可以根据条件判断选择是否装配Bean:

@Component
@Conditional(MyCondition.class)
public class PersonA {

}

MyCondition 类需要实现 org.springframework.context.annotation.Condition 接口:

public class MyCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //TODO
        return true;
    }
}

示例:

@Configuration
@ConditionalOnClass(Hibernate.class)
@EnableConfigurationProperties(HibernateProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class HibernateStoreAutoConfiguration {

	@Configuration
	@ConditionalOnSingleCandidate(DataSource.class)
	static class SimpleSessionFactoryConfiguration extends ApplicationObjectSupport {

		private final DataSource dataSource;

		private final HibernateProperties properties;

		SimpleSessionFactoryConfiguration(DataSource dataSource, HibernateProperties properties) {
			this.dataSource = dataSource;
			this.properties = properties;
		}

		@Bean({ HibernateStoreConstants.BEAN_PRIMARY_HIBERNATE_SESSION_FACTORY,
				HibernateStoreConstants.BEAN_NP_PRIMARY_HIBERNATE_SESSION_FACTORY })
		@Primary
		@Lazy
		@ConditionalOnMissingBean(name = { HibernateStoreConstants.BEAN_PRIMARY_HIBERNATE_SESSION_FACTORY,
				HibernateStoreConstants.BEAN_NP_PRIMARY_HIBERNATE_SESSION_FACTORY })
		public HibernateSessionFactoryBean sessionFactory() throws Exception {
			//TODO
		}

	}

	@Configuration
	@Import({ SimpleSessionFactoryConfiguration.class })
	static class HibernateTemplateConfiguration {

		@Bean({ HibernateStoreConstants.BEAN_PRIMARY_HIBERNATE_TEMPLATE,
				HibernateStoreConstants.BEAN_NP_PRIMARY_HIBERNATE_TEMPLATE })
		@Primary
		@ConditionalOnMissingBean(name = { HibernateStoreConstants.BEAN_PRIMARY_HIBERNATE_TEMPLATE,
				HibernateStoreConstants.BEAN_NP_PRIMARY_HIBERNATE_TEMPLATE })
		public HibernateTemplate hibernateTemplate(
				@Qualifier(HibernateStoreConstants.BEAN_PRIMARY_HIBERNATE_SESSION_FACTORY) SessionFactory sessionFactory) {
			//TODO
		}

	}
}

组件扫描

@ComponentScan

说明:

​ 通过 @ComponentScan 注解的扫描策略将Spring的Bean 类加载到IOC容器中。

常用属性如下:

  • basePackagesvalue :指定扫描路径,如果为空则以@ComponentScan注解所在类的目录为根目录,其所有子目录也会被扫描到;
  • basePackageClasses:指定具体扫描的类;
  • includeFilters:指定满足Filter条件的类;
  • excludeFilters:指定排除Filter条件的类;

注:SpringBoot 的启动类注解 @SpringBootApplication 就包含了该注解。

@component 和 @bean 两种方式注入Bean的区别:

相同点:

​ 二者都是将一个对象实例装配到Bean容器中;

不同点:

​ 在使用方式上,@Component 注解作用于类,而 @Bean 注解作用于方法;@Bean 注解比 @Component 注解的灵活性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring 容器时,只能通过 @Bean 来实现。

依赖注入(DI)

Spring提供依赖注入的三种方式:

  • 属性注入
  • 设置方法注入
  • 构造器注入

如果Bean容器中已经存在ServiceA类型的bean,我们要做ServiceB中注入ServiceA。

方式一:属性注入

@Service
public class ServiceB {
	@Autowired
    private ServiceA serviceA;
}

方式二:设置方法注入

@Service
public class ServiceB {
	
    private ServiceA serviceA;
    
    @Autowired
    public void test(ServiceA serviceA) {
        this.serviceA = serviceA;
    }
}

注:与设置方法的方法名无关;

方式三:构造器注入

@Service
public class ServiceB {
   
   @Autowired
   public JpaService(ServiceA serviceA) {
         this.serviceA = serviceA;
    }
    public JpaService() {}
}

注:当只有一个构造器时, @Autowired 注解可以省略,但多个构造方法不可以省略注解。

使用注解有两种方式注入Bean:

  • @Autowired
  • @Resource

@Autowired

@Autowired 是Spring提供的注解,用于Bean的自动注入。@Autowired 装配规则:

先按照 byType 方式查找Bean,如果发现多个,再按照属性名称查找指定Bean,如果还找不到则报错。

@Service
public class LogService {
    
    @Autowired
    private SysLogMapper sysLogMapper;
    
    @Autowired(required = false)
    private SysLogMapper sysLogMapper1;

    @Autowired
    @Qualifier("sysLogMapper")
    private SysLogMapper sysLogMapper2;
}

总结:

​ @Autowired 装配的对象默认是必须存在的,如果找不到则报错;若允许为null,则设置它的required 属性为false, @Autowired(required = false) 指定为非必须。@Quairler 指定具体Bean 名字来注入, 必须配合 @Autowired 一起使用,不能单独使用,否则启动报错;

@Resource

@Resource 是JDK内置的注解,Spring对其进行复用支持。

@Service
public class LogService {
    
    @Resource
    private SysLogMapper sysLogMapper;
    
    @Resource(name = "sysLogMapper",type = SysLogMapper.class)
    private SysLogMapper sysLogMapper2;
    
}

@Resource 装配规则:

  1. 如果没有配置name和type,@Resource 默认通过byName方式进行装配,名称为字段名,如果找不到再按照 byType方式装配;
  2. 如果手动指定name,则按照byName方式自动装配,找不到会抛出异常;
  3. 如果手动指定 type,则按照byType方式自动装配,找不到或找到多个都会抛出异常;
  4. 如果同时指定name 和 type,则按照此规则匹配,匹配不到抛出异常;

在Spring中,构建应用程序主干并由Spring IoC容器管理的对象称为Bean。

//获取应用Bean容器中定义的所有Bean的数量和名字列表;
int beanDefinitionCount = applicationContext.getBeanDefinitionCount();
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();

//根据具体Class类型查找Bean
StudentServiceImpl bean = applicationContext.getBean(StudentServiceImpl.class);
StudentServiceImpl2 bean2 = applicationContext.getBean(StudentServiceImpl2.class);
//根据Bean name 查找Bean (区分大小写):
Object bean3 = applicationContext.getBean("studentServiceImpl");
//查找指定类型的所有Bean的列表名称:(一般指定接口或父类)
String[] beanNamesForType = applicationContext.getBeanNamesForType(StudentService.class);

//查找标注指定注解的Bean 的名称列表:
String[] beanNamesForAnnotation = applicationContext.getBeanNamesForAnnotation(Service.class);

Bean 作用域

@Scope 注解用于指定Bean的作用域

作用域 描述 备注
singleton 单例模式(默认),当spring初始化 applicationContext 容器时,Spring默认会初始化所有该作用范围的Bean,加上lazy-init 就可以使用懒加载,避免预处理,
prototype 原型模式,每次getBean() 都会产生一个新的实例,创建后Spring将不再对其管理。
request 每次web 请求都会产生一个新的实例,与prototype 不同的是,创建后Spring依然对其进行监听和管理。 仅在在web环境生效
session 每次会话都会产生一个新的实例,其他与 request 作用域相同。 仅在在web环境生效
application 全局web域,类似于servlet中的application。 仅在在web环境生效

使用 @Scope 注解指定Bean 的作用域,默认是单例;

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderServiceImpl implements OrderService {}

可以使用 applicationContext 判断Bean是否是单例:

boolean singleton = applicationContext.isSingleton("studentServiceImpl");
boolean prototype = applicationContext.isPrototype("studentServiceImpl");

自定义作用域

实现 org.springframework.beans.factory.config.Scope 接口

定义scope

public class MetadataScope implements Scope {}

注册scope

@Configuration
public class AminoCoreConfiguration {
    
	@Bean(AminoConstants.BEANFACTORY_POST_PROCESSOR)
	public static CustomScopeConfigurer beanFactoryPostProcessor() {
		CustomScopeConfigurer configurer = new CustomScopeConfigurer();
		configurer.addScope(AminoConstants.SCOPE_METADATA, new MetadataScope());
		return configurer;
	}
}

使用Scope

@Service
@Scope("threadScope")
public class MessageServiceImpl implements MessageService {}

Bean 生命周期

Bean的生命周期流程大致如下:

1.首先容器启动后,会对scope为 singleton且非懒加载的bean进行实例化

2.按照Bean定义信息配置信息,注入所有的属性

3.(可选)如果Bean实现了BeanNameAware接口,会回调该接口的setBeanName()方法,传入该Bean的id,此时该Bean就获得了自己的BeanName(默认的BeanName为首字母小写的类名)。

4.(可选)如果Bean实现了BeanFactoryAware接口,会回调该接口的setBeanFactory()方法,传入该Bean的BeanFactory,这样该Bean就获得了自己所在的BeanFactory,

5.(可选)如果Bean实现了ApplicationContextAware接口,会回调该接口的setApplicationContext()方法,传入该Bean的ApplicationContext,这样该Bean就获得了自己所在的ApplicationContext,

6.(可选)如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialzation()方法

7.(可选)如果Bean配置了init-method方法(@PostConstruct 注解标注的方法),则会执行init-method配置的方法

8.(可选)如果Bean实现了InitializingBean接口,则会回调该接口的 afterPropertiesSet()方法

9.如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessAfterInitialization()方法,

10.经过流程9之后,就可以正式使用该Bean了,对于scope为 singleton的Bean,Spring的ioc容器中会缓存一份该bean的实例,而对于scope为prototype的Bean,每次被调用都会new一个新的对象,期生命周期就交给调用方管理了,不再是Spring容器进行管理了

11.(可选)容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destroy()方法,

12.(可选)如果Bean配置了destroy-method方法(**@PreDestroy ** 注解标注的方法),则会执行销毁方法,至此,整个Bean的生命周期结束

@PostConstruct 和 @PreDestroy 都是JDK内置的注解,Spring对其进行支持。

注解 描述
@PostConstruct Bean对象初始化时调用的方法
@PreDestroy Bean对象销毁时调用的方法

@PostConstruct 注解在实例方法上,相当于init-method,Bean对象初始化时调用;

@PreDestroy 注解在实例方法上,相当于destroy-method,Bean对象销毁时调用;

@Component
public class Person {

    @PostConstruct
    private void init(){
        //
    }
    @PreDestroy
    private void destroy(){
        
    }
}

循环依赖问题

​ Spring是如何解决循环依赖的问题的是面试中的高频问点,首先明确一下场景,如果问到 Spring 内部如何解决循环依赖,一定是默认的单例Bean中,属性互相引用的场景。关于解决循环依赖的描述请查看官网

​ 构造器循环依赖的情况,Spring也是无法解决的,应当避免这种写法;

​ 另外,原型(prototype)模式下的Bean中,属性循环依赖,Spring也是无法解决的,原因很简单,当A1对象创建时,发现要注入B1,当B1对象创建时,发现要注入A2,A2对象创建时,发现…… 这不就死循环了吗,所以也是没法解决的。

public class DefaultSingletonBeanRegistry{

	/** Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/** Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
}

首先看一种最简单情况,Bean A的某个属性依赖B,B又依赖A:

​ 容器实例化A后需要给A设置属性值,此时根据依赖关系需要实例化B,而B又依赖A,所以需要获取A实例,注意这里是关键,虽然Bean A 没有完全初始化完成,是个半成品,但A的实例已经有了,B完全可以持有A的引用,所以B可以正常实例化、初始化。所以A也可以正常初始化。

再看下面几种场景,首先Bean A的某个属性依赖B,B依赖C,此时分下面几种情况:

  • A 单例 B多例 C多例: 可以解决循环依赖

  • A 多例 B单例 C多例:无法解决循环依赖

  • A 多例 B多例 C单例 :无法解决循环依赖

​ 只有A单例这种情况是可以正常解决循环依赖的,关键点就在于最初的Bean是单例的,单例意味着已经在IOC容器中缓存了一份实例数据,其他引用该Bean的对象都可以正常创建。所以Spring 解决循环依赖的核心设计是引入了第三方的缓存

基于注解的容器配置

​ 基于注解的方式要比XML方式更好吗?答案是视情况而定。两种方式各有优缺点,XML方式可以更集中的管理配置数据,而不需要修改源代码;注解的方式通常语法更简洁,易用,但配置数据嵌入在源代码中,分布也相对分散些。

顺序 Order

​ 在Spring框架中,**@Order注解Ordered 接口都用于定义组件的顺序。两者都适用于Bean加载顺序、AOP切面的执行顺序和事件监听器的处理顺序。顺序值越小的类将先被加载和执行**。

​ 下面的例子,执行顺序为AminoApplicationRunner1、AminoApplicationRunner2、AminoApplicationRunner3。

@Component
@Order(1)
public class AminoApplicationRunner1 implements ApplicationRunner {

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.out.println("AminoApplicationRunner1...");
	}
}
@Component
@Order(2)
public class AminoApplicationRunner2 implements ApplicationRunner {

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.out.println("AminoApplicationRunner2...");
	}
}
@Component
@Order(3)
public class AminoApplicationRunner3 implements ApplicationRunner {

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.out.println("AminoApplicationRunner3...");
	}
}

注意,通过下面这种方式声明Bean的加载顺序,是不生效的。@Order 注解需要标注在类上才生效。

@Configuration
public class AminoBootConfiguration {

	@Bean
	@Order(2)
	public AminoApplicationRunner2 aminoApplicationRunner2() {
		return new AminoApplicationRunner2();
	}
}

容器刷新

​ 当ApplicationContext初始化结束或者刷新的时候触发ContextRefreshedEvent 事件。 这里的初始化结束是指所有的bean已经加载完毕, post-processor bean被激活, 单例bean被初始化, 同时ApplicationContext对象可以被使用了. 也可以明确调用refresh()方法触发. 但是要注意, 并不是所有的ApplicationContext实例都支持hot refresh的, 比如GenericApplicationContext是不支持hot refresh的。

​ 因为Spring boot中的ApplicationContext实例是 AnnotationConfigEmbeddedWebApplicationContext, 是继承于GenericApplicationContext的. 但是XmlWebApplicationContext 是支持hot refresh 的。

GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once

AOP 切面

​ AOP(Aspect Orient Programming)面向切面编程是对面向对象编程OOP的一种补充。OOP的关键在于类,而AOP的关键在于基于方法的切面。

应用场景

​ AOP在Spring 框架内部的重要应用是声明式事务@Transaction的实现。除此之外,Spring AOP让用户实现自定义切面,AOP常用的场景如下:

  • 权限控制
  • 日志记录
  • 统一异常处理
  • 事务处理
  • 缓存处理

AOP相关概念

  • 切面 Aspect

  • 切入点 PointCut

  • 连接点 JoinPoint

  • 通知 Advice

  • AOP代理

Spring AOP 包含以下几种类型的通知 Advice:

  • Before advice
  • After returning advice 连接点正常完成之后要运行的通知
  • After throwing advice: 连接点发生异常后运行
  • After (finally) advice 连接点完成后要运行 (无论正常还是异常)
  • Around advice 环绕通知

常用注解

注解 描述
@Aspect 定义切面类
@Before 定义前置通知
@AfterReturning 定义后置通知
@Around 定义环绕通知
@After 定义最终通知
@AfterThrowing 定义异常通知

它们对应的注解分别为:

//在 @Aspect 切面类中使用 @Before 注解简单地定义一个前置通知。
 @Aspect
 @Component
 public class DemoAspect {
     
     @Before("execution(* cn.codeartist.spring.aop.advice.*.*(..))")
     public void doBefore() {
         // 自定义逻辑
     }
 }

//方法正常返回,会执行后置通知,使用 @AfterReturning 注解定义后置通知。
 @AfterReturning("execution(* cn.codeartist.spring.aop.advice.*.*(..))")
 public void doAfterReturning() {
     // 自定义逻辑
 }

//用 @Around 注解来定义环绕通知,需要使用 ProceedingJoinPoint 作为参数,来执行目标方法调用。
@Around("execution(* cn.codeartist.spring.aop.advice.*.*(..))")
 public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
     // 方法执行前逻辑
     Object retVal = joinPoint.proceed();    //执行目标方法
     Object[] args = proceedingJoinPoint.getArgs(); //获取目标方法入参
     // 方法执行后逻辑
     return retVal;
 }

//使用 @After 注解定义,最终通知在方法正常退出和抛出异常时都会执行
 @After("execution(* cn.codeartist.spring.aop.advice.*.*(..))")
 public void doAfter() {}
 
//方法抛出异常的时候会执行异常通知,使用 @AfterThrowing 定义异常通知。
 @AfterThrowing("execution(* cn.codeartist.spring.aop.advice.*.*(..))")
 public void doAfterThrowing() {
     // 自定义逻辑
 }
 

AOP基本使用

Spring AOP当前仅支持方法执行连接点

使用步骤:

  1. 声明一个切面 @Aspect;
  2. 声明切入点 @Pointcut + 切入点表达式;
  3. 声明通知 ,编写业务逻辑;

示例:

@Aspect
@Component
public class LoggerAspect {

    @Pointcut("@annotation(com.jackpot.stock.aspect.Logger)")
    void logger(){}

    @Around("logger()")
    public Object recordLog0(ProceedingJoinPoint proceedingJoinPoint)throws Throwable {

        beforeProceed(); //在目标方法之前的功能增加
        Object object = proceedingJoinPoint.proceed();//目标方法的调用
		afterProceed();	//在目标方法之后功能增加
		object = updateResult(object);//修改目标方法的返回值     
        return object;
    }
    
    @Pointcut("execution(public * com.beecode.education.web.TestController.aspect(..))")
    void aspect(){}
    
    @AfterReturning("aspect()")
    public void aspectAop(){
      System.out.println("AfterReturning....");
    }
}

注:@Aspect 和 @Component 一定要一起使用,否则切面不生效;使用 @Order 注解指定加载顺序,数值越小优先级越高;

切入点表达式

参考:https://blog.csdn.net/keda8997110/article/details/50747923

切入点表达式的所有指示符:

  • execution:用于匹配方法执行的连接点。这是使用Spring AOP时要使用的主要切入点指示符。
  • within:将匹配限制为某些类型内的连接点(使用Spring AOP时,在匹配类型内声明的方法的执行)。
  • this:将匹配限制为连接点(使用Spring AOP时方法的执行),其中bean引用(Spring AOP代理)是给定类型的实例。
  • target:在目标对象(代理的应用程序对象)是给定类型的实例的情况下,将匹配限制为连接点(使用Spring AOP时方法的执行)。
  • args:在参数为给定类型的实例的情况下,将匹配限制为连接点(使用Spring AOP时方法的执行)。
  • @target:在执行对象的类具有给定类型的注释的情况下,将匹配限制为连接点(使用Spring AOP时方法的执行)。
  • @args:限制匹配的连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。
  • @within:将匹配限制为具有给定注释的类型内的连接点(使用Spring AOP时,使用给定注释的类型中声明的方法的执行)。
  • @annotation:将匹配点限制为连接点的主题(在Spring AOP中运行的方法)具有给定注释的连接点。

execution 是最常用的切入点表达式指示符,

包含4部分内容,从左到右依次为访问修饰符、包名、类名、方法签名

  • 第一部分为访问修饰符,* 表示无限制

  • 第二部分表示包名,cn.jmember.. 表示cn.jmember包及所有子包

  • 第三部分为类型名称,IPointcutService+ 表示IPointcutService类型及所有子类型

  • 最后表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个点号表示任何参数。*(..) 表示任何方法

实例:

// 任何公共方法的执行
public * *(..)
    
// cn.jmember包及所有子包下任何类的任何方法
* cn.jmember..*.*(..)
    
// cn.jmember包及所有子包下IPointcutService类型的任何只有一个参数的方法
* cn.jmember..IPointcutService.*(*)
// cn.jmember包及所有子包下IPointcutService类型及所有子类型的任何无参的方法
* cn.jmember..IPointcutService+.*()
// 
模式 描述
public * *(..) 任何公共方法的执行
* cn.jmember..IPointcutService.*() cn.jmember包及所有子包下IPointcutService接口中的任何无参方法
* cn.jmember...(..) cn.jmember包及所有子包下任何类的任何方法
* cn.jmember..IPointcutService.() cn.jmember包及所有子包下IPointcutService接口的任何只有一个参数方法
* (!cn.jmember..IPointcutService+).*(..) 非“cn.jmember包及所有子包下IPointcutService接口及子类型”的任何方法
* cn.jmember..IPointcutService+.*() cn.jmember包及所有子包下IPointcutService接口及子类型的的任何无参方法
* cn.jmember..IPointcut*.test*(java.util.Date) cn.jmember包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的;
* cn.jmember..IPointcut*.test*(..) throws IllegalArgumentException, ArrayIndexOutOfBoundsException cn.jmember包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException异常
* (cn.jmember..IPointcutService+&& java.io.Serializable+).*(..) 任何实现了cn.jmember包及所有子包下IPointcutService接口和java.io.Serializable接口的类型的任何方法
@java.lang.Deprecated * *(..) 任何持有@java.lang.Deprecated注解的方法
@java.lang.Deprecated @cn.jmember..Secure * *(..) 任何持有@java.lang.Deprecated和@cn.jmember..Secure注解的方法
@(java.lang.Deprecated || cn.jmember..Secure) * *(..) 任何持有@java.lang.Deprecated或@ cn.jmember..Secure注解的方法
(@cn.jmember..Secure *) *(..) 任何返回值类型持有@cn.jmember..Secure的方法
* (@cn.jmember..Secure ).(..) 任何定义方法的类型持有@cn.jmember..Secure的方法
* (@cn.jmember..Secure () , @cn.jmember..Secure (*)) 任何签名带有两个参数的方法,且这个两个参数都被@ Secure标记了,如public void test(@Secure String str1, @Secure String str1);
* *((@ cn.jmember..Secure ))或 *(@ cn.jmember..Secure *) 任何带有一个参数的方法,且该参数类型持有@ cn.jmember..Secure;如public void test(Model model);且Model类上持有@Secure注解
* *(@cn.jmember..Secure (@cn.jmember..Secure *) ,@ cn.jmember..Secure (@cn.jmember..Secure *)) 任何带有两个参数的方法,且这两个参数都被@ cn.jmember..Secure标记了;且这两个参数的类型上都持有@ cn.jmember..Secure;
* *(java.util.Map<cn.jmember..Model, cn.jmember..Model>, ..) 任何带有一个java.util.Map参数的方法,且该参数类型是以< cn.jmember..Model, cn.jmember..Model >为泛型参数;注意只匹配第一个参数为java.util.Map,不包括子类型;如public void test(HashMap<Model, Model> map, String str);将不匹配,必须使用“* *(java.util.HashMap<cn.jmember..Model,cn.jmember..Model>, ..)”进行匹配;而public void test(Map map, int i);也将不匹配,因为泛型参数不匹配
* *(java.util.Collection<@cn.jmember..Secure *>) 任何带有一个参数(类型为java.util.Collection)的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.jmember..Secure注解;如public void test(Collection collection);Model类型上持有@cn.jmember..Secure
* *(java.util.Set<? extends HashMap>) 任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型继承与HashMap;Spring AOP目前测试不能正常工作
* *(java.util.List<? super HashMap>) 任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型是HashMap的基类型;如public voi test(Map map);Spring AOP目前测试不能正常工作
* (<@cn.jmember..Secure *>) 任何带有一个参数的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.jmember..Secure注解;Spring AOP目前测试不能正常工作

Spring AOP 是纯Java实现的:

​ Spring AOP默认使用标准JDK动态代理用于AOP代理。这使得可以代理任何接口。

​ Spring AOP也可以使用CGLIB代理。这对于代理类而接口不是必需的。默认情况下,如果业务对象未实现接口,则使用CGLIB。由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。

AspectJ AOP

AspectJ 是一个面向切面的框架,该项目从2006年开始发布,直到现在。它拓展了Java语言,AspectJ 定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节码规范的Class文件。

AspectJ 与 Spring AOP 的区别与联系:

​ AspectJ可以做Spring AOP干不了的事情,它是AOP编程的完全解决方案,Spring AOP则致力于解决企业级开发中最普遍的AOP(方法织入)。

下表总结了 Spring AOP 和 AspectJ 之间的关键区别:

Spring AOP AspectJ
在纯 Java 中实现 使用 Java 编程语言的扩展实现
不需要单独的编译过程 除非设置 LTW,否则需要 AspectJ 编译器 (ajc)
只能使用运行时织入 运行时织入不可用。支持编译时、编译后和加载时织入
功能不强-仅支持方法级编织 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等……。
只能在由 Spring 容器管理的 bean 上实现 可以在所有域对象上实现
仅支持方法执行切入点 支持所有切入点
代理是由目标对象创建的, 并且切面应用在这些代理上 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入
比 AspectJ 慢多了 更好的性能
易于学习和应用 相对于 Spring AOP 来说更复杂

Spring AOP还是完全用AspectJ?

以下Spring官方的回答:(总结来说就是 Spring AOP更易用,AspectJ更强大)。

  • Spring AOP比完全使用AspectJ更加简单, 因为它不需要引入AspectJ的编译器/织入器到你开发和构建过程中。 如果你仅仅需要在Spring bean上通知执行操作,那么Spring AOP是合适的选择
  • 如果你需要通知domain对象或其它没有在Spring容器中管理的任意对象,那么你需要使用AspectJ。
  • 如果你想通知除了简单的方法执行之外的连接点(如:调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。

​ 当使用AspectJ时,你可以选择使用AspectJ语言(也称为“代码风格”)或@AspectJ注解风格。 如果切面在你的设计中扮演一个很大的角色,并且你能在Eclipse等IDE中使用AspectJ Development Tools (AJDT), 那么首选AspectJ语言 :- 因为该语言专门被设计用来编写切面,所以会更清晰、更简单。如果你没有使用 Eclipse等IDE,或者在你的应用中只有很少的切面并没有作为一个主要的角色,你或许应该考虑使用@AspectJ风格 并在你的IDE中附加一个普通的Java编辑器,并且在你的构建脚本中增加切面织入(链接)的段落。

统一异常处理

Spring的@ExceptionHandler可以用来统一处理方法抛出的异常,比如这样:

@ExceptionHandler()
public String handleExeption2(Exception ex) {
    System.out.println("抛异常了:" + ex);
    ex.printStackTrace();
    return "异常:默认";
}

@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常,比如这样:

@ExceptionHandler(NumberFormatException.class)
public String handleExeption(Exception ex) {
    System.out.println("抛异常了:" + ex);
    ex.printStackTrace();
     return "异常:默认";
}

就近原则

就近原则
当异常发生时,Spring会选择最接近抛出异常的处理方法。

比如之前提到的NumberFormatException,这个异常有父类RuntimeException,RuntimeException还有父类Exception,如果我们分别定义异常处理方法,@ExceptionHandler分别使用这三个异常类作为参数,比如这样

那么,当代码抛出NumberFormatException时,调用的方法将是注解参数NumberFormatException.class的方法,也就是handleExeption(),而当代码抛出IndexOutOfBoundsException时,调用的方法将是注解参数RuntimeException的方法。

注解方法的返回值
标识了@ExceptionHandler注解的方法,返回值类型和标识了@RequestMapping的方法是统一的,可参见@RequestMapping的说明,比如默认返回Spring的ModelAndView对象,也可以返回String,这时的String是ModelAndView的路径,而不是字符串本身。

有些情况下我们会给标识了@RequestMapping的方法添加@ResponseBody,比如使用Ajax的场景,直接返回字符串,异常处理类也可以如此操作,添加@ResponseBody注解后,可以直接返回字符串。

@ExceptionHandler(NumberFormatException.class)
@ResponseBody
public String handleExeption(Exception ex) {
    System.out.println("抛异常了:" + ex);
    ex.printStackTrace();
   return "异常:默认";
}

Resources 资源

常用资源类型:

  • ClassPathResource
  • FileSystemResource
  • UrlResource
  • PathResource
  • ServletContextResource
  • InputStreamResource
  • ByteArrayResource
//文件系统
Resource resource = appContext.getResource("file:c:\\testing.txt");
//类路径
Resource resource = appContext.getResource("classpath:com/yiibai/common/testing.txt");
//URL路径
Resource resource =  appContext.getResource("url:http://www.yourdomain.com/testing.txt");

Spring提供AntPathMatcher来进行Ant风格的路径匹配,Ant路径通配符支持 ?***

  • ?:匹配一个字符,如“config?.xml”将匹配“config1.xml”;

  • *:匹配零个或多个字符串,如“cn/*/config.xml”将匹配“cn/javass/config.xml”,但不匹配匹配“cn/config.xml”;而“cn/config-*.xml”将匹配“cn/config-dao.xml”;

  • **:匹配路径中的零个或多个目录,如“cn/**/config.xml”将匹配“cn /config.xml”,也匹配“cn/javass/spring/config.xml”;而“cn/javass/config-**.xml”将匹配“cn/javass/config-dao.xml”,即把“**”当做两个“*”处理。

示例:

@Service
public class ResourceService {

    @Value("classpath:files/test1.txt")
    private Resource resource;

    @Value("classpath:files/*.txt")
    private Resource[] resource2;

    @Value("file:E:/beecode/new-platform/test-war/metadata/build/**/*.hbm.xml")
    private Resource[] resource3;

    @Value("http://127.0.0.1:8082/test/echo")
    private Resource resource4;

    public void testTrans(ApplicationContext context){

        String str =  "file:E:/beecode/build/**/*.hbm.xml";
        Resource[] resource = context.getResources(str);
    }
}

在 Spring 中,classpathclasspath* 是用于加载资源的两个不同的前缀。它们在资源加载时的行为略有不同:

  1. classpath:
    • 使用 classpath: 前缀时,Spring 只会从第一个匹配的目录加载资源。
    • 如果有多个具有相同路径的资源,Spring 只会加载找到的第一个资源。
    • 示例:classpath:example.xml
  2. classpath*:
    • 使用 classpath*: 前缀时,Spring 会从所有匹配的目录中加载资源,并将它们合并成一个资源列表。
    • 如果有多个具有相同路径的资源,Spring 将加载所有找到的资源。
    • 示例:classpath*:example.xml

Events 事件监听

ApplicationListener 是Spring事件机制的一部分,与ApplicationEvent 结合完成ApplicationContext 的事件通知机制。

使用步骤:

  • 自定义Event事件
  • 编写监听器
  • 触发事件

自定义Event事件

public class MyEvent extends ApplicationEvent {

	private Long id;
	private String message;

	public MyEvent(Object source) {
		super(source);
	}

	public MyEvent(Object source, Long id, String message) {
		super(source);
		this.id = id;
		this.message = message;
	}
	//getter(),setter()......
}

编写监听器

@Component
public class MyListener implements ApplicationListener<MyEvent> {

	@Override
	public void onApplicationEvent(MyEvent event) {
		System.out.println("监听到事件: "+event.getId()+event.getMessage());
	}
}

触发事件

@SpringBootTest
class SsmBaseApplicationTests {
	
	@Autowired
    private ApplicationContext applicationContext;

	@Test
    public void testListenner() {
        MyEvent myEvent = new MyEvent("myEvent", 9527L, "~~~~~~~~~");
        applicationContext.publishEvent(myEvent);
    }
}

Spring 内置事件

Spring框架中的 SpringApplicationEvent 是一个基本事件类,用于表示应用程序的不同阶段和状态。下面是一些常见的SpringApplicationEvent事件:

  • ApplicationStartingEvent:表示应用程序开始启动,此事件在任何其他事件之前触发。
  • ApplicationEnvironmentPreparedEvent:表示应用程序环境准备完成,此事件在配置加载之前触发。
  • ApplicationPreparedEvent:表示应用程序准备完成,此事件在bean初始化之前触发。
  • ApplicationStartedEvent:表示应用程序已经启动,此事件在应用程序处理请求之前触发。
  • ApplicationReadyEvent:表示应用程序已经准备就绪,此事件在应用程序处理请求之后触发。
  • ApplicationFailedEvent:表示应用程序启动失败,此事件在启动过程中出现异常时触发。
  • ApplicationContextInitializedEvent:当 SpringApplication 启动、ApplicationContext 已准备好并且 ApplicationContextInitializer 已被调用但在加载任何 bean 定义之前发布的事件。

​ 这些是SpringApplicationEvent的一些常见事件,你可以根据需要监听这些事件,执行特定的逻辑来处理应用程序的不同阶段和状态。除了这些事件,Spring还提供了更多的扩展事件,也可以自定义和触发自己的应用程序事件。

Spring 也内置了一些事件

  • ContextRefreshedEvent
  • ApplicationStartedEvent
  • AvailabilityChangeEvent
  • ServletWebServerInitializedEvent

SpEL 表达式

SpEL使用 ExpressionParser接口表示解析器,提供 SpelExpressionParser默认实现;

下面是一个简单示例:

public class SpelTest {
    @Test
    public void test1() {
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("end", "!");
        System.out.println(expression.getValue(context));
    }
}

接下来让我们分析下代码:

1)创建解析器:SpEL使用ExpressionParser接口表示解析器,提供SpelExpressionParser默认实现;

2)解析表达式:使用ExpressionParser的 parseExpression来解析相应的表达式为 Expression对象。

3)构造上下文:准备比如变量定义等等表达式需要的上下文数据。

4)求值:通过Expression接口的getValue方法根据上下文获得表达式值。

工作原理

  • 1.首先定义表达式:“1+2”;
  • 2.定义解析器ExpressionParser实现,SpEL提供默认实现SpelExpressionParser;
    • 2.1.SpelExpressionParser解析器内部使用Tokenizer类进行词法分析,即把字符串流分析为记号流,记号在SpEL使用Token类来表示;
    • 2.2.有了记号流后,解析器便可根据记号流生成内部抽象语法树;在SpEL中语法树节点由SpelNode接口实现代表:如OpPlus表示加操作节点、IntLiteral表示int型字面量节点;使用SpelNodel实现组成了抽象语法树;
    • 2.3.对外提供Expression接口来简化表示抽象语法树,从而隐藏内部实现细节,并提供getValue简单方法用于获取表达式值;SpEL提供默认实现为SpelExpression;
  • 3.定义表达式上下文对象(可选),SpEL使用EvaluationContext接口表示上下文对象,用于设置根对象、自定义变量、自定义函数、类型转换器等,SpEL提供默认实现StandardEvaluationContext;
  • 4.使用表达式对象根据上下文对象(可选)求值(调用表达式对象的getValue方法)获得结果。

SpEL 语法

基本表达式支持:

  • 变量表达式
  • 算术运算符表达式
  • 括号优先级
  • 关系表达式
  • 逻辑表达式
  • 正则表达式
  • 字符串连接及截取表达式
  • 三目运算符
@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();

    String str1 = parser.parseExpression("'Hello World!'").getValue(String.class);
    int int1 = parser.parseExpression("1").getValue(Integer.class);
    long long1 = parser.parseExpression("-1L").getValue(long.class);
    float float1 = parser.parseExpression("1.1").getValue(Float.class);
    double double1 = parser.parseExpression("1.1E+2").getValue(double.class);
    int hex1 = parser.parseExpression("0xa").getValue(Integer.class);
    long hex2 = parser.parseExpression("0xaL").getValue(long.class);
    boolean true1 = parser.parseExpression("true").getValue(boolean.class);
    boolean false1 = parser.parseExpression("false").getValue(boolean.class);
    Object null1 = parser.parseExpression("null").getValue(Object.class);
}

SpEL支持加(+)、减(-)、乘(*)、除(/)、求余(%)、幂(^)运算。

@Test
public void test() {
	int result1 = parser.parseExpression("1+2-3*4/2").getValue(Integer.class);//-3
	int result2 = parser.parseExpression("4%3").getValue(Integer.class);//1
    int result3 = parser.parseExpression("2^3").getValue(Integer.class);//8
}

等于(==)、不等于(!=)、大于(>)、大于等于(>=)、小于(<)、小于等于(<=),区间(between)运算。

@Test
public void test() {
	parser.parseExpression("1>2").getValue(boolean.class);	//返回false
    parser.parseExpression("1 between {1, 2}").getValue(boolean.class);	//返回true
}

​ between运算符右边操作数必须是列表类型,且只能包含2个元素。第一个元素为开始,第二个元素为结束,区间运算是包含边界值的,即 xxx>=list.get(0) && xxx<=list.get(1);

且(and或者&&)、或(or或者||)、非(!或NOT)。

@Test
public void test() {
    boolean result1 = parser.parseExpression("2>1 and (!true or !false)").getValue(boolean.class);
    boolean result2 = parser.parseExpression("2>1 && (!true || !false)").getValue(boolean.class);

    boolean result3 = parser.parseExpression("2>1 and (NOT true or NOT false)").getValue(boolean.class);
    boolean result4 = parser.parseExpression("2>1 && (NOT true || NOT false)").getValue(boolean.class);
}

类相关表达式

​ 使用“T(Type)”来表示java.lang.Class实例,“Type”必须是类全限定名,“java.lang”包除外,即该包下的类可以不指定包名;使用类类型表达式还可以进行访问类静态方法及类静态字段。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    //java.lang包类访问
    Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
    System.out.println(result1);

    //其他包类访问
    String expression2 = "T(com.javacode.spel.SpelTest)";
    Class<SpelTest> value = parser.parseExpression(expression2).getValue(Class.class);
    System.out.println(value == SpelTest.class);

    //类静态字段访问
    int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
    System.out.println(result3 == Integer.MAX_VALUE);

    //类静态方法调用
    int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
    System.out.println(result4);
}

注:对于java.lang包里的可以直接使用“T(String)”访问;其他包必须是类全限定名;可以进行静态字段访问如“T(Integer).MAX_VALUE”;也可以进行静态方法访问如“T(Integer).parseInt(‘1’)”。

类实例化

类实例化同样使用java关键字“new”,类名必须是全限定名,但java.lang包内的类型除外,如String、Integer。

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    String result1 = parser.parseExpression("new String('路人甲java')").getValue(String.class);
    System.out.println(result1);

    Date result2 = parser.parseExpression("new java.util.Date()").getValue(Date.class);
    System.out.println(result2);	
}

变量定义及引用

​ 变量定义通过EvaluationContext接口的 setVariable(variableName, value)方法定义;在表达式中使用"#variableName"引用;除了引用自定义变量,SpE还允许引用根对象及当前上下文对象,使用"#root"引用根对象,使用"#this"引用当前上下文对象;

@Test
public void test() {
    ExpressionParser parser = new SpelExpressionParser();
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("name", "路人甲java");			//定义变量
    context.setVariable("lesson", "Spring系列");

    //获取name变量,lesson变量
    String name = parser.parseExpression("#name").getValue(context, String.class);
    System.out.println(name);
    String lesson = parser.parseExpression("#lesson").getValue(context, String.class);
    System.out.println(lesson);

    //StandardEvaluationContext构造器传入root对象,可以通过#root来访问root对象
    context = new StandardEvaluationContext("我是root对象");
    String rootObj = parser.parseExpression("#root").getValue(context, String.class);
    System.out.println(rootObj);

    //#this用来访问当前上下文中的对象
    String thisObj = parser.parseExpression("#this").getValue(context, String.class);
    System.out.println(thisObj);
}

自定义函数

目前只支持类静态方法注册为自定义函数;SpEL使用StandardEvaluationContext的registerFunction方法进行注册自定义函数,其实完全可以使用setVariable代替,两者其实本质是一样的;

@Test
public void test() {
    //定义2个函数,registerFunction和setVariable都可以,不过从语义上面来看用registerFunction更恰当
    StandardEvaluationContext context = new StandardEvaluationContext();
    Method parseInt = Integer.class.getDeclaredMethod("parseInt", String.class);
    context.registerFunction("parseInt1", parseInt);
    context.setVariable("parseInt2", parseInt);

    ExpressionParser parser = new SpelExpressionParser();
    System.out.println(parser.parseExpression("#parseInt1('3')").getValue(context, int.class));
    System.out.println(parser.parseExpression("#parseInt2('3')").getValue(context, int.class));
    
}

​ 此处可以看出“registerFunction”和“setVariable”都可以注册自定义函数,但是两个方法的含义不一样,推荐使用“registerFunction”方法注册自定义函数。

Bean引用

​ SpEL支持使用“@”符号来引用Bean,在引用Bean时需要使用BeanResolver接口实现来查找Bean,Spring提供BeanFactoryResolver实现。

@Test
public void test() {
    
    private BeanFactory beanFactory;	//赋值逻辑省略
    
    StandardEvaluationContext context = new StandardEvaluationContext();
    context.setBeanResolver(new BeanFactoryResolver(beanFactory));

    ExpressionParser parser = new SpelExpressionParser();
    User userBean = parser.parseExpression("@user").getValue(context, User.class);
}

基于注解风格的SpEL配置也非常简单,使用@Value注解来指定SpEL表达式,该注解可以放到字段、方法及方法参数上。

@Component
public class Test {
    
	@Value("#{myBean1.getBeanName()}")	# 调用bean的实例方法
    private String beanName;		

    @Value("#{'wyc'.toUpperCase()}")	# 调用字符串的实例方法
    private String name2;
}

表达式模版

表达式模版用于限制和规范传入的表达式内容,如下面的示例,表达式前缀为“#{”,后缀为“}”;使用 parseExpression解析时传入的模板必须以“#{”开头,以“}”结尾,如”#{‘Hello ‘}#{‘World!’}”。

@Test
public void testParserContext() {
	ExpressionParser parser = new SpelExpressionParser();
    TemplateParserContext templateParserContext = new TemplateParserContext("#{", "}");
    String template = "#{'Hello '}#{'World!'}";
    Expression expression = parser.parseExpression(template, templateParserContext);
    System.out.println(expression.getValue());
}

小结

  1. Spel功能是非常强大的,可以脱离spring环境独立运行。
  2. spel 可以用在一些动态规则的匹配方面,比如监控系统中监控规则的动态匹配;其他的一些条件动态判断等。

Spring MVC

Spring Web MVC是基于Servlet API 构建的 Web框架。与MVC并行,Spring Framework 5.0引入了一个全新的Web框架:Spring Web-Flux ,Spring Web-MVC 和 Spring Web-Flux 是Spring 提供的两个Web框架。

Controller

基于注解的控制器示例:

@RestController
@RequestMapping("/member/card")
public class MemberCardController extends BaseController {
    
    @PostMapping("open")
    public CommonResult openCard(@RequestBody MemberCardRequest memberCardRequest) {
        return CommonResult.success("注册成功");
    }

    @GetMapping("/no/phone/{number}")
    public CommonResult<MemberCardVo> queryMemCardByPhoneOrNo(@PathVariable String number){
		//TODO
        return CommonResult.success(memberCardVo);
    }
    @GetMapping("/no/phone")
    public CommonResult<MemberCardVo> queryMemCardByPhoneOrNo(@RequestParam String phone){
		//TODO
        return CommonResult.success(memberCardVo);
    }
}

​ @Controller 和 @RestController 用于标注一个类为 Web 控制器,其中 @RestController 包含了 @Controller 和 @ResponseBody 两个注解,方法体返回的内容自动加入到响应体中。

​ @RequestMapping 及其变体@GetMapping @PostMapping @PatchMapping 和 @DeleteMapping 等用于确定资源的访问路径。其中 @RequestMapping 可以接收任意类型方法的请求,@GetMapping 只能接收 Get方法的请求,@PostMapping 只能接收 Post方法的请求,其他类比。

​ Controller 接收客户端传递的HTTP请求参数有两种写法,分别使用@RequestParam 和 @PathVariable 注解实现。@RequestParam 方式是传统的传递方式,客户端的URL 请求样式为 host:port/path?phone=123456 ;@PathVariable 支持的是RESTful 开发风格,客户端的URL 请求样式为 host:port/path/phone/123456 ;

配置

在Spring 中同样可以使用Java 和XML 两种方式配置 MVC。如果使用 Java需要实现 WebMvcConfigurer 接口,通过实现接口方法来配置不同的功能。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    // Implement configuration methods...
}

拦截器

MVC 中所有的拦截器需要实现 HandlerInterceptor 接口,然后实现接口方法:

public class Intercept1 implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
        throws Exception {
        System.out.println("拦截前处理...");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, 
        ModelAndView modelAndView) throws Exception {
        System.out.println("拦截后处理...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
        Exception ex) throws Exception {
        System.out.println("拦截完成处理...");
    }
}

编写完拦截器后,注册拦截器到 WebMvcConfigurer 中才能使拦截器生效:

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        Intercept1 intercept1 = new Intercept1();
        //注册拦截器
        InterceptorRegistration interceptorRegistration = registry.addInterceptor(intercept1);
        //指定拦截器的匹配模式
        interceptorRegistration.addPathPatterns("/*test*");
    }
}

多个拦截器顺序

​ 如果多个拦截器匹配到同一个请求,那么拦截顺序按照责任链模式执行,即拦截前 preHandle 方法按照先注册先执行的规则,拦截后postHandle 和 afterCompletion 方法按照后注册先执行的规则执行。

Intercept1拦截前处理...
Intercept2拦截前处理...
Intercept3拦截前处理...

Intercept3拦截后处理...
Intercept2拦截后处理...
Intercept1拦截后处理...

Intercept3拦截完成处理...
Intercept2拦截完成处理...
Intercept1拦截完成处理...

注:

​ preHandle 方法在controller 方法前执行,postHandle 方法在controller 方法后执行,如果 preHandle 方法返回 false,那么将不会执行拦截器后续两个方法 postHandle 和 afterCompletion。并且请求也不会到达Controller。

​ 多个拦截器情况下,任何一个拦截器的preHandle 方法返回 false,请求也不会到达Controller。但是preHandle 方法返回 true的拦截器最终会执行 afterCompletion 方法。

配置类型转换

@Override
public void addFormatters(FormatterRegistry registry) {
    DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
    registrar.setUseIsoFormat(true);
    registrar.registerFormatters(registry);
}

配置跨域

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		registry.addMapping("/**").allowedMethods("GET","HEAD","POST","PUT","OPTIONS","DELETE");
        //或者
        registry.addMapping("/api/**")
                        .allowedOrigins("http://localhost:3000") // 允许的域
                        .allowedMethods("GET", "POST", "PUT", "DELETE") // 允许的方法
                        .allowedHeaders("Origin", "Accept", "Content-Type", "Authorization") // 允许的请求头
                        .exposedHeaders("Authorization") // 暴露的响应头
                        .allowCredentials(true) // 允许发送 Cookie
                        .maxAge(3600); // 预检请求的有效期
	}
}

上述配置允许来自 http://localhost:3000 的跨域请求,允许的方法是 GETPOSTPUTDELETE,允许的请求头包括 OriginAcceptContent-TypeAuthorization,并暴露 Authorization 响应头,同时允许发送 Cookie,预检请求的有效期为 3600 秒。

MVC 原理

Spring MVC 是基于Servlet API 进行开发的。

Spring MVC 设计中的四个组件:

  • Servlet 调度器 DispatcherServlet

  • 处理器映射 HandlerMapping

  • 处理器适配 HandleAdapter

  • 视图解析器 ViewResolver

DispatcherServlet 负责调度和分派,Spring 系统启动后,DispatcherServlet 并没有初始化,当第一次Http请求时, 才会初始化DispatcherServlet :

Initializing Spring DispatcherServlet 'dispatcherServlet'

Spring WebFlux

Spring Cloud GateWay 使用 WebFlux 框架实现,底层使用Netty 高性能网络工具包实现。

数据访问

Spring JPA

事务、DAO、 JDBC、 ORM

事务管理

声明式事务 Transactional

​ Spring 使用 AOP 实现声明式事务的功能,我们只需要一个 @Transaction 注解就可以完成声明式事务。@Transaction 注解标注在类或方法上,表明启用事务管理,标注在类上,表明这个类的所有public 修饰的非静态方法开始事务功能。

​ 默认情况下,Spring 的 @Transaction 只会对 unchecked 异常进行事务回滚, checked 异常则不会回滚;

注:RuntimeException 和 Error 属于unchecked 不受检查异常

​ 首先记住重要的一点,@Transactional 注解只对 Spring 管理的Bean 的public 方法起作用,不是Bean组件或不是public 修饰的非静态方法,都不会使事务生效。

@Service
public class LogService implements ILogService {

    @Resource
    private SysLogMapper sysLogMapper;
    
    @Transactional
    public void testTrans(){
        SysLog log = new SysLog();
        sysLogMapper.insert(log);
        //throw new RuntimeException("jack");
    } 
}
public @interface Transactional {
    
	String value() default "";
    //事务传播类型
    Propagation propagation() default Propagation.REQUIRED;
    //事务隔离级别
    Isolation isolation() default Isolation.DEFAULT;
    //超时时间
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    //只读
    boolean readOnly() default false;
    
    Class<? extends Throwable>[] rollbackFor() default {};
    
    String[] rollbackForClassName() default {};
    
    Class<? extends Throwable>[] noRollbackFor() default {};
    
    String[] noRollbackForClassName() default {};
}

事务传播行为

​ Spring中定义的事务传播机制是针对嵌套事务而言的,也就是一个事务方法调用另一个事务方法的场景。事务传播机制不是数据库层面的概念,而是编程语言中多个包含事务的方法互相调用的情况下,不同的事务传播类型有着不同的结果。

​ 注意一个大前提,在Spring中,A类中的两个事务方法a() 和 b() ,其中a 方法调用 b 方法, 此时两个事务都是不能生效的.

Propagation 枚举类定义了事务的7种传播类型:

传播类型 描述
REQUIRED (默认) 使用当前的事务,如果当前没有事务,则自己新建一个事务
SUPPORTS 如果当前有事务,则使用事务;如果当前没有事务,则不使用事务。
MANDATORY 该传播属性强制调用方必须存在一个事务,如果不存在,则抛出异常。
REQUIRES_NEW 如果当前有事务,则挂起该事务,并且自己创建一个新的事务给自己使用
NOT_SUPPORTED 如果当前有事务,则把事务挂起,自己不适用事务去运行数据库操作。
NEVER 如果当前有事务存在,则抛出异常
NESTED 如果当前有事务,则开启子事务(嵌套事务),嵌套事务是独立提交或者回滚;
如果当前没有事务,则同 REQUIRED

事务隔离级别

事务的四种隔离级别是数据库层面定义的概念,

传播类型 描述
READ_UNCOMMITTED(读未提交)
READ_COMMITTED(读已提交)
REPEATABLE_READ(可重复读)
SERIALIZABLE(串行化)

只读事务

​ 只读事务是数据库层面的概念,MySQL5.6 引入只读事务,只读事务中只能包含查询语句,不能包含增删改语句,否则报错。设计只读事务的目的是为了优化SQL执行效率。因为只包含查询语句的事务和可能包含任意DML语句的事务相比,更简单处理些,所以在事务层面做了区分,让只读事务的执行效率更高。

开启事务时指明只读事务:

START TRANSACTION READ ONLY;

如果在只读事务中执行写操作,那么会报错:

> 1792 - Cannot execute statement in a READ ONLY transaction.

Spring中编码时通过在 @Transaction 注解上指定readOnly = true 来开启只读事务,默认为 false;

@Transactional(readOnly = true)

@Transaction 注解失效

​ 在方法上标记 @Transaction 注解并不意味着事务可以正常生效,以下任意一种情况存在都会使 @Transactional 事务注解失效:

  1. 当前类不是Spring 所管理的Bean;
  2. 事务方法不是public修饰;
  3. 事务方法被同类的其他方法调用;
  4. 事务方法默认只对未受检查异常和错误进行回滚,其他异常不会回滚。如果想指定其他异常生效,使用 rollbackFor属性指定;

注:

​ Springboot 的启动类不需要开启事务管理注解,也可以直接在方法中使用事务注解。

编程式事务

https://cloud.tencent.com/developer/article/1697221

DAO 支持

一致的异常层次结构

​ Spring提供了从特定于技术的异常(例如SQLException)到其自己的异常类层次结构的便捷转换,该异常类层次结构以DataAccessException作为根异常。这些异常包装了原始异常,因此你永远不会丢失任何可能出错的信息。

​ 除了JDBC异常,Spring还可以包装JPA和Hibernate特定的异常,将它们转换为一组集中的运行时异常。这样,你就可以仅在适当的层中处理大多数不可恢复的持久性异常,而无需在DAO中使用烦人的样板捕获和抛出块以及异常声明。 (尽管你仍然可以在任何需要的地方捕获和处理异常。)

DAO 注解

如果你使用经典的 JDBC,则需要使用JdbcTemplate 或 NamedParameterJdbcTemplate:

@Repository
public class JdbcMovieFinder implements MovieFinder {

    @Autowired
	private JdbcTemplate jdbcTemplate;
   
    @Autowired
	private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

	// ...
}

如果你使用经典的Hibernate API,则需要注入SessionFactory

@Repository
public class HibernateMovieFinder implements MovieFinder {

    @Autowired
	private SessionFactory sessionFactory;

	// ...
}

如果你使用经典的 JPA ,则需要注入EntityManager

@Repository
public class JpaMovieFinder implements MovieFinder {

    @Autowired
    private EntityManager entityManager;

    // ...
}

JDBC

spring JDBC

ORM

spring ORM 项目

对 Hibernate 与 JPA的支持

集成整合

异步任务

第一步:配置类加上 @EnableAsync 注解,表示启用异步任务功能;

第二步: 配置线程池

@Configuration
@EnableAsync
 public class TaskExecutorConfig {
     
     @Bean
     public TaskExecutor taskExecutor(){
         ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
         executor.setCorePoolSize(10);   // 核心线程数
         executor.setMaxPoolSize(20);    // 最大线程数
         executor.setQueueCapacity(50);  // 任务队列容量
         return executor;
     }
 }

第三步: 编写异步任务

@Component
public class AsyncTask {
    
    //1.无返回值的异步任务
    @Async("taskExecutor")
    public void runTask1() {
        try {
            //TODO
        } catch (Exception e) {
            System.out.println("Exit Task 1");
        }
    }
    
   // 2.有返回值的异步任务
    @Async("taskExecutor")
    public Future<String> runTask(String taskName, Integer taskSecond) {
        String result = null;
        try{
            //TODO
        } catch (Exception e) {
            System.out.println( taskName +" have error");
        }
        return new AsyncResult<>(result);
    }
}

4 调用异步任务(Controller 或 Service 中调用)

注:

  • 要在标注 @Configuration 注解的配置类上标注 @EnableAsync 注解,表示开启异步功能;
  • @Async 注解不能应用于静态方法上,否则, @Async 注解会失效,该静态方法仍将使用同步调用
  • @Async 注解不能应用于本类的方法上,否则, @Async 注解会失效,本类的方法仍将使用同步调用。所以对于需要异步调用的方法不能放在本类中,应该抽取出来单独放在另一个类中

应用场景

​ 应用A调用应用B的接口,B的业务执行逻辑需要90s以上甚至更长时间,此时容易造成请求超时。这种情况下建议把应用B中的执行逻辑写成异步任务,这样就不会造成请求超时了。

异步任务配置

实现 AsyncConfigurer接口

@Configuration
@EnableAsync
public class AppConfig implements AsyncConfigurer {
 
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(7);
        executor.setMaxPoolSize(42);
        executor.setQueueCapacity(11);
        executor.setThreadNamePrefix("MyExecutor-");
        executor.initialize();
        return executor;
    }
 
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new MyAsyncUncaughtExceptionHandler();
    }
}

定时任务

1 在启动类或者配置类上配置 @EnableScheduling 注解,表示开启定时任务功能;

2 编写定时任务

@Component
public class MaintainTask {

    @Scheduled(cron ="*/1 * * * * ?")
    public void sayWord() {
        System.out.println("world");
    }
}

@Scheduled除过cron还有三种方式:fixedRate,fixedDelay,initialDelay

cron:表达式可以定制化执行任务,但是执行的方式是与fixedDelay相近的,也是会按照上一次方法结束时间开始算起。

  • fixedDelay:控制方法执行的间隔时间,是以上一次方法执行完开始算起,如上一次方法执行阻塞住了,那么直到上一次执行完,并间隔给定的时间后,执行下一次。
  • ***fixedRate:***是按照一定的速率执行,是从上一次方法执行开始的时间算起,如果上一次方法阻塞住了,下一次也是不会执行,但是在阻塞这段时间内累计应该执行的次数,当不再阻塞时,一下子把这些全部执行掉,而后再按照固定速率继续执行。
  • initialDelay:initialDelay = 10000 表示在容器启动后,延迟10秒后再执行一次定时器。

缓存

使用Spring Cache 三步走

  1. 加依赖
  2. 启用缓存
  3. 使用缓存注解

1 加依赖 gradle:

implementation 'org.springframework.boot:spring-boot-starter-cache'

2 在启动类加上@EnableCaching注解即可开启使用缓存。

3

常用注解

Spring Cache有几个常用注解,分别为@Cacheable@CachePut@CacheEvict@Caching@CacheConfig。除了最后一个CacheConfig外,其余四个都可以用在类上或者方法级别上,如果用在类上,就是对该类的所有public方法生效,下面分别介绍一下这几个注解。

@Cacheable

@Cacheble注解表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有值,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。这个注解一般用在查询方法上

@CachePut

加了@CachePut注解的方法,会把方法的返回值put到缓存里面缓存起来,供其它地方使用。它通常用在新增方法上

@CacheEvict

使用了CacheEvict注解的方法,会清空指定缓存。一般用在更新或者删除的方法上

@Caching

Java注解的机制决定了,一个方法上只能有一个相同的注解生效。那有时候可能一个方法会操作多个缓存(这个在删除缓存操作中比较常见,在添加操作中不太常见)。

Spring Cache当然也考虑到了这种情况,@Caching注解就是用来解决这类情况的,大家一看它的源码就明白了。

日志

SpringBoot默认使用的日志框架是slf4j+ logback

配置文件:classpath:logback.xml

<?xml version="1.0" encoding="UTF-8"?>
 
<!-- scan="true"开启对配置信息的自动扫描(默认时间为60秒扫描一次) 注:当此文件的配置信息发生变化时,此设置的作用就体现出来了,不需要重启服务 -->
<configuration scan="true">
 
	<!-- 通过property标签,来存放key-value数据,便于后面的动态获取,提高程序的灵活性 -->
	<property name="log-dir" value="log" />
	<property name="log-name" value="logFile" />
 
	<!-- >>>>>>>>>>>>>>>>>>>>>>>>>配置appender(可以配置多个)>>>>>>>>>>>>>>>>>>>>>>>>> -->
	<!-- 
	    name:自取即可, 
	    class:加载指定类(ch.qos.logback.core.ConsoleAppender类会将日志输出到>>>控制台), 
		patter:指定输出的日志格式 
	-->
	<appender name="consoleAppender"
		class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}:%L- %msg%n</pattern>
			<!-- 日志编码 -->
			<charset class="java.nio.charset.Charset">UTF-8</charset>
		</encoder>
	</appender>
 
	<!-- 
		name:自取即可, 
		class:加载指定类(ch.qos.logback.core.rolling.RollingFileAppender类会将日志输出到>>>指定的文件中), 
		patter:指定输出的日志格式 file:指定存放日志的文件(如果无,则自动创建) rollingPolicy:滚动策略>>>每天结束时,都会将该天的日志存为指定的格式的文件 
		FileNamePattern:文件的全路径名模板 (注:如果最后结尾是gz或者zip等的话,那么会自动打成相应压缩包) 
	-->
	<appender name="fileAppender"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<!-- 把日志文件输出到:项目启动的目录下的log文件夹(无则自动创建)下 -->
		<file>${log-dir}/${log-name}.log</file>
		<!-- 把日志文件输出到:name为logFilePositionDir的property标签指定的位置下 -->
		<!-- <file>${logFilePositionDir}/logFile.log</file> -->
		<!-- 把日志文件输出到:当前磁盘下的log文件夹(无则自动创建)下 -->
		<!-- <file>/log/logFile.log</file> -->
		<rollingPolicy
			class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- TimeBasedRollingPolicy策略会将过时的日志,另存到指定的文件中(无该文件则创建) -->
			<!-- 把因为 过时 或 过大  而拆分后的文件也保存到目启动的目录下的log文件夹下  -->
			<fileNamePattern>${log-dir}/${log-name}.%d{yyyy-MM-dd}.%i.log
			</fileNamePattern>
			<!-- 设置过时时间(单位:<fileNamePattern>标签中%d里最小的时间单位) -->
			<!-- 系统会删除(分离出去了的)过时了的日志文件 -->
			<!-- 本人这里:保存以最后一次日志为准,往前7天以内的日志文件 -->
			<MaxHistory>
				7
			</MaxHistory>
			<!-- 滚动策略可以嵌套; 
			         这里嵌套了一个SizeAndTimeBasedFNATP策略,
			            主要目的是: 在每天都会拆分日志的前提下,
			            当该天的日志大于规定大小时, 
				        也进行拆分并以【%i】进行区分,i从0开始 
		    -->
			<timeBasedFileNamingAndTriggeringPolicy
				class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<maxFileSize>5MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
		</rollingPolicy>
		<encoder>
			<!-- 日志输出格式 -->
			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}:%L- %msg%n</pattern>
			<!-- 日志编码 -->
			<charset class="java.nio.charset.Charset">UTF-8</charset>
		</encoder>
	</appender>
 
	<!-- >>>>>>>>>>>>>>>>>>>>>>>>>>>>>使用appender>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
	<!--
	    指定[哪个包]下使用[哪个appender],并设置 记录到日志文件中的日志的最下
	    级别(低于次级别的日志信息不回输出记录到日志文件中)
	    注:日志级别有: trace|debug|info|warn|error|fatal
	    注:当有多处指定了要记录的日志的最下日志级别时,走优先级最高的,优先级:
	       logback-spring.xml中 > 启动jar包时 > xxx.properties/xxx.yml中
	-->
	<!--<logger name="com" level="trace">-->
	<logger name="com">
		<!-- 指定使用哪个appender -->
		<appender-ref ref="fileAppender" />
	</logger>
	<!--
	    root:logger的根节点,appender-ref:确定使用哪个appender,将日志信息显示在console
		注:如果不指定配置此项的话,那么SpringBoot启动后,将不会在console打印任何信息
	-->
	<root>
		<appender-ref ref="consoleAppender" />
	</root>
</configuration>

RestTemplate

RestTemplate 用于客户端HTTP接口调用

单元测试

Spring Boot中引入单元测试很简单,依赖如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

下面是单元测试的基本示例:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {JeomoApplication.class})
public class JunitTest {

    @Resource
    private IMemPointsAdjustService memPointsAdjustService;

    @Test
    public void getOneDetailTest(){

        MemberPointsAdjust oneDetail = memPointsAdjustService.getOneDetail("1354736267872485377");
    }
}

测试 Controller

测试 Service

测试 Dao

断言 Assert

使用 Assert 断言:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {JeomoApplication.class})
public class JunitTest {

    @Resource
    private IMemberCardService memberCardService;

    @Test
    public void updateCardStatusTest(){

        int i = memberCardService.updateCardStatus("1354348482061713410", "3");
        Assert.assertTrue(i==1);//断言
    }
}

​ 如果断言失败,Junit 会抛出 java.lang.AssertionError 错误。

事务回滚

​ 有时我们不希望测试行为污染数据库,就可以在每次测试完成后回滚事务(前提是使用了支持事务的数据库存储引擎)。

​ 测试类回滚事务非常简单,在测试类上加上 @Transactional 注解就会自动回滚事务,无论是SQL执行成功或失败都会回滚。如果加了 @Transactional 注解并且不希望测试方法自动回滚,则可以使用 @Rollback(false) 手动控制。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {JeomoApplication.class})
@Transactional
public class JunitTest {

    @Resource
    private IMemberCardService memberCardService;

    @Test
    @Rollback(false)
    public void updateCardStatusTest(){

        memberCardService.updateCardStatus("1354348482061713410", "35");
    }
}

上面这个实例不会回滚事务,相当于没控制。把 @Rollback(false) 去掉或者改为 @Rollback(true) 就会回滚测试数据。

模拟请求 MockMvc

​ MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。

JUnit 5 使用新的 org.junit.jupiter 包。例如,org.junit.junit.Test变成了org.junit.jupiter.api.Test。

注解全览

Spring 框架可谓是把 Java的注解运用到极致。

IOC 注解

IOC 是控制反转

组件

@Controller

@Service

用于标注业务层组件

@Repository

用于标注数据访问层组件,即DAO组件

@Configuration

被标注@Configuration 的类是Java配置类。

@Component

通用组件

​ @Controller、@Service、@Repository、@Configuration 都继承自@Component,都是组件。

​ 注解@Component 表明这个类将被IoC容器扫描装配,IoC容器会把类名第一个字母作为小写,其他不变作为Bean的名称放入IoC容器中,value属性则可以自定义Bean的名称。

组件扫描

@ComponentScan

说明:

​ 通过 @ComponentScan 注解的扫描策略将Spring的Bean 类加入到IOC容器中。

常用属性如下:

  • basePackages, value :指定扫描路径,如果为空则以@ComponentScan注解所在类的目录为根目录,其所有子目录也会被扫描到;
  • basePackageClasses:指定具体扫描的类;
  • includeFilters:指定满足Filter条件的类;
  • excludeFilters:指定排除Filter条件的类;

注:SpringBoot 的启动类注解 @SpringBootApplication 就包含了该注解。

自动装配Bean

@Autowired

​ @Autowired 是我们使用最多的注解之一,用于装配Bean,可以标在字段、构造器、方法、方法参数、注解上。

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, 	        	ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

   boolean required() default true;

}

​ @Autowired 提供这样的规则:首先它会根据类型找到对应的Bean,如果对应类型的Bean不是唯一的,那么它会根据其属性名称或方法名称和Bean的名称进行匹配。如果匹配成功,就会使用该Bean,否则会抛出异常。

​ 还需要特别注意的是,@Autowired 默认必须找到对应的Bean,如果你不确定该Bean一定存在或者允许该属性为null,那么可以配置 required 属性为false。

@Autowired(required = false)
private RedisUtil redisUtil;

@Qualifier

​ @Qualifier 限定词注解用于消除歧义。

​ 如果要装配同一类型的多个Bean,单凭@Autowired注解,只能将注解下面的属性名修改为和Bean名称相同的名称,才可以正确装配。@Qualifier 限定词注解可以不需要刻意修改属性名称,将对应Bean的名称标注在@Qualifier 的value属性中即可。

@Autowired
@Qualifier("dog")
private Animal animal;

​ @Autowired 和 @Qualifier 组合在一起,就可以通过类型和名称一起找到Bean。

@Profile

Spring 提供了Profile 机制,使我们可以方便的在各个环境(开发、测试、生产)之间切换。

Spring提供两个参数供我们配置,来启动和修改Profile机制:

  • spring.profiles.active
  • spring.profiles.default
-Dspring.profiles.active=dev

此时生效的配置文件为application.yaml 和 application-dev.yaml

AOP注解

http 请求注解

@RestController

包含

@Controller
@ResponseBody

@Controller 组件中使用的注解:

@RequestMapping  	//请求路径
RequestMethod		//请求方法
@RequestParam		//请求参数
@RequestHeader		//请求头
@RequestBody		//请求体

@ResponseBody		//响应体

@RequestMapping

​ @RequestMapping 定义在类和方法上。类定义处的 @RequestMapping是初步的请求映射路径,方法定义处的@RequestMapping是请求映射路径。

​ @RequestMapping 有5个“子注解”,分别对应 GET,POST,PUT,DELETE,PATCH请求,这也正是现在 RESTful规范所使用的五种请求方法。

image-20200821143402916

@RequestParm

@GetMapping("test-set-string")
public String redisSetString(@RequestParam String key, @RequestParam String value) {
    boolean success = redisUtil.set("wyc0313", "888", 60L);
    return "success set string"+success;
}

缓存注解

@Cacheable

Redis 是缓存服务器

其他注解

@PostConstruct 是JDK内置的注解,Spring对其进行支持,注解在方法上,项目启动时会运行方法。

注意:运行方法的线程时Spring应用的启动线程,所以如果方法很耗时,那么项目启动也就会很慢。

Spring 源码设计

Spring IoC容器设计与原理

  • BeanFactory
  • ApplicationContext
  • BeanDefinition
  • BeanPostProcessor

Bean定义

Bean实例化

Bean 生命周期管理

AnnotationConfigWebApplicationContext

XmlWebApplicationContext

AnnotationConfigServletWebServerApplicationContext

BeanDefinition
GenericBeanDefinition
FactoryBean

BeanPostProcessor

BeanPostProcessor 接口是 Spring 框架提供的一种扩展机制,用于在 Bean 的初始化前后加一些自定义处理逻辑。在 Spring 容器启动时,会自动检测容器中所有实现了 BeanPostProcessor 接口的类,并在 Bean 的实例化、依赖注入和初始化等过程中回调它们提供的方法,来完成一些额外的处理逻辑。

BeanPostProcessor 接口中定义了两个方法:

  • postProcessBeforeInitialization(Object bean, String beanName):在 Bean 的初始化之前被回调。
  • postProcessAfterInitialization(Object bean, String beanName):在 Bean 的初始化之后被回调。

通过实现 BeanPostProcessor 接口,我们可以对 Bean 进行各种自定义的处理,比如属性注入、AOP 配置、属性加密解密等等。

public interface BeanPostProcessor {
	default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
	default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}
}

Spring容器启动过程

Spring容器启动的过程可以分为以下几个步骤:

  1. 加载Spring配置文件:Spring容器启动时,需要先读取配置文件,这些配置文件可以是XML文件、Java配置类、Properties文件等。

  2. 创建Bean实例:读取完配置文件后,Spring容器会开始创建Bean实例,也就是根据配置文件中的定义创建Java对象,存放在BeanFactory容器中,这些Java对象可以是普通Java类、Spring自带的类(例如ApplicationContextAware)或自定义的类。

  3. 解析Bean之间的关系:当Spring容器创建了Bean实例后,容器会开始解析这些Bean实例之间的依赖关系,通过DI(Dependency Injection)方式将Bean注入到其他Bean中,这样Bean之间的依赖关系就被建立了。

  4. 初始化Bean实例:Bean实例初始化的过程包括三个阶段:实例化、属性赋值和初始化方法调用。实例化阶段是通过Java反射机制创建Bean实例,属性赋值是将Bean的属性值通过DI方式注入到Bean中,初始化方法调用是在Bean实例创建完成后,调用Bean的初始化方法,例如init-method。

  5. 完成容器初始化:当所有的Bean实例被创建、属性被注入,并调用了初始化方法后,Spring容器初始化就完成了,可以开始提供服务。

总的来说,Spring容器启动的过程就是将Bean定义解析成Bean实例并注入依赖的过程,最终形成一个完整的应用程序。

AbstractApplicationContext

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
	@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// 告诉子类刷新内部 Bean 工厂
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// 初始化该Context的消息源
				initMessageSource();

				// 初始化Context事件多播程序
				initApplicationEventMulticaster();

				// 执行子类的自定义刷新逻辑
				onRefresh();

				// 注册监听器
				registerListeners();

				// 实例化所有剩余(非惰性初始化)单例
				finishBeanFactoryInitialization(beanFactory);

				// 最后一步:发布相应的事件
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}
}

beanFactory

DefaultListableBeanFactory

Spring Boot

发展历史:

版本 时间线
1.0 2014 年
2.0 2018 年
3.0 2022年

自动装配

首先看一个 Springboot 的启动类是怎么写的:

@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

@SpringBootApplication

@SpringBootApplication 注解是一个复合注解,包含了 @SpringBootConfiguration@EnableAutoConfiguration@ComponentScan 三个注解。

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

@SpringBootConfiguration 注解表明是一个配置类,没有特别之处;@ComponentScan 用于扫描项目内的组件,装配到Spring IOC 容器中,默认扫描范围是当前标注该注解的类所在的目录及其所有子目录;@EnableAutoConfiguration 用于扫描项目在pom 文件中依赖的所有jar 包中的组件,也就是自动装配依赖包中的组件到Spring IOC 容器中;

@EnableAutoConfiguration

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

}

​ EnableAutoConfiguration 通过 @Import 将 AutoConfigurationImportSelector 加入到IOC 容器中,AutoConfigurationImportSelector 通过 SpringFactoriesLoader 这个工具类读取工程下的 META-INF/spring.factories 文件

spring.factories 文件

@SpringBootApplication 注解所在的工程为 spring-boot-autoconfigure ,该工程的META-INF/ 目录下有一个 spring.factories 文本文件,里面分为几部分内容,其中一部分记录了SpringBoot 开启自动配置要加载的所有配置类:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
...目前共计120多个,50多种配置类

文件格式:key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration,value 使用英文逗号隔开的字符串数组。

​ 你可以打开几个 xxxAutoConfiguration ,既然是配置类,自然少不了 @Configuration 注解,表明这是一个配置类。

​ 它们基本上都是有条件的装配,标注 @ConditionalOnClass@ConditionalOnBean@ConditionalOnProperty 等注解,只有条件满足是才进行自动配置。

​ 最后,配置类的最终目的是把Bean 装配到Spring 容器中,@Import 注解用于将普通类实例化装配到IOC容器中。

重要的类和文件:

@SpringBootApplication
@ComponentScan
@EnableAutoConfiguration
@Import(AutoConfigurationImportSelector.class)
SpringFactoriesLoader
META-INF/spring.factories

注:工程下 META-INFO 目录下的 spring.factories 文本文件,类似于 Java SPI 机制。

排除自动配置

配置文件&参数

Spring Boot程序默认从application.properties或者application.yaml读取配置,如何将配置信息外置,方便配置呢?

查询官网,可以得到下面的几种方案:

通过命令行指定

SpringApplication会默认将命令行选项参数转换为配置信息
例如,启动时命令参数指定:

java -jar myproject.jar --server.port = 9000

从命令行指定配置项的优先级最高,不过你可以通过setAddCommandLineProperties来禁用

SpringApplication.setAddCommandLineProperties(false).

外置配置文件

Spring程序会按优先级从下面这些路径来加载application.properties配置文件

  • 当前目录下的/config目录
  • 当前目录
  • classpath里的/config目录
  • classpath 跟目录

因此,要外置配置文件就很简单了,在jar所在目录新建config文件夹,然后放入配置文件,或者直接放在配置文件在jar目录

自定义配置文件

如果你不想使用application.properties作为配置文件,怎么办?完全没问题

java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties

或者

java -jar -Dspring.config.location=D:\config\config.properties springbootrestdemo-0.0.1-SNAPSHOT.jar 

当然,还能在代码里指定

@SpringBootApplication
@PropertySource(value={"file:config.properties"})
public class SpringbootrestdemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootrestdemoApplication.class, args);
    }
}

按Profile不同环境读取不同配置

不同环境的配置设置一个配置文件,例如:

  • dev环境下的配置配置在application-dev.properties中;
  • prod环境下的配置配置在application-prod.properties中。

在application.properties中指定使用哪一个文件

spring.profiles.active = dev

当然,你也可以在运行的时候手动指定:

java -jar myproject.jar --spring.profiles.active = prod

增强条件装配

Spring提供根据条件装配Bean的功能:(@Conditional及@Conditional的拓展注解)

注解
@Conditional 需要实现Condition 接口,自定义判断逻辑
@ConditionalOnClass 当类路径下有指定的Class,条件生效
@ConditionalOnMissingClass 当类路径下没有指定的Class,条件生效
@ConditionalOnBean 当容器里有指定Bean,条件生效
@ConditionalOnMissingBean 当容器里没有指定Bean,条件生效
@ConditionalOnProperty 当指定的配置属性有指定的值时,条件生效
@ConditionalOnExpression 基于SpEL表达式的条件判断
@ConditionalOnResource 当类路径下有指定的资源时,条件生效

@ConditionalOnProperty 示例:

@RestController
@ConditionalOnProperty(name = "bap.manageNode", havingValue = "true")
public class DictCacheSyncController {
	
}

意思是只有配置文件中name为 bap.manageNode 变量的值为true 时才会装配该Bean;

配置文件:

# yml格式
bap:
  manageNode: true

# properties格式
bap.manageNode=true

校验web请求数据

Spring 中可以使用 @Validated 注解校验数据,如果请求的数据异常,就会抛出异常,统一由异常中心处理。

首先把 @Validated 注解标注在 Controller 的接口入参上

@RestController
@RequestMapping("/member/points")
public class MemberPointsAdjustController {

    @PostMapping("/adjust")
    public CommonResult pointsAdjust(@RequestBody @Validated MemberPointsAdjustReq memberPointsAdjustReq){
    
        return CommonResult.success("操作成功!");
    }
}

然后在请求实体类上加入具体的校验注解(位于 javax.validation.constraints 包),常用的有:

// 不能为Null
@NotNull
// 字符不能为Null,且不能为空
@NotBlank

// Min 和 Max 用于校验整数
@Min(value = 0)
@Max(value = 100)
// DecimalMax 和 DecimalMin 用于校验小数
@DecimalMax(value = "3.14")
@DecimalMin(value = "0.1")

//集合元素不能为空
@NotEmpty
// 用于校验字符/集合/Map/数组,不能用于数值;校验元素个数
@Size(min = 1, max = 900)

对于嵌套数据, 需要加入 @Valid 注解才能生效:

public class MemberPointRuleRequest extends BaseRequest {

    @Valid
    private MemberPointRuleInfo memberPointsRule;

    @Valid
    private List<MemberPointRuleDetail> memberPointsRuleList;
}

VO 返回给前端数据

public class MemberPointsAdjustVo{
    /** 操作时间 */
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime handleTime;
}

反序列化校验前端请求数据

使用 jackson 包下的 @JsonFormat 注解反序列化时间:

public class ProcessMyApprovedReq {
    
	@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Date startTime;
    
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime endTime;
}

注: spring包中的 @DateTimeFormat 注解与 java.time 包中的 LocalDateTime 不能配合使用,会报错。

数据库连接池

配置数据库连接池

SpringBoot 支持多种数据源类型,定义在 DataSourceAutoConfiguration 中,它们分别是:

org.apache.tomcat.jdbc.pool.DataSource
com.zaxxer.hikari.HikariDataSource
org.apache.commons.dbcp.BasicDataSource
org.apache.commons.dbcp2.BasicDataSource

HikariPool 为SpringBoot 2.x 默认的数据库连接池

配置文件

springboot的配置文件有以下几种:

  • 主配置文件:application.yml 和 bootstrap.yml(优先级更高)
  • profile 配置文件:application-xxx.yml

加载顺序

详细加载顺序

Spring Boot加载配置文件的顺序如下(优先级从高到低):

  1. **file:./config/**(jar所在目录的config子目录)
  2. **file:./**(jar所在目录)
  3. **classpath:/config/**(jar包内部的/config目录)
  4. **classpath:/**(jar包根目录)

如果同级目录和jar包内部均存在bootstrap.yml,则外部的file:./bootstrap.yml会覆盖内部的classpath:/bootstrap.yml

​ Spring 项目中,可以使用 xml 、 properties 或 yml 作为配置文件,新项目一般使用可读性更高的 yml 作为配置文件;

语法:

  • 使用缩进表示层级关系;
  • 缩进的空格数目无所谓,只要相同层级的元素左侧对齐即可;
  • 区分大小写;
  • 使用 # 表示注释;

示例:

spring:
  datasource:	# 数据源配置
    url: jdbc:mysql://127.0.0.1:3306/jackpot
    username: root
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver 
    type: org.apache.commons.dbcp2.BasicDataSource #指定数据库连接池
    dbcp2:
      initial-size: 5   #连接池初始化连接数
      max-idle: 8  
  webservices:
    path: http://127.0.0.1:8081/orderWebService

在Spring框架中使用 @Value 注解可以很方便的读取 yml 文件中的配置信息到注解标注的属性中:

@Service
public class WebServicePublish {
    
    @Value(value = "${spring.webservices.path}")
	private String webServiceUrl;
    //赋默认值
    @Value(value = "${spring.webservices.port:8080}")
	private String port;
}

引用变量

yml 文件中也可以引用变量:

amino:
  home: E:/beecode/new-platform
amino2: ${amino.home}/test

系统启动后执行方法

CommandLineRunnerApplicationRunner接口是Spring Boot框架中用于在应用程序启动后执行特定逻辑的回调接口,它们有以下区别:

  1. 功能:两个接口都用于在应用程序启动后执行逻辑,但它们的参数不同。CommandLineRunner接口的run方法接受字符串数组(String[])作为参数,用于接收命令行参数。而ApplicationRunner接口的run方法接受ApplicationArguments对象作为参数,用于获取更丰富的应用程序参数信息。

  2. 参数解析:CommandLineRunner接口的run方法直接接收原始的命令行参数字符串数组,开发人员需要自行解析和处理这些参数。而ApplicationRunner接口的run方法接收的ApplicationArguments对象提供了更方便的方法来解析命令行参数,例如获取选项值、非选项参数等。

  3. 使用场景:由于ApplicationArguments对象提供了更多的命令行参数信息,ApplicationRunner接口更适合在需要处理复杂命令行参数的场景下使用。而CommandLineRunner接口更适合在只需要简单处理命令行参数的场景下使用。

  4. 执行顺序:当多个实现了CommandLineRunner或ApplicationRunner接口的类存在时,它们的执行顺序是不确定的。如果需要控制执行顺序,可以使用@Order注解或实现Ordered接口来指定顺序。

​ 总结来说,CommandLineRunner和ApplicationRunner接口都用于在应用程序启动后执行逻辑,但它们的参数类型和使用场景有所不同。CommandLineRunner适用于简单的命令行参数处理,而ApplicationRunner提供了更丰富的应用程序参数信息,适用于需要处理复杂命令行参数的场景。

CommandLineRunner 接口

@Component
public class InitStartupRunner implements CommandLineRunner {

	@Override
	public void run(String... args) throws Exception {
		//TODO
	}
}

除了要继承接口,还要注册为spring Bean 。

ApplicationRunner 接口

小结:

​ 注解方式@PostConstruct 始终最先执行。

​ ApplicationListener 监听的如果是 ApplicationStartedEvent 事件,则会在CommandLineRunner和ApplicationRunner 之前执行。如果监听的是ApplicationReadyEvent 事件,则会在CommandLineRunner和ApplicationRunner 之后执行。

​ CommandLineRunner 和 ApplicationRunner 默认是ApplicationRunner先执行,如果双方指定了@Order 则按照@Order的大小顺序执行。

@ConfigurationProperties

​ 我们将大量的参数配置在 application.properties 或 application.yml 文件中,通过 @ConfigurationProperties 注解,我们可以方便的获取这些参数值。

配置文件:

com:
  nr:
    app:
      deployModelTables: false
      createManyToOneIndex: false

代码:

@ConfigurationProperties(prefix = StoreProperties.PROPERTIES_PREFIX, ignoreUnknownFields = true)
public class StoreProperties {

	public static final String PROPERTIES_PREFIX = "com.nr.app";

	private boolean deployModelTables = true;

	private boolean createManyToOneIndex = true;

	private boolean createEntityReferenceIndex = true;
}

​ 我们可以使用 @Value 注解或着使用 Spring Environment bean 访问这些属性,但是这种注入配置方式有时显得比较笨重。我们将使用更安全的方式(@ConfigurationProperties )来获取这些属性。

@ConfigurationProperties 的基本用法非常简单:我们为每个要捕获的外部属性提供一个带有字段的类。请注意以下几点:

  • 前缀定义了哪些外部属性将绑定到类的字段上
  • 类的属性名称必须与外部属性的名称匹配
  • 可以用一个值初始化一个字段来定义一个默认值
  • 类本身可以是包私有的,但类的字段必须有公共 setter 方法

激活

两种方式激活

方式一:在目标属性类上添加 @Component 注解并且可以被扫描到

方式二:在配置类上添加 @EnableConfigurationProperties 注解指定要激活的属性类

@Configuration
@EnableConfigurationProperties(StoreProperties.class)
public class StoreAutoConfiguration {}

无法转换的属性

​ 如果我们在 application.properties 属性上定义的属性不能被正确的解析会发生什么?假如我们为原本应该为布尔值的属性提供的值为 字符串’foo’,默认情况下,Spring Boot 将会启动失败,并抛出异常。

​ 当我们为属性配置错误的值时,而又不希望 Spring Boot 应用启动失败,我们可以设置 ignoreInvalidFields 属性为 true (默认为 false)

未知的属性

和上面的情况有些相反,如果我们在 application.properties 文件提供了属性类不知道的属性会发生什么?

​ 默认情况下,Spring Boot 会忽略那些不能绑定到 @ConfigurationProperties 类字段的属性然而,当配置文件中有一个属性实际上没有绑定到 @ConfigurationProperties 类时,我们可能希望启动失败。也许我们以前使用过这个配置属性,但是它已经被删除了,这种情况我们希望被触发告知手动从 application.properties 删除这个属性。

​ 为了实现上述情况,我们仅需要将 ignoreUnknownFields 属性设置为 false (默认是 true)

​ 那么 @ConfigurationProperties 注解满足我们的全部需要了吗?其实不然,Spring 官网明确给出了该注解和 @Value 注解的对比:

如果使用 SpEL 表达式,我们只能选择 @Value 注解。

@AutoConfigureBefore

@AutoConfigureAfter 和 @AutoConfigureBefore 用于指定自动配置类的加载顺序。

@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@AutoConfigureBefore({ HibernateStoreAutoConfiguration.class, JdbcStoreAutoConfiguration.class })
public class HibernateStoreAutoConfiguration {}

​ 上面的示例表明 HibernateStoreAutoConfiguration 配置类在 HibernateStoreAutoConfiguration 和 JdbcStoreAutoConfiguration 之前加载,在 DataSourceAutoConfiguration 之后加载。

​ 注意:这两个注解只能标注在自动配置类上才能生效,自定义配置类是不生效的。在spirng.factories 文件中指定的配置类为自动配置类。

Actuator

​ Actuator是SpringBoot项目中一个非常强大一个功能,有助于对应用程序进行监视和管理,通过Restful api请求来监管、审计、收集应用的运行情况。

​ Actuator的核心是端点Endpoint,它用来监视应用程序及交互,spring-boot-actuator中已经内置了非常多的Endpoint(health、info、beans、metrics、httptrace、shutdown等等),同时也允许我们自己扩展自定义的Endpoints。

​ 每个Endpoint都可以启用和禁用。要远程访问Endpoint,还必须通过JMX或HTTP进行暴露,大部分应用选择HTTP,Endpoint 的ID默认映射到一个带/actuator前缀的URL。例如,health端点默认映射到 /actuator/health

添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Endpoint 端点

Actuator的核心就是Endpoint,每一个Endpoint都代表着监控某一项数据。

Actuator 默认不开放全部端点,只开放几个端点。在浏览器输入:http://localhost:8080/actuator 查看默认开放的端点。

我们可以看到端点的访问地址都是以**/actuator**为前缀,这是SpringBoot默认设置的地址,我们可以在配置文件中修改,如下配置:

management:
  endpoints:
    web:
      base-path: /myActuator
端点 描述
auditevents 获取当前应用暴露的审计事件信息
beans 获取应用中所有的 Spring Beans 的完整关系列表
caches 获取公开可以用的缓存
conditions 获取自动配置条件信息,记录哪些自动配置条件通过和没通过的原因
configprops 获取所有配置属性,包括默认配置,显示一个所有 @ConfigurationProperties 的整理列版本
env 获取所有环境变量
flyway 获取已应用的所有Flyway数据库迁移信息,需要一个或多个 Flyway Bean
liquibase 获取已应用的所有Liquibase数据库迁移。需要一个或多个 Liquibase Bean
health 获取应用程序健康指标(运行状况信息)
httptrace 获取HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应交换)。需要 HttpTraceRepository Bean
info 获取应用程序信息
integrationgraph 显示 Spring Integration 图。需要依赖 spring-integration-core
loggers 显示和修改应用程序中日志的配置
logfile 返回日志文件的内容(如果已设置logging.file.name或logging.file.path属性)
metrics 获取系统度量指标信息
mappings 显示所有@RequestMapping路径的整理列表
scheduledtasks 显示应用程序中的计划任务
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序
shutdown 关闭应用,要求endpoints.shutdown.enabled设置为true,默认为 false
threaddump 获取系统线程转储信息
heapdump 返回hprof堆转储文件
jolokia 通过HTTP公开JMX bean(当Jolokia在类路径上时,不适用于WebFlux)。需要依赖 jolokia-core
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖 micrometer-registry-prometheus

暴露所有端点

接下来暴露所有的端点Endpoint,如下配置:

management:
  endpoints:
    web:
      exposure:
        include: '*' #暴露所有端点
      base-path: /actuator #配置端点访问前缀

下面是Springboot2.7 版本可以暴露所有的端点信息:

{
    "_links":{
        "self":{
            "href":"http://127.0.0.1:8082/actuator",
            "templated":false
        },
        "beans":{
            "href":"http://127.0.0.1:8082/actuator/beans",
            "templated":false
        },
        "caches-cache":{
            "href":"http://127.0.0.1:8082/actuator/caches/{cache}",
            "templated":true
        },
        "caches":{
            "href":"http://127.0.0.1:8082/actuator/caches",
            "templated":false
        },
        "health":{
            "href":"http://127.0.0.1:8082/actuator/health",
            "templated":false
        },
        "health-path":{
            "href":"http://127.0.0.1:8082/actuator/health/{*path}",
            "templated":true
        },
        "info":{
            "href":"http://127.0.0.1:8082/actuator/info",
            "templated":false
        },
        "conditions":{
            "href":"http://127.0.0.1:8082/actuator/conditions",
            "templated":false
        },
        "configprops":{
            "href":"http://127.0.0.1:8082/actuator/configprops",
            "templated":false
        },
        "configprops-prefix":{
            "href":"http://127.0.0.1:8082/actuator/configprops/{prefix}",
            "templated":true
        },
        "env":{
            "href":"http://127.0.0.1:8082/actuator/env",
            "templated":false
        },
        "env-toMatch":{
            "href":"http://127.0.0.1:8082/actuator/env/{toMatch}",
            "templated":true
        },
        "loggers":{
            "href":"http://127.0.0.1:8082/actuator/loggers",
            "templated":false
        },
        "loggers-name":{
            "href":"http://127.0.0.1:8082/actuator/loggers/{name}",
            "templated":true
        },
        "heapdump":{
            "href":"http://127.0.0.1:8082/actuator/heapdump",
            "templated":false
        },
        "threaddump":{
            "href":"http://127.0.0.1:8082/actuator/threaddump",
            "templated":false
        },
        "metrics-requiredMetricName":{
            "href":"http://127.0.0.1:8082/actuator/metrics/{requiredMetricName}",
            "templated":true
        },
        "metrics":{
            "href":"http://127.0.0.1:8082/actuator/metrics",
            "templated":false
        },
        "scheduledtasks":{
            "href":"http://127.0.0.1:8082/actuator/scheduledtasks",
            "templated":false
        },
        "mappings":{
            "href":"http://127.0.0.1:8082/actuator/mappings",
            "templated":false
        }
    }
}

还可以指定不暴露某个端点,下面首先暴露所有端点,然后又指定不暴露:infobeansenv这三个端点。

management:
  endpoints:
    web:
      base-path: /actuator #配置端点访问前缀
      exposure:
        include: '*'  #暴露所有端点
        exclude: info,beans,env #在暴露所有端点的前提下,可以排除某个端点(不暴露)

禁用所有端点:

management:
  endpoints:
    web:
      exposure:
        include: '*' #暴露所有端点
      base-path: /actuator #配置端点访问前缀
    enabled-by-default: false #禁用所有端点

关闭所有端点之后,可以指定暴露某个端点,如下配置:

management:
  endpoints:
    web:
      exposure:
        include: '*' #暴露所有端点
      base-path: /actuator #配置端点访问前缀
    enabled-by-default: false #禁用所有端点
  endpoint:
    info:
      enabled: true #上面关闭所有端点之后,又暴露指定的端点,这里暴露info端点

Tomcat

SpringBoot 默认采用的是内置的Tomcat,方便项目启动,不需要单独部署web容器。

application.yml 内容:(下面是几个重要配置参数)

server:
  port: 8082
  servlet:
    context-path: /    #访问根路径
  tomcat:
    threads:
      min-spare: 10   #最小线程数
      max: 200   #最大线程数
    max-connections: 8192 #最大连接数
    accept-count: 1000   #最大等待队列长度
    max-http-form-post-size: 2MB  #请请求体最大长度kb
    connection-timeout: 12000  #连接建立超时时间
    keep-alive-timeout: 30000 # 一个连接上30秒内没有请求,自动断开连接
    max-keep-alive-requests: 100 # 一个连接上超过100个请求,自动断开连接

经验:

max-threads线程数的经验值为:

  • 1核2g内存,线程数经验值200
  • 4核8g内存,线程数经验值800

4核8g内存,建议值:

server:
  tomcat:
    threads:
      min-spare: 100   #最小线程数
      max: 800   #最大线程数
    max-connections: 1000 #最大连接数
    accept-count: 1000   #最大等待队列长度

静态资源

将静态文件集成到 Java 项目

​ 如果你的前端项目使用Vue开发,那么打包后会有一个dist 文件夹,将 Vue 生成的 dist 文件夹内容复制到 Java 项目的 src/main/resources/static 目录下(该目录是Spring Boot 默认的静态资源路径)。

WebSocket

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
 </dependency>

README

作者:银法王

版权声明:本文遵循知识共享许可协议3.0(CC 协议): 署名-非商业性使用-相同方式共享 (by-nc-sa)

参考:

​ [Spring Framework 官方文档(Version 5.2.4.RELEASE)][https://docs.spring.io/spring-framework/docs/5.2.4.RELEASE/spring-framework-reference]

​ [Spring Framework 官方文档(最新版本)][https://docs.spring.io/spring-framework/docs/current/reference/html]

SpringBoot 官网

Spring5参考指南(中文)

修改记录:

  2020-03-14 第一次修订

  2021-01-22


Spring开发笔记
http://jackpot-lang.online/2020/03/08/Java/Spring 实战笔记/
作者
Jackpot
发布于
2020年3月8日
许可协议