SpringMVC相关配置
自动配置原理
现状1:以前要访问jpg\css、js 等这些静态资源文件,需要在web.xml配置 ,在springboot不需要配置,只需要放在约定文件夹中就可以(约定大于配置)
原理1:WebMvcAutoConfiguration中做了以下配置,从而实现当访问/webjars/** 时就会去classpath:/META-INF/resources/webjars/ 对应进行映射
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
}
现状2:静态资源文件放在以下路径"classpath:/META-INF/resources/", “classpath:/resources/”, “classpath:/static/”, "classpath:/public/"便可访问
原理2:WebMvcAutoConfiguration中配置了静态资源的映射地址,可以通过spring.resources.static‐locations属性来指定具体静态资源地址
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
public String[] getStaticLocations() {
return this.staticLocations;
}
this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
现状3:controller中可以直接返回html的视图,默认html资源地址是"classpath:/META-INF/resources/", “classpath:/resources/”, “classpath:/static/”, “classpath:/public/”,可以通过设置spring.mvc.view.prefix和spring.mvc.view.suffix来设置静态资源前缀和后缀
原理3:WebMvcAutoConfiguration中配置了默认视图
@Bean
@ConditionalOnMissingBean
public InternalResourceViewResolver defaultViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.mvcProperties.getView().getPrefix());
resolver.setSuffix(this.mvcProperties.getView().getSuffix());
return resolver;
}
//配置properties文件
#spring.mvc.view.prefix=/pages/
spring.mvc.view.suffix=.html
//示例代码
@Controller
@RequestMapping("/skill")
public class skillController {
@GetMapping("/hello")
public String hello3(){
return "/hello";
}
}
现状4:如果配置了@EnableWebMvc的话,那么SpringMVC的所有自动配置都将失效
原理4:EnableWebMvc 中@Import({DelegatingWebMvcConfiguration.class}),而WebMvcAutoConfiguration 中配置了@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@Configuration
@EnableWebMvc
public class MyWebMvcConfigurer implements WebMvcConfigurer {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
定制配置
配置1:定制视图控制器、拦截器和跨域配置(跨域配置还可通过@CrossOrigin直接在控制类或者方法指定配置)
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
/**
* 添加视图控制器
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/testView").setViewName("hello");
}
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TimeInterceptor())
.addPathPatterns("/**");
}
/**
* 全局CORS配置
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/user/*") // 映射服务器中那些http接口运行跨域访问
.allowedOrigins("http://localhost:8081") // 配置哪些来源有权限跨域
.allowedMethods("GET","POST","DELETE","PUT"); // 配置运行跨域访问的请求方法
}
}
public class TimeInterceptor implements HandlerInterceptor {
LocalDateTime begin;
Logger logger = LoggerFactory.getLogger(TimeInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
begin = LocalDateTime.now();
logger.info("当前请求:"+request.getRequestURI()+"开始运行!");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
LocalDateTime end = LocalDateTime.now();
Duration between = Duration.between(begin, end);
logger.info("当前请求:"+request.getRequestURI()+"耗时"+between+"毫秒!");
}
}
Json开发
常用注解
@JsonIgnore
进行排除json序列化,将它标注在属性上将不会进行json序列化
@JsonFormat(pattern = “yyyy-MM-dd hh:mm:ss”,locale = “zh”)
进行日期格式化
@JsonInclude(JsonInclude.Include.NON_NULL)
当属性值为null时则不进行json序列化
@JsonProperty(“uname”)
来设置别名
自定义转化
@JsonComponent
public class UserJsonCustom {
public static class Serializer extends JsonObjectSerializer<User> {
@Override
protected void serializeObject(User user, JsonGenerator jgen, SerializerProvider provider) throws IO Exception {
jgen.writeObjectField("id",user.getId());
//{"id","xxx"}
jgen.writeObjectField("uname","xxxxx");
/*jgen.writeFieldName(""); 单独写key
jgen.writeObject(); 单独写value
*/
// 1. 一次查不出完整的数据返回给客户端, 需要根据需求去做一些个性化调整
// 2. 根据不同的权限给他返回不同的序列化数据
}
}
public static class Deserializer extends JsonObjectDeserializer<User> {
@Override
protected User deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec, JsonNode tree) throws IOException {
User user=new User();
user.setId(tree.findValue("id").asInt());
return user;
}
}
}
国际化
国际化自动配置类
MessageSourceAutoConfiguration是国际化自动配置类
国际化实现步骤
①添加国际化资源文件
②配置资源文件位置
Springboot默认的国际化资源文件位置是类路径下的messages文件夹
也可以通过spring.messages.basename属性配置国际化资源文件位置
spring.messages.basename=i18n.message
如果找不到对应的资源文件,那么MessageSourceAutoConfiguration自动配置类将不生效,原理如下:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean(
name = {"messageSource"},
search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(-2147483648)
//@Conditional 自定义条件匹配 会传入一个实现了Condition接口的类——ResourceBundleCondition
// ResourceBundleCondition 会重写matches, 自定义匹配规则 , 如果该方法返回true 就匹配成功
@Conditional({MessageSourceAutoConfiguration.ResourceBundleCondition.class})
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
matches
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
ConditionOutcome outcome = this.getMatchOutcome(context, metadata);
this.logOutcome(classOrMethodName, outcome);
this.recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
} catch (NoClassDefFoundError var5) {
throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + var5.getMessage() + " not found. Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)", var5);
} catch (RuntimeException var6) {
throw new IllegalStateException("Error processing condition on " + this.getName(metadata), var6);
}
}
getMatchOutcome
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String basename = context.getEnvironment().getProperty("spring.messages.basename", "messages");
ConditionOutcome outcome = (ConditionOutcome)cache.get(basename);
if (outcome == null) {
outcome = this.getMatchOutcomeForBasename(context, basename);
cache.put(basename, outcome);
}
return outcome;
}
getMatchOutcomeForBasename
private ConditionOutcome getMatchOutcomeForBasename(ConditionContext context, String basename) {
Builder message = ConditionMessage.forCondition("ResourceBundle", new Object[0]);
String[] var4 = StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(basename));
int var5 = var4.length;
for(int var6 = 0; var6 < var5; ++var6) {
String name = var4[var6];
// 根据basename获取 该类路径下的所有propeties的资源文件
Resource[] var8 = this.getResources(context.getClassLoader(), name);
int var9 = var8.length;
for(int var10 = 0; var10 < var9; ++var10) {
Resource resource = var8[var10];
// 只要basename的类路径下有资源文件就会匹配成功
if (resource.exists()) {
return ConditionOutcome.match(message.found("bundle").items(new Object[]{resource}));
}
}
}
return ConditionOutcome.noMatch(message.didNotFind("bundle with basename " + basename).atAll());
}
③解析请求头中的accept-language
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "spring.mvc",
name = {"locale"}
)
public LocaleResolver localeResolver() {
// 当配置spring.mvc.locale‐resolver=fiexd就会使用配置文件中的本地化语言:spring.mvc.locale=en_US 就可以设死本地化语言
if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
} else {
// 默认就是使用AcceptHeaderLocaleResolver 作为本地化解析器
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
//spring.mvc.locale=en_US 作为默认的本地化语言
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
}
public Locale resolveLocale(HttpServletRequest request) {
Locale defaultLocale = this.getDefaultLocale();
// 当Accept‐Languag为null 才会使用使用配置文件中设置的locale:spring.mvc.locale
if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
return defaultLocale;
} else {
// 当Accept‐Languag不为null时,使用request.getLocale();
Locale requestLocale = request.getLocale();
List<Locale> supportedLocales = this.getSupportedLocales();
if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
if (supportedLocale != null) {
return supportedLocale;
} else {
return defaultLocale != null ? defaultLocale : requestLocale;
}
} else {
return requestLocale;
}
}
}
④解析url参数中?local=,随意切换本地语言,进行缓存
添加CookieLocaleResolver,代替自动化配置中的AcceptHeaderLocaleResolver
/**
*配置国际化处理类
* @return
*/
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setCookieMaxAge(60*60*24*30);
localeResolver.setCookieName("locale");
return localeResolver;
}
配置LocaleChangeInterceptor拦截器获取请求中的local=,设置到CookieLocaleResolver 中
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor())
.addPathPatterns("/**");
}
⑤通过messageResource 获取国际化信息
可以通过ResourceBundleMessageSource的getMessage方法获取国际化字段信息,获取local時,使用LocaleContextHolder.getLocale(),LocaleContextHolder就是一个Locale持有器,SpringMVC底层会自动将LocaleResovler中的语言设置进去
@Autowired
ResourceBundleMessageSource messageSource;
@GetMapping("/testLocale")
@ResponseBody
public String testLocale(HttpServletRequest request){
String message = messageSource.getMessage("user.query.success", null, LocaleContextHolder.getLocale());
return message;
}
异常处理
异常处理自动配置类
ErrorMvcAutoConfiguration是Springboot统一异常处理自动配置类
统一异常处理流程
当异常发生时,如果请求头是text/html,那么请求会交给errorHtml方法处理,其他的交给error方法处理
@Controller
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//调用DefaultErrorViewResolver的resolveErrorView方法匹配返回页面然后组装ModelAndView
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
//如果找不到对应的返回视图,那么直接使用springboot默认错误页面
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
//查询需要从request中获取的错误信息,可以在配置文件中设置server.error.xxx来配置
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(new Include[]{Include.EXCEPTION});
}
if (this.isIncludeStackTrace(request, mediaType)) {
options = options.including(new Include[]{Include.STACK_TRACE});
}
if (this.isIncludeMessage(request, mediaType)) {
options = options.including(new Include[]{Include.MESSAGE});
}
if (this.isIncludeBindingErrors(request, mediaType)) {
options = options.including(new Include[]{Include.BINDING_ERRORS});
}
return options;
}
DefaultErrorViewResolver的resolveErrorView方法解析
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//查询是否有对应异常的返回视图
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
//如果找不到返回视图,那么查询是否有4xx和5xx的通用返回视图
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
//查询视图模板中是否有对应的返回视图
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
//如果视图模板中没有对应返回视图,那么从this.resourceProperties.getStaticLocations();中对应的资源位置找
//默认是"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}
return null;
}
定制统一异常处理
方式一:创建一个继承AbstractErrorController的类就可以了,然后重写errorHtml和error方法就可以了
@Controller
@RequestMapping("/error")
public class CustomErrorController extends AbstractErrorController {
方式二:使用@ControllerAdivce注解
@ControllerAdvice
public class GeneralExceptionHandler {
@ExceptionHandler(Exception.class)
public ModelAndView handleException(HttpServletRequest request,HttpServletResponse reponse, Exception ex,HandlerMethod handle){
System.out.println("全局异常处理");
// 如果当前请求是ajax就返回json
// 1.根据用户请求的处理方法,是否是一个返回json的处理方法
//RestController restAnnotation = handle.getClass().getAnnotation(RestController.class); // 获得类上 面的某个注解
//ResponseBody responseBody = handle.getMethod().getAnnotation(ResponseBody.class);//获得方法上面的某 个注解
// if(restAnnotation!=null || responseBody!=null){ }
// 2.可以根据请求头中的类型Content‐Type包含application/json
if(request.getHeader("Accept").indexOf("application/json")>‐1){
// 可以直接输出json reponse.getWriter().write(); 或者集成jackson
// 集成jackson的方式:
ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
// 通常会根据不同的异常返回不同的编码
modelAndView.addObject("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
modelAndView.addObject("message",ex.getMessage());
return modelAndView;
}else{
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("ex", ex);
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
return modelAndView;
}
}
}