SpringBoot
一、创建SpringBoot项目编写HelloWorld
项目结构:
在启动类同级目录下创建controller包,编写HelloController
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello world!";
}
}
启动测试
二、pom.xml文件分析(依赖管理)
<!--本项目存在一个父项目spring-boot-starter-parent-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!--项目spring-boot-starter-parent也存在一个父项目spring-boot-dependencies-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.7</version>
</parent>
<!--spring-boot-dependencies是用来管理SpringBoot应用里面的所有依赖版本,我们在导入下列依赖时可以不写版本号,但如果导入第三方的依赖还是需要版本号的-->
<properties>
<activemq.version>5.16.4</activemq.version>
<antlr2.version>2.7.7</antlr2.version>
<appengine-sdk.version>1.9.96</appengine-sdk.version>
<artemis.version>2.19.1</artemis.version>
<aspectj.version>1.9.7</aspectj.version>
<assertj.version>3.21.0</assertj.version>
<atomikos.version>4.0.6</atomikos.version>
·····
</properties>
当然我们可以修改SpringBoot管理依赖的版本号
eg.导入mysql依赖
<!--SpringBoot管理的mysql版本号为8.0.28-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
我们可以通过properties修改依赖版本号
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.43</mysql.version>
</properties>
引入场景启动器starter
,这个场景下的依赖会自动引入。官方启动器命名spring-boot-starter-*
,第三方
的启动器命名*-spring-boot-starter
SpringBoot支持的所有starters
:
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring-boot-starter-web下的所有依赖-->
<!--SpringBoot web自动配置json、tomcat、springMVC相关的功能-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.6.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.19</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.19</version>
<scope>compile</scope>
</dependency>
</dependencies>
测试SpringBoot web自动配置SpringMVC相关功能:
@SpringBootApplication
public class SpringBoot01Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBoot01Application.class, args);
//获取容器中所有定义Bean的名称
String[] names = context.getBeanDefinitionNames();
for (String s :names) {
System.out.println(s);
}
}
}
前端控制器DispatcherServlet
:
文件上传相关:
编码过滤器:
······
三、SpringBoot启动类分析(自动配置)
1.SpringBoot默认包扫描位置为启动类所在的包及其子包
测试在com包下新建TestController访问404:
@RestController
public class TestController {
@RequestMapping("/test")
public String hello(){
return "test...";
}
}
解决方法:
-
将TestController放到启动类同级目录及其子目录下
-
在启动类上添加属性
scanBasePackage
,将包扫描路径更改为com目录下 @SpringBootApplication(scanBasePackages = “com”)
-
@SpringBootApplication注解等同于
@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan()
在@ComponentScan(“com”)中指定包扫描路径为com下
2.@Configuration
编写pojo
@Data
@NoArgsConstructor
public class User {
private String username;
private String password;
private String role;
private Pet pet;
public User(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
}
通过配置类向容器中注入组件
//标识该类为配置类
@Configuration(proxyBeanMethods = false)
public class MyConfig {
//@Bean标注的方法相当于一个Bean,方法名为Bean的name,方法返回值为Bean的类型
@Bean
public User user1(){
User user = new User("张三", "123456", "visitor");
user.setPet(pet1());
return user;
}
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
boolean proxyBeanMethods() default true;
}
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBoot01Application.class, args);
//SpringBoot中Bean默认是单例的,每次获取的都是容器中的同一个
User user1 = context.getBean("user1", User.class);
User user2 = context.getBean("user1", User.class);
System.out.println(user1 == user2); //true
MyConfig bean = context.getBean(MyConfig.class);
System.out.println(bean);
User user11 = bean.user1();
User user22 = bean.user1();
System.out.println(user11 == user22);
}
/*
proxyBeanMethods = true时:
com.qingsongxyz.config.MyConfig$$EnhancerBySpringCGLIB$$7ab6555@5c41d037
true
-------------------------------------------------------------------------
proxyBeanMethods = false时:
com.qingsongxyz.config.MyConfig@31ff1390
false
*/
proxyBeanMethods
:代理Bean的方式
- Full模式(true默认),采用CGLIB代理,调用配置类中方法获取Bean时会先去容器中找有没有相同类型的Bean,没有的时候才会创建新的Bean,保证Bean的单例。适用于配置类中Bean之间有依赖关系,获取时得到容器中的Bean
- Lite模式(false),适用于配置类中的Bean之间无依赖关系,无需判断容器中有没有该Bean,加速容器启动
3.@Import
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
Class<?>[] value();
}
传入Class数组,向容器中注入该类型的Bean,Bean的名称为全限定名
//向容器中注入User类型的Bean
@Import({User.class})
@Configuration(proxyBeanMethods = false)
public class MyConfig {
@Bean
public User user1(){
User user = new User("张三", "123456", "visitor");
return user;
}
}
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBoot01Application.class, args);
String[] names = context.getBeanNamesForType(User.class);
for (String name : names) {
System.out.println(name);
}
}
/*
com.qingsongxyz.pojo.User ---> @Import注入的
user1
*/
4.@Conditional
SpringBoot使用条件装配控制是否注入Bean
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Group {
private String name;
private Integer userNum;
private String time;
}
@Configuration(proxyBeanMethods = false)
//当容器中没有user1时配置类生效,类中的Bean会注入
@ConditionalOnMissingBean(name = "user1")
public class MyConfig {
//没有注入user1
//@Bean
public User user1(){
User user = new User("张三", "123456", "visitor");
return user;
}
@Bean
public Group group1(){
return new Group("1组", 50, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBoot01Application.class, args);
Group group1 = context.getBean("group1", Group.class);
System.out.println(group1);
}
//Group(name=1组, userNum=50, time=2022-04-27 09:22:39)
5.@ImportResource
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ImportResource {
@AliasFor("locations")
String[] value() default {};
@AliasFor("value")
String[] locations() default {};
Class<? extends BeanDefinitionReader> reader() default BeanDefinitionReader.class;
}
用来将bean.xml里面的Bean导入容器
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="u1" class="com.qingsongxyz.pojo.User">
<property name="username" value="王五"/>
<property name="password" value="123"/>
<property name="role" value="visitor"/>
</bean>
</beans>
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "user1")
@ImportResource("classpath:bean.xml")
public class MyConfig {
//@Bean
public User user1(){
User user = new User("张三", "123456", "visitor");
return user;
}
@Bean
public Group group1(){
return new Group("1组", 50, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBoot01Application.class, args);
User u1 = context.getBean("u1", User.class);
System.out.println(u1);
}
//User(username=王五, password=123, role=visitor)
6.@ConfigurationProperties
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {
@AliasFor("prefix")
String value() default "";
@AliasFor("value")
String prefix() default "";
boolean ignoreInvalidFields() default false;
boolean ignoreUnknownFields() default true;
}
将组件和配置文件绑定,prefix
指定配置文件中以什么开头的内容
//与SpringBoot配置文件application.properties中前缀为user的内容绑定
@ConfigurationProperties(prefix = "user")
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String username;
private String password;
private String role;
}
user.username=lisi
user.password=asdf
user.role=visitor
@RestController
public class HelloController {
@Autowired
private User user;
@RequestMapping("/hello")
public String hello(){
return "Hello world!";
}
@RequestMapping("/user")
public User user(){
return user;
}
}
注意:
@ConfigurationProperties
需要在容器中才能生效,使用@Component
将组件注入到容器中
或者在配置类上加@EnableConfigurationProperties
注解,开启该类的配置绑定,并将其加入到容器中
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "user1")
@ImportResource("classpath:bean.xml")
//开启User类的配置绑定
@EnableConfigurationProperties(User.class)
public class MyConfig {
//@Bean
public User user1(){
User user = new User("张三", "123456", "visitor");
return user;
}
@Bean
public Group group1(){
return new Group("1组", 50, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
*7.核心注解@SpringBootApplication分析
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
......
}
@SpringBootConfiguration
:
//标识为SpringBoot配置类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
@EnableAutoConfiguration
:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
//向容器中注入AutoConfigurationImportSelector
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
@AutoConfigurationPackage
:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//向容器中注入Registrar
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
AutoConfigurationPackages.Registrar
:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//将启动类所在包及其子包下的所有组件注入容器
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
}
}
断点调试:
AutoConfigurationImportSelector
:
//调用getAutoConfigurationEntry()方法,返回自动配置信息
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//获取注解元信息
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//获取候选配置
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//去除重复的配置
configurations = this.removeDuplicates(configurations);
//排除一些配置并过滤掉
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
//返回最终的配置信息
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
//获取候选配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
//加载工厂名
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
//加载Spring工厂 加载项目所有"META-INF/spring.factories"下的组件
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
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,\
......
#共133个
虽然SpringBoot启动就会自动加载所有配置类,但是在许多配置类中都有加载条件的(按条件加载)
eg. AopAutoConfiguration
切面相关自动配置类
@Configuration(
proxyBeanMethods = false
)
//如果项目中spring.aop.auto = true或者没有该属性时加载配置类
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"auto"},
havingValue = "true",
matchIfMissing = true
)
public class AopAutoConfiguration {
public AopAutoConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
//容器中没有org.aspectj.weaver.Advice这个类时配置生效 ---> java动态代理
@ConditionalOnMissingClass({"org.aspectj.weaver.Advice"})
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class ClassProxyingConfiguration {
ClassProxyingConfiguration() {
}
@Bean
static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
return (beanFactory) -> {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory;
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
};
}
}
@Configuration(
proxyBeanMethods = false
)
//容器中有org.aspectj.weaver.Advice这个类时配置生效 ---> Cglib代理
@ConditionalOnClass({Advice.class})
static class AspectJAutoProxyingConfiguration {
AspectJAutoProxyingConfiguration() {
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = true
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "true",
matchIfMissing = true
)
static class CglibAutoProxyConfiguration {
CglibAutoProxyConfiguration() {
}
}
@Configuration(
proxyBeanMethods = false
)
@EnableAspectJAutoProxy(
proxyTargetClass = false
)
@ConditionalOnProperty(
prefix = "spring.aop",
name = {"proxy-target-class"},
havingValue = "false"
)
static class JdkDynamicAutoProxyConfiguration {
JdkDynamicAutoProxyConfiguration() {
}
}
}
}
SpringBoot自动配置类往往会和对应的配置文件进行绑定,在配置文件中编写了属性的默认值
如果我们需要修改配置,只需要在SpringBoot配置文件中修改相应的属性值即可
eg. 修改tomcat端口号
修改application.properties
即可
server.port=8081
8.查看生效的自动配置
修改application.properties
的debug属性
debug=true
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
AopAutoConfiguration matched:
- @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)
AopAutoConfiguration.ClassProxyingConfiguration matched:
- @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition)
- @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)
DispatcherServletAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
- found 'session' scope (OnWebApplicationCondition)
DispatcherServletAutoConfiguration.DispatcherServletConfiguration matched:
- @ConditionalOnClass found required class 'javax.servlet.ServletRegistration' (OnClassCondition)
- Default DispatcherServlet did not find dispatcher servlet beans (DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition)
DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration matched:
- @ConditionalOnClass found required class 'javax.servlet.ServletRegistration' (OnClassCondition)
- DispatcherServlet Registration did not find servlet registration bean (DispatcherServletAutoConfiguration.DispatcherServletRegistrationCondition)
DispatcherServletAutoConfiguration.DispatcherServletRegistrationConfiguration#dispatcherServletRegistration matched:
- @ConditionalOnBean (names: dispatcherServlet types: org.springframework.web.servlet.DispatcherServlet; SearchStrategy: all) found bean 'dispatcherServlet' (OnBeanCondition)
......
Negative matches:
-----------------
ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)
AopAutoConfiguration.AspectJAutoProxyingConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'org.aspectj.weaver.Advice' (OnClassCondition)
......
Exclusions:
-----------
None
Unconditional classes:
----------------------
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
四、dev-tools(热部署)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
修改源代码时,使用Ctrl + F9
快捷键重新编译项目
每当类路径上的文件发生更改时,使用spring boot devtools的应用程序就会自动重新启动。会监视类路径上指向目录的任何条目的更改。更改静态资源和视图模板不需要重新启动应用程序。
五、SpringBoot配置文件详解
1.配置文件位置
SpringBoot启动时会自动查找并加载一下位置的.properties/.yaml
配置文件(优先级从高到低):
- 项目路径 ./config/
- 项目路径 ./config
- 项目路径 ./
- 类路径resources ./config
- 类路径resources ./
每个properties
文件内容
(1)server.port=8080
(2)server.port=8081
(3)server.port=8082
(4)server.port=8083
(5)server.port=8084
删除 ./config/v1/application.properties:
删除 ./config/application.properties
删除 application.properties
删除 resources/config/application.properties
2.yaml文件的使用
<!--自定义类配置提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<!--打包时排除自定义类处理器-->
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private String gender;
private String[] hobby;
private List<String> pets;
private Map<String, String> scores;
private List<Map<String, String>> relationship;
}
#yaml中Map的key不要用中文
person:
name: tom
age: 18
gender: 男
hobby:
- 打游戏
- 游泳
- 跑步
pets: [大黄, 小白, 小黑]
scores:
math: 80
chinese: 90
relationship:
- {father: 王海}
- {mother: 李丽}
- {brother: 王力}
@RequestMapping("/person")
public Person person(){
return person;
}
六、静态资源处理
// WebMvcAutoConfiguration类中
@Configuration(
proxyBeanMethods = false
)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
// WebMvcAutoConfigurationAdapter和配置文件WebMvcProperties(spring.mvc)
// 、WebProperties(spring.web)绑定
@EnableConfigurationProperties({WebMvcProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware
{
......
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//isAddMappings默认为true开启SpringBoot资源处理器,设置为false可将其关闭
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
/*
添加webjars的资源处理器,访问路径为/webjars/**,映射到项目所有
/META-INF/resources/webjars/路径
*/
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
//添加静态资源处理器,默认访问路径为/**,默认映射到CLASSPATH_RESOURCE_LOCATIONS
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}
//处理webjars资源,将项目中所有/META-INF/resources/webjars/路径添加到locationValues集合中
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
this.addResourceHandler(registry, pattern, (registration) -> {
registration.addResourceLocations(locations);
});
}
public ResourceHandlerRegistration addResourceLocations(String... locations) {
this.locationValues.addAll(Arrays.asList(locations));
return this;
}
//真正添加资源处理器函数
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, Consumer<ResourceHandlerRegistration> customizer) {
if (!registry.hasMappingForPattern(pattern)) {
ResourceHandlerRegistration registration = registry.addResourceHandler(new String[]{pattern});
//根据传入Consumer不同分别处理webjars和静态资源
customizer.accept(registration);
//添加资源缓存
registration.setCachePeriod(this.getSeconds( this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache()
.getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
this.customizeResourceHandlerRegistration(registration);
}
}
......
}
/*
静态资源映射路径
1.classpath:/META-INF/resources/
2.classpath:/resources/
3.classpath:/static/
4.classpath:/public/
*/
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
以下四个路径中的图片均能访问:
关闭SpringBoot资源处理器:
spring:
web:
resources:
add-mappings: false #关闭SpringBoot资源处理器
修改静态资源访问路径:
spring:
mvc:
static-path-pattern: /res/** #默认访问路径为/**,现在访问静态资源为/res/**
修改静态资源映射路径:
spring:
web:
resources:
#默认映射路径为CLASSPATH_RESOURCE_LOCATIONS,现在为classpath:/my/
static-locations: [classpath:/my/]
mvc:
static-path-pattern: /res/**
七、欢迎页和页签图标
// WebMvcAutoConfiguration类中
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
//创建一个欢迎页处理器映射
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
//设置拦截器
welcomePageHandlerMapping.setInterceptors(this.getInterceptors
(mvcConversionService, mvcResourceUrlProvider));
//设置跨域访问配置
welcomePageHandlerMapping.setCorsConfigurations(
this.getCorsConfigurations());
return welcomePageHandlerMapping;
}
//WelcomePageHandlerMapping构造函数
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
//只有访问路径为默认路径/**时,欢迎页配置生效(自动转发到index.html)
//如果在配置文件中修改spring.mvc.staticPathPattern后欢迎页失效
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
this.setRootViewName("forward:index.html");
} else if (this.welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
this.setRootViewName("index");
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>welcome</title>
</head>
<body>
<h1>Welcome</h1>
</body>
</html>
#配置静态资源访问路径后 欢迎页404
spring:
mvc:
static-path-pattern: /res/**
页签图标favicon.ico
在CLASSPATH_RESOURCE_LOCATIONS
(静态资源映射路径)中存在favicon.ico
则会识别为页签图标
但是在配置spring.mvc.staticPathPattern
静态资源访问路径后,页签图片无法显示
八、表单提交支持Restful风格
使用HiddenHttpMethodFilter
解决表单提交put、delete、patch请求
//能够处理put、delete、patch请求
static{
ALLOWED_METHODS = Collections.unmodifiableList(
Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
}
@RestController
public class RestfulController {
@GetMapping("/get")
public String get(){
return "get方式提交...";
}
@PostMapping("/post")
public String post(){
return "post方式提交...";
}
@PutMapping("/put")
public String put(){
return "put方式提交...";
}
@DeleteMapping("/delete")
public String delete(){
return "delete方式提交...";
}
}
<form action="/get" method="get">
<input type="submit" value="get方式提交">
</form>
<form action="/post" method="post">
<input type="submit" value="post方式提交">
</form>
<form action="/put" method="post">
<!--传入参数_method表示表单真正的请求方式put-->
<input name="_method" hidden value="put">
<input type="submit" value="put方式提交">
</form>
<form action="/delete" method="post">
<!--传入参数_method表示表单真正的请求方式delete-->
<input name="_method" hidden value="delete">
<input type="submit" value="delete方式提交">
</form>
spring:
hiddenmethod:
filter:
enabled: true #开启 HiddenHttpMethodFilter,处理put,delete,patch请求
注意:
只有表单发送put、delete、patch请求时需要开启该过滤器,而使用ajax或Postman等方式无影响
九、请求映射分析
DispatchServlet
继承关系:
HttpServletBean
类中未重写doService相关方法
FrameworkServlet
类中也未重写:
而在DispatchServlet
类中重写该方法:
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
//进行一系列的配置
......
try {
//调用doDispatch方法
this.doDispatch(request, response);
} finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted() && attributesSnapshot != null) {
this.restoreAttributesAfterInclude(request, attributesSnapshot);
}
if (this.parseRequestPath) {
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
//决定当前请求由哪个处理器处理
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
//当所有处理器都不能处理该请求时,返回404
noHandlerFound(processedRequest, response);
return;
}
//决定当前请求由哪个处理器适配器处理
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//执行处理器(控制器中的方法)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (pageNotFoundLogger.isWarnEnabled()) {
pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
}
if (this.throwExceptionIfNoHandlerFound) {
//抛出404异常
throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
new ServletServerHttpRequest(request).getHeaders());
}
else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
//返回能够处理当前请求的处理器适配器
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
处理器映射器:
处理器适配器:
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//调用处理器适配器中的handleInternal方法
return handleInternal(request, response, (HandlerMethod) handler);
}
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
//同步代码块
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
//非同步代码块
mav = invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
//将request和response封装为ServletWebRequest
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
//设置参数解析器
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
//设置返回值处理器
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
//调用ServletInvocableHandlerMethod中的invokeAndHandle方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
//执行请求,获取处理器返回值
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//设置响应状态
setResponseStatus(webRequest);
//如果返回值为null
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
//如果产生了异常原因
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
//调用返回值处理器处理返回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
InvocableHandlerMethod:
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
//获取请求参数值
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
//真正执行处理器
return this.doInvoke(args);
}
InvocableHandlerMethod:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
//创建Object数组保存参数值
Object[] args = new Object[parameters.length];
//循环解析所有的参数值
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
//判断参数解析器中是否有能否解析该参数的,如果没有抛出异常
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//解析该参数返回参数值
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
if (logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}
HandlerMethodArgumentResolverComposite:
public boolean supportsParameter(MethodParameter parameter) {
//调用getArgumentResolver方法查找有没有能解析改参数的参数解析器
return this.getArgumentResolver(parameter) != null;
}
HandlerMethodArgumentResolverComposite:
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
//第一次解析时缓存为空,之后解析先从缓存argumentResolverCache中取,如果没有在取查找
HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
if (result == null) {
Iterator var3 = this.argumentResolvers.iterator();
//迭代参数解析器集合,判断是否能解析该参数
while(var3.hasNext()) {
HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
if (resolver.supportsParameter(parameter)) {
//如果能够解析就将该参数解析器保存到缓存之中方便下次使用
result = resolver;
this.argumentResolverCache.put(parameter, resolver);
break;
}
}
}
return result;
}
HandlerMethodArgumentResolverComposite:
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
//获取解析该参数的参数解析器(由于之前判断能否解析时已经存入缓存能够很快获取到)
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
} else {
//调用该参数解析器类中的resolveArgument方法解析参数
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
InvocableHandlerMethod:
protected Object doInvoke(Object... args) throws Exception {
Method method = this.getBridgedMethod();
try {
//method.invoke(this.getBean(), args)执行处理器
return KotlinDetector.isSuspendingFunction(method) ? CoroutinesUtils.invokeSuspendingFunction(method, this.getBean(), args) : method.invoke(this.getBean(), args);
} catch (IllegalArgumentException var5) {
this.assertTargetBean(method, this.getBean(), args);
String text = var5.getMessage() != null ? var5.getMessage() : "Illegal argument";
throw new IllegalStateException(this.formatInvokeError(text, args), var5);
} catch (InvocationTargetException var6) {
Throwable targetException = var6.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException)targetException;
} else if (targetException instanceof Error) {
throw (Error)targetException;
} else if (targetException instanceof Exception) {
throw (Exception)targetException;
} else {
throw new IllegalStateException(this.formatInvokeError("Invocation failure", args), targetException);
}
}
}
请求 /book 测试解析自定义POJO类,http://localhost:8081/bookid=1&bookName=三国演义&author=罗贯中
&image=image1.jpg&price=30&description=厉害
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private String id;
private String bookName;
private String author;
private String image;
private Double price;
private String description;
}
@RequestMapping("/book")
public String getBook(Book book){
return book.toString();
}
//自定义POJO类由ModelAttributeMethodProcessor解析器解析
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
//保存解析后的参数值
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
} else {
try {
//创建空的参数(POJO)对象
attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
} catch (BindException var10) {
if (this.isBindExceptionRequired(parameter)) {
throw var10;
}
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
} else {
attribute = var10.getTarget();
}
bindingResult = var10.getBindingResult();
}
}
if (bindingResult == null) {
//数据绑定对象,将请求中的数据绑定到POJO对象里面
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
//绑定请求参数值,给新创建的空对象设置值
this.bindRequestParameters(binder, webRequest);
}
this.validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
-------------------------------------------------------------------------------------------
//创建空的参数(POJO)对象
ServletModelAttributeMethodProcessor:
protected final Object createAttribute(String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
String value = getRequestValueForAttribute(attributeName, request);
if (value != null) {
Object attribute = createAttributeFromRequestValue(
value, attributeName, parameter, binderFactory, request);
if (attribute != null) {
return attribute;
}
}
//调用父类的createAttribute方法
return super.createAttribute(attributeName, parameter, binderFactory, request);
}
ModelAttributeMethodProcessor:
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
MethodParameter nestedParameter = parameter.nestedIfOptional();
//获取参数Class对象
Class<?> clazz = nestedParameter.getNestedParameterType();
//获取无参构造函数
Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);
//通过反射构造空对象返回
Object attribute = this.constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
if (parameter != nestedParameter) {
attribute = Optional.of(attribute);
}
return attribute;
}
//数据绑定
ServletModelAttributeMethodProcessor:
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
Assert.state(servletRequest != null, "No ServletRequest");
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
//进行数据绑定
servletBinder.bind(servletRequest);
}
ServletRequestDataBinder:
public void bind(ServletRequest request) {
//封装请求参数值
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
MultipartRequest multipartRequest = (MultipartRequest)WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
this.bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
} else if (StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/form-data")) {
HttpServletRequest httpServletRequest = (HttpServletRequest)WebUtils.getNativeRequest(request, HttpServletRequest.class);
if (httpServletRequest != null && HttpMethod.POST.matches(httpServletRequest.getMethod())) {
StandardServletPartUtils.bindParts(httpServletRequest, mpvs, this.isBindEmptyMultipartFiles());
}
}
this.addBindValues(mpvs, request);
//进行数据绑定
this.doBind(mpvs);
}
WebDataBinder:
protected void doBind(MutablePropertyValues mpvs) {
//对数据进行一系列检测
this.checkFieldDefaults(mpvs);
this.checkFieldMarkers(mpvs);
this.adaptEmptyArrayIndices(mpvs);
//调用父类doBind方法绑定数据
super.doBind(mpvs);
}
DataBinder:
protected void doBind(MutablePropertyValues mpvs) {
//对数据进行一系列检测
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
//应用属性值
applyPropertyValues(mpvs);
}
DataBinder:
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
//绑定请求参数
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
AbstractPropertyAccessor:
public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) throws BeansException {
List<PropertyAccessException> propertyAccessExceptions = null;
//保存所有参数值的集合
List<PropertyValue> propertyValues = pvs instanceof MutablePropertyValues ? ((MutablePropertyValues)pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues());
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = true;
}
try {
Iterator var6 = propertyValues.iterator();
//循环绑定参数值
while(var6.hasNext()) {
PropertyValue pv = (PropertyValue)var6.next();
try {
//设置参数值
this.setPropertyValue(pv);
} catch (NotWritablePropertyException var14) {
if (!ignoreUnknown) {
throw var14;
}
} catch (NullValueInNestedPathException var15) {
if (!ignoreInvalid) {
throw var15;
}
} catch (PropertyAccessException var16) {
if (propertyAccessExceptions == null) {
propertyAccessExceptions = new ArrayList();
}
propertyAccessExceptions.add(var16);
}
}
} finally {
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = false;
}
}
if (propertyAccessExceptions != null) {
PropertyAccessException[] paeArray = (PropertyAccessException[])propertyAccessExceptions.toArray(new PropertyAccessException[0]);
throw new PropertyBatchUpdateException(paeArray);
}
}
AbstractNestablePropertyAccessor:
public void setPropertyValue(PropertyValue pv) throws BeansException {
AbstractNestablePropertyAccessor.PropertyTokenHolder tokens = (AbstractNestablePropertyAccessor.PropertyTokenHolder)pv.resolvedTokens;
if (tokens == null) {
String propertyName = pv.getName();
AbstractNestablePropertyAccessor nestedPa;
try {
nestedPa = this.getPropertyAccessorForPropertyPath(propertyName);
} catch (NotReadablePropertyException var6) {
throw new NotWritablePropertyException(this.getRootClass(), this.nestedPath + propertyName, "Nested property in path '" + propertyName + "' does not exist", var6);
}
tokens = this.getPropertyNameTokens(this.getFinalPath(nestedPa, propertyName));
if (nestedPa == this) {
pv.getOriginalPropertyValue().resolvedTokens = tokens;
}
//设置值
nestedPa.setPropertyValue(tokens, pv);
} else {
this.setPropertyValue(tokens, pv);
}
}
AbstractNestablePropertyAccessor:
protected void setPropertyValue(AbstractNestablePropertyAccessor.PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
if (tokens.keys != null) {
this.processKeyedProperty(tokens, pv);
} else {
//设置值
this.processLocalProperty(tokens, pv);
}
}
AbstractNestablePropertyAccessor:
private void processLocalProperty(AbstractNestablePropertyAccessor.PropertyTokenHolder tokens, PropertyValue pv) {
AbstractNestablePropertyAccessor.PropertyHandler ph = this.getLocalPropertyHandler(tokens.actualName);
if (ph != null && ph.isWritable()) {
Object oldValue = null;
PropertyChangeEvent propertyChangeEvent;
try {
Object originalValue = pv.getValue();
Object valueToApply = originalValue;
if (!Boolean.FALSE.equals(pv.conversionNecessary)) {
if (pv.isConverted()) {
valueToApply = pv.getConvertedValue();
} else {
if (this.isExtractOldValueForEditor() && ph.isReadable()) {
try {
oldValue = ph.getValue();
} catch (Exception var8) {
Exception ex = var8;
if (var8 instanceof PrivilegedActionException) {
ex = ((PrivilegedActionException)var8).getException();
}
if (logger.isDebugEnabled()) {
logger.debug("Could not read previous value of property '" + this.nestedPath + tokens.canonicalName + "'", ex);
}
}
}
//转换服务,将请求中的String类型参数转换为POJO类中的属性类型
valueToApply = this.convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
}
pv.getOriginalPropertyValue().conversionNecessary = valueToApply != originalValue;
}
ph.setValue(valueToApply);
} catch (TypeMismatchException var9) {
throw var9;
} catch (InvocationTargetException var10) {
propertyChangeEvent = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
if (var10.getTargetException() instanceof ClassCastException) {
throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), var10.getTargetException());
} else {
Throwable cause = var10.getTargetException();
if (cause instanceof UndeclaredThrowableException) {
cause = cause.getCause();
}
throw new MethodInvocationException(propertyChangeEvent, cause);
}
} catch (Exception var11) {
propertyChangeEvent = new PropertyChangeEvent(this.getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
throw new MethodInvocationException(propertyChangeEvent, var11);
}
} else if (pv.isOptional()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring optional value for property '" + tokens.actualName + "' - property not found on bean class [" + this.getRootClass().getName() + "]");
}
} else if (!this.suppressNotWritablePropertyException) {
throw this.createNotWritablePropertyException(tokens.canonicalName);
}
}
WebDataBinder中的转换器:
自定义转换器:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>书籍</title>
</head>
<body>
<form action="/book" method="post">
书籍:<input type="text" name="book" value="1,三国演义,罗贯中,image1.jpg,30,厉害"><br>
<button type="submit">添加书籍</button>
</form>
</body>
</html>
添加自定义转换器:
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
//向WebDataBinder中添加自定义转换器
registry.addConverter(new Converter<String, Book>() {
@Override
public Book convert(String source) {
if(!StringUtils.isEmpty(source))
{
//解析以逗号分割的各个属性值
Book book = new Book();
String[] strings = source.split(",");
book.setId(strings[0]);
book.setBookName(strings[1]);
book.setAuthor(strings[2]);
book.setImage(strings[3]);
book.setPrice(Double.valueOf(strings[4]));
book.setDescription(strings[5]);
return book;
}
return null;
}
});
}
}
返回值处理分析:
HandlerMethodReturnValueHandlerComposite:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//查找能够处理当前返回值的返回值处理器
HandlerMethodReturnValueHandler handler = this.selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
} else {
//调用该返回值处理器处理返回值
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
}
HandlerMethodReturnValueHandlerComposite:
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = this.isAsyncReturnValue(value, returnType);
Iterator var4 = this.returnValueHandlers.iterator();
HandlerMethodReturnValueHandler handler;
//循环遍历返回值处理器
do {
do {
if (!var4.hasNext()) {
return null;
}
handler = (HandlerMethodReturnValueHandler)var4.next();
} while(isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler));
//找到能够处理的处理器时退出循环
} while(!handler.supportsReturnType(returnType));
return handler;
}
RequestResponseBodyMethodProcessor:
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
//通过消息转换器输出结果
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
AbstractMessageConverterMethodProcessor:
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
//如果返回值是字符串
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
//保存返回值,valueType(return 数据类型),targetType(处理器返回值类型)
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}
//如果返回值是资源类型
if (isResourceType(value, returnType)) {
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE;
}
catch (IllegalArgumentException ex) {
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
//如果之前处理过则直接使用该MediaType
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> acceptableTypes;
try {
//从请求头中的ACCEPT获取浏览器接受数据类型
acceptableTypes = getAcceptableMediaTypes(request);
}
catch (HttpMediaTypeNotAcceptableException ex) {
int series = outputMessage.getServletResponse().getStatus() / 100;
if (body == null || series == 4 || series == 5) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring error response content (if any). " + ex);
}
return;
}
throw ex;
}
//保存服务器能够处理的数据类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
//保存经过内容协商后的数据类型集合
List<MediaType> mediaTypesToUse = new ArrayList<>();
//内容协商
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
//如果浏览器接受的数据类型和服务器生产的数据类型适配则加入集合
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
return;
}
//将集合依据权重进行排序
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}
if (selectedMediaType != null) {
//去除权重值
selectedMediaType = selectedMediaType.removeQualityValue();
//循环遍历消息转换器
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
//如果该消息转换器能够写出该类型数据
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
//使用该消息转换器写出
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
if (body != null) {
Set<MediaType> producibleMediaTypes =
(Set<MediaType>) inputMessage.getServletRequest()
.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass()));
}
}
AbstractMessageConverterMethodProcessor:
protected List<MediaType> getProducibleMediaTypes(
HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {
Set<MediaType> mediaTypes =
(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<>(mediaTypes);
}
List<MediaType> result = new ArrayList<>();
//循环遍历所有的消息转换器
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && targetType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) {
//如果该消息转换器支持该类型数据,则添加到producibleTypes集合中
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result);
}
AbstractGenericHttpMessageConverter:
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
HttpHeaders headers = outputMessage.getHeaders();
this.addDefaultHeaders(headers, t, contentType);
//如果输出数据为流数据
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage)outputMessage;
streamingOutputMessage.setBody((outputStream) -> {
this.writeInternal(t, type, new HttpOutputMessage() {
public OutputStream getBody() {
return outputStream;
}
public HttpHeaders getHeaders() {
return headers;
}
});
});
} else {
//调用消息转换器写出数据
this.writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
请求头接受数据类型:
WebMVC自动配置MessageConverter:
WebMvcAutoConfiguration:
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
this.messageConvertersProvider
.ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters()));
}
HttpMessageConverter:
private final List<HttpMessageConverter<?>> converters;
public List<HttpMessageConverter<?>> getConverters() {
return this.converters;
}
HttpMessageConverter:
public HttpMessageConverters(boolean addDefaultConverters, Collection<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,
addDefaultConverters ? getDefaultConverters() : Collections.emptyList());
combined = postProcessConverters(combined);
this.converters = Collections.unmodifiableList(combined);
}
HttpMessageConverter:
private List<HttpMessageConverter<?>> getDefaultConverters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport", null)) {
converters.addAll(new WebMvcConfigurationSupport() {
public List<HttpMessageConverter<?>> defaultMessageConverters() {
return super.getMessageConverters();
}
}.defaultMessageConverters());
}
else {
converters.addAll(new RestTemplate().getMessageConverters());
}
reorderXmlConvertersToEnd(converters);
return converters;
}
WebMvcConfigurationSupport:
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
WebMvcConfigurationSupport:
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(new StringHttpMessageConverter());
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new ResourceRegionHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Throwable ex) {
// Ignore when no TransformerFactory implementation is available...
}
}
messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
messageConverters.add(new AtomFeedHttpMessageConverter());
messageConverters.add(new RssChannelHttpMessageConverter());
}
if (!shouldIgnoreXml) {
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
else if (jaxb2Present) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
}
if (kotlinSerializationJsonPresent) {
messageConverters.add(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
messageConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2SmilePresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build()));
}
if (jackson2CborPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build()));
}
}
//静态代码块初始化标志位(查看容器中是否有相应的类)
static {
ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
基于请求 format 参数的内容协商:
开启ParameterContentNegotiationStrategy
:
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启参数 format 进行内容协商
默认参数名为format表示返回值的格式,默认值包括xml、json格式
对于xml需要导入依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
@RequestMapping("/book")
public Book getBook(){
Book book = new Book();
book.setId("1");
book.setBookName("三国演义");
book.setAuthor("罗贯中");
book.setImage("image1.jpg");
book.setPrice(30.0);
book.setDescription("厉害");
return book;
}
测试:
当开启format参数内容协商策略后,内容协商包含两种策略,参数策略优先级大于请求头策略
使用Postman修改请求头
自定义消息转换器:
public class MyMessageConverter implements HttpMessageConverter<Book> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
//判断是否能够写出Book类型数据
return clazz.isAssignableFrom(Book.class);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
//定义消息转换器支持的数据类型
return MediaType.parseMediaTypes("application/x-qingsong;charset=GBK");
}
@Override
public Book read(Class<? extends Book> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Book book, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//定义写出格式,将Book属性以号冒分割输出
String data = "";
data += book.getId() + ";" +
book.getBookName() + ";" +
book.getAuthor() + ";" +
book.getImage() + ";" +
book.getPrice() + ";" +
book.getDescription();
OutputStream outputStream = outputMessage.getBody();
//设置编码为GBK
outputStream.write(data.getBytes("GBK"));
}
}
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyMessageConverter());
}
}
参数化策略默认格式只有xml、json,自定义格式无法通过参数化策略,使用请求头策略测试:
修改默认内容协商配置:
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Map<String, MediaType> map = new HashMap<>();
map.put("xml", MediaType.APPLICATION_XML);
map.put("json", MediaType.APPLICATION_JSON);
//添加自定义消息转换器参数和相应格式
map.put("qingsong", MediaType.parseMediaType("application/x-qingsong;charset=GBK"));
//参数化内容协商策略
ParameterContentNegotiationStrategy strategy1 = new ParameterContentNegotiationStrategy(map);
//请求头内容协商策略
HeaderContentNegotiationStrategy strategy2 = new HeaderContentNegotiationStrategy();
//重新配置两种策略
configurer.strategies(Arrays.asList(strategy1, strategy2));
}
}
十、Springboot启动分析
//核心启动类
@SpringBootApplication
public class SpringBootApplication {
public static void main(String[] args) {
//传入本类的class对象和main方法参数
SpringApplication.run(SpringBootApplication.class, args);
}
}
SpringApplication:
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class[]{primarySource}, args);
}
SpringApplication:
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
//创建SpringApplication对象调用run方法
return (new SpringApplication(primarySources)).run(args);
}
SpringApplication:
public ConfigurableApplicationContext run(String... args) {
//保存开始时间用于计算应用启动用时
long startTime = System.nanoTime();
//查找并初始化bootstrapRegistryInitializers
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
ConfigurableApplicationContext context = null;
//进入headless模式
this.configureHeadlessProperty();
//查找SpringApplicationRunListener
SpringApplicationRunListeners listeners = this.getRunListeners(args);
//通知所有的SpringApplicationRunListeners应用启动
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
//保存命令行参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//准备环境
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
this.configureIgnoreBeanInfo(environment);
//打印banner
Banner printedBanner = this.printBanner(environment);
//创建容器
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
//准备容器,通知每一个监听器容器准备并加载好
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//初始化容器
this.refreshContext(context);
//protected修饰的空函数,用于自定义逻辑
this.afterRefresh(context, applicationArguments);
//保存应用启动用时
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
//打印启动日志
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
}
//通知每一个监听器容器已经启动
listeners.started(context, timeTakenToStartup);
//调用容器中的Runner
this.callRunners(context, applicationArguments);
} catch (Throwable var12) {
//处理过程中的异常并通知
this.handleRunFailure(context, var12, listeners);
throw new IllegalStateException(var12);
}
try {
//保存应用准备用时
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
//通知每一个监听器容器已经准备好
listeners.ready(context, timeTakenToReady);
//返回容器
return context;
} catch (Throwable var11) {
//处理过程中的异常并通知
this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var11);
}
}
//属性保存初始化引导器
private List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;
//构造函数
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
...
//查找初始化引导器
this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
...
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return this.getSpringFactoriesInstances(type, new Class[0]);
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = this.getClassLoader();
//从spring.factories文件中获取相应的bootstrapRegistryInitializers
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
//创建bootstrapRegistryInitializers
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
//在META-INF/spring.factories文件中查找org.springframework.boot.BootstrapRegistryInitializer
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
private DefaultBootstrapContext createBootstrapContext() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
//遍历并初始化每一个引导器
this.bootstrapRegistryInitializers.forEach((initializer) -> {
initializer.initialize(bootstrapContext);
});
return bootstrapContext;
}
private void configureHeadlessProperty() {
//进入headless模式(在该模式下,系统缺少了显示设备、键盘或鼠标)
System.setProperty("java.awt.headless", System.getProperty("java.awt.headless", Boolean.toString(this.headless)));
}
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class[]{SpringApplication.class, String[].class};
//在META-INF/spring.factories文件中查找org.springframework.boot.SpringApplicationRunListener
return new SpringApplicationRunListeners(logger, this.getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args), this.applicationStartup);
}
//遍历所有的SpringApplicationRunListeners通知应用启动
void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
this.doWithListeners("spring.boot.application.starting", (listener) -> {
listener.starting(bootstrapContext);
}, (step) -> {
if (mainApplicationClass != null) {
step.tag("mainApplicationClass", mainApplicationClass.getName());
}
});
}
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
//获取当前应用环境
ConfigurableEnvironment environment = this.getOrCreateEnvironment();
//配置转换服务和环境参数
this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach((Environment)environment);
//通知所有监听器应用环境准备好
listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);
DefaultPropertiesPropertySource.moveToEnd((ConfigurableEnvironment)environment);
Assert.state(!((ConfigurableEnvironment)environment).containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");
//将应用环境绑定到Spring中
this.bindToSpringApplication((ConfigurableEnvironment)environment);
if (!this.isCustomEnvironment) {
environment = this.convertEnvironment((ConfigurableEnvironment)environment);
}
ConfigurationPropertySources.attach((Environment)environment);
return (ConfigurableEnvironment)environment;
}
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
} else {
switch(this.webApplicationType) {
case SERVLET:
//如果是servlet应用返回servlet环境
return new ApplicationServletEnvironment();
case REACTIVE:
//如果是webflux应用返回响应式环境
return new ApplicationReactiveWebEnvironment();
default:
return new ApplicationEnvironment();
}
}
}
protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
if (this.addConversionService) {
//设置转换服务
environment.setConversionService(new ApplicationConversionService());
}
//设置环境参数
this.configurePropertySources(environment, args);
this.configureProfiles(environment, args);
}
void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
//通知所有监听器应用环境准备好了
this.doWithListeners("spring.boot.application.environment-prepared", (listener) -> {
listener.environmentPrepared(bootstrapContext, environment);
});
}
//打印banner
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Mode.OFF) {
return null;
} else {
ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader((ClassLoader)null);
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter((ResourceLoader)resourceLoader, this.banner);
return this.bannerMode == Mode.LOG ? bannerPrinter.print(environment, this.mainApplicationClass, logger) : bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
}
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
this.postProcessApplicationContext(context);
//遍历并初始化每一个ApplicationContextInitializer
this.applyInitializers(context);
//通知每一个监听器应用上下文准备好
listeners.contextPrepared(context);
//关闭启动引导器
bootstrapContext.close(context);
//打印日志
if (this.logStartupInfo) {
this.logStartupInfo(context.getParent() == null);
this.logStartupProfileInfo(context);
}
//配置beanFactory
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
((AbstractAutowireCapableBeanFactory)beanFactory).setAllowCircularReferences(this.allowCircularReferences);
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
Set<Object> sources = this.getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
//加载配置
this.load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
protected void applyInitializers(ConfigurableApplicationContext context) {
Iterator var2 = this.getInitializers().iterator();
//遍历并初始化每一个ApplicationContextInitializer
while(var2.hasNext()) {
ApplicationContextInitializer initializer = (ApplicationContextInitializer)var2.next();
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}
void contextPrepared(ConfigurableApplicationContext context) {
//通知每一个监听器应用上下文准备好
this.doWithListeners("spring.boot.application.context-prepared", (listener) -> {
listener.contextPrepared(context);
});
}
//加载环境
protected void load(ApplicationContext context, Object[] sources) {
if (logger.isDebugEnabled()) {
logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
}
BeanDefinitionLoader loader = this.createBeanDefinitionLoader(this.getBeanDefinitionRegistry(context), sources);
if (this.beanNameGenerator != null) {
loader.setBeanNameGenerator(this.beanNameGenerator);
}
if (this.resourceLoader != null) {
loader.setResourceLoader(this.resourceLoader);
}
if (this.environment != null) {
loader.setEnvironment(this.environment);
}
loader.load();
}
void contextLoaded(ConfigurableApplicationContext context) {
//通知每一个监听器应用上下文加载好
this.doWithListeners("spring.boot.application.context-loaded", (listener) -> {
listener.contextLoaded(context);
});
}
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
shutdownHook.registerApplicationContext(context);
}
//刷新上下文
this.refresh(context);
}
void registerApplicationContext(ConfigurableApplicationContext context) {
this.addRuntimeShutdownHookIfNecessary();
Class var2 = SpringApplicationShutdownHook.class;
synchronized(SpringApplicationShutdownHook.class) {
this.assertNotInProgress();
context.addApplicationListener(this.contextCloseListener);
this.contexts.add(context);
}
}
//protected 空函数,用于自定义上下文刷新后的业务
protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
}
void started(ConfigurableApplicationContext context, Duration timeTaken) {
//通知每一个监听器应用上下文已经启动
this.doWithListeners("spring.boot.application.started", (listener) -> {
listener.started(context, timeTaken);
});
}
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList();
//获取容器中的ApplicationRunner
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
//获取容器中的CommandLineRunner
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//按照@Order注解进行排序
AnnotationAwareOrderComparator.sort(runners);
Iterator var4 = (new LinkedHashSet(runners)).iterator();
//遍历每一个Runner调用run方法
while(var4.hasNext()) {
Object runner = var4.next();
if (runner instanceof ApplicationRunner) {
this.callRunner((ApplicationRunner)runner, args);
}
if (runner instanceof CommandLineRunner) {
this.callRunner((CommandLineRunner)runner, args);
}
}
}
void ready(ConfigurableApplicationContext context, Duration timeTaken) {
//通知每一个监听器应用上下文已经准备好
this.doWithListeners("spring.boot.application.ready", (listener) -> {
listener.ready(context, timeTaken);
});
}
总结:
- springboot应用启动先去META-INF/spring.factories文件中查找bootstrapRegistryInitializers(启动引导器)、ApplicationListener(应用监听器)创建并初始化它们。然后在META-INF/spring.factories文件中查找SpringApplicationRunListener,创建并初始化它们,然后通知每一个SpringApplicationRunListener应用启动。
- 然后开始配置环境(设置转换服务、命令行参数),通知每一个SpringApplicationRunListener环境准备好。
- 打印banner图标,配置应用上下文,初始化每一个ApplicationContextInitializer(调用initialize方法),配置BeanFactory,加载上下文,通知每一个SpringApplicationRunListener上下文加载好。
- 刷新上下文,创建容器中的组件,通知每一个SpringApplicationRunListener应用上下文已经启动,调用容器中所有的ApplicationRunner、CommandLineRunner,通知每一个SpringApplicationRunListener应用上下文已经准备好
- 最后返回应用上下文,如果中途出现异常则会抛出异常并打印日志
测试:
//自定义ApplicationListener
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener onApplicationEvent()被执行...");
}
}
//自定义SpringApplicationRunListener
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
private SpringApplication application;
private String[] args;
public MySpringApplicationRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener starting()被执行...");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener environmentPrepared()被执行...");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener contextPrepared()被执行...");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener contextLoaded()被执行...");
}
@Override
public void started(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("MySpringApplicationRunListener started()被执行...");
}
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
System.out.println("MySpringApplicationRunListener ready()被执行...");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener failed()被执行...");
}
}
//自定义ApplicationContextInitializer
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("MyApplicationContextInitializer initialize()被执行...");
}
}
//自定义ApplicationRunner
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner run()被执行...");
}
}
//自定义CommandLineRunner
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner run()被执行...");
}
}
SpringApplicationRunListener、ApplicationListener、ApplicationContextInitializer都是需要在spring.factories
文件中声明进行创建:
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
com.qingsongxyz.adminserver.listener.MyApplicationContextInitializer
# Application Listeners
org.springframework.context.ApplicationListener=\
com.qingsongxyz.adminserver.listener.MyApplicationListener
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
com.qingsongxyz.adminserver.listener.MySpringApplicationRunListener
MyApplicationListener onApplicationEvent()被执行...
MySpringApplicationRunListener starting()被执行... //1
MyApplicationListener onApplicationEvent()被执行...
MySpringApplicationRunListener environmentPrepared()被执行... //2
MyApplicationContextInitializer initialize()被执行...
MyApplicationListener onApplicationEvent()被执行...
MySpringApplicationRunListener contextPrepared()被执行... //3
MyApplicationListener onApplicationEvent()被执行...
MyApplicationListener onApplicationEvent()被执行...
MyApplicationListener onApplicationEvent()被执行...
MyApplicationListener onApplicationEvent()被执行...
MySpringApplicationRunListener started()被执行... //4
MyApplicationRunner run()被执行...
MyCommandLineRunner run()被执行...
MyApplicationListener onApplicationEvent()被执行...
MyApplicationListener onApplicationEvent()被执行...
MySpringApplicationRunListener ready()被执行... //5
MyApplicationListener onApplicationEvent()被执行...
MyApplicationListener onApplicationEvent()被执行...
十一、拦截器
登录拦截:
@Slf4j
@Controller
public class LoginController {
@Autowired
private HttpSession session;
@GetMapping("/login")
public String login(){
return "/login.html";
}
@PostMapping("/login")
public String login(String username, String password){
log.info("username = " + username + " password = " + password);
//存储登录用户名到session中
session.setAttribute("loginUser", username);
return InternalResourceViewResolver.REDIRECT_URL_PREFIX + "/index.html";
}
}
//定义拦截器
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
//重写preHandle方法,在进入处理器之前拦截判断是否登录
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if(session.getAttribute("loginUser") != null)
{
return true;
}
log.info("拦截的请求:" + request.getRequestURI());
//转发到登录页
request.getRequestDispatcher("/login.html").forward(request, response);
return false;
}
}
//注册拦截器
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有请求,但不包括登录
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/login.html");
}
}
十二、异常处理
ErrorMvcAutoConfiguration自动配置默认错误页面,对于浏览器返回空白页,对于机器客户端返回错误json数
据。
1.自定义错误页面
直接将400.html、500.html放入静态资源路径中即可,之后产生相应的错误码时会跳转到我们自定义的错误页
面。
@RequestMapping("/hello")
public String hello() {
int i = 1 / 0; //人为产生500异常
return "Hello world!";
}
2.自定义全局异常类
//@ControllerAdvice和@ExceptionHandler(填入Throwable的子类)一起使用,捕获该异常
@Slf4j
@ControllerAdvice
public class GlobalException {
@ExceptionHandler({Exception.class})
public String handleException(Exception e){
log.info(e.getMessage()); //日志输出异常信息
return "/error/5xx.html"; //产生异常跳转到500页面
}
}
十三、文件上传下载
<a href="/file/download/1.jpg">下载图片</a>
<hr>
<form action="/file/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files" value="上传多个文件" multiple>
<input type="submit" value="上传">
</form>
@Slf4j
@RestController
@RequestMapping("/file")
public class FileController {
private static String realPath = "D:\\java project\\SpringBoot01\\src\\main\\resources\\static\\upload";
@PostMapping("/upload")
public String upload(MultipartFile[] files, HttpSession session){
log.info(String.valueOf(files.length));
if(files.length != 0)
{
for (MultipartFile file : files) {
File destFile = new File(realPath + "\\" + file.getOriginalFilename());
try {
file.transferTo(destFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
return "文件上传成功!";
}
@GetMapping("/download/{filename}")
public String download(@PathVariable("filename") String filename, HttpServletResponse response, HttpSession session) throws IOException {
//设置响应头,将文件以附件形式输出
response.setHeader("content-disposition", "attachment;filename=" + filename);
String filepath = realPath + "\\" + filename;
//以流的形式对外输出
IOUtils.copy(new FileInputStream(filepath), response.getOutputStream());
return "文件" + filename + "下载成功!";
}
}
十四、数据访问
1.数据源分析
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean
{
private String driverClassName;
private String url;
private String username;
private String password;
......
}
spring:
datasource:
username: root
password: 1234
url: jdbc:mysql://localhost:3306/jdbc
driver-class-name: com.mysql.cj.jdbc.Driver
2.整合druid数据源
导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Druid数据源和配置文件DruidStatProperties、DataSourceProperties绑定
Druid相关配置
spring:
datasource:
username: root
password: 1234
url: jdbc:mysql://localhost:3306/jdbc
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
aop-patterns: com.qingsongxyz.* # 配置Spring监控
filters: 'stat,wall'
stat-view-servlet:
enabled: true # 打开监控统计功能
login-username: admin # 用户名
login-password: admin # 密码
reset-enable: true
web-stat-filter:
enabled: true # Web关联监控配置
filter:
stat:
enabled: true # 开启sql监控
wall:
enabled: true # 开启防火墙
db-type: mysql
config:
delete-allow: false
drop-table-allow: false
3.整合mybatis
导入mybatis启动器
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
编写mapper层:
@Mapper
public interface TeacherMapper {
List<Teacher> getList();
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qingsongxyz.mapper.TeacherMapper">
<select id="getList" resultType="com.qingsongxyz.pojo.Teacher">
select * from teacher
</select>
</mapper>
# 配置xml文件路径
mybatis:
config-location: classpath:mapper/
编写Service层:
public interface TeacherService {
List<Teacher> getList();
}
@Service
public class TeacherServiceImpl implements TeacherService {
@Autowired
private TeacherMapper teacherMapper;
@Override
public List<Teacher> getList() {
return teacherMapper.getList();
}
}
测试:
@Autowired
private TeacherService teacherService;
@RequestMapping("/list")
public List<Teacher> list(){
return teacherService.getList();
}
4.整合mybatis-plus
导入mybatis-plus启动器:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.4</version>
</dependency>
编写mapper层:
@Mapper
public interface TeacherMapper extends BaseMapper<Teacher> {
}
编写Service层:
public interface TeacherService extends IService<Teacher> {
}
@Service
public class TeacherServiceImpl extends ServiceImpl<TeacherMapper, Teacher> implements TeacherService {
}
测试:
@Autowired
private TeacherService teacherService;
@RequestMapping("/sql")
public Teacher sql(){
return teacherService.selectById('1');
}
十五、发送邮件
导入启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
mail:
username: xxx@qq.com # 邮箱
password: xxx # 授权码
host: smtp.qq.com # qq邮件服务器
#开启加密验证
properties:
mail:
smtp:
ssl:
enable: true
debug: true #显示发送邮件详细信息
登录qq邮箱,开启服务,获取授权码:
@RestController
public class MailController {
@Autowired
JavaMailSenderImpl mailSender;
//发送简单邮件
@RequestMapping("/mailMessage")
public String mail(){
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setSubject("springBoot send mail..."); //主题
simpleMailMessage.setText("test mail..."); //正文
simpleMailMessage.setFrom("xxx@qq.com"); //自己的qq邮箱
simpleMailMessage.setTo("xxx@qq.com"); //收件人的qq邮箱
mailSender.send(simpleMailMessage);
return "邮件发送成功!";
}
//发送携带附件的邮件
@RequestMapping("/mailAttachment")
public String mail1(){
MimeMessage mimeMessage = mailSender.createMimeMessage();
try {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("springBoot send mail...");
helper.setText("springBoot send mail and attachment...");
helper.setFrom("xxx@qq.com");
helper.setTo("xxx@qq.com");
helper.addAttachment("favicon.ico", new File("D:\\java project\\SpringBoot01\\src\\main\\resources\\static\\favicon.ico")); //附件
} catch (MessagingException e) {
e.printStackTrace();
}
mailSender.send(mimeMessage);
return "邮件发送成功!";
}
}
测试:
十六、异步任务
同步任务(synchronous):
指的是在主线程上串行执行的那些任务,只有前一个任务执行完毕,才能执行后一个任务
异步任务(asynchronous)::
不用阻塞当前线程等待执行完成,允许后续操作,执行时间长的操作都会使用异步处理
在SpringBoot启动类上添加注解
//开启异步任务
@EnableAsync
@SpringBootApplication
public class SpringBoot01Application {
...
}
public interface AsyncService {
void async();
void sync();
}
//在异步任务方法上加上注解 @Async
@Slf4j
@Service
public class AsyncServiceImpl implements AsyncService {
@Async
@Override
public void async() {
log.info("异步任务开始...");
//休眠5s ---> 模拟进行慢操作
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("异步任务结束...");
}
@Override
public void sync(){
log.info("同步任务开始...");
//休眠5s ---> 模拟进行慢操作
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("同步任务结束...");
}
}
@RestController
public class AsyncController {
@Autowired
private AsyncService asyncService;
@RequestMapping("/sync")
public String sync(){
//模拟慢操作 5s
asyncService.sync();
return "同步任务完成...";
}
@RequestMapping("/async")
public String async(){
//模拟慢操作 5s
asyncService.async();
return "异步任务完成...";
}
}
测试:
十七、定时任务
在SpringBoot启动类上添加注解
//开启定时任务
@EnableScheduling
@SpringBootApplication
public class SpringBoot01Application {
...
}
cron表达式:
cron 表达式是一个字符串,该字符串由 5 个空格分为 6 个部分表示:
秒 分 时 日 月 星期
名称 | 可填值 |
---|---|
秒 | 0-59 - * / |
分 | 0-59 - * / |
小时 | 0-23 - * / |
日期 | 1-31 - * ? / L W |
月份 | 1-12 JAN-DEC - * / |
星期 | 1-7 SUN-SAT - * ? / L # |
符号 | 含义 |
---|---|
* | 表示所有值 |
? | 表示未说明的值,即不关心为何值 |
- | 表示一个指定的范围 |
/ | 表示触发步进(step),/ 前面的值代表初始值(* 等同0 ),后面的值代表偏移量 |
L | 若日期为L,表示当月的最后一天触发;若星期为L,表示星期的最后一天(星期六)触发 |
W | 若日期为W,表示在本月内离当天最近的工作日触发 |
# | 表示具体的周数,# 前面代表星期,# 后面代表本月第几周 |
//在定时任务上加上注解@Scheduled 需要填写属性cron表达式
@Slf4j
@Service
public class ScheduledServiceImpl{
@Autowired
private JavaMailSenderImpl mailSender;
@Scheduled(cron = "0 45 14 * * *")
public void Scheduled() {
log.info("定时任务执行");
}
@Scheduled(cron = "30 45 14 * * *")
public void Scheduled1() {
log.info("定时任务执行");
}
}
十八、整合Swagger 3
导入启动器:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
如果SpringBoot版本为2.6及以上会和Swagger3产生冲突:
# 解决SpringBoot2.6以上版本和Swagger3的冲突问题
spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
测试访问/swagger-ui/index.html:
1.修改Controller文档信息:
@Api(tags = "用户类测试") //设置controller中文标签
@RestController
public class UserController {
@Autowired
private UserService userService;
@ApiOperation("测试获取所有用户") //设置方法中文标签
//设置返回值情况,返回码,返回信息
@ApiResponses({
@ApiResponse(code = 400, message = "参数有误")
})
@RequestMapping("/list")
public List<User> list() {
return userService.list();
}
@ApiOperation("测试通过用户名获取用户")
//设置参数信息,参数中文名,参数类型(query、path...)
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用户名", required = true, paramType = "query", dataTypeClass = String.class)
})
@PostMapping("/user")
public User getUser(String username) {
return userService.getById(username);
}
}
2.修改实体类文档信息
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
//实体类中文标签
@ApiModel("用户实体类")
public class User {
//实体类属性中文标签
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("是否删除")
private Integer deleted;
@ApiModelProperty("创建时间")
private Date createTime;
@ApiModelProperty("修改时间")
private Date updateTime;
private Long version;
}
3.修改小组信息
//默认值
static {
DEFAULT = new ApiInfo("Api Documentation",
"Api Documentation",
"1.0", "urn:tos",
DEFAULT_CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
Swagger配置:
@Configuration
public class SwaggerConfig {
@Autowired
private Environment environment;
@Bean
public Docket createRestApi(){
//获取系统环境,只有dev、test环境才开启Swagger
Profiles profiles = Profiles.of("dev", "test");
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.OAS_30)
.select()
//.paths(PathSelectors.ant("/list/**")) //访问路径过滤
.apis(RequestHandlerSelectors. basePackage("com.qingsongxyz.mybatisplus.controller")) //包过滤
.build()
.apiInfo(createApiInfo())
.enable(flag);
}
@Bean
public ApiInfo createApiInfo() {
//配置个人信息
return new ApiInfo("qingsongxyz Swagger",
"qingsongxyz Api Documentation",
"3.0",
"http:x.x.x.x",
new Contact("qingsongxyz", "http:x.x.x.x", "xxx@qq.com"),
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
}
测试只有开发环境和测试环境启动Swagger:
# application.yml
spring:
profiles:
active: pro # 生产环境
# application-dev.yml 开发环境配置
#spring:
#profiles: dev # SpringBoot2.4以上版本被弃用,推荐使用以下写法
spring:
config:
activate:
on-profile: [dev]
# application-pro.yml 生产环境配置
spring:
config:
activate:
on-profile: [pro]
server:
port: 8081
4.分组
@Configuration
public class SwaggerConfig {
@Autowired
private Environment environment;
//第一组
@Bean
public Docket createRestApi(){
//获取系统环境,只用dev、test环境才开启Swagger
Profiles profiles = Profiles.of("dev", "test");
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.OAS_30)
.groupName("开发1组") //第一组组名
.select()
//.paths(PathSelectors.ant("/list/**")) //访问路径过滤
.apis(RequestHandlerSelectors.basePackage("com.qingsongxyz.mybatisplus.controller")) //包过滤
.build()
.apiInfo(createApiInfo())
.enable(flag);
}
@Bean
public ApiInfo createApiInfo() {
return new ApiInfo("qingsongxyz Swagger",
"qingsongxyz Api Documentation",
"3.0",
"http:x.x.x.x",
new Contact("qingsongxyz", "http:x.x.x.x", "xxx@qq.com"),
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
//--------------------------------------------------------------------
//第二组
@Bean
public Docket createRestApi2(){
//获取系统环境,只用dev、test环境才开启Swagger
Profiles profiles = Profiles.of("dev", "test");
boolean flag = environment.acceptsProfiles(profiles);
return new Docket(DocumentationType.OAS_30)
.groupName("开发2组") //第二组组名
.select()
//.paths(PathSelectors.ant("/list/**")) //访问路径过滤
.apis(RequestHandlerSelectors.basePackage("com.qingsongxyz.mybatisplus.controller")) //包过滤
.build()
.apiInfo(createApiInfo2())
.enable(flag);
}
@Bean
public ApiInfo createApiInfo2() {
return new ApiInfo("Tom Swagger",
"Tom Api Documentation",
"3.0",
"http:x.x.x.x",
new Contact("Tom", "http:x.x.x.x", "xxx@qq.com"),
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList());
}
}
十九、POI、EasyExcel进行Excel读写
导入依赖:
<!--日期工具类-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.14</version>
</dependency>
<!--alibaba Excel-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
EasyExcel是对POI的改进,是在POI的基础上开发的,内部包含POI
1.POI简单数据写入
@SpringBootTest
public class POITest {
//项目路径
private static String PATH;
static {
PATH = System.getProperty("user.dir") + "\\src\\main\\resources\\static\\excel";
}
/**
* 写入Excel 03版本 后缀名为.xls 最大65536行
*
* @throws IOException
*/
@Test
public void write03() throws IOException {
//1.创建工作簿
Workbook workbook = new HSSFWorkbook();
//2.创建表
Sheet sheet = workbook.createSheet("sheet1");
//3.创建行
Row row1 = sheet.createRow(0);
//4.创建单元格并填值
// (1,1)
Cell cell11 = row1.createCell(0);
cell11.setCellValue("姓名");
// (1,1)
Cell cell12 = row1.createCell(1);
cell12.setCellValue("张三");
// (2,1)
Row row2 = sheet.createRow(1);
Cell cell21 = row2.createCell(0);
cell21.setCellValue("年龄");
// (2,2)
Cell cell22 = row2.createCell(1);
cell22.setCellValue("18");
// (3,1)
Row row3 = sheet.createRow(2);
Cell cell31 = row3.createCell(0);
cell31.setCellValue("性别");
// (3,2)
Cell cell32 = row3.createCell(1);
cell32.setCellValue("男");
FileOutputStream outputStream = new FileOutputStream(PATH + "\\" + "excel03.xls");
workbook.write(outputStream);
outputStream.close();
System.out.println("写入excel03.xls文件成功!");
}
/**
* 写入Excel 07版本 后缀名为.xlsx 没有容量限制
*
* @throws IOException
*/
@Test
public void write07() throws IOException {
//1.创建工作簿
Workbook workbook = new XSSFWorkbook();
//2.创建表
Sheet sheet = workbook.createSheet("sheet1");
//3.创建行
Row row1 = sheet.createRow(0);
//4.创建单元格并填值
// (1,1)
Cell cell11 = row1.createCell(0);
cell11.setCellValue("姓名");
// (1,1)
Cell cell12 = row1.createCell(1);
cell12.setCellValue("张三");
// (2,1)
Row row2 = sheet.createRow(1);
Cell cell21 = row2.createCell(0);
cell21.setCellValue("年龄");
// (2,2)
Cell cell22 = row2.createCell(1);
cell22.setCellValue("18");
// (3,1)
Row row3 = sheet.createRow(2);
Cell cell31 = row3.createCell(0);
cell31.setCellValue("性别");
// (3,2)
Cell cell32 = row3.createCell(1);
cell32.setCellValue("男");
FileOutputStream outputStream = new FileOutputStream(PATH + "\\" + "excel07.xlsx");
workbook.write(outputStream);
outputStream.close();
System.out.println("写入excel07.xlsx文件成功!");
}
}
2.POI写入大数据测试效率
/**
* 写满65535行 Excel 03版本 测试效率 超出行数报错
*
* @throws IOException
*/
@Test
public void writeBigData03() throws IOException {
long start = System.currentTimeMillis();
//1.创建工作簿
Workbook workbook = new HSSFWorkbook();
//2.创建表
Sheet sheet = workbook.createSheet("sheet1");
//写入65536行数据(写满所有行)
for (int rowNum = 0; rowNum < 65536; rowNum++) {
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 5; cellNum++) {
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum);
}
}
FileOutputStream outputStream = new FileOutputStream(PATH + "\\" + "excel03BigData.xls");
workbook.write(outputStream);
outputStream.close();
long end = System.currentTimeMillis();
System.out.println("写入excel03BigData.xls文件成功,用时: " + (double) (end - start) / 1000);
}
/**
* 写100000行 Excel 07版本 测试效率
*
* @throws IOException
*/
@Test
public void writeBigData07() throws IOException {
long start = System.currentTimeMillis();
//1.创建工作簿
Workbook workbook = new XSSFWorkbook();
//2.创建表
Sheet sheet = workbook.createSheet("sheet1");
//写入10万行数据
for (int rowNum = 0; rowNum < 100_000; rowNum++) {
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 5; cellNum++) {
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum);
}
}
FileOutputStream outputStream = new FileOutputStream(PATH + "\\" + "excel07BigData.xlsx");
workbook.write(outputStream);
outputStream.close();
long end = System.currentTimeMillis();
System.out.println("写入excel07BigData.xlsx文件成功,用时: " + (double) (end - start) / 1000);
}
/**
* 写100000行 Excel 03版本 测试效率
*
* @throws IOException
*/
@Test
public void writeBigData07Plus() throws IOException {
long start = System.currentTimeMillis();
//1.创建工作簿
Workbook workbook = new SXSSFWorkbook();
//2.创建表
Sheet sheet = workbook.createSheet("sheet1");
//写入10万行数据
for (int rowNum = 0; rowNum < 100_000; rowNum++) {
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 5; cellNum++) {
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum);
}
}
FileOutputStream outputStream = new FileOutputStream(PATH + "\\" + "excel07BigDataPlus.xlsx");
workbook.write(outputStream);
outputStream.close();
//清除缓存文件
((SXSSFWorkbook)workbook).dispose();
long end = System.currentTimeMillis();
System.out.println("写入excel07BigDataPlus.xlsx文件成功,用时: " + (double) (end - start) / 1000);
}
/*
HSSFWorkbook:写入65536行 用时2.296s 超出65536行报错
XSSFWorkbook:写入100000行 用时6.159s
SXSSFWorkbook:写入100000行 用时1.229s 效率更高
*/
3.POI读取
//需要校验每个单元格的数据类型
@Test
public void read() throws IOException {
FileInputStream inputStream = new FileInputStream(PATH + "\\" + "学生信息03.xls");
Workbook workbook = new HSSFWorkbook(inputStream);
//获取第一表
Sheet sheet = workbook.getSheetAt(0);
//获取表的行数
int rows = sheet.getPhysicalNumberOfRows();
for (int i = 0; i < rows; i++) {
Row row = sheet.getRow(i);
//获取每一行的单元格数
int cells = row.getPhysicalNumberOfCells();
for (int j = 0; j < cells; j++) {
Cell cell = row.getCell(j);
if(cell != null)
{
switch (cell.getCellType())
{
case NUMERIC :
System.out.print("[NUMERIC]");
System.out.print(cell.getNumericCellValue() + "\t");
break;
case STRING:
System.out.print("[STRING]");
System.out.print(cell.getStringCellValue() + "\t");
break;
case BLANK:
System.out.print("[BLANK]");
break;
case BOOLEAN:
System.out.print("[BOOLEAN]");
System.out.print(cell.getBooleanCellValue() + "\t");
break;
case ERROR:
System.out.print("[ERROR]");
break;
}
}
}
System.out.println();
}
inputStream.close();
}
/*
[STRING]姓名 [STRING]年龄 [STRING]性别 [STRING]学号 [STRING]学院 [STRING]班级 [STRING]专业 [STRING]学费 [STRING]是否完成学业
[STRING]张三 [NUMERIC]18.0 [STRING]男 [NUMERIC]1001.0 [STRING]计算机学院 [NUMERIC]1901.0 [STRING]网路工程 [NUMERIC]5199.5 [BOOLEAN]true
[STRING]李四 [NUMERIC]20.0 [STRING]男 [NUMERIC]1002.0 [STRING]商学院 [NUMERIC]1803.0 [STRING]金融 [NUMERIC]4725.3 [BOOLEAN]false
[STRING]王五 [NUMERIC]21.0 [STRING]男 [NUMERIC]1003.0 [STRING]文学院 [NUMERIC]2101.0 [STRING]中文 [NUMERIC]3947.8 [BOOLEAN]true
*/
4.EasyExcel简单数据写入
创建实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
@ContentRowHeight(20) //设置Excel中所有内容行(除去表头的所有行)的行高
public class Book {
@TableId(type = IdType.ASSIGN_ID)
@ExcelIgnore //该字段不写入Excel
private String id;
@TableField(value = "bookName")
@ExcelProperty(value = "书名") //该字段在Excel表头中对应的中文
private String bookName;
@ExcelProperty(value = "作者")
private String author;
@ColumnWidth(20)
@ExcelProperty(value = "图片")
private String image;
@ExcelProperty(value = "价格")
private double price;
@ColumnWidth(100) //设置在Excel中的列宽
@ExcelProperty(value = "介绍")
private String description;
}
@SpringBootTest
public class EasyExcelTest {
//项目路径
private static String PATH;
static {
PATH = System.getProperty("user.dir") + "\\src\\main\\resources\\static\\excel";
}
@Test
public void write(){
ExcelWriterBuilder writeWorkBook = EasyExcel.write(PATH + "\\" + "easyExcel.xlsx", Book.class);
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
sheet.doWrite(initBook()); //写入函数
System.out.println("写入easyExcel.xlsx文件成功!");
}
//写入excel的数据
private List<Book> initBook(){
ArrayList<Book> bookList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Book book = new Book();
book.setId(String.valueOf(i));
book.setBookName("book" + i);
book.setAuthor("author" + i);
book.setImage("/image" + i);
DecimalFormat df = new DecimalFormat("#.00");
book.setPrice(Double.parseDouble(df.format(new Random().nextDouble() * 10)));
book.setDescription("description" + i);
bookList.add(book);
}
return bookList;
}
}
5.EasyExcel读取
创建事件监听器:
public class BookListener extends AnalysisEventListener<Book> {
//每成功读取一行调用
@Override
public void invoke(Book book, AnalysisContext analysisContext) {
bookList.add(book);
}
//所有的数据读取完毕调用
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("完成Excel读取...");
}
}
@Test
public void read(){
ExcelReaderBuilder readWorkBook = EasyExcel.read(PATH + "\\" + "easyExcel.xlsx", Book.class, new BookListener());
ExcelReaderSheetBuilder sheet = readWorkBook.sheet();
sheet.doRead(); //读取函数
}
6.Web导入Excel插入数据
导入fastjson
:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
BookMapper:
public interface BookMapper extends BaseMapper<Book> {
}
BookService:
public interface BookService extends IService<Book> {
}
BookServiceImpl:
@Service
public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements BookService {
}
修改事件监听器,添加数据库操作:
public class BookListener extends AnalysisEventListener<Book> {
private BookService bookService;
private List<Book> bookList = new ArrayList<>();
public BookListener() {
}
//构造注入bookService
public BookListener(BookService bookService) {
this.bookService = bookService;
}
@Override
public void invoke(Book book, AnalysisContext analysisContext) {
bookList.add(book);
System.out.println(book);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("完成Excel读取...");
if(bookService != null)
{
//批处理插入书籍 书籍的id由MybatisPlus雪花算法自动生成
boolean b = bookService.saveBatch(bookList);
System.out.println(b ? "更新数据库成功!" : "更新数据库失败!!!");
}
}
}
生成的Excel模版自适应列宽:
public class CustomCellWriteWidthConfig extends AbstractColumnWidthStyleStrategy {
private Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
Map<Integer, Integer> maxColumnWidthMap = CACHE.get(writeSheetHolder.getSheetNo());
if (maxColumnWidthMap == null) {
maxColumnWidthMap = new HashMap<>();
CACHE.put(writeSheetHolder.getSheetNo(), maxColumnWidthMap);
}
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
if (columnWidth >= 0) {
if (columnWidth > 254) {
columnWidth = 254;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
Sheet sheet = writeSheetHolder.getSheet();
sheet.setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
}
/**
* 计算长度
* @param cellDataList
* @param cell
* @param isHead
* @return
*/
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
CellData cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch (type) {
case STRING:
// 换行符
int index = cellData.getStringValue().indexOf("\n");
return index != -1 ?
cellData.getStringValue().substring(0, index).getBytes().length + 1 : cellData.getStringValue().getBytes().length + 1;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
ExcelController:
@RestController
@RequestMapping("/excel")
public class ExcelController {
@Autowired
private BookService bookService;
//导入Excel
@PostMapping("/upload")
public String upload(@RequestParam("files") MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), Book.class, new BookListener(bookService)).autoCloseStream(Boolean.TRUE).sheet().doRead();
return "读取成功!";
}
//下载Excel模版
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
try {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("书籍信息表模版", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 这里需要设置不关闭流
EasyExcel.write(response.getOutputStream(), Book.class)
.registerWriteHandler(new CustomCellWriteWidthConfig())
.autoCloseStream(Boolean.FALSE)
.autoTrim(Boolean.TRUE)
.sheet("书籍信息表模版")
.doWrite(initBook());
} catch (Exception e) {
// 重置response
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = MapUtils.newHashMap();
map.put("status", "failure");
map.put("message", "下载文件失败" + e.getMessage());
response.getWriter().println(JSON.toJSONString(map));
}
}
//模版样例数据
private List<Book> initBook(){
ArrayList<Book> bookList = new ArrayList<>();
Book book = new Book();
book.setId(String.valueOf(1));
book.setBookName("西游记");
book.setAuthor("吴承恩");
book.setImage("/image/西游记.jpg");
DecimalFormat df = new DecimalFormat("#.00");
book.setPrice(Double.parseDouble(df.format(new Random().nextDouble() * 10)));
book.setDescription("《西游记》是中国古代第一部浪漫主义章回体长篇神魔小说。全书主要描写了孙悟空出世及大闹天宫后,遇见了唐僧、猪八戒、沙僧和白龙马,西行取经,一路上历经艰险,降妖除魔,经历了九九八十一难,终于到达西天见到如来佛祖,最终五圣成真的故事。");
bookList.add(book);
return bookList;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>welcome</title>
</head>
<body>
<h1>Welcome</h1>
<a href="/excel/download">下载excel模版</a>
<hr>
<form action="/excel/upload" method="post" enctype="multipart/form-data">
<input type="file" name="files" value="上传多个文件" multiple>
<input type="submit" value="上传">
</form>
</body>
</html>
导入Excel:
二十、Actuator指标监控
1.介绍
Actuator是Springboot提供的用来对应用系统进行监控的功能模块,可帮助您在将应用程序推送到生产环境时
对其进行监控和管理,可以选择使用 HTTP 或 JMX 来管理和监视您的应用程序,审计、健康和指标收集也可
以自动应用于您的应用程序。
Endpoints(端点):可以监视应用程序并与之交互 ,Spring Boot 包含许多内置端点,并允许添加自定义端点。
可以启用或禁用每个单独的端点并通过 HTTP 或 JMX 暴露它们(使它们可远程访问)。 当端点被启用和公
开时,它被认为是可用的。
2.配置Endpoints
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Http访问:
JMX(Jconsole访问):
常用端点信息及开启状态:
ID | 描述 | JMX | Web |
---|---|---|---|
beans | 显示应用程序中所有 Spring bean 的完整列表。 | √ | × |
caches | 公开可用的缓存。 | √ | × |
conditions | 显示在配置和自动配置类上评估的条件以及它们匹配或不匹配的原因。 | √ | × |
configprops | 显示所有的整理列表 @ConfigurationProperties . | √ | × |
env | 暴露 Spring 的属性 ConfigurableEnvironment . | √ | √ |
health | 显示应用程序运行状况信息。 | √ | × |
httptrace | 显示 HTTP 跟踪信息(默认情况下,最近 100 个 HTTP 请求-响应交换)。 | √ | × |
info | 显示任意应用程序信息。 | √ | × |
loggers | 显示和修改应用程序中记录器的配置。 | √ | × |
metrics | 显示当前应用程序的“指标”信息。 | √ | × |
mappings | 显示所有的整理列表 @RequestMapping 路径。 | √ | × |
scheduledtasks | 显示应用程序中的计划任务。 | √ | × |
sessions | 允许从 Spring Session 支持的会话存储中检索和删除用户会话。 需要使用 Spring Session 的基于 servlet 的 Web 应用程序。 | √ | × |
shutdown | 让应用程序正常关闭。 默认禁用。 | √ | × |
startup | 显示由 ApplicationStartup . 需要 SpringApplication 配置一个 BufferingApplicationStartup . | √ | × |
threaddump | 执行线程转储。 | √ | × |
heapdump | 返回一个堆转储文件。 在 HotSpot JVM 上,一个 HPROF - 格式文件被返回。 在 OpenJ9 JVM 上,一个 PHD - 格式文件被返回。 | 不适用 | × |
logfile | 返回日志文件的内容(如果 logging.file.name 或者 logging.file.path 属性已设置)。 支持使用HTTP Range 标头检索部分日志文件的内容。 | 不适用 | × |
management:
endpoints:
web:
exposure:
include: '*' # web方式暴露所有端点
自定义健康指标:
实现HealthIndicator接口,实现getHealth方法:
@Component
public class MyDIYHealthIndicator implements HealthIndicator {
@Override
public Health health() {
//模拟自定义健康标准
boolean flag = true;
Map<String, Object> details = new HashMap<>();
Health health;
if (flag) {
details.put("code", "200");
details.put("status", "success");
//返回携带健康信息的对象
health = Health.up().withDetails(details).build();
} else {
details.put("code", "500");
details.put("status", "failure");
//返回携带无法服务信息的对象
health = Health.outOfService().withDetails(details).build();
}
//返回相应的健康对象
return health;
}
}
继承AbstractHealthIndicator抽象类,重写doHealthCheck方法:
@Component
public class MyDIYHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//模拟自定义健康标准
boolean flag = true;
Map<String, String> details = new HashMap<>();
if(flag)
{
//返回健康状态
//builder.up();
builder.status(Status.UP);
details.put("code", "200");
details.put("status", "success");
}else {
//返回无法服务状态
//builder.down();
builder.status(Status.OUT_OF_SERVICE);
details.put("code", "500");
details.put("status", "failure");
}
//返回相应信息
builder.withDetails(details);
}
}
自定义info信息:
实现InfoContributor接口,返回info信息
@Component
public class MyInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("appName", "SpringBoot01")
.withDetail("appVersion", "1.0.0");
}
}
自定义端点Endpoint:
Operation | HTTP method |
---|---|
@ReadOperation | GET |
@WriteOperation | POST |
@DeleteOperation | DELETE |
@Component
@Endpoint(id = "myService") //标识端点的名称
public class MyEndpoint {
@ReadOperation //响应get请求
public Map startService(){
return Collections.singletonMap("info","start service...");
}
@WriteOperation //响应post请求
public void stopService(){
System.out.println("stop service...");
}
}
3.Boot Admin Server
前端显示监控信息,参考官方文档:https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started
另外创建一个Springboot应用作为服务器
导入依赖:
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在启动类上添加@EnableAdminServer注解开启监控功能
@EnableAdminServer //开启监控功能
@SpringBootApplication
public class AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServerApplication.class, args);
}
}
在被监控的应用中添加依赖:
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.6.7</version>
</dependency>
spring:
boot:
admin:
client:
url : http://localhost:8080 # 服务器的ip地址
instance:
prefer-ip: true # ip地址作为URL(默认为主机名连接不上服务器)
application:
name: SpringBoot01
management:
endpoints:
enabled-by-default: true # 关闭所有端点
web:
exposure:
include: '*' # web方式暴露所有端点
被监控的应用信息: