上篇地址
内容协商机制
什么是内容协商机制?
就是为不同的请求者提供不同类型的返回值 ;假设一个需要JSON格式,另一客户端需要XML格式,还有人需要yaml格式,我们能否通过一种更为简便的方式为不同的客户端提供不同的返回值类型呢?
多端内容适配
我们希望可以辨别请求来自那种类型的客户端,或者说我们需要知道对方需要哪种格式的返回值。有以下2中方式可以进行区分
-
基于请求头 的内容协商(默认开启)
- 前端发送的http包中的请求头字段:Accept ;常见的有
application/json
、text/xml
、text/yaml
- 前端发送的http包中的请求头字段:Accept ;常见的有
-
基于请求参数 的内容协商(默认关闭)
- 在url参数上添加format参数:
/projects/spring-boot?format=json
- 根据参数协商规则 ,优先返回 json 类型数据
- 在url参数上添加format参数:
开启参数内容协商
ini
# 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名。默认是 format
spring.mvc.contentnegotiation.parameter-name=type
为Springboot增加xml类型协商功能
springboot没有对返回数据xml化的功能,需要引入依赖
xml
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
我们创建一个pojo来测试一下
less
@JacksonXmlRootElement // 可以写出为xml文档
@Data
public class Person {
private Long id;
private String userName;
private String email;
private Integer age;
}
我们需要给pojo加上@JacksonXMLRootElement
注解,Controller正常写即可
发个请求试一试
导入这个依赖后,他会默认优先返回xml,此时我们就需要指定请求头,或者指定请求参数为json才能让他返回JSON
增加yaml类型协商功能与原理分析
Springboot允许我们添加我们自己的内容协商机制,在WebMVCConfig中,我们可以添加一个MessageConverter
,也就是内容转换器。
其实Springboot中有着大量的内容转换器 ,比如我们的JSON格式,他就是由Springboot自带的MessageConverter
完成的内容转换。
我们先看一段代码
typescript
@Configuration
public class WebMvcConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
//TODO 添加yaml处理转换器到内容转换器合集
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyMessageConverter());
}
};
}
}
这个方法提供了一个参数:内容转换器集合 ,这个集合就是用来存放内容转换器的。也就是说,我们只需要将一个能够把object转换为yaml的内容转换器放进去,就算完成了配置(我在上面放了个MyMessageConverter,这个待会教大家写)。
而这个内容转换器,需要我们去配置,但不需要我们去做内容转换的内部实现,我们需要导入一个jar包
xml
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
导入上面的jar包后,我们就可以去配置内容转换器了,不过在此之前,我们先定义一下我们这个内容协商的名号,就是我们在Accept字段中指明或者在请求参数中需要带上的 ,我们称之为媒体类型。
ini
spring.mvc.contentnegotiation.media-types.yaml=text/yaml
spring.mvc.contentnegotiation.media-types
需要传入一个Map,yaml=text/yaml其实是Map<String, MediaType>
的Properties写法。
注意前面这个text
不要乱写,不然浏览器不知道是什么格式,不当成文本解析,会变成下载文件!
下面这段代码就是我们需要自己定义的内容转换器
首先要继承一个类AbstractHttpMessageConverter<?>
,这个类是Springboot提供给我们的,可以算是Springboot对于内容转换器的写法规范,至于为什么要将泛型指定为object,是因为我们只对对象类型的内容作类型转换,其他的一概不管
java
public class MyMessageConverter extends AbstractHttpMessageConverter<Object> {
private ObjectMapper objectMapper;
public MyMessageConverter(){
//TODO 指定媒体类型,让我们自己写的这个MessageConverter与我们的媒体类型关联在一起
//TODO 还需要指定转换的字符集类型。
super(new MediaType("text","yaml", StandardCharsets.UTF_8));
//TODO yaml依赖提供的方法,构建一个不需要加---的yaml格式工厂
//TODO 然后使用它创建一个对象映射者,我们让这一步在构造器内直接完成
YAMLFactory factory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
this.objectMapper = new ObjectMapper(factory);
}
//该方法表示对哪些类型支持,
//我们无脑对所有Object类型支持,所以无需判断直接返回true
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
//该方法表示对HandlerMapping收到的输入流进行处理
// 我们只关心向前端返回时的输出流的内容格式转换,这东西不用管
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
//该方法对输出的流进行转换
//参数o是handlerMapping处理之后的对象,
//TODO 我们需要将转换之后的内容写进输出流
@Override
protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//try with 写法自动关流
try (OutputStream os = outputMessage.getBody()){
this.objectMapper.writeValue(os,o);
}
}
}
springboot底层做了什么?
springboot底层在最后做文件类型转换时,他会去遍历这个内容转换器集合 ,看看有没有符合前端需求的媒体类型的转换器,有的话他就把object拿出来转换一下,发给前端。
杂项之service层获取Request对象
我们都知道,在Controller层能直接获取request对象与response对象,但是在Service层则无法直接获取,但是在Springboot中,在WebMvcAutoConfigurationAdapter
中,帮我们配置了一个过滤器,
less
@Bean
@ConditionalOnMissingBean({RequestContextListener.class, RequestContextFilter.class})
@ConditionalOnMissingFilterBean({RequestContextFilter.class})
public static RequestContextFilter requestContextFilter() {
return new OrderedRequestContextFilter();
}
}
这个过滤器其中一个步骤会把Request跟Response对象放到一个叫ServletRequestAttributes
的对象里,然后再放进另一个对象中。
ini
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
this.initContextHolders(request, attributes);
另一个对象:
typescript
private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
上面就是他的存放过程,通过上面可知:在这个名为RequestContextHolder
对象中,存放着了ServletRequestAttributes
对象,而这个所谓的RequestAttributes
,就是个存放请求跟响应对象的容器
说完了存放过程,我们演示一下怎么把它拿出来:
scss
public String getUri(){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
return requestAttributes.getRequest().getRequestURI();
}
我们会发现,他有个getRequestAttributes()方法,这个方法能取出一个RequestAttributes
类型的对象,而我们放进去的其实是个ServletRequestAttributes
类型的对象,所以我们强转一下 。通过这个对象,直接抓取在底层放进去的request就行了,拿到request对象,内部的参数就都能拿到了。
Springboot整合数据层
Mybatis为我们提供了自己的Starter,方便我们整合,同时我们还需要引入mysql连接Java的依赖
xml
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<!--java连接mysql-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.1.0</version>
</dependency>
MybatisX插件
同时建议大家使用IDE插件MybatisX 他可以帮我们快速生成Mapper.xml以及相关的SQL标签,
快速生成XML实现
对准类名alt+enter ,选择下图的Generate mapper of xml
快速生成sql标签
对准方法名alt+enter ,选择下图的Generate statement
整合配置实操
1.在主启动类上面使用@MapperScan
标签指定Mapper接口的包位置
less
@SpringBootApplication()
@EnableConfigurationProperties(pig.class)
@MapperScan({"com.atguigu.pro02.mapper"})
public class Pro02Application {
public static void main(String[] args) {
var ioc = SpringApplication.run(Pro02Application.class, args);
pig bean = ioc.getBean(pig.class);
}
}
2. 配置数据库
ini
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库类型必须选这个
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.url=自己的url
spring.datasource.username=数据库用户名
spring.datasource.password=密码
3. 还需要指定Mapper的xml实现所在的位置 ,下面的参数项调整会帮我们配驼峰映射
ini
#指定xml地址
mybatis.mapper-locations=classpath:/mapper/*.xml
#参数项调整
mybatis.configuration.map-underscore-to-camel-case=true
springboot的Profiles环境隔离用法
springboot允许我们为不同的运行时定义不同的环境配置。
@Profile注解
该注解可以被添加在类 或者方法 上,内部可以随便定义需要的运行环境 ,表示该类或者该方法在该环境下生效。
less
@Data
@Profile("dev")
@Repository
public class Person {
private Long id;
private String userName;
private String email;
private Integer age;
}
如何设置运行环境
ini
static Logger logger = LoggerFactory.getLogger(Pro02Application.class);
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(Pro02Application.class);
springApplication.setAdditionalProfiles("dev");
ConfigurableApplicationContext ioc = springApplication.run(args);
Person person = ioc.getBean(Person.class);
logger.info("person对象:{}",person);
}
或者:
ini
spring.profiles.active=dev
或者 (ide)添加运行环境配置
ini
--spring.profiles.active=dev
或者java -jar 项目名 --spring.profiles.active=dev
多个运行时环境配置
点开源码,我们发现,运行时环境的开启其实是个数组,也就是说,我们可以同时开启多个运行时环境
ini
#表示在任何环境下都保持激活的环境
spring.profiles.include=log,exceptionHandler
#创建一个包含多个环境的环境分组
spring.profiles.group.testgroup=dev
#开启指定的环境
spring.profiles.active=testgroup
如果我们使用多种方法同时指定了多个运行时环境,则多种环境会同时运行。
多文件定义运行环境
我们可以为指定的运行环境写专门的配置文件,命名为application-自定义名字.Properties
我们在真正的主启动类上指定所要运行的环境
ini
spring.profiles.active=prod
总结
当多个配置文件内容发生冲突时,则以开启的那个配置为准。 而且遵循:
主启动类定义 <Properties文件 <项目外配置文件 <运行时指令
也就是说,当内部的配置文件有问题,我们可以随时写外部配置文件改变它们的错误配置,也可以直接指定运行时参数
杂项之Properties相关语法
1. 配置文件之间的互相导入:
有点类似于node里面的import,引入后就会立即生效,但是引入的配置优先级会低于我们自己定义的
arduino
spring.config.import=classpath:/xxx.xxx
2. 配置属性之间的相互引用
ini
aaa=ccc+${server.port}
引用测试
kotlin
@RestController
public class helloController {
Logger logger = LoggerFactory.getLogger(getClass());
//此处的bbb为默认值,保证在数据不存在时作为保底
@Value("${aaa:bbb}")
String aaa;
@GetMapping("aa")
public String getAa(){
return aaa;
}
}
这种语法在@Value
注解与Properties配置文件
中都适用,比如为上面的port也配置默认值
ini
aaa=ccc+${server.port:8080}
springboot测试与断言
导入依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<version>3.1.4</version>
</dependency>
在项目test文件夹下进行相关的测试,
- 对于容器中存在的对象,我们可以直接
@AutoWired
- 如果你更改了主启动类的位置,那么,测试代码也必须与主启动类在相同的结构下,(这是因为项目打包机制会把测试代码打在同样的文件夹下,不了解也不重要)
相关的断言工具
scss
class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
}
其实对于测试,实在没有什么可以讲的,跟Junit用法几乎完全一致 ,唯一多了一点就是要给类上面加个@SpringBootTest
注解。
还有就是这些断言机制,如果你根本不熟悉这些断言,你根本不会想到使用断言 ;如果你熟悉它们,它们真的会帮到你很多,任何springboot框架的老手,都一定能熟练用断言机制。
Springboot事件驱动开发
设想一个场景,一个用户登录了,我们需要给用户做以下3个功能
- 增加1点累计登录积分
- 还需要给用户发一张优惠券,
- 还需要记录用户登录后的信息状态
我们尝试实现一下这个功能
常规实现
我们需要一个登录的Controller和3个Service
less
@RestController
@RequestMapping("login")
public class loginController {
@Autowired
sysService sysService;
@Autowired
accountService accountService;
@Autowired
couponService couponService;
@GetMapping("/{userName}/{password}")
public void getMapping(@PathVariable String userName,@PathVariable String password){
sysService.Login(userName,password);
accountService.Login(userName);
couponService.Login(userName);
}
}
下面是3个Service
arduino
@Service
public class sysService {
private Logger logger = LoggerFactory.getLogger(sysService.class);
public void Login(String userName ,String password){
logger.info("用户:{}登录成功,密码为:{}",userName,password);
}
}
@Service
public class accountService {
private Logger logger = LoggerFactory.getLogger(accountService.class);
public void Login(String userName){
logger.info("用户:{}登录成功,积分+1",userName);
}
}
@Service
public class couponService {
private Logger logger = LoggerFactory.getLogger(couponService.class);
public void Login(String userName){
logger.info("用户:{}登录成功,优惠券下发一张",userName);
}
}
常规实现的缺点
在这里我们就会发现一个很严重的问题,就是我们需要引入大量的Service,如果登录相关的功能再添加,就会越来越复杂,而且代码之间的耦合也会越来越高。
基于事件开发的思路
我们尝试将登录看做一个事件 而非一串动作,我们把登录这件事发布出去,然后让对这件事关心的人做出反应。这是一种很高明的模式,减轻了发布者与响应者之间的耦合。
要完成事件的发布与接收,我们需要以下几个对象
-
可以被发布的事件本身
-
能发送事件的工具
-
能接收事件的对象
基于事件开发实操
我们先来创建一个可以被发送的事件本身(登陆成功事件 ),他需要继承ApplicationEvent
并实现方法。
scala
//TODO 这里不需要放入ioc,下面的发布者会用到它,会自动放进去
public class LoginSuccessEvent extends ApplicationEvent {
//TODO 这里会接收一个资源,传什么大家自己定
public LoginSuccessEvent(Object source) {
super(source);
User user = (User)source;
System.out.println( user.getUserName()+"登录啦~~");
}
}
然后我们创建一个发送任何事件的工具,需要实现ApplicationEventPublisherAware
接口
typescript
@Component
public class EventPublisher implements ApplicationEventPublisherAware {
//TODO 定义一个发布者,类型与接口要实现的方法参数的类型要一致
ApplicationEventPublisher publisher;
//TODO 定义一个名为:发布 的方法,借助Springboot给我们赋值的这个发布者,把事件发布出去
public void publish(ApplicationEvent event){
publisher.publishEvent(event);
}
//TODO 把接口实现方法传进来的这个发布者赋值给我们自己的发布者
//TODO 这个方法的参数在Springboot启动时会自动传进来,我们不用自己传
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher=applicationEventPublisher;
}
}
我们现在来创建一个对登陆成功事件感兴趣的人,它同样需要实现一个接口:ApplicationListener
,同时需要定义泛型作为监听的事件类型
java
@Service
public class accountService implements ApplicationListener<LoginSuccessEvent> {
private Logger logger = LoggerFactory.getLogger(accountService.class);
@Override
public void onApplicationEvent(LoginSuccessEvent event) {
User user = (User)event.getSource();
logger.info("用户:{}登录成功,积分+1",user.getUserName());
}
}
我们还有另一种写法,直接在方法上添加@EventListener
注解,并将方法的参数改为指定的事件:
java
@Order()
@Service
public class couponService {
private Logger logger = LoggerFactory.getLogger(couponService.class);
@EventListener
public void onLoginSuccess(LoginSuccessEvent event){
//TODO 抓取资源并强转为User类型
User user = (User) event.getSource();
logger.info("用户:{}登录成功,优惠券下发一张",user);
}
}
最后,这个User对象也贴一下吧
less
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String userName;
private String password;
}
Controller层的写法
less
//TODO 把发布者注入进来
@Autowired
EventPublisher eventPublisher;
@GetMapping("/{userName}/{password}")
public void getMapping(@PathVariable String userName,@PathVariable String password){
//TODO 创建事件所需对象
User user = new User(userName,password);
//TODO 传入user对象当做source构建出时间对象
LoginSuccessEvent loginSuccessEvent = new LoginSuccessEvent(user);
//TODO 把事件对象发布出去
eventPublisher.publish(loginSuccessEvent);
}
关于事件被触发的先后顺序:
我们可以使用@Order
注解来定义,数字越小越靠前 ,但是要注意,这只在同一种写法下生效!继承接口写法 默认要比@EventListener
注解慢触发。
中篇结语
写到这里已经卡的不行了,决定再启一篇写剩下的内容
关于Springboot核心原理与自定义Starter
我贴一下地址,也是分出去写了,大家感兴趣的可以去看