SpringBoot 初见
SpringBoot 学习笔记
SpringBoot-HelloWorld
创建一个 maven 工程,在 pom.xml 文件中写入配置
配置一个父工程用于集中管理依赖
org.springframework.boot
spring-boot-starter-parent
2.3.10.RELEASE
导入 SpringBoot 的依赖
org.springframework.boot
spring-boot-starter-web
其次要在 maven 的配置文件 settings.xml 中更改默认的编译环境,防止出错
jdk-1.8
true
1.8
1.8
1.8
1.8
为了方便后期打包我们在这里直接引入插件
org.springframework.boot
spring-boot-maven-plugin
2.3.10.RELEASE
注意:如果在引入父工程时 xml 文件有爆红,导入 boot 的依赖即可解决,若引入插件爆红,那么手动加上版本号即可,版本号与父工程中的版本号保持一致即可!
创建主程序 MainApplication
创建主程序类并声明为 SpringBoot 程序
/**
* Description:声明主程序类
*
* @author atroot@126.com @zhengyd
* @create 2021.4.17 14:22
*/
@SpringBootApplication //声明这是一个SpringBoot应用
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
编写控制器类
/**
* Description:控制器类
*
* @author atroot@126.com @zhengyd
* @create 2021.4.17 14:25
*/
//@ResponseBody
//@Controller
@RestController //可以看做是对上边两个注解的整合
public class HelloCrotroller {
@RequestMapping(value = "/hello")
public String hello() {
return "Hello SpringBoot";
}
}
运行测试
运行 MainApplication 程序中的主方法
SpringApplication.run(MainApplication.class, args);
入门
spring-boot 中的配置文件
在 spring-boot
中我们只有一个配置文件就是 application.properties
可以对 spring-boot 进行配置
可以配置什么?
实例:
server.port=8080
springboot 中的自动配置管理
org.springframework.boot
spring-boot-starter-parent
2.3.4.RELEASE
org.springframework.boot
spring-boot-dependencies
2.3.4.RELEASE
我们并不需要关心版本号,如果有需求要改动版本号我们在 pom 中引入版本号即可,或者直接加上version也是可行的
1、引入依赖默认都可以不写版本
2、引入非版本仲裁的jar,要写版本号。
- 查看 spring-boot-dependencies 里面规定当前依赖的版本 用的 key。
- 在当前项目里面重写配置
版本号
场景启动器starter
SpringBoot所支持的场景启动器
场景启动器,几乎会将我们会用到的包全部通过依赖导入
*-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器
在所有的场景启动器中都有如下依赖(版本有所区别而已)
org.springframework.boot
spring-boot-starter
2.3.10.RELEASE
compile
自动配置
例如:当我们引入 spring-boot-starter-mvc 的时候,就会帮我们引入其相关的依赖,例如 tomcat
org.springframework.boot
spring-boot-starter-tomcat
2.3.10.RELEASE
compile
SpringBoot 会帮我们配置好全套组件
综上,SpringBoot 会帮我们配置好
- Tomcat、SpringMVC、web 常见的问题(字符的编码问题,SpringBoot 会帮我们配好 web 的常见开发场景)
- 默认的包结构(SpringBoot 会默认扫描 MainApplication 所在的包及其子包下的所有组件),只要遵守约定,无须进行配置包扫描,如果要进行包扫描配置,可以使用注解 ComponentScan 来指定包扫描路径,或者使用SpringBootApplication(scanBasePackages = "指定扫描范围")
@SpirngBootApplication
//等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
各种默认配置都有默认值
- 默认配置的值,都是会映射到某个类上,例如
spring.servlet.multipart.file-size-threshold
就会绑定在MultipartProperties
类上 - 配置文件的值,最终会绑定在类上,那么那个类就会在容器中创建对象
按需加载所有的自动配置项
- 引入那些场景(stater),那么对应的场景的自动配置才会开启
- 所有的自动配置都在 spring-boot-autoconfigure 包里
容器的组件添加
在 SpringBoot 中添加组件的方式我们主要采用注解类的方式
1、xml 的配置方式,过于繁琐 不推荐使用
首先在 bean 类的类首加上注解 component 或者其他几个
@Component
public class User {
private String name;
private Integer age;
.....
其次在创建一个 xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
2、在 SpringBoot 中我们推荐使用较为轻巧的注解的方式进行 bean 组件添加
首先创建一个配置类
MyConfig
- 给配置类标注注解 @Configuration,声明该类为配置类
- 在类中,给要添加到容器中的组件添加注解 @Bean 注解,表示将该组件放入 spring 容器中,id 为其方法名,也可以通过@Bean中带参数的方式指定id名
@Bean(value = "tomcat")
- 配置类的本身也是一个组件
- @Configuration 注解中的参数---
proxyBeanMethods
(代理 Bean 方法),默认值为 true。
Full(proxyBeanMethods = true)保证每个 @Bean 方法被调用多少次返回的组件都是单实例的
Lite(proxyBeanMethods = false
)每个 @Bean 方法被调用多少次返回的组件都是新创建的
应用场景:当组件之间无依赖关系的时候就用flase
,Lite 模式,减少判断,加速容器的启动过程
当组件之间有依赖关系的时候,就用 true,Full 模式,保证调用到的都是单实例的组件。
/**
* Description: 配置类
* @author atroot@126.com @zhengyd
* @create 2021.4.17 21:23
*/
@Configuration(proxyBeanMethods = true) //告诉SpringBoot这是一个配置类--- 与配置文件等效
public class MyConfig {
@Bean //将组件放入容器中,并且默认以方法名作为其id,返回的对象即为在容器中的实例
public User user() {
return new User("张三", 15);
}
@Bean(value = "tomcat") //也可以通过value来指定id
public Pet pet() {
return new Pet("张三的宠物");
}
}
组件添加
1、之前 spring 用过的注解,这里也可以继续使用
- @Bean、@Component、@Controller、@Service、@Responsitory
- @ComponentScan 定义包扫描规则、@Import({User.class, DBHelper.class})给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
- @Conditional 条件装配:满足conditional指定的条件,才进行组件注入
@Configuration(proxyBeanMethods = true) //告诉SpringBoot这是一个配置类--- 与配置文件等效
public class MyConfig {
@Bean //将组件放入容器中,并且默认以方法名作为其id,返回的对象即为在容器中的实例
public User user() {
return new User("张三", 15);
}
@ConditionalOnBean(name = "user") //当容器中有user的时候,才会创建该bean
@Bean(value = "tomcat") //也可以通过value来指定id
public Pet pet() {
return new Pet("张三的宠物");
}
}
2、原生配置文件导入
@ImportResource("classpath:bean.xml")
@Configuration(proxyBeanMethods = true) //告诉SpringBoot这是一个配置类--- 与配置文件等效
@ImportResource("classpath:bean.xml")
public class MyConfig {
@Bean(value = "user")
public User user() {
return new User("张三", 15);
}
@Bean(value = "tomcat") //也可以通过value来指定id
public Pet pet() {
return new Pet("张三的宠物");
}
}
//******测试*********
User hh = run.getBean("hh", User.class);
System.out.println(hh); //User{name='李四', age=16}
Pet pp = run.getBean("pp", Pet.class);
System.out.println(pp); //Pet{name='jerry'}
3、@ConfigurationProperties + @EnableConfigurationProperties
两种配置方式
3.1、@ConfigurationProperties + @Component 把配置文件application.properties配置文件中的对应的属性注入类
@Component //把该类放入到容器中
@ConfigurationProperties(prefix = "mycar") //从配置文件applicationProperties中读取mycar打头的属性注入到该bean
public class Car {
......
}
@RestController
public class HelloCrotroller {
@Autowired //自动注入
Car car;
@RequestMapping(value = "/car")
public Car getCar(){
return car;
}
}
3.2、@EnableConfigurationProperties + @ConfigurationProperties
@EnableConfigurationProperties(Car.class)
//1、开启Car配置绑定功能
//2、把这个Car这个组件自动注册到容器中
public class MyConfig {
}
//@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
......
}
打印容器里的所有组件得知其 bean 被命名为 mycar-com.theoldzheng.bean.Car
自动配置原理
@SpingBootApplication可以理解为是众多注解的合成。
@SpringConfiguration 代表这是一个配置类
@ComponentScan 组件扫描
@EnableAutoConfiguration
@AutoConfigurationPackage
@AutoConfigurationPackage //指定了默认的包配置
利用 Registrar 给容器中导入一系列组件,将 MainApplication 所在的包下的所有组件导入
AutoConfigurationImportSelector.class
@Import(AutoConfigurationImportSelector.class)
1、利用 getAutoConfigurationEntry(annotationMetadata);
给容器中批量导入一些组件
2、调用 List
获取到所有需要导入到容器中的配置类
3、利用工厂加载 Map
得到所有的组件
4、从 META-INF/spring.factories
位置来加载一个文件。
默认扫描我们当前系统里面所有 META-INF/spring.factories
位置的文件
spring-boot-autoconfigure-2.3.4.RELEASE.jar
包里面也有 META-INF/spring.factories
按需开启配置项
虽然我们127个场景的所有自动配置启动的时候默认全部加载。xxxxAutoConfiguration
按照条件装配规则(@Conditional
),最终会按需配置。
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
}
总结:
- SpringBoot 先加载所有的自动配置类 xxxxxAutoConfiguration
- 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties 和配置文件进行了绑定
- 生效的配置类就会给容器中装配很多组件
- 只要容器中有这些组件,相当于这些功能就有了
- 定制化配置
- 用户直接自己 @Bean 替换底层的组件
- 用户去看这个组件是获取的配置文件什么值就去修改。
xxxxxAutoConfiguration ---> 组件 ---> xxxxProperties里面拿值 ----> application.properties
最佳实践:
引入场景依赖 场景依赖
根据需求查看修改配置
快乐 coding......
开发小工具
lombok 插件
导入依赖
org.projectlombok
lombok
在 IDEA 插件库中下载 Lombok 后即可使用
@NoArgsConstructor //无参构造
@AllArgsConstructor //有参构造 其他非全参构造情况需要另说
@Data //生成getter和setter
@ToString //toString
@EqualsAndHashCode
@Slf4j //日志
log.info("请求进来了....");
dev-tools
导入依赖
org.springframework.boot
spring-boot-devtools
true
热部署:ctrl + F9
SpringBoot 核心技术
配置文件 yaml
YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。适合用于以数据为中心的配置文件
基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格(IDEA支持自动将Tab转化为两个空格)
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
- 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义
数据类型
字面量:不可再分的值
k: v
对象:键值对儿的集合
#行内写法
k: {k1: v1,k2: v2,k3: v3}
#或者层级写法
k:
k1: v1
k2: v2
k3: v3
数组
#行内写方法
k: [k1,k2,k3]
k:
- v1
- v2
- v3
简单实例
定义一个 Person 类,并加上在配置文件中的前缀
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
...
}
在配置类中的 @EnableConfigurationProperties
中加上 Persion.class
@Configuration(proxyBeanMethods = true) //告诉SpringBoot这是一个配置类--- 与配置文件等效
@ImportResource("classpath:bean.xml")
@EnableConfigurationProperties({Car.class, Person.class})
public class MyConfig {
...
}
在 Controller 中测试,可以返回 JSON 数据到页面,不能够直接显示在控制台
@RestController
public class HelloCrotroller {
@Autowired
Person person
@RequestMapping(value = "/person")
public Person person(){
return person;
}
}
yaml 提示功能开启
编写 yaml 没有提示?
根据官方文档提示,我们要想实现提示功能,需要导入一个依赖,以及我们可以通过排除项,不把他打包到 jar 包中
导入文件后,需要进行重写启动一次程序才会生效,实现 yaml 文件的提示功能
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-maven-plugin
org.springframework.boot
spring-boot-configuration-processor
简单功能分析
静态资源访问
只要放在如下类路径下即可直接对静态文件进行访问:
/static
、/resources
、/META-INF/resources
访问规则:当前项目根路径/静态资源名 即可进行访问
原理:静态映射/**
请求进来的时候,先去 Controller 看是否能被处理,不能处理的请求再交给静态资源处理器,若静态资源也找不到那么就返回 404NotFound 页面
配置
spring:
mvc:
#配置访问前缀(默认无前缀)
static-path-pattern: /res/**
#指定默认的访问路径,可以设置为数组
resources:
static-locations: classpath:/test
webjar
自动映射 /webjars/**
走你
导入依赖
org.webjars
jquery
3.5.1
访问地址 http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径
欢迎页支持
静态资源路径下放 index.html、Controller 能处理/index
请求 两种方式都可以
静态资源路径下的 index.html
- 可以配置静态资源的路径,但是不能配置静态资源的访问前缀,会导致 index.html 不能访问
spring:
# mvc:
# static-path-pattern: /res/**
resources:
static-locations: classpath:/test
- Controller 能处理
/index
请求的
自定义 Favicon (网站图标)
favicon.ico 放在静态资源目录下即可(任何一个)
同样的自定义资源访问前缀会导致 Favicon 自定义失效
静态资源的处理原理
配置类只有一个有参构造器
//有参构造器所有参数的值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象
//WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
//ListableBeanFactory beanFactory Spring的beanFactory
//HttpMessageConverters 找到所有的HttpMessageConverters
//ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
//DispatcherServletPath
//ServletRegistrationBean 给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider messageConvertersProvider,
ObjectProvider resourceHandlerRegistrationCustomizerProvider,
ObjectProvider dispatcherServletPath,
ObjectProvider> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
静态资源处理默认规则
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
//webjars的规则
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
//
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
spring:
mvc:
static-path-pattern: /res/**
resources:
#禁用所有资源
add-mappings: false
#缓存控制 以秒为单位
cache:
cachecontrol:
s-max-age: 10000
底层---静态资源的访问规则
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
欢迎页的底层实现
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Optional welcomePage, String staticPathPattern) {
if (welcomePage.isPresent() && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,必须是/**
logger.info("Adding welcome page: " + welcomePage.get());
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 调用Controller /index
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
请求参数处理
-
@mapping
-
REST风格支持,同一个请求名,通过不同的请求方法 GET 获取用户 DELETE 删除用户 PUT 修改用户 POST 保存用户
-
核心 Filter;HiddenHttpMethodFilter
用法: 表单 method=post,隐藏域 _method=put
SpringBoo t中手动开启 -
自定义
_method
实现 REST 风格
Controller
@RestController
public class HelloController {
//@RequestMapping(value = "/user",method = RequestMethod.GET)
@GetMapping(value = "/user") //Spring支持
public String getUser() {
return "GET-张三";
}
@PostMapping(value = "/user")
public String saveUser() {
return "POST-张三";
}
@PutMapping(value = "/user")
public String putUser() {
return "PUT-张三";
}
@DeleteMapping(value = "/user")
public String deleteUser() {
return "DELETE-张三";
}
}
application.yaml 开启 filter
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启表单的REST功能
html
自定义 "_method"
@Configuration(proxyBeanMethods = false)
public class MyConfig {
/**
* 自定义 "_method" -> "_m"
* @return methodFilter
*/
@Bean
public HiddenHttpMethodFilter getMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
当 REST 使用客户端直接发请求的时候无须经过 Filter ,直接可以访问
请求映射原理
请求映射从 DispatcherServlet 开始分析
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);
// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);
//HandlerMapping:处理器映射。/xxx->>xxxx
RequestMappingHandlerMapping:保存了所有 @RequestMapping 和 handler 的映射规则。
所有的配置都会在 HandlerMapping 中!
- SpringBoot 自动配置欢迎页的 WelcomePageHandlerMapping 。访问
/
能访问到 index.html; - SpringBoot 自动配置了默认 的 RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的 HandlerMapping 看是否有请求信息。
如果有就找到这个请求对应的 handler
如果没有就是下一个 HandlerMapping - 我们需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping 。自定义 HandlerMapping
普通参数与基本注解
注解
@PathVariable
、@RequestHeader
、@ModelAttribute
、@RequestParam
、@MatrixVariable
、@CookieValue
、@RequestBody
@PathVariable()
,从请求地址中获取参数
@RestController
public class Demo02Controller {
@RequestMapping(value = "/user/{id}/name/{name}")
public Map getPerson(@PathVariable("id") String id,
@PathVariable("name") String name,
@PathVariable Map pv, //将请求地址中的所有参数,用map封装
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map Headers,
@RequestParam("username") String username,
@RequestParam("hobby") List list,
@RequestParam Map param,
@CookieValue Cookie cookie, //获取Cookie值,当浏览器没有Cookie的时候不能使用
@CookieValue("_ga") String _ga) {
HashMap map = new HashMap<>();
map.put(id, name);
map.put("pv", pv);
// map.put("User-Agent", userAgent);
// map.put("Headers", Headers);
map.put("username", username);
map.put("hobby", list);
map.put("param", username);
map.put("cookie", cookie);
map.put("_ga", _ga);
System.out.println(name + " > " + id);
return map;
}
}
获取请求体( Post 请求特有),通过表单提交过来,获取请求体
@PostMapping(value = "/save")
public Map postMethod(@RequestBody String content) {
HashMap map = new HashMap<>();
map.put("content", content);
return map;
}
requestAttribute 的使用(注意,这里的请求转发视为一次请求)
@Controller
public class Demo03Controller {
@GetMapping(value = "/goto")
public String goToPage(HttpServletRequest request) {
request.setAttribute("msg", "msg获取成功!");
request.setAttribute("code", "200 OK");
return "forward:/success"; //转发给success请求
}
@ResponseBody //作为响应体
@GetMapping(value = "/success")
public Map successPage(@RequestAttribute String msg,
@RequestAttribute String code,
HttpServletRequest request) {
HashMap map = new HashMap<>();
map.put("msg", msg);
map.put("code", code);
System.out.println("msg" + msg);
System.out.println("code" + code);
Object msg1 = request.getAttribute("msg");
Object code1 = request.getAttribute("code");
Object msg11 = map.put("msg1", msg1);
Object code11 = map.put("code1", code1);
System.out.println("msg11" + msg11);
System.out.println("code11" + code11);
return map;
}
}
矩阵变量
1、语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd
2、SpringBoot默认是禁用了矩阵变量的功能
手动开启:原理。对于路径的处理。UrlPathHelper进行解析。
removeSemicolonContent(移除分号内容)支持矩阵变量的
3、矩阵变量必须有url路径变量才能被解析
开启矩阵变量功能
@Configuration(proxyBeanMethods = false)
public class MyConfig {
//打开矩阵变量支持--->关闭自动去除分号;
@Bean
public WebMvcConfigurer webMvcConfigurer() { //匿名实现接口
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
//或者实现接口,重写方法
}
}
@RestController
public class Demo03Controller {
//请求路径格式 http://localhost:8080/cars/sell;brand=BYD;price=999
@GetMapping(value = "/cars/{path}")
public Map carsSell(@MatrixVariable("brand") String brand,
@MatrixVariable("price") List price,
@PathVariable("path") String path) {
HashMap map = new HashMap<>();
map.put("brand", brand);
map.put("price", price);
map.put("path", path);
return map;
}
// http://localhost:8080/boss/bid;id=1001/eid;id=1011
@RequestMapping(value = "/boss/{bossId}/{employeeId}")
public Map employee(@MatrixVariable(value = "id", pathVar = "bossId") String bid,
@MatrixVariable(value = "id", pathVar = "employeeId") String eid) {
HashMap map = new HashMap<>();
map.put("bid",bid);
map.put("eid",eid);
return map;
}
}
ServletAPI
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
ServletRequestMethodArgumentResolver 以上的部分参数
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
复杂参数
Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes( 重定向携带数据)、ServletResponse(response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map map, Model model, HttpServletRequest request 都是可以给request域中放数据,
request.getAttribute();
Map、Model类型的参数,会返回 mavContainer.getModel();---> BindingAwareModelMap 是Model 也是Map
mavContainer.getModel(); 获取到值的
自定义对象参数
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Pet {
private String name;
private Integer age;
}
Controller
@ResponseBody
@PostMapping("/saveuser")
public Person getPerson(Person person){
return person;
}
自定义封装规则
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
//自定义Bean的绑定--->自定义转换器Converter
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter() {
@Override
public Pet convert(String s) {
Pet pet = new Pet();
String[] split = s.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
});
}
};
POJO 封装过程
ServletModelAttributeMethodProcessor
参数处理原理
- HandlerMapping中找到能处理请求的Handler(Controller.method())
- 为当前Handler 找一个适配器 HandlerAdapter; RequestMappingHandlerAdapter
- 适配器执行目标方法并确定方法参数的每一个值
HandlerAdapter
0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程的
......
执行目标方法
// Actually invoke the handler.
//DispatcherServlet -- doDispatch
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
mav = invokeHandlerMethod(request, response, handlerMethod); //执行目标方法
//ServletInvocableHandlerMethod
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
参数解析器
HandlerMethodArgumentResolver
确定将要执行的目标方法的每一个参数的值是什么;
SpringMVC 目标方法能写多少种参数类型。取决于参数解析器。
- 当前解析器是否支持解析这种参数
- 支持就调用 resolveArgument
返回值处理器
如何确定目标方法每一个参数的值
============InvocableHandlerMethod==========================
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
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) {
continue;
}
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 ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
挨个判断所有参数解析器那个支持解析这个参数
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
自定义类型参数 封装POJO
ServletModelAttributeMethodProcessor 这个参数处理器支持
是否为简单类型。
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
@Override
@Nullable
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 = 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 {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
if (isBindExceptionRequired(parameter)) {
// No BindingResult parameter -> fail with BindException
throw ex;
}
// Otherwise, expose null/empty value and associated BindingResult
if (parameter.getParameterType() == Optional.class) {
attribute = Optional.empty();
}
bindingResult = ex.getBindingResult();
}
}
if (bindingResult == null) {
// Bean property binding and validation;
// skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
// Add resolved attribute and BindingResult at the end of the model
Map bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);
return attribute;
}
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean -- Integer)
byte -- > file
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
//自定义Bean的绑定--->自定义转换器Converter
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter() {
@Override
public Pet convert(String s) {
Pet pet = new Pet();
String[] split = s.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
});
}
};
目标方法执行完成
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据。
处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
InternalResourceView:
@Override
protected void renderMergedOutputModel(
Map model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including [" + getUrl() + "]");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to [" + getUrl() + "]");
}
rd.forward(request, response);
}
}
暴露模型作为请求域属性
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
protected void exposeModelAsRequestAttributes(Map model,
HttpServletRequest request) throws Exception {
//model中的所有数据遍历挨个放在请求域中
model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}
数据响应与内容协商
响应 Json 数据给客户端
@jackson.jar + @ResponseBody
导入依赖、加 @ResponseBody注解
org.springframework.boot
spring-boot-starter-web
web场景自动引入了json场景
org.springframework.boot
spring-boot-starter-json
2.3.4.RELEASE
compile
这样我们就可以直接将数据写给浏览器客户端
返回值解析器
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
RequestResponseBodyMethodProcessor
@Override
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);
// Try even with null return value. ResponseBodyAdvice could get involved.
// 使用消息转换器进行写出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
返回值解析器原理
1、返回值解处理器判断是否支持这种类型返回值 supportsRetrunType
2、返回值处理器调用 handleReturnValue 进行处理
3、RequestResponseBodyMethodProcessor 可以处理 将数据写为 json
- 利用 MessgeConverters 进行处理,将数据写为 json
- 内容协商,浏览器默认会以请求头的方式告诉服务器他能接收什么样的数据类型
- 服务器最终根据自己自身的能力,决定服务器能生产出什么样类型的数据
- SpringMVC 会挨个遍历所有容器底层的 HttpMessageConverter
- 得到 MappingJackson2HttpMessageConverter 可以将对象写为 json
- 利用 MappingJackson2HttpMessageConverter 将对象转为json再写出去
SpringMVC 到底支持哪些返回值
ModelAndView
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
HttpHeaders
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
有 @ModelAttribute 且为对象类型的
@ResponseBody 注解 ---> RequestResponseBodyMethodProcessor;
HTTPMessageConverter 原理
MessageConverter 规范
HttpMessageConverter: 看是否支持将此 Class 类型的对象,转为 MediaType 类型的数据。
例子:Person 对象转为 JSON。或者 JSON 转为 Person
默认的MessageConverter
0 - 只支持 Byte 类型的
1 - String
2 - String
3 - Resource
4 - ResourceRegion
5 - DOMSource.**class *_ SAXSource._class) \ StAXSource.class StreamSource.class _Source._class
6 - MultiValueMap
7 - true
8 - true
9 - 支持注解方式 xml 处理的。
最终 MappingJackson2HttpMessageConverter 把对象转为 JSON(利用底层的jackson的objectMapper转换的)
内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
引入依赖
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
postman 分别测试返回 json 和 xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能。
spring:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式
确定客户端接收什么样的内容类型;
- Parameter策略优先确定是要返回json数据(获取请求头中的format的值)
最高权重直接匹配到第一个,而第一个就是json
- 最终进行内容协商返回给客户端json即可
内容协商原理
- 判断当前响应头中是否已经有确定的媒体类型。MediaType
- 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)【application/xml】
- contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
- HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
- 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
- 找到支持操作Person的converter,把converter支持的媒体类型统计出来。
- 客户端需要【application/xml】。服务端能力【10种、json、xml】
- 进行内容协商的最佳匹配媒体类型
- 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
导入了 jackson 处理 xml 的包,xml 的 converter 就会自动进来
WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
自定义 MessageConverter
**实现多协议数据兼容。json、xml、my-converter **
0、@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理
1、Processor 处理方法返回值。通过 MessageConverter 处理
2、所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
3、内容协商找到最终的 messageConverter;
自定义一个 Converter 类,并实现HttpMessageConverter
接口,重写里面的方法
/**
* Description: 自定义Converter
*
* @author atroot@126.com @ZYD
* @create 2021.4.25 21:06
*/
public class MyConverter implements HttpMessageConverter {
@Override
public boolean canRead(Class clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class); //只要是Person就可以写
}
@Override
public List getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/my-converter"); //定义解析媒体类型
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String data = person.getUserName() + "--" + person.getAge() + "--" + person.getBirth() + "--" + person.getPet();
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
在配置类 MyConfig中
配置 Spring-MVC 的入口配置
//自定义Spring的入口
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List> converters) {
converters.add(new MyConverter());
}
};
}
视图解析
thymeleaf 使用
org.springframework.boot
spring-boot-starter-thymeleaf
他会自动配置好 thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {...}
自动配好的策略
- 1、所有 thymeleaf 的配置值都在 ThymeleafProperties
- 2、配置好了 SpringTemplateEngine
- 3、配好了 ThymeleafViewResolver
- 4、我们只需要直接开发页面
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
页面开发 引入 xmlns:th="http://www.thymeleaf.org"
Title
哈哈
去百度
去百度2
构建后台管理项目
1、创建项目
thymeleaf、web-starter、devtools、lombok
2、静态资源的处理
自动配置好了,只需要静态资源放到static文件夹下
3、路径构建
th:action:@{/login}
4、模板抽取
th:insert/replace/include
5、页面跳转
以 login 为例
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){
if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}
6、数据渲染
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
//表格内容的遍历
List users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);
return "table/dynamic_table";
}
拦截器
自定义拦截器,并实现接口 HandlerInterceptor
- 拦截器的原理
- 1、根据当前请求,找到扫 HandlerExecutionChain 【可以处理 handler 以及 handler 的所有拦截器】
-
2、先来顺序执行所有拦截器的preHandle方法
如果当前拦截器的 preHandle 回值为 true,则执行下一个拦截器的 preHandle
如果当前拦截器返回为 false,那么直接倒叙执行已经执行了的拦截其的 afterCompletion
- 3、如果任何一个拦截器但会f alse,直接跳出不执行目标方法
- 4、所有拦截器都返回 true,则执行目标方法
- 5、倒叙执行所有的拦截其的 postHandle 方法
- 6、前面的步骤有任何异常都会导致直接出发倒叙的 afterCompletion
- 7、页面成功渲染完成之后,也会出发倒叙的 afterCompletion
package com.atroot.admin.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* Description: 自定义拦截器
* @author atroot@126.com @ZYD
* @create 2021.5.9 17:01
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前-前置环绕通知
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("拦截请求是{}",request.getRequestURI());
log.info("现在执行的是:{preHandle}");
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null) {
return true;
}
// response.sendRedirect("/"); //无法携带数据
request.setAttribute("msg","未登录,请登录后再试!");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行之后-后置环绕通知
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("现在执行的是:{postHandle}");
}
/**
* 页面渲染之后
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("现在执行的是:{afterCompletion}");
}
}
在配置类中进行注册
/**
* Description:自定义拦截器
*
* 1、编写一个拦截器实例HandlerInterceptor接口
* 2、拦截器注册到容器中,并实现WebMvcConfigurer的addInterceptors
* 3、指定拦截规则(如果是拦截所有的话,那么静态资源也会被拦截,可以使用精确拦截或者排除拦截项)
*
* @author atroot@126.com @ZYD
* @create 2021.5.9 16:58
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") //添加拦截路径
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**"); //添加排除路径
}
}
文件上传
单文件上传或者多文件的上传都需要在表单中加上媒体类型为 enctype="multipart/form-data"
多文件上传的需要加上multiple
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestPart("headPhoto") MultipartFile headPhoto, //文件用RequestPart()
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("email:{},username:{},password:{},headPhoto:{}", email, username, password, headPhoto);
for (MultipartFile multipartFile : photos) {
if (!multipartFile.isEmpty()) {
String originalFilename = multipartFile.getOriginalFilename(); //获取原始文件名
log.info("photos:{}", originalFilename);
multipartFile.transferTo(new File("D:\\zyd\\" + originalFilename)); //将文件直接上传到指定的位置
}
}
if (!headPhoto.isEmpty()) {
headPhoto.transferTo(new File("D:\\zyd\\" + headPhoto.getOriginalFilename()));
}
return "main";
}
遇到的上传文件受限问题,通过设置改变文件上传大小即可单位为字节
spring:
servlet:
multipart:
max-file-size: 1000000
异常处理
当我们用了错误的请求的时候
给客户端 postman 响应的 json 数据
{
"timestamp": "2021-05-10T12:59:04.664+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/fafs"
}
给浏览器客户端则响应一个 Whitelabel Error Page
要对错误页进行自定义,可以添加view为error
若要完全替换默认行为,则可以实现 ErrorController,并注册到容器中,或者添加 ErrorAttributes 类型的组件,以使用现有的机制替换
定制错误处理逻辑
-
error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
-
@ControllerAdvice+@ExceptionHandler处理全局异常--->底层是 ExceptionHandlerExceptionResolver 支持的
/**
* Description: 处理整个Web controller的异常
*
* @author atroot@126.com @ZYD
* @create 2021.5.11 18:51
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class}) //定义处理的异常类型
public String handleArithmeticException(Exception e) {
log.error("异常:{}", e.toString());
return "login"; //视图地址
}
}
- @ResponseStatus+自定义异常
/**
* Description: 自定义异常并在应用中抛出以达到应用的的目的
*
* @author atroot@126.com @ZYD
* @create 2021.5.11 20:21
*/
@ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "用户数量超标") //设置状态码和产生异常的原因,在controller中直接抛出
public class UserTooManyException extends RuntimeException{
public UserTooManyException() {
}
public UserTooManyException(String message) {
super(message);
}
}
@GetMapping("/dynamic_table")
public String dynamic_table(Model model) {
List users = Arrays.asList(new User("zxc", "123456"), new User("zhangsan", "123456"));
if (users.size() >= 1) {
throw new UserTooManyException("用户太多啦!");
}
model.addAttribute("users", users);
return "table/dynamic_table";
}
- Spring 底层的异常,如 参数类型转换异常;DefaultHandlerExceptionResolver 处理框架底层的异常。
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
- 自定义 HandlerExceptionResolver 处理异常,可以作为全局异常处理规则
@Component
@Order(value = HIGHEST_PRECEDENCE) //设置优先级为最高
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
response.sendError(512,"自定义的错误解析器");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
- ErrorViewResolver 实现自定义处理异常;
- response.sendError 。error请求就会转给controller
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
- basicErrorController 要去的页面地址是 ErrorViewResolver ;
其他源码细节查看语雀笔记
Web原生组件注入
注意:该注入不会经过spring的过滤器
在 application 配置类上添加包扫描
@SpringBootApplication
@ServletComponentScan(basePackages = "com.atroot.admin") //指定原生servlet的扫描路径
public class SpringBootMvc02Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootMvc02Application.class, args);
}
}
编写 servlet
@WebServlet(urlPatterns = "/myServlet") //需要标明访问路径
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("servlet!");
}
}
编写 Filter
@WebFilter(urlPatterns = "/myServlet") //需要标明拦截的路径
@Slf4j
public class MyFilter extends HttpFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.warn("过滤器初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.warn("过滤器工作");
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.warn("过滤器销毁");
}
}
编写 Listener
@WebListener
@Slf4j
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.warn("监听器初始化");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.warn("监听器销毁");
}
}
或者将 @WebListener、@WebFilter、@WebServlet注解去掉,直接编写一个配置类用于原生web组件的注入
分别使用
ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean
/**
* Description: 自定义配置类,用@Bean来实现对Web原生组件的注入
*
* @author atroot@126.com @ZYD
* @create 2021.5.12 10:45
*/
//proxyBeanMethods = true 默认开启为true //开启代理模式,保证组件是单实例的(存在依赖关系,那么就建议开启代理bean)
@Configuration
public class MyRegistrationConfig {
@Bean
// @ConditionalOnBean(name = "myFilter")
public ServletRegistrationBean myServlet() {
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet, "/myServlet");
}
@Bean
public FilterRegistrationBean myFilter() {
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/myServlet"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener() {
MyListener myListener = new MyListener();
return new ServletListenerRegistrationBean(myListener);
}
}
嵌入式服务器
可以通过修改依赖的方式引入其他的服务器
Tomcat
,Jetty
, orUndertow
都是 Spring 支持的服务器
数据访问
配置数据源(数据连接池)、数据库驱动等
导入场景(当我们导入场景的时候,自动配置会帮我们自动配置好 Hikaeri)
org.springframework.boot
spring-boot-starter-data-jdbc
导入数据库驱动
mysql
mysql-connector-java
对源码的分析我们可以得出如下小结论
- 修改数据源的相关配置的前缀为:spring.dataSource
- 数据库连接池的配置是在容器中没有的情况下才会配置
- 底层会帮我们配好数据库连接池:Hikaeri
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
spring 中有版本的自动仲裁机制,会帮我们自动选定 mysql 的版本号(当前默认的版本号为 8.0.23)
如何更改导入的数据 库驱动的版本号?
1、直接在导入的时候写入版本
mysql
mysql-connector-java
8.0.23
2、也可以在导入之后,用 properties 属性来设置版本号
1.8
8.0.23
...
mysql
mysql-connector-java
测试
@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
// jdbcTemplate.queryForObject("select * from account_tbl")
// jdbcTemplate.queryForList("select * from account_tbl",)
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
}
}
使用 Druid 数据源
Druid 说明文档
整合第三方技术的两种方式
- 自定义
- 找到场景的 stater
1、自定义
引入 Druid 的依赖
com.alibaba
druid
1.1.14
配置数据源,并将其放入容器中(定义一个配置类,在配置类中,将其放入容器)
@ConfigurationProperties("spring.datasource")
@Bean
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// druidDataSource.setUrl("");
// druidDataSource.setUsername("");
druidDataSource.setFilters("stat,wall");
return druidDataSource;
}
application.yaml
spring:
datasource:
url: jdbc:mysql:///test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
配置监控
/**
* 配置druid的监控页功能 配置一个Servlet到容器中
*/
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(statViewServlet,"/druid/*");
servletRegistrationBean.addInitParameter("loginUsername","admin");
servletRegistrationBean.addInitParameter("loginPassword","admin");
return servletRegistrationBean;
}
/**
* 配置WebStatFilter的监控功能
* @return FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean webStatFilterFilterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
webStatFilterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
webStatFilterFilterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return webStatFilterFilterRegistrationBean;
}
}
2、引入场景 starter
com.alibaba
druid-spring-boot-starter
1.1.10
配置 yaml
spring:
datasource:
url: jdbc:mysql:///test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
# initial-size:
# max-active:
filters: stat,wall
#监控页
stat-view-servlet:
enabled: true
login-username: admin
login-password: admin
reset-enable: false
#监控web
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
filter:
#sql监控功能
stat:
enabled: true
slow-sql-millis: 1000
log-slow-sql: true
#防火墙配置
wall:
enabled: true
config:
drop-table-allow: false
update-allow: false
#开启spring监控功能
aop-patterns: com.atroot.admin.*
SpringBoot 配置示例
配置项列表
整合MyBatis
github 文档
引入 MyBatis 的场景 stater
(直接在Maven仓库里找)
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.4
配置模式
- 全局配置文件
- SqlSessionFactory: 自动配置好了
- SqlSession:自动配置了 SqlSessionTemplate 组合了 SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class);
- Mapper: 只要我们写的操作MyBatis的接口标准了 **@Mapper **** 就会被自动扫描进来 **
他的自动配置类,将大部分的配置都为我们写好了,所以我们直接在 yaml 中配置功能即可
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml 全局配置文件的位置 可以不写全局配置文件mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml #声明mapper的路径,该路径下的所有的.xml文件都是mapper.xml
configuration: #配置项,该配置项不能与上面的配置路径同时存在,推荐用该方式进行配置
map-underscore-to-camel-case: true #开启驼峰命名,解决部分数据库与实体类的对应问题
测试一个 helloworld
定义一个实体类 Account.java
/**
* Description: 定义一个实体类,用于测试MyBatis的整合
*
* @author atroot@126.com @ZYD
* @create 2021.5.15 17:22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private String id;
private String name;
private String sex;
}
定义 **mapper **接口
/**
* Description: 定义操作数据库的AccountMapper接口
*
* @author atroot@126.com @ZYD
* @create 2021.5.15 16:39
*/
@Mapper
public interface AccountMapper {
/**
*Description: 通过Id查询一个User
*@Param [id]
*@return com.atroot.admin.bean.User
*/
public Account getAccountById(int id);
}
定义对应的 mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
定义一个 Service 的实现类 AccountService.java
@Service
public class AccountService {
@Autowired
AccountMapper accountMapper;
public Account getAccountById(int id) {
return accountMapper.getAccountById(id);
}
}
在 Controller 中写
@Autowired
AccountService accountService;
@ResponseBody
@GetMapping("/getAcc")
public Account getUser(@RequestParam("id") int id){
return accountService.getAccountById(id);
}
最后完成测试