背景
最近为了给kafka加性能指标采集功能,调研后发现spring-kafka在2.3版本之后就自带了Micrometer指标采集功能。但是当前项目的spring-boot版本是2.0.2.RELEASE,对应的spring-kafka版本是2.1.6.RELEASE,所以准备将spring-boot版本升级到2.7.18,这是2.x系列的最高版本,对应的spring-kafka版本是2.8.11。
版本升级
module | 升级前version | 升级后version |
---|---|---|
spring-boot | 2.0.2.RELEASE | 2.7.18 |
spring-webmvc | 5.0.6.RELEASE | 5.3.31 |
spring-kafka | 2.1.6.RELEASE | 2.8.11 |
不兼容的地方
Spring boot
2.6版本开始默认禁用Bean的循环依赖
项目启动会检测是否存在循环依赖,存在就报如下错误。
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
collectionController (field private ***.biz.manager.CollectionManager ***.web.controller.CollectionController.collectionManager)
┌─────┐
| collectionManagerImpl (field private ***.biz.manager.FunnyManager ***.biz.manager.impl.CollectionManagerImpl.funnyManager)
↑ ↓
| funnyManagerImpl (field private ***.biz.manager.CollectionManager ***.biz.manager.impl.FunnyManagerImpl.collectionManager)
└─────┘
Action:
Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
2.6版本在org.springframework.boot.SpringApplication类里增加了allowCircularReferences属性来控制循环依赖是否允许,默认值是false。
private boolean allowCircularReferences;
/**
* Sets whether to allow circular references between beans and automatically try to
* resolve them. Defaults to {@code false}.
* @param allowCircularReferences if circular references are allowed
* @since 2.6.0
* @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean)
*/
public void setAllowCircularReferences(boolean allowCircularReferences) {
this.allowCircularReferences = allowCircularReferences;
}
所以,要保持和2.6版本之前行为一样的话,就把allowCircularReferences属性设置为true。设置可以添加配置spring.main.allow-circular-references=true,或通过SpringApplication
或 SpringApplicationBuilder
对象直接设置属性。
2.1版本禁用Bean覆盖
当出现同名bean时,会判断是否允许覆盖beanDefinition,不允许则抛出BeanDefinitionOverrideException异常。实现逻辑如下:
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
if (!isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
// ...
this.beanDefinitionMap.put(beanName, beanDefinition);
}
2.1版本在org.springframework.boot.SpringApplication类里增加了allowBeanDefinitionOverriding属性来控制是否允许bean覆盖,默认值是false。
private boolean allowBeanDefinitionOverriding;
/**
* Sets if bean definition overriding, by registering a definition with the same name
* as an existing definition, should be allowed. Defaults to {@code false}.
* @param allowBeanDefinitionOverriding if overriding is allowed
* @since 2.1.0
* @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding(boolean)
*/
public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) {
this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding;
}
所以,要和老版本兼容的话,就把allowBeanDefinitionOverriding属性设置为true。设置可以添加配置spring.main.allow-bean-definition-overriding=true,或通过SpringApplication
对象直接设置属性。
默认的路径匹配策略改成了PATH_PATTERN_PARSER
2.6版本之前默认策略是ANT_PATH_MATCHER,改成PATH_PATTERN_PARSER会遇到IllegalArgumentException错误。
java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".
解决方案是将策略回滚到ANT_PATH_MATCHER:
**spring.mvc.pathmatch.matching-strategy=***ANT_PATH_MATCHER*
Spring webmvc
Cors不允许将allowedOrigins设置为*
原来为了方便,会将跨域的请求来源设置为代表允许来自所有host的请求。5.3开始增加了allowedOrigins值的校验,不允许为,否则抛出IllegalArgumentException异常。
/**
* Validate that when {@link #setAllowCredentials allowCredentials} is {@code true},
* {@link #setAllowedOrigins allowedOrigins} does not contain the special
* value {@code "*"} since in that case the "A***ess-Control-Allow-Origin"
* cannot be set to {@code "*"}.
* @throws IllegalArgumentException if the validation fails
* @since 5.3
*/
public void validateAllowCredentials() {
if (this.allowCredentials == Boolean.TRUE &&
this.allowedOrigins != null && this.allowedOrigins.contains(ALL)) {
throw new IllegalArgumentException(
"When allowCredentials is true, allowedOrigins cannot contain the special value \\"*\\" " +
"since that cannot be set on the \\"A***ess-Control-Allow-Origin\\" response header. " +
"To allow credentials to a set of origins, list them explicitly " +
"or consider using \\"allowedOriginPatterns\\" instead.");
}
}
另外,5.3增加了allowedOriginPatterns属性来代替allowedOrigins的功能。所以,要允许所有host的跨域请求的话,把allowedOriginPatterns设置为*。
@Configuration
public class CorsConfig implements WebMv***onfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许跨域访问的路径
.allowedOriginPatterns("*") // 允许跨域访问的源
}
}
/**
* Alternative to {@link #setAllowedOrigins} that supports more flexible
* origins patterns with "*" anywhere in the host name in addition to port
* lists. Examples:
* <ul>
* <li>{@literal https://*.domain1.***} -- domains ending with domain1.***
* <li>{@literal https://*.domain1.***:[8080,8081]} -- domains ending with
* domain1.*** on port 8080 or port 8081
* <li>{@literal https://*.domain1.***:[*]} -- domains ending with
* domain1.*** on any port, including the default port
* </ul>
* <p>In contrast to {@link #setAllowedOrigins(List) allowedOrigins} which
* only supports "*" and cannot be used with {@code allowCredentials}, when
* an allowedOriginPattern is matched, the {@code A***ess-Control-Allow-Origin}
* response header is set to the matched origin and not to {@code "*"} nor
* to the pattern. Therefore allowedOriginPatterns can be used in ***bination
* with {@link #setAllowCredentials} set to {@code true}.
* <p>By default this is not set.
* @since 5.3
*/
public CorsConfiguration setAllowedOriginPatterns(@Nullable List<String> allowedOriginPatterns) {
if (allowedOriginPatterns == null) {
this.allowedOriginPatterns = null;
}
else {
this.allowedOriginPatterns = new ArrayList<>(allowedOriginPatterns.size());
for (String patternValue : allowedOriginPatterns) {
addAllowedOriginPattern(patternValue);
}
}
return this;
}
静态文件是否存在的判断方式变了
5.3版本开始,ClassPathResource类型的资源文件,判断是否可读的isReadable()方法的逻辑改成了文件存在且内容不为空。当我们访问一个内容为空的资源文件时,spring返回404。
例如,访问http://localhost:8080/hello.html,spring会在/META-INF/resource、resources、static、public这几个目录下查找hello.html。如果文件放在static文件夹下,实际查找的是/static/hello.html文件。如果是jar包里,则完整的路径是这样jar:file:/opt/apps/demo.jar!/BOOT-INF/classes!/static/hello.html。
然后我们看看5.3前后版本代码,对这个文件是否可读判断的差异。
5.3版本之前,jar开头的文件直接返回true。
// 5.3之前
@Override
public boolean isReadable() {
try {
URL url = getURL();
// file/vfsfile/vfs开头的url
if (ResourceUtils.isFileURL(url)) {
// Proceed with file system resolution
File file = getFile();
return (file.canRead() && !file.isDirectory());
}
else {
return true;
}
}
catch (IOException ex) {
return false;
}
}
5.3版本开始,jar开头的文件会通过con.getContentLengthLong()获取文件长度,如果是0的话就返回false。
@Override
public boolean isReadable() {
URL url = resolveURL();
return (url != null && checkReadable(url));
}
boolean checkReadable(URL url) {
try {
// file/vfsfile/vfs开头的url
if (ResourceUtils.isFileURL(url)) {
// Proceed with file system resolution
File file = getFile();
return (file.canRead() && !file.isDirectory());
}
else {
// Try InputStream resolution for jar resources
URLConnection con = url.openConnection();
customizeConnection(con);
if (con instanceof HttpURLConnection) {
HttpURLConnection httpCon = (HttpURLConnection) con;
httpCon.setRequestMethod("HEAD");
int code = httpCon.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
httpCon.disconnect();
return false;
}
}
long contentLength = con.getContentLengthLong();
if (contentLength > 0) {
return true;
}
else if (contentLength == 0) {
// Empty file or directory -> not considered readable...
return false;
}
else {
// Fall back to stream existence: can we open the stream?
getInputStream().close();
return true;
}
}
}
catch (IOException ex) {
return false;
}
}
所以,5.3开始,静态文件不能是空文件,否则会返回404。
RequestMappingInfo#getPatternsCondition()返回null
5.3开始新增了pathPatternsCondition属性,它和patternsCondition是互斥的,所以getPatternsCondition()可能会返回null了。可以通过getActivePatternsCondition()方法获取RequestCondition对象:
/**
* Returns either {@link #getPathPatternsCondition()} or
* {@link #getPatternsCondition()} depending on which is not null.
* @since 5.3
*/
@SuppressWarnings("unchecked")
public <T> RequestCondition<T> getActivePatternsCondition() {
if (this.pathPatternsCondition != null) {
return (RequestCondition<T>) this.pathPatternsCondition;
}
else if (this.patternsCondition != null) {
return (RequestCondition<T>) this.patternsCondition;
}
else {
// Already checked in the constructor...
throw new IllegalStateException();
}
}
引用
Bean循环引用:Spring Boot 2.6 Release Notes · spring-projects/spring-boot Wiki · GitHub
BeanDefinition覆盖:Spring Boot 2.1 Release Notes · spring-projects/spring-boot Wiki · GitHub
Spring-kafka监控文档:Spring for Apache Kafka