SpringBoot详解

2023-12-01 11:01:40 浏览数 (1)

配置文件详解

SpringBoot实质上是Spring与SpringMVC的再度封装,对一些常见的应用场景进行了默认的配置,使得开发者在使用SpringBoot进行开发的时候无需编写复杂的配置文件,我们可以从项目的运行日志中发现一些信息:

从日志中可以看到,项目是通过Tomcat启动的,端口号是8080,项目路径是''

但考虑到一些特定的场景下我们需要去修改SpringBoot的默认配置,所以SpringBoot也为我们提供了它的配置文件。

如果使用idea的快速构建工具创建一个SpringBoot应用,它会为我们创建一个application.properties文件,这就是SpringBoot的配置文件,SpringBoot同时支持两种类型的配置文件:xml和yaml。所以你也可以将配置文件名修改为application.yaml

需要注意的是配置文件名是不可以随意更改的,也就是说在SpringBoot中你只能定义application.properties或者application.yaml两种配置文件,但其实application.yml也是可以的,它也属于yaml文件类型,配置文件也只能放在resources目录下。

properties文件

properties类型的配置文件已经非常熟悉了,它通过键值对的形式对某些内容进行配置,比如:

代码语言:javascript复制
jdbc.jdbcDriver=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql:///test
jdbc.jdbcUser=root
jdbc.jdbcPassword=123456

而一般情况下,SpringBoot提供的默认配置完全够用,除非你有一一些特殊的需求,比如修改项目启动的服务器端口号(在application.properties中添加配置信息):

代码语言:javascript复制
server.port=8000

此时启动项目,观察日志发现端口号修改生效:

亦或者是修改项目的访问路径:

代码语言:javascript复制
server.servlet.context-path=/springbootdemo

接下来编写控制器:

代码语言:javascript复制
package com.wwj.springbootdemogitchat.controller;

@RestController
public class TestController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello springboot;
    }
}

此时启动项目,访问:http://localhost:8080/springbootdemo/hello即可。

yml文件

properties格式文件大家都很熟悉,就不需要过多介绍了,下面来看看yml格式的配置文件在SpringBoot中的应用。

yml不是一种标记语言,通常以.yml为后缀,是一种直观的能够被电脑识别的数据序列化格式,并且容易阅读。

在yml中是以缩进作为区分每一级别配置的标准,所以刚才配置的端口号和项目访问路径,在yml文件中应该这样配置:

代码语言:javascript复制
server:
  port: 8000
  servlet:
    context-path: /springbootdemo

需要注意的是在每个冒号的后面都必须跟上一个空格:

根据缩进能够判断配置的层次关系,比如这里portservlet的缩进量一样,所以它俩是同级的,都处在server下。

数据类型

如同properties那样,在yml中也能够定义一些值,那么它都有哪些数据类型呢?这些数据类型又该如何定义么?

字面值

字面值,顾名思义,就是普通的值,包括字符串、数值、布尔类型等,它们的定义方法也很简单:

代码语言:javascript复制
name: zhangsan
age: 20
isMan: true

只需要注意一点,在配置数据源的时候,若数据库密码是以数字0开头的,则需要将密码用引号括起来,否则SpringBoot会将密码作为八进制数值进行解析,导致密码错误,连接失败。

对象

对于对象的属性值定义,是这样进行编写的:

代码语言:javascript复制
student:
  id: 1
  name: zhangsan
  age: 20
Map

map集合存放的是键值对,所以写法与对象属性差不多:

代码语言:javascript复制
id: 1
name: zhangsan
age: 20
数组

然后是数组,数组需要使用-表示数组中的元素:

代码语言:javascript复制
nums:
  - 1
  - 2
  - 3
  - 4

-的后面也必须跟上一个空格,数组也可以使用行内写法:

代码语言:javascript复制
nums: [1,2,3,4]
值的注入

掌握了yml中一些数据类型的定义后,我们的目的是如何将这些值注入到JavaBean中,下面一起来看看。

首先在yml文件中定义一些数据:

代码语言:javascript复制
student:
  id: 1
  name: zhangsan
  graduated: false
  subject:
    - java
    - python
    - mysql
  birth: 1998/09/10
  teachers:
    xiaoliu: java
    xiaochen: python
    xiaohong: mysql
  address:
    province: JiangXi
    city: NanChang
    street: DaiShan

在这里我们定义了在数据类型中提到的所有类型数据,接下来就是将这些值注入到JavaBean中了,先提供JavaBean:

代码语言:javascript复制
@Component
@ConfigurationProperties(prefix = "student")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
    private Integer id;
    private String name;
    private Boolean graduated;
    private String[] subject;
    private Date birth;
    private Map<String,String> teachers;
    private Address address;
}
代码语言:javascript复制
@Component
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address {
    private String province;
    private String city;
    private String street;
}

首先肯定是要将Student放入容器中,然后使用@ConfigurationProperties(prefix = "student"),其中prefix指定的是yml文件中的数据前缀:

接下来可以测试一下:

代码语言:javascript复制
@SpringBootTest
class SpringbootDemoGitchatApplicationTests {

    @Autowired
    private Student student;

    @Test
    void contextLoads() {
        System.out.println(student);
    }
}

这里直接将Student对象装配进来,然后输出:

代码语言:javascript复制
Student(id=1, name=zhangsan, graduated=false, subject=[java, python, mysql], birth=Thu Sep 10 00:00:00 CST 1998, teachers={xiaoliu=java, xiaochen=python, xiaohong=mysql}, address=Address(province=JiangXi, city=NanChang, street=DaiShan))

数据成功输出,说明配置文件中的值已经被注入到JavaBean中。

这是较为复杂的数据注入,在SprinBoot底层也大量使用了这种方式来读取配置文件中的值,而对于一些比较简单的数据,SprinBoot也为我们提供了更加优雅的解决方案:@Value

比如这样的一条数据:

代码语言:javascript复制
myapp:
  config:
    info: test

此时可采用@Value注解直接将配置文件中的值注入到JavaBean中的某个属性,如下:

代码语言:javascript复制
@Data
@NoArgsConstructor
@AllArgsConstructor
@Component
public class Config {

    @Value("${myapp.config.info}")
    private String info;
}

需要注意的是,若项目中同时存在application.propertiesapplication.yml两个配置文件,则SpringBoot会优先以application.properties为准。

相关注解介绍

由于SpringBoot遵循约定大于配置原则,使得其大大减少了配置文件的使用,即使需要配置,也推荐使用注解的方式进行,基于此,了解SpringBoot中的注解非常重要。

首先回顾一下Spring注解驱动开发中的一些常用注解:

@Configuration

使用@Configuration注解标记一个类后,这个类将成为配置类,加载这个类中的配置可以取代之前的配置文件。

代码语言:javascript复制
@Configuration
public class MyConfig {
}

@Bean

使用@Bean注解用于将一个类加入到容器中,它相当于配置文件中的<bean>标签,比如这样一段配置:

代码语言:javascript复制
<bean id="student" class="com.wwj.springboot.bean.Student"></bean>

而使用@Bean注解可以这样配置:

代码语言:javascript复制
@Configuration
public class MyConfig {

    @Bean
    public Student student(){
        return new Student();
    }
}

其中方法名为bean的id,测试如下:

代码语言:javascript复制
@Test
void test() {
    ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
    Student student = (Student) context.getBean("student");
    System.out.println(student);
}

@Import

该注解的作用与@Bean类似,但是它能够更方便地将类注册到容器中,比如:

代码语言:javascript复制
@Configuration
@Import(Student.class)
public class MyConfig {

}

@Conditional

该注解可以限制一个类在满足特定条件下才被注册到容器中,关于@Conditional的注解种类非常多,SpringBoot底层也大量使用到了该注解:

我们可以来看看对于Redis的自动配置:

代码语言:javascript复制
@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

该配置类标注了@ConditionalOnClass({RedisOperations.class})注解,该注解的意思是只有当给定的类名在类路径上存在时,即当你将Redis的相关环境引入之后,SpringBoot才会将该类注册到容器中,使得当前的配置生效。

该配置类中的方法上也标注了一个注解:@ConditionalOnMissingBean(name = {"redisTemplate"}),该注解的意思是只有当给定的Bean不存在时,才会将方法中创建的Bean注册到容器中,它保证了该Bean的实例只能有一个。

类似的注解还有很多很多,就不一一介绍了。

@ComponentScan

该注解是一个扫描注解,用于扫描整个项目中的组件,比如:

代码语言:javascript复制
@SpringBootApplication
@ComponentScan("com.wwj.springbootdemogitchat.controller")
public class SpringbootDemoGitchatApplication implements ApplicationContextAware {

    public static void main(String[] args) {

        //修改启动依赖的配置文件
        SpringApplication.run(SpringbootDemoGitchatApplication.class, args);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for (String name : beanDefinitionNames) {
            System.out.println(name);
        }
    }
}

我们首先在类上标注了@ComponentScan("com.wwj.springbootdemogitchat.controller")注解,通过该注解,SpringBoot将扫描com.wwj.springbootdemogitchat.controller包下的类,而该类下只有一个TestController控制器。该类还实现了ApplicationContextAware接口,目的是获取ApplicationContext对象,并通过该对象获取到IOC容器中所有注册的Bean:

默认情况下,SpringBoot其实为我们配置了@ComponentScan注解,也就是说,即使我们不配置@ComponentScan,SpringBoot也会进行组件扫描,而且扫描的是启动类所在的包及其子包下的所有类,看下面的项目结构:

启动类当前处于com.wwj.springbootdemogitchat包,所以SpringBoot将扫描该包及其子包,也就是扫描这个项目下的所有类,看看运行结果也能够印证这一点:

综上所述,我们需要注意一点,就是不要轻易修改启动类的位置,虽然你也可以通过配置@ComponentScan注解来修改扫描路径,但既然SpringBoot已经为我们提供了默认的配置,我们只需要愉快地使用就好了。

还有需要说明的是,该注解同配置文件一样,也可以修改只扫描或者排除的类,写法是一样的:

代码语言:javascript复制
@ComponentScan(value = "com.wwj.springbootdemogitchat",
               useDefaultFilters = false,
               includeFilters = {
                   @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
               },
               excludeFilters = {
                   @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Service.class)
               })

以上介绍的都是Spring中的一些注解,下面将介绍SpringBoot中特有的注解及其作用。

@SpringBootConfiguration

该注解实质上就是@Configuration注解,只不过SpringBoot为了命名风格,重新定义了具有SpringBoot特色的@Configuration注解:

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

@EnableAutoConfiguration

该注解用于启用自动化配置功能,这在SpringBoot中是一个非常重要的注解,我们来看看它的源码:

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

首先该注解上标注了@Import({AutoConfigurationImportSelector.class}),SpringBoot会通过AutoConfigurationImportSelector.class将所有需要导入的组件以全类名的方式返回,这些组件就会被注册到容器中,并给容器导入自动配置类。

@AutoConfigurationPackage

该注解用于指定自动化配置的包,与@EnableAutoConfiguration同理:

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}

它也会通过@Import注解中配置的Registrar.class将启动类所在的包及其子包中的组件注册到容器中。

@SpringBootApplication

该注解用于声明某个类为SpringBoot的启动类,该注解是上述注解的一个集合,即标注该注解便能够完成上述所有注解的功能:

代码语言:javascript复制
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
)
public @interface SpringBootApplication {
    ......
}

SpringBoot基本原理

说完了SpringBoot中的注解之后,终于可以来看看SpringBoot是如何实现自动配置的,只有掌握了SpringBoot中的自动配置原理,才能在默认与定制配置中游刃有余。

首先,SpringBoot会去读取org.springframework.boot:spring-boot-autoconfigure:2.3.5.RELEASE.jar下的spring.facotries文件:

来看看这个文件里写了些什么:

代码语言:javascript复制
# Initializers
org.springframework.context.ApplicationContextInitializer=
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=
org.springframework.boot.autoconfigure.condition.OnBeanCondition,
org.springframework.boot.autoconfigure.condition.OnClassCondition,
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,

......

这里仅截取了文件的部分内容,但不难看出,该文件里编写的都是一些类的全类名,SpringBoot会读取该文件中的EnableAutoConfiguration属性值:

该属性下配置了非常多的配置类,比如里面有一个HttpEncodingAutoConfiguration自动配置类,来看看它的源码:

代码语言:javascript复制
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ServerProperties.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(prefix = "server.servlet.encoding",value = {"enabled"},matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
    private final Encoding properties;

    public HttpEncodingAutoConfiguration(ServerProperties properties) {
        this.properties = properties.getServlet().getEncoding();
    }

    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
        return filter;
    }

    @Bean
    public HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
        return new HttpEncodingAutoConfiguration.LocaleCharsetMappingsCustomizer(this.properties);
    }

    static class LocaleCharsetMappingsCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
        private final Encoding properties;

        LocaleCharsetMappingsCustomizer(Encoding properties) {
            this.properties = properties;
        }

        public void customize(ConfigurableServletWebServerFactory factory) {
            if (this.properties.getMapping() != null) {
                factory.setLocaleCharsetMappings(this.properties.getMapping());
            }

        }

        public int getOrder() {
            return 0;
        }
    }
}

其中@Configuration(proxyBeanMethods = false)注解用于声明该类是一个配置类,并配置了proxyBeanMethods 属性为false,主要是为了取消Bean的代理,提升性能;

@EnableConfigurationProperties注解是为了使@ConfigurationProperties标注的类生效:

代码语言:javascript复制
@ConfigurationProperties(prefix = "server",ignoreUnknownFields = true)
public class ServerProperties {
    ......
}

@ConditionalOnWebApplication(type = Type.SERVLET)注解是@Conditional的衍生注解,这在前面有提到过,而这个注解的作用是检测当前是否拥有Web环境的IOC容器,若是有,才加载该Bean;

@ConditionalOnClass({CharacterEncodingFilter.class})表示只有配置了CharacterEncodingFilter过滤器才加载该Bean;

@ConditionalOnProperty(prefix = "server.servlet.encoding",value = {"enabled"},matchIfMissing = true)表示只有配置了server.servlet.encoding属性值为enabled才加载该Bean,但是它后面又配置了matchIfMissing = true,表示前面限定的属性值并不是必须的。

@ConditionalOnWebApplication(type = Type.SERVLET)@ConditionalOnClass({CharacterEncodingFilter.class})@ConditionalOnProperty(prefix = "server.servlet.encoding",value = {"enabled"},matchIfMissing = true)一起共同构成该自动配置类的限定条件,只有当这些条件均满足之后,该配置类才会生效。

而具体的配置其实是在ServerProperties类中:

代码语言:javascript复制
@ConfigurationProperties(prefix = "server",ignoreUnknownFields = true)
public class ServerProperties {
    private Integer port;
    private InetAddress address;
    @NestedConfigurationProperty
    private final ErrorProperties error = new ErrorProperties();
    private ServerProperties.ForwardHeadersStrategy forwardHeadersStrategy;
    private String serverHeader;
    private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8L);
    private Shutdown shutdown;
    @NestedConfigurationProperty
    private Ssl ssl;
    @NestedConfigurationProperty
    private final Compression compression;
    @NestedConfigurationProperty
    private final Http2 http2;
    private final ServerProperties.Servlet servlet;
    private final ServerProperties.Tomcat tomcat;
    private final ServerProperties.Jetty jetty;
    private final ServerProperties.Netty netty;
    private final ServerProperties.Undertow undertow;
    ......
}

若想使该类生效,需要使用@EnableConfigurationProperties注解。

该类配置了@ConfigurationProperties注解,并指定prefix为server,则它会去读取配置文件中server节点下的数据,并注入到该类中,类中还配置了一些默认的属性值,通过该类就能够进行一些场景下的默认配置。

比如RedisProperties:

代码语言:javascript复制
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
    private String clientName;
    private RedisProperties.Sentinel sentinel;
    private RedisProperties.Cluster cluster;
    private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
    private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
    ......
}

该类中便配置了一些默认值,比如默认使用Redis中的0号数据库,host默认为本机,端口默认为6379。

所以倘若在项目中使用到了Redis,而又需要修改Redis的默认端口号为6380,该怎么做呢?如果你根本没了解过Redis在SpringBoot中是如何配置的,你基本是不会配置的,只能到百度上找。但倘若你十分清楚SpringBoot的自动配置原理,你根本无需寻找百度,只需要找到Redis的配置类,然后查看一下类中的属性是怎么写的,那么你就会配置了:

代码语言:javascript复制
spring:
  redis:
    port: 6380

首先是配置类的前缀,然后需要修改什么属性值,就在后面拼上即可:spring.redis.XXX

包括在最开始我们配置的项目访问路径和端口号,都可以在配置类中找到:

配置方法就是数据前缀 内部类名(如果有内部类的话) 属性名,server.servlet.contextPath,细心的同学可能会发现,在配置文件中配置的不是这样的,而是这样:

代码语言:javascript复制
server:
  servlet:
    context-path: /springbootdemo

这是由于yaml文件的特性,使得contextPath这样的变量可以写为context-path,它也有一个专有名词,叫松散绑定

以下三种写法都是可以的:

代码语言:javascript复制
server:
  servlet:
    context-path: /springbootdemo
    
server:
  servlet:
    contextpath: /springbootdemo
    
server:
  servlet:
    contextPath: /springbootdemo

而在构建SpringBoot项目时引入的starter中就有XXXAutoConfigure:

正因为这样,才使得我们在仅仅引入了spring-boot-starter-web依赖之后,没有编写任何的配置,却能够正常地进行Web开发。

SpringBoot整合MyBatis

首先引入依赖:

代码语言:javascript复制
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

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

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.5</version>
</dependency>

创建数据表:

代码语言:javascript复制
CREATE TABLE student(
	id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(10),
	age INT
);

创建实体类:

代码语言:javascript复制
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Student {
    private Integer id;
    private String name;
    private Integer age;
}

创建Mapper接口:

代码语言:javascript复制
public interface StudentMapper {

    List<Student> getAll();
}

编写Mapper配置文件:

代码语言:javascript复制
<?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.wwj.springbootdemogitchat.dao.StudentMapper">
    <select id="getAll" resultType="com.wwj.springbootdemogitchat.bean.Student">
        select * from student
    </select>
</mapper>

到这里MyBatis需要的接口和配置文件都准备好了,接下来需要在SpringBoot的配置文件中配置MyBatis的相关信息,比如数据源,mapper配置文件位置等。

那么数据源该怎么配呢?其实很简单,我们可以猜测一下,SpringBoot中肯定有一个对应的DataSourceAutoConfiguration,在idea中使用组合键:Ctrl Shift Alt N进行搜索:

果然有这么一个自动配置类,来看看它的源码:

代码语言:javascript复制
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(type = {"io.r2dbc.spi.ConnectionFactory"})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
	......
}

可以看到@EnableConfigurationProperties({DataSourceProperties.class})中声明的就是配置类的位置了:

代码语言:javascript复制
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    private ClassLoader classLoader;
    private String name;
    private boolean generateUniqueName = true;
    private Class<? extends DataSource> type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String jndiName;
    private DataSourceInitializationMode initializationMode;
    private String platform;
    private List<String> schema;
    private String schemaUsername;
    private String schemaPassword;
    private List<String> data;
    private String dataUsername;
    private String dataPassword;
    private boolean continueOnError;
    private String separator;
    private Charset sqlScriptEncoding;
    private EmbeddedDatabaseConnection embeddedDatabaseConnection;
    private DataSourceProperties.Xa xa;
    private String uniqueName;
	......
}

既然找到了配置类,那么接下来的事情就非常简单了,比如配置驱动,那么它的键名就应该是spring.datasource.driverClassName;配置url,键名就应该是spring.datasource.url;若是配置连接数据库的用户名和密码。则键名分别是spring.datasource.usernamespring.datasource.password

由此,我们在SpringBoot的配置文件中对数据源进行如下配置:

代码语言:javascript复制
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql:///springboot
    username: root
    password: 123456

接着需要指定mapper配置文件的位置,找找该怎么配置:

配置如下:

代码语言:javascript复制
mybatis:
  mapper-locations: classpath:mappers/*.xml

需要注意,若是想在日志中看到生成的sql语句,则需要将日志打印级别设置为debug:

代码语言:javascript复制
logging:
  level: com.wwj.springbootdemogitchat.dao:debug

最好指定某个包的日志打印级别,比如这里指定了Mapper接口所在包的日志打印级别为debug,若不指定包名,则配置将全局生效,这样会使得整个项目的日志非常多,不方便查看。

最后需要在启动类上标注@MapperScan注解,将mapper接口扫描到容器中:

代码语言:javascript复制
@SpringBootApplication
@MapperScan("com.wwj.springbootdemogitchat.dao")
public class SpringbootDemoGitchatApplication {

    public static void main(String[] args) {

        SpringApplication.run(SpringbootDemoGitchatApplication.class, args);
    }
}

这样MyBatis的整合就完成了,测试一下:

代码语言:javascript复制
@Autowired
private StudentMapper studentMapper;

@Test
void test() {
    List<Student> studentList = studentMapper.getAll();
    for (Student student : studentList) {
        System.out.println(student);
    }
}

运行结果:

代码语言:javascript复制
2020-11-09 13:47:45.171 DEBUG 19028 --- [           main] c.w.s.dao.StudentMapper.getAll           : ==>  Preparing: select * from student
2020-11-09 13:47:45.216 DEBUG 19028 --- [           main] c.w.s.dao.StudentMapper.getAll           : ==> Parameters: 
2020-11-09 13:47:45.271 DEBUG 19028 --- [           main] c.w.s.dao.StudentMapper.getAll           : <==      Total: 2
Student(id=1, name=zhangsan, age=20)
Student(id=2, name=lisi, age=25)

MyBatis逆向工程

如果使用MyBatis逆向工程的话,前面繁琐的操作都可以略去,只需要进行少量配置即可,下面一起来看看。

首先把实体类、mapper接口和mapper配置文件统统删掉,然后引入MyBatis的逆向插件:

代码语言:javascript复制
<!-- MyBatis逆向插件 -->
<plugin>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-maven-plugin</artifactId>
    <version>1.3.0</version>

    <!-- 插件的依赖 -->
    <dependencies>
        <dependency>
            <groupId>org.mybatis.generator</groupId>
            <artifactId>mybatis-generator-core</artifactId>
            <version>1.3.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
    </dependencies>
</plugin>

然后添加运行程序:

选择maven:

并在右侧填写运行参数:

代码语言:javascript复制
mybatis-generator:generate -e

然后编写generatorConfig.xml文件(文件名必须为generatorConfig.xml):

代码语言:javascript复制
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <!-- mybatis-generator:generate -->
    <context id="atguiguTables" targetRuntime="MyBatis3">
        <commentGenerator>
            <!-- 是否去除自动生成的注释 true:是;false:否 -->
            <property name="suppressAllComments" value="true" />
        </commentGenerator>
        <!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
        <jdbcConnection
                driverClass="com.mysql.jdbc.Driver"
                connectionURL="jdbc:mysql://localhost:3306/springboot"
                userId="root"
                password="123456">
        </jdbcConnection>
        <!-- 默认 false,把 JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true 时把
        JDBC DECIMAL
        和 NUMERIC 类型解析为 java.math.BigDecimal -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>
        <!-- targetProject:生成 Entity 类的路径 -->
        <javaModelGenerator targetProject=".srcmainjava" targetPackage="com.wwj.springbootdemogitchat.bean">
            <!-- enableSubPackages:是否让 schema 作为包的后缀 -->
            <property name="enableSubPackages" value="false" />
            <!-- 从数据库返回的值被清理前后的空格 -->
            <property name="trimStrings" value="true" />
        </javaModelGenerator>
        <!-- targetProject:XxxMapper.xml 映射文件生成的路径 -->
        <sqlMapGenerator targetProject=".srcmainresources"
                         targetPackage="mappers">
            <!-- enableSubPackages:是否让 schema 作为包的后缀 -->
            <property name="enableSubPackages" value="false" />
        </sqlMapGenerator>
        <!-- targetPackage:Mapper 接口生成的位置 -->
        <javaClientGenerator type="XMLMAPPER"
                             targetProject=".srcmainjava"
                             targetPackage="com.wwj.springbootdemogitchat.dao">
            <!-- enableSubPackages:是否让 schema 作为包的后缀 -->
            <property name="enableSubPackages" value="false" />
        </javaClientGenerator>
        <!-- 数据库表名字和我们的 entity 类对应的映射指定 -->
        <table tableName="student" domainObjectName="Student" />
    </context>
</generatorConfiguration>

执行maven程序即可生成实体类、mapper接口和mapper配置文件,然后在SpringBoot的配置文件中配置数据源和mapper配置文件的位置,最后在启动类上标注@MapperScan注解就可以了。

SpringBoot整合Redis

首先引入依赖:

代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后启动虚拟机中的redis,此时我们若想操作虚拟机中的redis,则需要修改SprinBoot的默认配置,因为redis默认配置的ip为当前主机:

修改host为虚拟机的ip:

代码语言:javascript复制
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql:///springboot?serverTimezone=UTC
    username: root
    password: 123456
  # 配置redis
  redis:
    host: 192.168.190.134

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

logging:
  level:
    com.wwj.springbootdemogitchat.dao: debug

此时使用SpringBoot提供的RedisTemplate对redis进行操作:

代码语言:javascript复制
@SpringBootTest
class SpringbootDemoGitchatApplicationTests {

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Test
    void test() {
        //获取ValueOperations对象, 用于操作redis中的string类型数据
        ValueOperations<Object, Object> operations = redisTemplate.opsForValue();
        //存入数据
        operations.set("key1","value1");
        operations.set("key2","value2");
        operations.set("key3","value3");
    }
}

查看redis中的数据:

代码语言:javascript复制
127.0.0.1:6379> keys *
1) "xacxedx00x05tx00x04key1"
2) "xacxedx00x05tx00x04key2"
3) "xacxedx00x05tx00x04key3"

说明RedisTemplate帮助我们完成了对象数据的序列化,然后可以通过RedisTemplate取出数据:

代码语言:javascript复制
@SpringBootTest
class SpringbootDemoGitchatApplicationTests {

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Test
    void test() {
        //获取ValueOperations对象, 用于操作redis中的string类型数据
        ValueOperations<Object, Object> operations = redisTemplate.opsForValue();
        //存入数据
        operations.set("key1","value1");
        operations.set("key2","value2");
        operations.set("key3","value3");
        //取出数据
        System.out.println(operations.get("key1"));;
        System.out.println(operations.get("key2"));;
        System.out.println(operations.get("key3"));;
    }
}

我们也可以将泛型指定为String,这样就没有序列化的过程,那么存储到redis中的键值就是正常的了:

代码语言:javascript复制
@SpringBootTest
class SpringbootDemoGitchatApplicationTests {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Test
    void test() {
        //获取ValueOperations对象, 用于操作redis中的string类型数据
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        //存入数据
        operations.set("key1","value1");
        operations.set("key2","value2");
        operations.set("key3","value3");
        //取出数据
        System.out.println(operations.get("key1"));;
        System.out.println(operations.get("key2"));;
        System.out.println(operations.get("key3"));;
    }
}
代码语言:javascript复制
127.0.0.1:6379> keys *
1) "xacxedx00x05tx00x04key1"
2) "xacxedx00x05tx00x04key2"
3) "key5"
4) "key6"
5) "key4"
6) "xacxedx00x05tx00x04key3"

而事实上,我们无需这么做,因为SpringBoot已经为我们封装了一个StringRedisTemplate对象,它继承自RedisTemplate<String, String>:

代码语言:javascript复制
public class StringRedisTemplate extends RedisTemplate<String, String>{
    ......
}

用法与RedisTemplate相同:

代码语言:javascript复制
@SpringBootTest
class SpringbootDemoGitchatApplicationTests {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void test() {
        //获取ValueOperations对象, 用于操作redis中的string类型数据
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        //存入数据
        operations.set("key4","value4");
        operations.set("key5","value5");
        operations.set("key6","value6");
        //取出数据
        System.out.println(operations.get("key4"));;
        System.out.println(operations.get("key5"));;
        System.out.println(operations.get("key6"));;
    }
}

SpringBoot整合Thymeleaf

thymeleaf是一种视图模板技术,其实关于视图模板,我们早已有所接触,jsp就是视图模板的一种,然而jsp的缺陷是显而易见的。

首先jsp是依赖于服务器的,若是没有服务器,jsp的页面展示是无法看到的,而且,由于SpringBoot推荐将项目打成jar包,而jsp因为不容易从jar包中读取,所以SpringBoot并没有默认提供对jsp的支持。

而thymeleaf的出现则解决了这些问题,在thymeleaf中,所有动态数据都不会影响到原本HTML页面的显示,下面一起来看看在SpringBoot中该如何使用thymeleaf。

首先引入依赖:

代码语言:javascript复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

关于thymeleaf如何配置可以查看ThymeleafProperties:

只需spring.thymeleaf前缀拼接类属性即可,下面来配置一下:

代码语言:javascript复制
spring:
  thymeleaf:
    prefix:
      classpath: /templates/
    suffix: .html
    # 禁用缓存
    cache: false

若是你的resources目录下有templates文件夹:

那么thymeleaf的前缀和后缀是可以不用配置的,因为SpringBoot默认配置了前缀为/templates/,后缀为.html:

但若想用默认配置,则项目类路径下必须有一个templates目录。

配置完成后,编写一个控制器:

代码语言:javascript复制
@Controller
public class TestController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
}

经过该控制器处理后,方法返回值hello会被拼接成/templates/hello.html,所以我们在templates目录下新建hello.html:

代码语言:javascript复制
<!DOCTYPE html>
<!-- 此处引入thymeleaf名称空间 xmlns:th="http://www.thymeleaf.org" -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="后端显示内容">前端显示内容</h1>
</body>
</html>

我们直接打开该页面看看效果:

然后启动项目,访问http://localhost:8080/hello,看看效果:

到这里,应该能感受到thymeleaf的好处了吧,SpringBoot与thymeleaf的整合也就完成了,非常简单,引入依赖就可以直接使用了,那么接下来就需要学习一下thymeleaf中的语法了。

替换属性值

在刚才的案例中我们已经使用到了thymeleaf的替换属性值语法:

代码语言:javascript复制
<h1 th:text="后端显示内容">前端显示内容</h1>

服务器在解析页面时,会读取th:text中的值,并用它替换标签中原本的内容。

当然了,thymeleaf的强大远不止如此,它可以替换任意标签、任意属性,比如:

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <input type="text" value="前端显示内容" th:value="后端显示内容">
</body>
</html>

显示效果:

又或者是:

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <input type="text" th:type="password">
</body>
</html>

显示效果:

访问属性域

接下来看看在thymeleaf中如何访问域对象中的属性,首先向域对象中存储一些数据:

代码语言:javascript复制
@Controller
public class TestController {

    @Autowired
    private ServletContext servletContext;

    @RequestMapping("/hello")
    public String hello(ModelMap map, HttpSession session){
        map.put("request_name","request");
        session.setAttribute("session_name","session");
        servletContext.setAttribute("application_name","application");
        return "hello";
    }
}

然后在页面中将它们都取出来:

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1 th:text="${request_name}"></h1>
        <h1 th:text="${session.session_name}"></h1>
        <h1 th:text="${application.application_name}"></h1>
    </body>
</html>

通过{属性名}的方式可以直接将它们从域对象中取出来,但需要注意,request域无法通过{域对象(request).request_name}方式获取属性值,但session和application可以;而session和application域无法通过{session_name}、{applicaton_name}的方式获取属性值,因为如果不指定域对象,则默认是从request域中获取。

除了以上的方式,还可以通过这样获取属性值:

代码语言:javascript复制
<h1 th:text="${#httpServletRequest.getAttribute('request_name')}"></h1>
<h1 th:text="${#httpSession.getAttribute('session_name')}"></h1>
<h1 th:text="${#servletContext.getAttribute('application_name')}"></h1>

不过由于太繁琐,不推荐使用。

综上所述,若是想取request域中的属性值,则使用{属性名}即可;若是想取session和application域中的属性值,则使用{session.属性名}、

解析url地址

HTML页面中链接的都是相对路径的资源,当这些页面部署到服务器上时是需要将它们的路径都修改为绝对地址的,既然要修改为绝对地址就需要获取到当前的项目路径。在之前的jsp上,我们通常都是用${pageContext.request.contextPath},然而这种采用EL表达式取值的方式在thymeleaf中是不管用的,我们需要使用thymeleaf中的语法:

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <a href="../login.html" th:href="@{/login.html}">登录页面</a>
</body>
</html>

比如替换超链接中的资源路径,其中@{}能够将contextPath的内容与括号内的值拼接起来,并通过th:href替换原标签的属性值,看效果:

选择和遍历

在页面上显示数据避免不了的就是选择和遍历了,通过选择和遍历数据,才能使得页面上的数据更加丰富多彩。

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:if="${not #strings.isEmpty(request_name)}">request_name不为空</h1>
    <h1 th:if="${#strings.isEmpty(request_name)}">request_name为空</h1>
</body>
</html>

strings对象还能够调用其它非常多的方法,这里就不具体介绍了,来看看如何遍历数据:

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1 th:text="${str}" th:each="str : ${strList}"></h1>
</body>
</html>

遍历操作其实非常简单,首先通过th:each进行遍历,{strList}将从请求域中取出数据,并将每一项数据命名为str,然后通过{str}就可以取出每一项数据了。

对于比较复杂的数据,比如对象集合,其遍历原理也是如此,首先准备好数据:

代码语言:javascript复制
@Controller
public class TestController {

    @Autowired
    private ServletContext servletContext;

    @RequestMapping("/hello")
    public String hello(ModelMap map){
        List<Student> studentList = new ArrayList<>();
        Student student = new Student(1,"zhangsan",20);
        Student student2 = new Student(2,"lisi",21);
        Student student3 = new Student(3,"wangwu",22);
        studentList.add(student);
        studentList.add(student2);
        studentList.add(student3);
        map.put("studentList",studentList);
        return "hello";
    }
}

然后在页面上进行遍历:

代码语言:javascript复制
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <table>
        <thead>
            <tr>
                <td>id</td>
                <td>name</td>
                <td>age</td>
            </tr>
        </thead>
        <tbody>
            <tr th:each="stu : ${studentList}">
                <td th:text="${stu.id}"></td>
                <td th:text="${stu.name}"></td>
                <td th:text="${stu.age}"></td>
            </tr>
        </tbody>
    </table>
</body>
</html>

运行结果:

需要注意的一点是,只要th:each写在了哪个标签上,那么这个标签就会循环出现,所以在遍历对象集合时,应该将th:each放在<tr>标签上,然后在<td>中取出对应的属性值。

0 人点赞