SpringBoot笔记

TMaize 于 2020-05-25 发布

Spring 推荐使用 Maven 和 Gradle 构建项目,这里是以 Maven 为例。推荐使用官方的工具生成项目模板。另外注意的是项目结构不再是 webapp 了,但是它仍然是可以打包为 war 文件的

引导类

SpringBoot 的启动方法,默认情况下,引导类的同级和子级的 java 文件上的注解会被自动扫描

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

如果打包为 war 包,引导类需要继承 SpringBootServletInitializer 重写 configure 方法

@SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

父项目

注意在打为 war 包时,请在版本至少 8.0 以上的 tomcat 中运行,因为 SpringBoot 需要通过 SPI 机制来注册 servlet 或者 filtes 等,因此至少要支持 Servlet3.0 以上的 tomcat 才可以。

因此在打为 war 包的时候需要继承 SpringBootServletInitializer 来进行初始化

打包的产物有

xxx.jar // fatjar
xxx.jar.original // 不包含任何jar
xxx.war // 包含所有的jar,exclude会放到WEB-INF/lib-provided,这样的做好处是既能够通过java -jar xxx.war运行,又能部署到tomcat
xxx.war.original // 不含exclude的jar,只能部署到tomcat中

注意:以 war 包形式部署到 tomcat 后,application.yml 里面 server 的配置都不会生效的,context-path 为 war 包的文件名

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>${packaging.type}</packaging>


    <groupId>com.demo</groupId>
    <artifactId>sb</artifactId>
    <version>1</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <packaging.type>jar</packaging.type>
    </properties>

    <profiles>
        <profile>
            <id>war</id>
            <properties>
                <packaging.type>war</packaging.type>
            </properties>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                    <scope>provided</scope>
                </dependency>
            </dependencies>
        </profile>
    </profiles>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
package com.demo.sb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

// 打war包支持,需要继承SpringBootServletInitializer类,重新configure方法
@SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {


    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }


    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(DemoApplication.class);
    }

}

不继承父项目

有时候项目会有统一的父项目,无法去继承 SpringBoot 的父项目时。官方推荐在内部导入版本控制,然后再打包时完善下打包插件即可

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>

    <groupId>com.demo</groupId>
    <artifactId>sb</artifactId>
    <version>1</version>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.3.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>


    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.demo.sb.DemoApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

配置文件

全量的配置文件见 spring-boot-autoconfigure-2.3.0.RELEASE.jar 下面的META-INF/spring-configuration-metadata.json

从父项目来看会顺序加载如下的配置文件,越往后优先级越高来覆盖前面的值,推荐使用 yaml 格式

<includes>
  <include>**/application*.yml</include>
  <include>**/application*.yaml</include>
  <include>**/application*.properties</include>
</includes>

另外还支持启动时传入配置,这个优先级是最高的

java -Dserver.port=80 -jar sb-1.jar
java -jar xx.jar --server.port=80

读取配置文件

获取单个值

@Value("${server.port}")
private Integer port;

获取多个值,定义 Bean,通过 prefix 获取 prefix 后面的值

@Component
@ConfigurationProperties(prefix = "server")
public class AppConfig extends HashMap<String, Object> {
}

使用 JdbcTemplate

添加配置文件

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=UTF8
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: 123456789
    username: root

添加依赖

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

然后就可以直接在 Service 或者 Controller 中注入 JdbcTemplate,默认使用的连接池是 HikariPool

@Autowired
private JdbcTemplate jdbcTemplate;

使用 JPA

JPA 的是 Java Persistence API 的简写,是 Sun 官方提出的一种 ORM 规范,Hibernate、TopLink 等 ORM 框架 都是 JPA 的实现,其中 Hibernate 已获得 Sun 的兼容认证。Spring-data-jpa 就是 Spring 框架对 JPA 的整合,依赖于 Hibernate。默认使用的连接池是 HikariCP,无需再次引入

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

配置文件

spring:
  jpa:
    database: mysql
    show-sql: true
    database-platform: org.hibernate.dialect.MySQL57Dialect # 使用 InnoDB
    hibernate:
      ddl-auto: update

创建实体

// 由于使用的是Java的ORM规范,这里注解导入的包都是javax.persistence下面的
@Entity
@Table(name = "t_music", indexes = {@Index(name = "idx_title", columnList = "title", unique = true)})
public class Music {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20, nullable = false)
    private String title;
}

创建 Repository

// 由于是继承,默认也包含了一些常用的查询
@Repository
public interface MusicRepository extends JpaRepository<Music, Long> {
    // 符合JPA命名规则的方法定义
    // 无需自己实现方法,JPA会很据方法名自动推断
    Music findFirstByTitle(String title);

    @Query(value = "select * from t_music", nativeQuery = true)
    List<Music> customQuery();
}

然后就可以直接在 Service 或者 Controller 中注入 MusicRepository

@Autowired
private MusicRepository musicRepository;

使用 MyBatis

配置好数据源,mysql 依赖,添加 MyBatis 依赖

<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.0.1</version>
</dependency>

追加 MyBatis 配置

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml

创建数据实体

package com.demo.sb.model;

public class User {
    private Long id;
    private String name;
    private Date createTime;
    // xxx
}

创建 Mapper 接口

package com.demo.sb.mapper;

@Mapper
public interface UserMapper {
    User findById(Long id);
}

创建 Mapper 配置文件,resources/mapper/UserMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.demo.sb.mapper.UserMapper">
    <select id="findById" parameterType="long" resultType="com.demo.sb.model.User">
        select * from t_user where id = #{id}
    </select>
</mapper>

在代码中注入 Mapper 实例

// IDEA 波浪线警告,使用@Resource替代@Autowired
private UserMapper userMapper;

使用 Redis

添加配置

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    password: ''
    timeout: 10000 # 连接超时时间
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 1
        max-wait: 10000 # 连接池最大阻塞等待时间
        time-between-eviction-runs: 1200000 # 每2分钟运行一次空闲连接回收器,数值应小于redis配置文件中的timeout

redis 配置 ,通过CONFIG GET *查看

bind 127.0.0.1
port 6379
maxclients 1000
databases 16
maxmemory 524288000
timeout 300
save 15 1

添加起步依赖,默认使用的链接池是 lettuce

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

注入 RedisTemplate

@Autowired
private RedisTemplate<String, String> redisTemplate;

使用 Jackson

注意这个配置是为 web 返回 json 做转换用的,如果在别的地方也想用到同样的处理可以通过@Autowired注入ObjectMapper

spring:
  jackson:
    date-format: yyyy/MM/dd HH:mm:ss
    time-zone: GMT+8

有时候需要自定义某些转换规则,可以使用 Jackson2ObjectMapperBuilder 自己去定义一个

SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
format.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
ObjectMapper mapper = new Jackson2ObjectMapperBuilder()
        .timeZone(format.getTimeZone())
        .dateFormat(format)
        .build();

关于解析时,由于泛型擦除没有Map<String,Date>.class类型,可以使用 TypeFactory 解决

String json = "{\"date\":\"2020/05/24 23:57:46\"}";
MapType type = objectMapper.getTypeFactory().constructMapType(Map.class, String.class, Date.class);
Map<String, Date> map = objectMapper.readValue(json, type);
System.out.println(map.get("date").getTime());

视图渲染

静态视图:配置类为org.springframework.boot.autoconfigure.web.ResourceProperties.java,可以看到会优先从spring.resources前缀去取值,然后再依次去classpath:/META-INF/resources/classpath:/resources/classpath:/static/classpath:/public/取值

一般都是放在classpath:/public/目录下,直接context-path/path即可访问,注意如果静态路径和 controller 路径相同,优先使用 controller

错误视图:当使用浏览器浏览访问遇到服务器端错误的时候,Spring Boot 默认会配置一个 whitelabel 错误页面,若使用 web 容器自带的可以通过server.error.whitelabel.enabled=false来关闭

错误页对应的路由是/error,如果自己的路由非要使用/error路径可以通过server.error.path修改。可以通过代码来声明错误页面的转发位置

@Component
public class ErrorConfig implements ErrorPageRegistrar {

    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        ErrorPage errorPage = new ErrorPage(HttpStatus.NOT_FOUND, "/customError404");
        registry.addErrorPages(errorPage);
    }
}

简单点的可以在 templates 下写一个 error.html,可以使用${error},${status},${message}变量,如果项根据错误的状态码来判断展示,可以建立/error/500.html

模板视图:SpringBoot 推荐使用 Thymeleaf 模板引擎

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring:
  thymeleaf:
    mode: HTML5
    # 开发配置为false,避免修改模板重启服务器
    cache: false
    # 配置模板路径,默认是templates,可以不用配置
    prefix: classpath:/templates/
@GetMapping("/view1")
public String view1(Model model, HttpServletRequest request) {
    model.addAttribute("param1", "参数1");
    request.setAttribute("param2", "参数2");
    return "view1.html";
}

templates/view1.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!--/* 等价于contextPath */-->
    <script th:src="@{/assets/js/jQuery.js}"></script>
    <title>Document</title>
  </head>
  <body>
    <p th:text="${param1}"></p>
    <p th:text="${param2}"></p>
    <p th:text="${user?.age}"></p>
    <p th:text="${#request.contextPath}"></p>
  </body>
</html>

拦截器

创建拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {
    // 进入controller层之前拦截请求
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("getRequestURI:" + request.getRequestURI());
        return true;
    }
    // 处理请求完成后视图渲染之前的处理操作
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }
    // 视图渲染之后的操作
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**");

    }
}

Controller 切面

可以通过 @ControllerAdvice 来实现 Controller 的增强

@ControllerAdvice
@RestControllerAdvice
public class ControllerHandler {
    // 注入全局参数
    // model.getAttribute("global")
    @ModelAttribute(name = "global")
    public Map<String, String> addGlobal() {
        Map<String, String> data = new HashMap<>();
        data.put("role", "admin");
        return data;
    }

    // 专门用来捕获和处理Controller层的异常
    // 一般用来记录日志
    // 统一错误响应格式
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object exceptionHandler(Exception e, HttpServletRequest request) {
        // log
        Map<String, Object> result = new HashMap<>();
        result.put("code", 500);
        result.put("msg", e.getMessage());
        return result;
    }
}

Junit 测试

使用起步依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
public class MapperTests {
    @Resource
    private UserMapper userMapper;

    @Test
    void findById() {
        System.out.println(userMapper.findById(2L));
    }
}

Log 日志

Spring Boot 默认使用 slf4j+logback 日志系统,无需再次引入依赖的配置。默认输出到控制台,默认的日志级别为 INFO,可以在 yml 配置中进行简略配置

logging:
  level:
    root: info
    com.demo.springmvc.controller.TestController: debug
// 导入的是slf4j的包
private static final Logger logger = LoggerFactory.getLogger(TestController.class);

上面只适用于简单情况,如果需要配置按天分隔,按级别分隔则需要单独写配置文件。由于是使用的 logback 实现,需要添加logback.xml文件,另外命名也有讲究,官方推荐命名为logback-spring.xml,带 spring 后缀的可以使用<springProfile>这个标签

# 不配置也可以
logging:
  config: classpath:logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <contextName>logback</contextName>
    <!-- 定义变量后,可以使用${}来使用变量。 -->
    <property name="log.path" value="D:/logs"/>
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>

    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>trace</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 输出到文件,level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/error.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>365</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 设置某一个包或者具体的某一个类的日志打印级别 -->
    <!-- addtivity,是否向上级logger传递打印信息-->
    <!-- 注意,CONSOLE的level设置为tarce,root的level设置为info,这样自定义类的TRACE才会出来,同时影响root的level -->
    <logger name="com.demo.springmvc.controller.TestController" level="TRACE"></logger>

    <springProfile name="dev">
        <!-- 根据spring的环境进行加入某些节点 -->
    </springProfile>

    <!-- 用来指定最基础的日志输出级别,只有一个level属性,对于没有配置logger的类,将会使用此level-->
    <root level="info">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>
</configuration>

开发时热加载

官方提供 spring-boot-devtools 插件,引入即可做到自动重启,在 IDEA 中需要做额外的配置,实际使用并不好使,自动重载慢。

推荐项目使用 debug 模式运行然后在运行配置是做下调整,使用 IDEA 自带的热加载效果反而好一点

01

参考

官方文档

SpringBoot 整合 Sping Data JPA

Thymeleaf 教程

spring boot 2.x 拦截器

springboot2.0 整合 logback 日志(详细)