键盘敲烂,月薪过万作业不做,等于没学
当前系列: 从JSP到Spring 修改讲义

概述

SpringBoot仍然基于Spring框架,所以和SpringMVC是“并列”关系,在Spring的基础上,还有SpringCloud等框架:

但SpringBoot旨在提供一种简洁、快速、“开箱即用”的项目构建方式:

  • 内置了tomcat
  • 预置了一些常用package
  • 大幅减少了xml文件配置的使用
  • ……

所以SpringBoot一经推出,就大受欢迎。SpringBoot上面也可以构建MVC项目,但接下来我们的课程,以构建前后端分离的Restful(复习)后端为准。

另见:环境搭建


SpringDataJPA

以注册到数据库为例……

配置Hibernate

首先需要Add Starters(引入依赖组件)。演示:

  • SpringBoot项目上右键 - Spring - Add Starters,选中

  • pom.xml中相应的添加了:
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
    	<groupId>mysql</groupId>
    	<artifactId>mysql-connector-java</artifactId>
    	<scope>runtime</scope>
    </dependency>
  • Maven Dependencies中多出了Hibernate和mysql相关
然后,需要配置数据库连接信息。SpringBoot项目为我们提供了(src - resources -)application.properties文件:
spring.datasource.url=jdbc:mysql://localhost:3306/17bang
spring.datasource.username=root
spring.datasource.password=

spring.jpa.hibernate.ddl-auto=update

PS:

  • .properties格式文件一样可用于log4j等(代替.xml)
  • 除了.properties文件,SpringBoot也支持.yam文件(换行缩进层级组织)
  • 还可以使用API方式(即声明一个类,类里面加注解方法等)进行配置,不推荐,因为这样不便于部署发布(部署发布不应该改源代码而应该改配置文件)
SpringBoot会根据database的connection url自动判断应该连接的数据库、调用相应的connector……

repository

SpringBoot为我们提供了一个(基于Spring Data JPA的)Repository的接口:
@Indexed    //便于(使用索引)更快的扫描到
public interface Repository<T, ID> {

演示:其继承结构,找到SimpleJpaRepository,查看其中的实现……注意:entityManager已自动存在于上下文中

为了便于其他所有Repository重用(拿到entityManager对象和少声明Interger参数),定义一个AbstractRepository:

public abstract class AbstractRepository<T extends BaseEntity> 
	extends SimpleJpaRepository<T, Integer>{
	protected EntityManager em;
	public AbstractRepository(Class<T> domainClass , EntityManager em ) {
		super(domainClass, em);
		this.em = em;
再由UserRepository继承自AbstractRepository:
@Repository
public class UserRepository extends AbstractRepository<User> {
	public UserRepository(EntityManager em) {
		super(User.class, em);

注意:只有在Application所在package中的所有@Repository、@Sevice等才会被SpringBoot默认扫描

演示:在UserController中直接调用entity和repository,在数据库中生成数据……

@Autowired
private UserRepository userRepository;

@PostMapping("/register")
public int Register(User user) {		
	userRepository.save(user);
	return user.getId();

@Transactional

演示:观察save(entity)的源代码,里面调用persist()方法前后都没有transaction,但在方法上方标记了@Transactional……

@Transactional
public <S extends T> S save(S entity) { 

于是事务的生成、提交和回滚都由容器/上下文(Spring Data框架)负责(这仍然是属于Spring而不仅仅是SpringBoot的,即:SpringMVC中也可以使用)。

可以注解在方法上,也可以注解在类上。注解在类上,等同于其所有方法被注解了;

注意方法必须是public的,否则只会事务无效,不会有提示。

@Transactional类中的方法还可以再注解不同属性的的@Transactional,演示(常用的模式):

@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {

@想一想@:事务何时启动,何时提交/回滚呢?以及事务结束以后,connection又怎么处理呢?……

要解决这些问题:

演示:在application.properties中配置显示SpringBoot和Hibernate的log,

logging.level.root=debug
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate=DEBUG
logging.file.name=c:\\log\\spring.log
声明并调用两个事务方法:
@Transactional
public void A() {
	System.out.println("a()....");
}

@Transactional(readOnly = true)
public void B() {
	System.out.println("b()....");
}
public boolean hasName(String username) {
	userRepository.A();
	userRepository.B();
演示说明:
  1. Spring会自动生成EntityManager对象,并启动事务
  2. 不会在方法结束后就立即提交事务
  3. 以便后续还有事务方法的时候,可以在当前线程查找是否有未提交的事务并重用同一未提交事务
  4. 在生成响应(reponse)之前提交事务

#高阶面试题:为什么同一个对象间的方法调用,@Transactional不起作用?#

对比:

  • 分别调用A()和B(),如上所示
  • 在A()中调用B()
@Transactional
public void A() {
	B();  //此时不会生成transaction
}

@Transactional
public void B() {}

@Transactional产生作用依赖于proxy机制,

  1. 从外部调用某方法时,是通过proxy对象实现的,这时候proxy对象中可添加事务逻辑
  2. 非proxy对象中调用对象内方法,不需要经过proxy对象,也就无法产生事务效果

其他要利用AOP才能实现的注解也是一样的,比如@Cacheable……


interceptor配置

生成自定义的interceptor同MVC所示,但如何配置哪些页面需要NeedLogOn呢?

@Configuration  //该类为一个配置类,功能等同于一个bean xml配置文件 public class NeedLogOnConfig extends WebMvcConfigurationSupport {
	@Override
	protected void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new NeedLogOnInterceptor())
			.addPathPatterns("/user/hasName");

WebMvcConfigurationSupport,顾名思义,是一个MVC的配置,可以通过添加@EnableWebMvc引入

@EnableWebMvc public class Application {
这样SpringBoot项目就可以变成一个SpringBoot的MVC项目。

PS:和@ControllerAdvice对应的是@RestControllerAdvice


@ResponseBody

当广泛的使用Ajax之后,很多前端喜欢直接向后台发送JSON格式的数据。

演示:无法自动绑定……

这时候,如果想要让Spring仍然能够自动Model绑定,需要在model前添加@RequestBody注解:

public int Register(@RequestBody User user
即告诉Spring框架:从request的body中取值进行绑定

#理解#:

  • 此时JSON数据确实位与request的body中,除JSON以外,text/xml等也一样
  • 之前无论是url参数还是FormData,都没有在request body中


Model验证

首先需要通过starter或pom.xml添加validation引用:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

然后其他同MVC一样,错误消息存储在BindingResult中

public int Register(@Validated User user, BindingResult error, 
		HttpServletResponse response ) {
	if (error.hasErrors()) {  try {
			response.sendError(501, error.getAllErrors().toString());
PS:在前后端分离的项目中,验证用户输入并给予友好提示是前端的事情……


RestTemplate

微服务(复习)的开发环境中,我们很容易碰到这种场景:在Java Code中(而不是页面中通过a标签)调用另外一个url资源。

这时候使用Spring内置的RestTemplate就非常方便:

@Autowired    //通过依赖注入引入
private RestTemplate template;
注意RestTemplate没有默认内置,需要在Application.java中配置:
@Bean    //让Spring生成这样一个Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}
@试一试@:如果没有这个配置……

然后调用其方法,指定要访问的uri,获取结果:

//url必须是绝对路径,协议域名都可以封装
String host = "http://localhost:9099"; 
String url = host +"/user/hasName/"+ user.getName(); 
ResponseEntity<Boolean> responseEntity = 
		template.getForEntity(url, Boolean.class);
Boolean duplicated = responseEntity.getBody();
System.out.println(responseEntity.getStatusCode());


单元测试

复习:JUnit

因为没有UI层,所以开发调试除了使用postman模拟HTTP请求,还可以引入单元测试启动项目,检查其结果是否符合预期。

PS:MVC也可以单元测试,但用得少,@想一想@:为什么?

  1. handler method之间不会产生复杂依赖,不会互相影响,单元测试的收益很小
  2. 对UI层而言,问题通常不是出现在handler method本身,而是handler method和view的配合上,单纯测试handler method意义不大
  3. handler method由HTTP请求激发,模拟HTTP请求(比如带着cookie)以及响应都是很麻烦的
  4. ……
SpringBoot做restful后端,引入unit test的理由稍微强了那么一点点……

SpringBoot默认就是引入了unit test相关依赖的:

<artifactId>spring-boot-starter-test</artifactId>
可以有两种单元测试方式:
  1. 直接调用调用Controller里面的handler methods(不推荐)
  2. 模拟发起一个HTTP请求(以下演示使用TestRestTemplate发起请求)

TestRestTemplate测试环境

引入测试类,并为其添加两个注解:

@RunWith(SpringRunner.class)    //运行在Spring环境中
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerTest {

@SpringBootTest(和@WebMvcTest相对应,且不能混用)中webEnvironment也是必须的,指定生成一个随机端口的tomcat服务器环境,不写的话会使用一个Mock环境,无法自动依赖注入关键的:

@Autowired
private TestRestTemplate template;
TestRestTemplate和RestTemplate非常类似,但更适宜于测试环境:
  • 容错性更强:4xx和5xx的响应不会抛出异常,而是将其status code等保留在responseEntity中
  • 能够携带身份验证用cookie

新建一个测试方法:

@Test
public void RegisterTest() throws Exception {
注意为@Test选择正确的package:
package org.junit.jupiter.api;

测试的难点是发送请求时正确的设置其内容,即

ResponseEntity<Integer> response = template.postForEntity(host + "/user/register", request, Integer.class);
中request的内容,它可以包括:
  • 新用户的用户名密码
    User fg = new User();
    fg.setName("fg");
  • cookie信息
这些都可以用HttpEntity对象封装:
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.COOKIE, "amount=6");
HttpEntity<User> request = new HttpEntity<User>(fg, headers);
最后Assert:
Assert.isTrue(response.getStatusCode().is2xxSuccessful(), "请求未能成功处理");
Assert.isTrue(response.getBody() > 0, "存入数据后id值不对");
不要忘了被测试方法参数中 @CookieValue 的注解
public int Register(@RequestBody User user, @CookieValue Integer amount) {


Swagger

可用于自动生成后端API的文档,解释:

  1. 为什么需要文档?不然前端咋知道一个一个的uri究竟是干什么的?
  2. 为什么要用swagger,把文档写在代码里?同步集中……

通过maven引入packages:

<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger2</artifactId>
	<version>2.9.2</version>
</dependency>
<dependency>
	<groupId>io.springfox</groupId>
	<artifactId>springfox-swagger-ui</artifactId>
	<version>2.9.2</version>
</dependency>
添加一个SwaggerConfig类:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
理论上就可以跑/swagger-ui.html页面了,然鹅并没有……


addResourceHandlers

因为我们之前声明了一个:

public class NeedLogOnConfig extends WebMvcConfigurationSupport {
导入了MVC的默认配置,会对/swagger-ui.html进行dispatch……

演示:(复习:dispatch和resource

  • 可以通过访问/hello.gif查看src/main/resources/hello.gif
  • 但不能查看src/main/resources/static/hello.gif
因为SpringBoot的默认resource包括:
  • /resources/
  • /static/
  • /public/
  • /META-INF/resources/

要想让/swagger-ui.html避开这种dispatch(复习),就需要在@Configuration类中通过@Override addResourceHandlers进行配置:

@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {

调用registry的addXXX()方法:

registry.addResourceHandler("/swagger-ui.html")
	.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
	.addResourceLocations("classpath:/META-INF/resources/webjars/");

即:对/swagger-ui.html及其要使用的/webjars下请求,不要使用MVC的dispatch机制,而是直接到classpath的/META-INF/resources/……

SpringBoot中的classpath包括main下面的java和resources,以及maven仓库地址

PS:

  • 这些swagger相关的文件应该都是动态生成的
  • Swagger不显示的另外一个原因也可能是被interceptor拦截了,这时候需要调用.excludePathPatterns()方法排除……
#帮助理解#:配置能访问src/main/resources/static/hello.gif
registry.addResourceHandler("/**")
	.addResourceLocations("classpath:/static/");

Docket和ApiInfo

在SwaggerConfig中可以通过注解一个返回值为Docket的Bean对Swagger进行自定义:
@Bean
public Docket createRestApi() {
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            ;
}
其中apiInfo()方法返回的就是一个ApiInfo对象:
private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
            .title("一起帮·源栈欢迎你")
            .description("大神小班·拎包入住")
            .build();
}

其中就可以设置Swagger页面的title、description等。

Docket可以不止一个:

  • 不同的Docket首先可以使用group进行区分
  • 配置不同ApiInfo
    private ApiInfo apiInfoForArticle() {
        return new ApiInfoBuilder()
                .title("一起帮·文章模块")
                .description("")
                .build();
    }

  • Docket中可以指定Controller的basePackage等,确定该Docket中应包含(扫描)的Controller
@Bean
public Docket createRestApiForArticles() {
    return new Docket(DocumentationType.SWAGGER_2)
    		.groupName("文章")
            .apiInfo(apiInfoForArticle())
            .select()
            .apis(RequestHandlerSelectors.basePackage("bangren.controller.Article"))
            .build()
            ;
}

@ApiXxx

相对于上述全局配置,日常开发中用得更多的是注解在:

  • Controller
    @Api(value = "用户模块")
    public class UserController {
  • handler method
    @ApiOperation(value = "检查用户名是否已使用", consumes = "put", produces = "boolean")
    @ApiImplicitParams(
    		@ApiImplicitParam(    //必须置于@ApiImplicitParams之中,不能单独解析	
    				name="username", 
    				required = true, 					
    				example = "飞哥")
    )
    @ApiResponses({
    	@ApiResponse(code = 200, message = "true:重复;false:不重复")
    })	
    public boolean hasName(@PathVariable String username) {
    我们应尽量让swagger自动解析,比如参数的类似啥的,但是这种@CookieValue还无法自动解析:
    @ApiImplicitParams({
    		@ApiImplicitParam(name = "amount", value = "注册失败次数", paramType = "cookie")
    })
    public int Register( @RequestBody  User user, @CookieValue Integer amount) {
    
    另外,user不是基本类型,需要注解在
  • model
    @ApiModel(value = "新注册用户信息")
    public class User extends BaseEntity {
    每个属性再单独注解
    @ApiModelProperty(value = "用户名", required = true)
    private String name;


附录:同MVC比较:

  1. dispatch到handler method:相同
  2. 静态resource:前端可以独立部署,SpringBoot可以不管;要管的话,通过resource handler配置
  3. ViewResolver(比如freemarker):不需要
  4. Model绑定:一样的,注意@RequestBoby获取JSON数据
  5. 底层原理(DI/AOP):相同
  6. 架构:可以(但不建议)更简单,不需要Service层
  7. cookie/session:相同
  8. 页面跳转(PRG):不需要
  9. filter和listener:可以不用;interceptor需要引入MVC相关配置;其他一样
  10. 业务逻辑:一样的
  11. 文件上传/下载:一样,但注意SpringBoot可以作为一个独立组件发布,getRealPath可能失效,所以文件磁盘路径一般都是写死(在配置文件中)
  12. 异常处理:不用页面跳转,其他和Ajax处理一样
  13. 安全/性能:一样

更多内容,可查看官方文档


作业

选择一起帮某一功能模块(比如用户/文章/广告……),将其做成一个Restful风格的微服务项目,通过postman和单元测试开发调试……

学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 从JSP到Spring 中继续学习:

下一课: 已经是最后一课了……

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码