SpringBoot 默认支持以下路径的文件作为 web 静态资源。
By default, Spring Boot serves static content from a directory called
/static
(or/public
or/resources
or/META-INF/resources
) in the classpath or from the root of theServletContext
.
Developing Web Applications ➡Spring MVC Framework➡Static Content
org.springframework.web.servlet.resource.ResourceHttpRequestHandler
serves static resources optimized for superior browser performance (according to the guidelines of Page Speed, YSlow, etc.)
org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
Stores registrations of resource handlers for serving static resources such as images, css files and others through Spring MVC including setting cache headers optimized for efficient loading in a web browser.
org.springframework.boot.autoconfigure.web.ResourceProperties
org.springframework.boot.autoconfigure.web.WebMvcProperties
自动配置入口:org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter
配置实现:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
// 此处的cache 是指 HTTP协议的 Cache-Control
Integer cachePeriod = this.resourceProperties.getCachePeriod();
// 默认:/**, spring.mvc.static-path-pattern
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(
registry.addResourceHandler(staticPathPattern)
// 默认: [/META-INF/resources/, /resources/, /static/, /public/]
.addResourceLocations(this.resourceProperties.getStaticLocations())
.setCachePeriod(cachePeriod));
}
}
# relocating all resources to /resources/**
spring.mvc.static-path-pattern=/resources/**
静态资源目录配置
spring.web.resources.static-locations=/webjars/**
<mvc:default-servlet-handler/>
<mvc:resources location="/static/" mapping="/static/**" cache-period="864000"/>
/**
- 查找占位符匹配的后缀索引。
- 因为Spring支持嵌套的占位符表示,所以配对的查找是这个方法核心要解决的
- 逻辑:遍历字符串buf,匹配遇到的占位符前缀和后缀,如果是配套的后缀,则返回索引。
- 因为有嵌套占位符的情况,需要一个临时的变量记录内嵌占位符的出现次数,通过成对匹配的计算(出现前缀加1,出现后缀减1),防止错误返回内嵌占位符的后缀索引。
- @param buf 配置字符串,比如:${foo:${defaultFoo}}
- @param startIndex 占位符在 buf中的index,初始值 = buf.indexOf("${");
*/
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + this.placeholderPrefix.length();
// 嵌套的占位符出现次数标识变量,出现前缀加1,出现后缀减1。
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
// 检测后缀,两种情况
if (StringUtils.substringMatch(buf, index, this.placeholderSuffix)) {
// 1. 该后缀属于内嵌占位符,继续遍历
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + this.placeholderSuffix.length();
}
// 2. 目标占位符的后缀
else {
return index;
}
}
// 检测前缀,证明遇到的了内嵌占位符
else if (StringUtils.substringMatch(buf, index, this.simplePrefix)) {
withinNestedPlaceholder++;
index = index + this.simplePrefix.length();
}
else {
index++;
}
}
return -1;
}
/**
* Test whether the given string matches the given substring
* at the given index.
* 思路:以被查询的字符串,循环匹配每个字符,遇到不符合直接返回不匹配。
* 匹配查找和数据库的join 查询,小表驱动大表有异曲同工之处
* @param str the original string (or StringBuilder)
* @param index the index in the original string to start matching against
* @param substring the substring to match at the given index
*/
public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {
// 遍历 toFind 字符串
for (int j = 0; j < substring.length(); j++) {
int i = index + j;
// 如果原字符串不包含(长度不够)或者字符不匹配,立即失败
if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
return false;
}
}
return true;
}
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name ‘fooController’:
Failed to introspect bean class [com.foo.FooController] for lookup method metadata: could not find class that it depends on;
nested exception is java.lang.NoClassDefFoundError: com/foo/Result
// 该方法的返回类型 ResponseResult extend Result
@RequestMapping("/getList")
public ResponseResult<Foo> getList(@RequestBody Query params){
//...
}
根据错误提示,在代码中并没有找到 Result 类直接使用。
再进一步看日志,introspect bean 出现的问题,推测是进行反射的操作异常。
通过查看方法的返回类型,其父类就是Result,但是在当前的工程中,并没有引入对应的maven 依赖。
结果就是编译通过,但是运行时异常。
引入需要的jar 到当前的工程(classpath)。
上述提到的introspect bean 过程,在Spring 中的相关源码
// org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName) {
// Let's check for lookup methods here..
if (!this.lookupMethodsChecked.contains(beanName)) {
try {
// Spring 初次缓存beanClass 的相关method。
ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) {
// ...
}
});
}
// 本次的异常打印日志
catch (NoClassDefFoundError err) {
throw new BeanCreationException(beanName, "Failed to introspect bean class [" + beanClass.getName() +
"] for lookup method metadata: could not find class that it depends on", err);
}
this.lookupMethodsChecked.add(beanName);
}
}
应用为了数据加密,引用了一个加密组件。使用的Maven 依赖,启动应用后,直接报错:
java.lang.NoClassDefFoundError: Could not initialize class com.foo.security.handle.AcesCipherText
一开始错误理解为
ClassNotFoundException
,以为是包下载、编译的问题,反复clean,update snapshot。后来才发现是 ClassLoader 试图加载类的定义时出现错误,是在加载到类文件之后发生的。
public final class AcesCipherTextHandle<E> extends BaseTypeHandler<E> {
public AcesPlainTextHandle() {
}
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
return AcesCipherTextService.getIns().getResult(rs, columnName);
}
static {
// 没有找到对应的配置文件,导致必要的加密参数为空,直接 throw RuntimeException。结果是Class加载失败。
// 发生在Class.forName() 调用过程中
LoadProperties.loadProps();
}
}
A class initialization block is a block of statements preceded by the
static
keyword that’s introduced into the class’s body.When the class loads, these statements are executed.
并发编程在实际生产应用中,如果使用不当,会有意想不到的诡异问题。
文章摘取一段Java AtomicLong 在统计功能上的使用,分析并发计数应该注意的点。
/**
* jetty 服务中,需要统计全局的连接数、请求数量之类的指标。提供了CounterStatistic 类用于统计这些数据。
* 其中,Statistic 就是借助AtomicLong 来实现total, maximum 数量的并发计数。
*
* @from org.eclipse.jetty.util.statistic.CounterStatistic
*/
public class CounterStatistic {
protected final AtomicLong _max = new AtomicLong();
protected final AtomicLong _curr = new AtomicLong();
/**
* 计数增加的操作,同时会更新max 的值。
* @param delta the amount to add to the count,传入负值就是减少操作
*/
public void add(final long delta) {
long value=_curr.addAndGet(delta);
// max 赋值并没有使用 AtomicLong#getAndSet, why?
// 因为在并发环境下,只有确定当前的max 是真正的最大值时,才能赋值到max 变量中。
long oldValue = _max.get();
// case1:如果第一次 value <= oldValue,那么直接跳过,max 维持不变。
// case2: 如果第一次 value > oldValue,但是因为并发问题,没有compareAndSet 成功。再比较时,value <= oldValue,则直接跳过。需要考虑重试赋值过程中并发的问题。
while (value > oldValue) {
// 如果赋值成功,则说明value 是此刻的最大值。
if (_max.compareAndSet(oldValue, value))
break;
oldValue = _max.get();
}
}
}
CAS 全称是 compare and swap
,是一种用于在多线程环境下实现同步功能的机制。
CAS 是一条 CPU 的原子指令( cmpxchg 指令),不会造成数据不一致问题。Java Unsafe提供的 CAS 方法(如 compareAndSwapXXX )底层实现即为 CPU 指令 cmpxchg。
Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力。