SpringBoot AOP演示项目-日志篇
源码地址 springboot-aop
一句话简介知识点
Spring AOP面向切面编程,可以用来配置 事务、做日志、权限验证、在用户请求时做一些处理等等
CommandLineRunner,实现在项目启动后执行的功能可以用来做 预先数据的加载
面向切面编程主要知识点
- 执行点(Executepoint) - 类初始化,方法调用。
- 连接点(Joinpoint) - 执行点+方位的组合,可确定Joinpoint,比如类开始初始化前,类初始化后,方法调用前,方法调用后。
- 切点(Pointcut) - 在众多执行点中,定位感兴趣的执行点。Executepoint相当于数据库表中的记录,而Pointcut相当于查询条件。
- 增强(Advice) - 织入到目标类连接点上的一段程序代码。除了一段程序代码外,还拥有执行点的方位信息。
- 目标对象(Target) - 增强逻辑的织入目标类
- 引介(Introduction) - 一种特殊的增强(advice),它为类添加一些额外的属性和方法,动态为业务类添加其他接口的实现逻辑,让业务类成为这个接口的实现类。
- 代理(Proxy) - 一个类被AOP织入后,产生一个结果类,它便是融合了原类和增强逻辑的代理类。
- 切面(Aspect) - 切面由切点(Pointcut)和增强(Advice/Introduction)组成,既包括横切逻辑定义,也包括连接点定义。
AOP工作重点:
- 如何通过切点(Pointcut)和增强(Advice)定位到连接点(Jointpoint)上;
- 如何在增强(Advice)中编写切面的代码。
主要注解
- @Pointcut切点注解
- @Before 在切点方法之前执行
- @After 在切点方法之后执行
- @AfterReturning 切点方法返回后执行
- @AfterThrowing 切点方法抛异常执行
- @Around 属于环绕增强,能控制切点执行前,执行后,,用这个注解后,程序抛异常,会影响@AfterThrowing这个注解
执行前后顺序是这样
- @Around环绕通知
- @Before通知执行
- @Before通知执行结束
- @Around环绕通知执行结束
- @After后置通知执行了!
- @AfterReturning第一个后置返回通知的返回值:18
参考文档:
logback配置
logback-spring.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<!-- scan 配置文件如果发生改变,将会被重新加载 scanPeriod 检测间隔时间-->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<contextName>logback</contextName>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- 日志存储级别 -->
<springProperty scope="context" name="rootlevel" source="logging.level.root" />
<springProperty scope="context" name="busilevel" source="logging.level.com.xncoding" />
<!-- 日志存储路径 -->
<springProperty scope="context" name="logPath" source="logging.path" />
<!-- 普通日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志命名:单个文件大于128MB 按照时间+自增i 生成log文件 -->
<fileNamePattern>${logPath}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>128MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 最大保存时间:30天-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %p ${PID:-} --- [%15thread] %logger:%-3L : %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 错误日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志命名:单个文件大于2MB 按照时间+自增i 生成log文件 -->
<fileNamePattern>${logPath}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>2MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 最大保存时间:180天-->
<maxHistory>180</maxHistory>
</rollingPolicy>
<append>true</append>
<!-- 日志格式 -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %p ${PID:-} --- [%15thread] %logger:%-3L : %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!-- 日志级别过滤器 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志格式 -->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %p ${PID:-} --- [%15thread] %logger:%-3L : %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- 屏蔽kafka的警告 -->
<logger name="org.apache.kafka" level="ERROR"/>
<!-- additivity 避免执行2次 -->
<logger name="com.xncoding" level="${busilevel}" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</logger>
<root level="${rootlevel}">
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>
application.yml 配置
##########################################################
################## 所有profile共有的配置 #################
##########################################################
################### 项目启动端口 ###################
server.port: 8092
################### spring配置 ###################
spring:
profiles:
active: dev
---
#####################################################################
######################## 开发环境profile ##########################
#####################################################################
spring:
profiles: dev
logging:
level:
root: INFO
com.xncoding: DEBUG
path: D:/logs/springboot-aop
参考链接
- logback
继承自 log4j
,它建立在有十年工业经验的日志系统之上。它比其它所有的日志系统更快并且更小
,包含了许多独特并且有用的特性。详细参考文档 logback 常用配置(详解)
Lombok 实战
- @Data : 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法
- @AllArgsConstructor : 注在类上,提供类的全参构造
- @NoArgsConstructor : 注在类上,提供类的无参构造
- @Setter : 注在属性上,提供 set 方法
- @Getter : 注在属性上,提供 get 方法
- @EqualsAndHashCode : 注在类上,提供对应的 equals 和 hashCode 方法
- @Log4j/@Slf4j : 注在类上,提供对应的 Logger 对象,变量名为 log ,private static final Logger log = LoggerFactory.getLogger(UserController.class);
- @EqualsAndHashCode(callSuper = false) 作用就是自动的给model bean实现equals方法和hashcode方法,callSuper = false 表示生成的 hashcode 包括父类属性 详情参考
今天主要说下@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor
- @NoArgsConstructor
1.1 @NoArgsConstructor 实战使用
@NoArgsConstructor
在类上使用,它可以提供一个无参构造器,比如:
@NoArgsConstructor
public class User {
private String username;
private String password;
}
// 编译后:
public class User {
private String username;
private String password;
public User() { }
}
当然,有时候我们会使用到单例模式,这个时候我们需要将构造器私有化,那么就可以使用这样一个属性access
设置构造器的权限:
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class User {
private String username;
private String password;
}
// 编译后:
public class User {
private String username;
private String password;
private User() {
}
}
当类中有final字段没有被初始化时,编译器会报错,但是也可用@NoArgsConstructor(force = true)
,那么Lombok就会为没有初始化的final
字段设置默认值 0 / false / null
, 这样编译器就不会报错,如下所示:
@NoArgsConstructor(force = true)
public class User {
private final String gender;
private String username;
private String password;
}
// 编译后:
public class User {
private final String gender = null;
private String username;
private String password;
public User() { }
}
对于具有约束的字段(例如使用了@NonNull
注解的字段),不会生成字段检查,如下所示:
@NoArgsConstructor(force = true)
public class User {
private final String gender;
@NonNull
private String username;
private String password;
}
// 编译后:
public class User {
private final String gender = null;
@NonNull
private String username;
private String password;
public User() {
}
}
1.2 @NoArgsConstructor 配置详解
首先来看看 @NoArgsConstructor
注解源码部分的实现:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NoArgsConstructor {
String staticName() default "";
// 在构造方法的入参上添加注解。e.g. @NonNull
// up to JDK7: @EqualsAndHashCode(onParam=@__({@AnnotationsGoHere}))
// from JDK8: @EqualsAndHashCode(onParam_={@AnnotationsGohere})
AnyAnnotation[] onConstructor() default {};
// 设置构造方法的访问权限,默认是 public
AccessLevel access() default lombok.AccessLevel.PUBLIC;
// 是否强制对未赋值的final字段初始化值。
boolean force() default false;
@Deprecated
@Retention(RetentionPolicy.SOURCE)
@Target({})
@interface AnyAnnotation {}
}
看到有这样一个注解属性:staticName
,是代表什么意思呢?
也就是说是否生成静态的构造方法,如果生成静态的构造方法,那么原来的实例构造方法将会被私有(private),然后创建一个你指定名称的静态构造方法,并且是public的,如下所示:
@NoArgsConstructor(staticName = "UserHa")
public class User {
private String username;
private String password;
}
// 编译后:
public class User {
private String username;
private String password;
private User() { }
public static User UserHa() {
return new User();
}
}
- @RequiredArgsConstructor
2.1 @RequiredArgsConstructor 实战使用
@RequiredArgsConstructor
也是在类上使用,但是这个注解可以生成带参或者不带参的构造方法。
若带参数,只能是类中所有带有 @NonNull注解的和以final修饰的未经初始化的字段,如下所示:
@RequiredArgsConstructor
public class User {
private final String gender;
@NonNull
private String username;
private String password;
}
// 编译后:
public class User {
private final String gender;
@NonNull
private String username;
private String password;
public User(String gender, @NonNull String username) {
if (username == null) {
throw new NullPointerException("username is marked @NonNull but is null");
} else {
this.gender = gender;
this.username = username;
}
}
}
如果针对User类使用@RequiredArgsConstructor
注解实现空参构造方法,那么我们就要这样定义实体类:
@RequiredArgsConstructor
public class User {
private final String gender = "男";
private String username;
private String password;
}
当然如果我们要是想生成空参构造,仅适用上一个注解@NoArgsConstructor
就可以了。
2.2 @RequiredArgsConstructor 配置详解
先撸注解源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface RequiredArgsConstructor {
// 是否使用静态构造器,并制定静态构造器的名称
String staticName() default "";
// 构造器入参添加注解
AnyAnnotation[] onConstructor() default {};
@Deprecated
@Retention(RetentionPolicy.SOURCE)
@Target({})
@interface AnyAnnotation {}
}
没什么好说的,可以参见@NoArgsConstructor
- @AllArgsConstructor
3.1 @AllArgsConstructor 实战使用
@AllArgsConstructor同样是在类上使用,该注解提供一个全参数的构造方法,默认不提供无参构造。
需要注意的是,这里的全参不包括已初始化的final字段(主要是final字段,一旦被赋值不允许再被修改)。
@AllArgsConstructor
public class User {
private final String gender;
private String username;
private String password;
}
// 编译后
public class User {
private final String gender;
private String username;
private String password;
public User(String gender, String username, String password) {
this.gender = gender;
this.username = username;
this.password = password;
}
}
3.2 @AllArgsConstructor 配置详解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AllArgsConstructor {
// 是否使用静态构造器,并制定静态构造器的名称
String staticName() default "";
// 构造器入参添加注解
AnyAnnotation[] onConstructor() default {};
// 设置构造器的访问权限
AccessLevel access() default lombok.AccessLevel.PUBLIC;
@Deprecated
@Retention(RetentionPolicy.SOURCE)
@Target({})
@interface AnyAnnotation {}
}
同样这几个属性配置方式,也可以参见@NoArgsConstructor
- Constructor 全局配置
# 如果设置为true,则lombok将向生成的构造函数添加 @java.beans.ConstructorProperties。 lombok.anyConstructor.addConstructorProperties = [true | false] (default: false) # 是否禁用这几种构造器注解 lombok.[allArgsConstructor|requiredArgsConstructor|noArgsConstructor].flagUsage = [warning | error] (default: not set) lombok.anyConstructor.flagUsage = [warning | error] (default: not set) # 是否支持复制现有的注解,在本类中使用。e.g. @Nullable/@NonNull annotations lombok.copyableAnnotations = [A list of fully qualified types] (default: empty list)
测试 addConstructorProperties
全局配置文件内容
config.stopBubbling = true
# 首先清除原有配置
clear lombok.anyConstructor.addConstructorProperties
# 设置新的配置
lombok.anyConstructor.addConstructorProperties = true
实体类
@AllArgsConstructor
public class User {
private String username;
private String password;
}
// 编译后:
public class User {
private String username;
private String password;
@ConstructorProperties({"username", "password"}) // 被添加
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
处理文件上传(MultipartResolver)
简介
MultipartResolver 用于处理文件上传,当收到请求时 DispatcherServlet 的 checkMultipart() 方法会调用 MultipartResolver 的 isMultipart() 方法判断请求中是否包含文件。如果请求数据中包含文件,则调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析,然后将文件数据解析成 MultipartFile 并封装在 MultipartHttpServletRequest (继承了 HttpServletRequest) 对象中,最后传递给 Controller,在 MultipartResolver 接口中有如下方法:
- boolean isMultipart(HttpServletRequest request); // 是否是 multipart
- MultipartHttpServletRequest resolveMultipart(HttpServletRequest request); // 解析请求
- void cleanupMultipart(MultipartHttpServletRequest request);
MultipartFile 封装了请求数据中的文件,此时这个文件存储在内存中或临时的磁盘文件中,需要将其转存到一个合适的位置,因为请求结束后临时存储将被清空。在 MultipartFile 接口中有如下方法:
- String getName(); // 获取参数的名称
- String getOriginalFilename(); // 获取文件的原名称
- String getContentType(); // 文件内容的类型
- boolean isEmpty(); // 文件是否为空
- long getSize(); // 文件大小
- byte[] getBytes(); // 将文件内容以字节数组的形式返回
- InputStream getInputStream(); // 将文件内容以输入流的形式返回
- void transferTo(File dest); // 将文件内容传输到指定文件中
MultipartResolver 是一个接口,它的实现类如下图所示,分为 CommonsMultipartResolver 类和 StandardServletMultipartResolver 类。 其中 CommonsMultipartResolver 使用 commons Fileupload 来处理 multipart 请求,所以在使用时,必须要引入相应的 jar 包;而 StandardServletMultipartResolver 是基于 Servlet 3.0来处理 multipart 请求的,所以不需要引用其他 jar 包,但是必须使用支持 Servlet 3.0的容器才可以,以tomcat为例,从 Tomcat 7.0.x的版本开始就支持 Servlet 3.0了。
一、CommonsMultipartResolver
1 使用方式
1.1 配置文件
<!-- 定义文件上传解析器 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设定默认编码 -->
<property name="defaultEncoding" value="UTF-8"></property>
<!-- 设定文件上传的最大值为5MB,5*1024*1024 -->
<property name="maxUploadSize" value="5242880"></property>
<!-- 设定文件上传时写入内存的最大值,如果小于这个参数不会生成临时文件,默认为10240 -->
<property name="maxInMemorySize" value="40960"></property>
<!-- 上传文件的临时路径 -->
<property name="uploadTempDir" value="fileUpload/temp"></property>
<!-- 延迟文件解析 -->
<property name="resolveLazily" value="true"/>
</bean>
1.2 上传表单
要在 form 标签中加入 enctype=”multipart/form-data” 表示该表单要提交文件。
<form action="${pageContext.request.contextPath}/test/file-upload.do" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
1.3 处理文件
@RequestMapping("/file-upload")
public ModelAndView upload(@RequestParam(value = "file", required = false) MultipartFile file,
HttpServletRequest request, HttpSession session) {
// 文件不为空
if(!file.isEmpty()) {
// 文件存放路径
String path = request.getServletContext().getRealPath("/");
// 文件名称
String name = String.valueOf(new Date().getTime()+"_"+file.getOriginalFilename());
File destFile = new File(path,name);
// 转存文件
try {
file.transferTo(destFile);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
// 访问的url
String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ request.getContextPath() + "/" + name;
}
ModelAndView mv = new ModelAndView();
mv.setViewName("other/home");
return mv;
}
2 源码分析
CommonsMultipartResolver 实现了 MultipartResolver 接口,resolveMultipart() 方法如下所示,其中 resolveLazily 是判断是否要延迟解析文件(通过XML可以设置)。当 resolveLazily 为 flase 时,会立即调用 parseRequest() 方法对请求数据进行解析,然后将解析结果封装到 DefaultMultipartHttpServletRequest 中;而当 resolveLazily 为 true 时,会在 DefaultMultipartHttpServletRequest 的 initializeMultipart() 方法调用 parseRequest() 方法对请求数据进行解析,而 initializeMultipart() 方法又是被 getMultipartFiles() 方法调用,即当需要获取文件信息时才会去解析请求数据,这种方式用了懒加载的思想。
@Override
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
Assert.notNull(request, "Request must not be null");
if (this.resolveLazily) {
//懒加载,当调用DefaultMultipartHttpServletRequest的getMultipartFiles()方法时才解析请求数据
return new DefaultMultipartHttpServletRequest(request) {
@Override //当getMultipartFiles()方法被调用时,如果还未解析请求数据,则调用initializeMultipart()方法进行解析 protected void initializeMultipart() {
MultipartParsingResult parsingResult = parseRequest(request);
setMultipartFiles(parsingResult.getMultipartFiles());
setMultipartParameters(parsingResult.getMultipartParameters());
setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
}
};
} else {
//立即解析请求数据,并将解析结果封装到DefaultMultipartHttpServletRequest对象中
MultipartParsingResult parsingResult = parseRequest(request);
return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
}
}
在上面的代码中可以看到,对请求数据的解析工作是在 parseRequest() 方法中进行的,继续看一下 parseRequest() 方法源码
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
// 获取请求的编码类型
String encoding = determineEncoding(request);
FileUpload fileUpload = prepareFileUpload(encoding);
try {
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, encoding);
} catch (...) {}
}
在 parseRequest() 方法中,首先调用了 prepareFileUpload() 方法来根据编码类型确定一个 FileUpload 实例,然后利用这个 FileUpload 实例解析请求数据后得到文件信息,最后将文件信息解析成 CommonsMultipartFile (实现了 MultipartFile 接口) 并包装在 MultipartParsingResult 对象中。
二、StandardServletMultipartResolver
1 使用方式
1.1 配置文件
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</bean>
这里并没有配置文件大小等参数,这些参数的配置在 web.xml 中
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<multipart-config>
<!-- 临时文件的目录 -->
<location>d:/</location>
<!-- 上传文件最大2M -->
<max-file-size>2097152</max-file-size>
<!-- 上传文件整个请求不超过4M -->
<max-request-size>4194304</max-request-size>
</multipart-config>
</servlet>
1.2 上传表单
要在 form 标签中加入 enctype=”multipart/form-data” 表示该表单要提交文件。
<form action="${pageContext.request.contextPath}/test/file-upload.do" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
1.3 处理文件
1.3.1 通过 MultipartFile 类型的参数
@RequestMapping("/file-upload")
public ModelAndView upload(@RequestParam(value = "file", required = false) MultipartFile file,
HttpServletRequest request, HttpSession session) {
// 文件不为空
if(!file.isEmpty()) {
// 文件存放路径
String path = request.getServletContext().getRealPath("/");
// 文件名称
String name = String.valueOf(new Date().getTime()+"_"+file.getOriginalFilename());
File destFile = new File(path,name);
// 转存文件
try {
file.transferTo(destFile);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
// 访问的url
String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ request.getContextPath() + "/" + name;
}
ModelAndView mv = new ModelAndView();
mv.setViewName("other/home");
return mv;
}
1.3.2 通过 MultipartHttpServletRequest 类型的参数
@RequestMapping("/file-upload")
public ModelAndView upload(MultipartHttpServletRequest request, HttpSession session) {
// 根据页面input标签的name
MultipartFile file = request.getFile("file");
// 文件不为空
if(!file.isEmpty()) {
// 文件存放路径
String path = request.getServletContext().getRealPath("/");
// 文件名称
String name = String.valueOf(new Date().getTime()+"_"+file.getOriginalFilename());
File destFile = new File(path,name);
// 转存文件
try {
file.transferTo(destFile);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
// 访问的url
String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
+ request.getContextPath() + "/" + name;
}
ModelAndView mv = new ModelAndView();
mv.setViewName("other/home");
return mv;
}
2 源码分析
StandardServletMultipartResolver 实现了 MultipartResolver 接口,resolveMultipart() 方法如下所示,其中 resolveLazily 是判断是否要延迟解析文件(通过XML可以设置)。
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
super(request);
// 判断是否立即解析
if (!lazyParsing) {
parseRequest(request);
}
}
对请求数据的解析工作是在 parseRequest() 方法中进行的,继续看一下 parseRequest() 方法源码
private void parseRequest(HttpServletRequest request) {
try {
Collection<Part> parts = request.getParts();
this.multipartParameterNames = new LinkedHashSet<String>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<String, MultipartFile>(parts.size());
for (Part part : parts) {
String disposition = part.getHeader(CONTENT_DISPOSITION);
String filename = extractFilename(disposition);
if (filename == null) {
filename = extractFilenameWithCharset(disposition);
}
if (filename != null) {
files.add(part.getName(), new StandardMultipartFile(part, filename));
} else {
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files);
} catch (Throwable ex) {}
}
parseRequest() 方法利用了 servlet3.0 的 request.getParts() 方法获取上传文件,并将其封装到 MultipartFile 对象中。
Docket(Swagger2中应用)
修饰符和类型 | 方法和说明 | 描述 |
---|---|---|
Docket | additionalModels(com.fasterxml.classmate.ResolvedType first, com.fasterxml.classmate.ResolvedType… remaining) | 添加不属于任何批注或可能是隐式的其他模型的方法 |
Docket | alternateTypeRules(AlternateTypeRule… alternateTypeRules) | 添加模型替换规则(alternateTypeRules) |
Docket | apiDescriptionOrdering(com.google.common.collect.Ordering |
控制com.wordnik.swagger.model.ApiDescription订购方式。 |
Docket | apiInfo(ApiInfo apiInfo) | 设置api的meta信息,包括在json ResourceListing响应中。 |
Docket | apiListingReferenceOrdering(com.google.common.collect.Ordering |
控制ApiListingReference的排序方式。 |
DocumentationContext | configure(DocumentationContextBuilder builder) | 通过合并/覆盖用户指定的值来构建Docket。 |
Docket | consumes(java.util.Set |
|
Docket | directModelSubstitute(java.lang.Class clazz, java.lang.Class with) | 用提供的替代品直接替代模型类,例如 directModelSubstitute(LocalDate.class, Date.class) 用Date替代LocalDate |
Docket | enable(boolean externallyConfiguredFlag) | 可以从外部控制此swagger插件实例的自动初始化。 |
Docket | enableUrlTemplating(boolean enabled) | 决定是否对路径使用url模板。 |
Docket | extensions(java.util.List |
添加到api的供应商扩展 |
Docket | forCodeGeneration(boolean forCodeGen) | 将其设置为true以便使文档代码生成友好 |
Docket | genericModelSubstitutes(java.lang.Class… genericClasses) | 用其直接参数化类型替换每个泛型类。 |
DocumentationType | getDocumentationType() | java.lang.String getGroupName()获取插件的组名。 |
Docket | globalOperationParameters(java.util.List |
添加将应用于所有操作的默认参数。 |
Docket | globalResponseMessage(org.springframework.web.bind.annotation.RequestMethod requestMethod,java.util.List |
在http请求方法级别覆盖默认的http响应消息。 |
Docket | groupName(java.lang.String groupName) | 如果存在多个Docket实例,则每个实例必须具有此方法提供的唯一groupName。 |
Docket | host(java.lang.String host) | |
Docket | ignoredParameterTypes(java.lang.Class… classes) | 添加忽略的控制器方法参数类型,以便框架不会为这些特定类型生成摇摇欲坠的模型或参数信息。 |
boolean | isEnabled() | 是否启用了插件| |
Docket | operationOrdering(com.google.common.collect.Ordering |
提供用于操作的排序模式注意:@see #732以防您想知道为什么指定位置可能不起作用的情况。 |
Docket | pathMapping(java.lang.String path) | 将servlet路径映射(如果有)添加到apis基本路径的可扩展性机制。 |
Docket | pathProvider(PathProvider pathProvider) | 确定生成的特定于URL的URL。 |
Docket | produces(java.util.Set |
|
Docket | protocols(java.util.Set |
|
Docket | securityContexts(java.util.List |
配置哪些api操作(通过正则表达式模式)和HTTP方法将安全性上下文应用于api。 |
Docket | securitySchemes(java.util.List<? extends SecurityScheme> securitySchemes) | 配置全局com.wordnik.swagger.model.SecurityScheme适用于所有或某些api操作。 |
ApiSelectorBuilder | select() | 启动用于api选择的构建器。 |
boolean | supports(DocumentationType delimiter) | |
Docket | tags(Tag first, Tag… remaining) | 将全局标签添加到摘要的方法 |
Docket | useDefaultResponseMessages(boolean apply) | 允许忽略预定义的响应消息默认值 |
示例
package sc.whorl.system.config.swagger2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import java.time.LocalDate;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class Swagger2Config {
/**
* 通过 createRestApi函数来构建一个DocketBean
* 函数名,可以随意命名,喜欢什么命名就什么命名
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("sc.whorl.logic.rmiservice.units.service"))
.paths(PathSelectors.any())
.build()
.pathMapping("/")
.directModelSubstitute(LocalDate.class, String.class)
.genericModelSubstitutes(ResponseEntity.class)
.useDefaultResponseMessages(false)
.enableUrlTemplating(true);
}
//构建 api文档的详细信息函数
@SuppressWarnings("deprecation")
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
//页面标题
.title("RESTful API")
//创建人
.contact("sc-whorl")
//版本号
.version("1.0")
//描述
.description("初始化api")
.build();
}
}
HandlerMethodArgumentResolver解析器说明
前面已经讲了很多关于 过滤器 相关内容,接下来会陆续讲到对拦截的数据,我们还能做些啥?
- SpringMVC解析器用于解析
request
请求参数并将绑定数据到Controller的入参上。 - 自定义一个参数解析器需要实现
HandlerMethodArgumentResolver接口
,重写supportsParameter和resolveArgument方法
,配置文件中加入resolver配置。 - 如果需要多个解析器同时生效需要在一个解析器中对其他解析器做兼容
为什么要自定义一个解析器呢?
源于需要对前端请求参数进行手动URLDecode,也即除了Web容器自动decode一次,代码内还需要再decode一次。
针对这种需求,首先想到的是filter
或者interceptor
实现,但是由于HttpServletRequest对象本身是不提供setParameter()方法的,因此想要修改request中的参数值为decode后的值是不易达到的。
SpringMVC的HandlerMethodArgumentResolver,解析器;其功能就是解析request请求参数并绑定数据到Controller的入参上。因此自定义解析器加入URLDecode逻辑即可完全满足需求。
下面,就一步一步的完成一个解析器由简到繁的实现过程。
实现一个极其简单的参数解析器
实现HandlerMethodArgumentResolver接口,重写supportsParameter和resolveArgument方法,配置文件中加入resolver配置。
示例代码如下:
- 自定义解析器实现
public class MyArgumentsResolver implements HandlerMethodArgumentResolver {
/**
* 解析器是否支持当前参数
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 指定参数如果被应用MyParam注解,则使用该解析器。
// 如果直接返回true,则代表将此解析器用于所有参数
return parameter.hasParameterAnnotation(MyParam.class);
}
/**
* 将request中的请求参数解析到当前Controller参数上
* @param parameter 需要被解析的Controller参数,此参数必须首先传给{@link #supportsParameter}并返回true
* @param mavContainer 当前request的ModelAndViewContainer
* @param webRequest 当前request
* @param binderFactory 生成{@link WebDataBinder}实例的工厂
* @return 解析后的Controller参数
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return null;
}
}
- 自定义注解
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyParam {
}
- 在springmvc配置文件中注册解析器
<mvc:annotation-driven>
<!--MyArgumentsResolver-->
<mvc:argument-resolvers>
<bean class="xxx.xxx.xxx.MyArgumentsResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>
好了,现在解析器会把所有应用了@MyParam注解的参数都赋值为null。
实现一个解析原始类型的参数解析器
对于如何解析原始类型参数,SpringMVC已经有了一个内置的实现——RequestParamMethodArgumentResolver
,因此完全可以参考这个实现来自定义我们自己的解析器。
如上所述,解析器逻辑的主要部分都在resolveArgument方法内,这里就说说自定义该方法的实现。
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 解析器中的自定义逻辑——urldecode
Object arg = URLDecoder.decode(webRequest.getParameter(parameter.getParameterName()), "UTF-8");
// 将解析后的值绑定到对应的Controller参数上,利用DataBinder提供的方法便捷的实现类型转换
if (binderFactory != null) {
// 生成参数绑定器,第一个参数为request请求对象,第二个参数为需要绑定的目标对象,第三个参数为需要绑定的目标对象名
WebDataBinder binder = binderFactory.createBinder(webRequest, null, parameter.getParameterName());
try {
// 将参数转到预期类型,第一个参数为解析后的值,第二个参数为绑定Controller参数的类型,第三个参数为绑定的Controller参数
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
} catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
parameter.getParameterName(), parameter, ex.getCause());
} catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
parameter.getParameterName(), parameter, ex.getCause());
}
}
return arg;
}
添加解析对象类型参数的功能
对于如何解析对象类型参数,SpringMVC内也有了一个内置的实现——ModelAttributeMethodProcessor
,我们也是参考这个实现来自定义我们自己的解析器。
同样,resolveArgument方法示例如下
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String name = parameter.getParameterName();
// 查找是否已有名为name的参数值的映射,如果没有则创建一个
Object attribute = mavContainer.containsAttribute(name)
? mavContainer.getModel().get(name)
: this.createAttribute(name, parameter, binderFactory, webRequest);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
// 进行参数绑定
this.bindRequestParameters(binder, webRequest);
}
// 将参数转到预期类型,第一个参数为解析后的值,第二个参数为绑定Controller参数的类型,第三个参数为绑定的Controller参数
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
return attribute;
}
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) throws UnsupportedEncodingException {
// 将key-value封装为map,传给bind方法进行参数值绑定
Map<String, String> map = new HashMap<>();
Map<String, String[]> params = request.getParameterMap();
for (Map.Entry<String, String[]> entry : params.entrySet()) {
String name = entry.getKey();
// 执行urldecode
String value = URLDecoder.decode(entry.getValue()[0], "UTF-8");
map.put(name, value);
}
PropertyValues propertyValues = new MutablePropertyValues(map);
// 将K-V绑定到binder.target属性上
binder.bind(propertyValues);
}
同时支持多个参数解析器生效
到目前为止,不论对于原始类型或者对象类型的参数,我们都可以自定义一个参数解析器了,但是还有一个很严重的问题存在——无法让自定义解析器和现有解析器同时生效。
举个例子,public String myController(@Valid @MyParam param, BindingResult result){},这个方法在执行时是会报错的。他会提示类似如下报错:
An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the @RequestBody or the @RequestPart arguments
是SpringMVC不支持同时使用两个解析器吗?public String myController(@Valid @ModelAttribute param, BindingResult result){},也是两个内置解析器,没有任何问题。
再去看ModelAttributeMethodProcessor的实现,原来是对@Valid做了兼容处理。
因此, 如果需要多个解析器同时生效需要在一个解析器中对其他解析器做兼容。
这里仅以对@Valid进行兼容处理为例,在解析对象类型的解析器实现中进行修改
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String name = parameter.getParameterName();
// 查找是否已有名为name的参数值的映射,如果没有则创建一个
Object attribute = mavContainer.containsAttribute(name)
? mavContainer.getModel().get(name)
: this.createAttribute(name, parameter, binderFactory, webRequest);
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
// 进行参数绑定,此方法实现不再赘述,可到上节查看
this.bindRequestParameters(binder, webRequest);
// -----------------------------------对@Valid做兼容----------------------------------------------------
// 如果使用了validation校验, 则进行相应校验
if (parameter.hasParameterAnnotation(Valid.class)) {
// 如果有校验报错,会将结果放在binder.bindingResult属性中
binder.validate();
}
// 如果参数中不包含BindingResult参数,直接抛出异常
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// 关键,使Controller中接下来的BindingResult参数可以接收异常
Map bindingResultModel = binder.getBindingResult().getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
// -----------------------------------对@Valid做兼容----------------------------------------------------
// 将参数转到预期类型,第一个参数为解析后的值,第二个参数为绑定Controller参数的类型,第三个参数为绑定的Controller参数
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
return attribute;
}
/**
* 检查参数中是否包含BindingResult参数
*/
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
int i = methodParam.getParameterIndex();
Class[] paramTypes = methodParam.getMethod().getParameterTypes();
boolean hasBindingResult = paramTypes.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]);
return !hasBindingResult;
}
OK,到这里,我们自定义的解析器已经可以算是一个完善的参数解析器了,如果有对其他解析器做兼容的需要,只要参照此类方法稍作修改即可。
HandlerInterceptorAdapter解析器说明
简介
ThreadLocal是用来处理多线程并发问题的一种解决方案。ThreadLocal是的作用是提供线程的局部变量,在多线程并发环境下,提供了与其他线程隔离的局部变量。通常这样的设计的情况是因为这个局部变量是不适合放在全局变量进行同步处理的。
应用场景
在Service中,怎么获取当前登录用户信息? 最简单的将用户信息存到session中,controller层将session中存的用户信息取出,再作为方法参数传到service层。但是这样做的话,太low了。
分析
ThreadLocal类,它是在当前线程(Thread.currentThread())的ThreadLocalMap对象中添加值,key为ThreadLocal对象,也就是说ThreadLocal类用来提供线程内部的局部变量。我们都知道Http请求就是一个线程,只要我们在这个线程中,添加了我们想要的request、session对象,那么响应服务器请求的Controller、Service、Dao等这些层面的代码不就都可以通过当前线程(Thread.currentThread())取出request、session对象了!但是我们怎么在请求到Controller等Control控制器之前,添加这些信息呢?很容易我们想到了Filter过滤器或者Interceptor拦截器,因为Http请求会调用Filter的doFilter或者Interceptor的preHandle进行处理,是在同一个线程中,可以在Filter或者Interceptor中用ThreadLocal来实现! ThreadLocal的核心get方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
实战(Interceptor实现方案)
1.自定义BaseContextHandler类:
package io.fredia.femicro.common.context;
import static org.junit.Assert.assertEquals;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.fredia.femicro.common.constant.CommonConstants;
import io.fredia.femicro.common.util.StringHelper;
/**
* 上下文处理基类
*
* @author : Fredia
* @since : 2018年6月1日
* @version : v1.0.0
*/
public class BaseContextHandler {
public static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<Map<String, Object>>();
public static void set(String key, Object value) {
Map<String, Object> map = threadLocal.get();
if (map == null) {
map = new HashMap<String, Object>();
threadLocal.set(map);
}
map.put(key, value);
}
public static Object get(String key){
Map<String, Object> map = threadLocal.get();
if (map == null) {
map = new HashMap<String, Object>();
threadLocal.set(map);
}
return map.get(key);
}
public static String getUserID(){
Object value = get(CommonConstants.CONTEXT_KEY_USER_ID);
return returnObjectValue(value);
}
public static String getUsername(){
Object value = get(CommonConstants.CONTEXT_KEY_USERNAME);
return returnObjectValue(value);
}
public static String getName(){
Object value = get(CommonConstants.CONTEXT_KEY_USER_NAME);
return StringHelper.getObjectValue(value);
}
public static String getToken(){
Object value = get(CommonConstants.CONTEXT_KEY_USER_TOKEN);
return StringHelper.getObjectValue(value);
}
public static void setToken(String token){set(CommonConstants.CONTEXT_KEY_USER_TOKEN,token);}
public static void setName(String name){set(CommonConstants.CONTEXT_KEY_USER_NAME,name);}
public static void setUserID(String userID){
set(CommonConstants.CONTEXT_KEY_USER_ID,userID);
}
public static void setUsername(String username){
set(CommonConstants.CONTEXT_KEY_USERNAME,username);
}
private static String returnObjectValue(Object value) {
return value==null?null:value.toString();
}
public static void remove(){
threadLocal.remove();
}
}
2.自定义UserAuthRestInterceptor拦截器
package io.fredia.femicro.auth.client.interceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import io.fredia.femicro.auth.client.annotation.IgnoreUserToken;
import io.fredia.femicro.auth.client.config.UserAuthConfig;
import io.fredia.femicro.auth.client.jwt.UserAuthUtil;
import io.fredia.femicro.auth.common.util.jwt.IJWTInfo;
import io.fredia.femicro.common.context.BaseContextHandler;
/**
* 用户授权拦截器
*
* @author : Fredia
* @since : 2018年3月16日
* @version : v1.0.0
*/
public class UserAuthRestInterceptor extends HandlerInterceptorAdapter {
private Logger logger = LoggerFactory.getLogger(UserAuthRestInterceptor.class);
@Autowired
private UserAuthUtil userAuthUtil;
@Autowired
private UserAuthConfig userAuthConfig;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
//此处为业务代码,可以忽略
String token = request.getHeader(userAuthConfig.getTokenHeader());
if (StringUtils.isEmpty(token)) {
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equals(userAuthConfig.getTokenHeader())) {
token = cookie.getValue();
}
}
}
}
IJWTInfo infoFromToken = userAuthUtil.getInfoFromToken(token);
//上下文属性保存
BaseContextHandler.setUsername(infoFromToken.getUniqueName());
BaseContextHandler.setName(infoFromToken.getName());
BaseContextHandler.setUserID(infoFromToken.getId());
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//上下文属性值清除,防止内存泄漏
BaseContextHandler.remove();
super.afterCompletion(request, response, handler, ex);
}
}
回顾一下基础知识:
preHandle:预处理回调方法,实现处理器的预处理(如登录检查),第三个参数为响应的处理器;
- 返回值:
true表示继续流程(如调用下一个拦截器或处理器);
false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
postHandle:后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView
(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView
也可能为null。
afterCompletion:整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally
中的finally
,但仅调用处理器执行链中preHandle返回true的拦截器的afterCompletion。
3.Controller使用ThreadLocal工具
package io.fredia.femicro.common.rest;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import io.fredia.femicro.common.biz.BaseBiz;
import io.fredia.femicro.common.context.BaseContextHandler;
import io.fredia.femicro.common.msg.ObjectRestResponse;
import io.fredia.femicro.common.msg.TableResultResponse;
import io.fredia.femicro.common.util.Query;
import lombok.extern.slf4j.Slf4j;
/**
* controller基类
*
* @author : Fredia
* @since : 2018年6月1日
* @version : v1.0.0
*/
@Slf4j
public class BaseController<Biz extends BaseBiz,Entity> {
public String getCurrentUserName(){
return BaseContextHandler.getUsername();
}
}
4.总结
实现一个功能有N种方案,我们可以选择低耦合的方法去实现,在保证效率的情况下,也要考虑性能问题。threadlocal实现简单,但是它和io很像,如果数据量大,它所占用的内存空间无法释放会导致内存泄漏,毕竟不自带内存生命周期管理的一切都要慎重使用。
DefaultServletHandlerConfigurer解析器说明
简介
通过将请求转发到Servlet容器的“默认” Servlet,配置用于处理静态资源的请求处理程序。当Spring MVC DispatcherServlet映射到“ /” 时,将使用此方法,从而覆盖Servlet容器对静态资源的默认处理。 由于此处理程序的配置优先级最低,因此它实际上允许所有其他处理程序映射处理该请求,并且如果没有一个处理,则此处理程序可以将其转发到“默认” Servlet。
方法说明
修饰符和类型 | 方法和说明 | 描述 |
---|---|---|
protected SimpleUrlHandlerMapping | buildHandlerMapping() | 返回一个处理程序映射实例,Ordered.LOWEST_PRECEDENCE 该DefaultServletHttpRequestHandler实例的排序顺序为:包含映射到的实例”/**“;或者null如果未启用默认servlet处理。 |
void | enable() | 启用转发到“默认” Servlet的功能。 |
void | enable(String defaultServletName) | 启用转发到由给定名称标识的“默认” Servlet的功能。 |
339 post articles, 43 pages.