SpringBoot入门基础

时间:2024-05-25 13:35:20

目录

SpringBoot入门 (一) HelloWorld. 2

一 什么是springboot 1

二 入门实例... 1

SpringBoot入门 (二) 属性文件读取... 16

一 自定义属性... 15

二 自定义属性配置文件... 18

SpringBoot入门 (三) 日志配置... 21

日志格式及内容... 21

日志输出... 22

输出文件... 22

集成log4j 23

SpringBoot入门 (四) 数据库访问之JdbcTemplate. 29

一 JDBC回顾... 28

二 JdbcTemplate使用示例... 29

三 测试,使用Junit Test 34

四 连接池配置... 36

SpringBoot入门 (五) 数据库访问之spring data jpa. 39

一 什么是Spring Data JPA.. 38

二 简单示例... 38

三 测试... 42

SpringBoot入门 (六) 数据库访问之Mybatis. 46

一 什么是Mybatis. 46

二 SpringBoot集成Mybatis. 47

三 测试... 51

SpringBoot入门 (七) Redis访问操作... 53

一 什么是Redis. 53

二 SpringBoot集成Redis. 54

三 测试... 55

SpringBoot入门 (八) Cache使用... 60

一 为什么要使用缓存... 59

二 使用Cache. 60

1 @Cacheable. 61

2 @CachePut 62

3 CacheEvict 63

SpringBoot入门 (九) MQ使用... 65

一 什么是MQ.. 64

二 SpringBoot集成Active MQ.. 65

SpringBoot入门 (十) 发送邮件... 73

一 邮件发送过程... 72

二 发送邮件示例... 73

SpringBoot入门 (十一) 数据校验... 81

一 什么是数据校验... 80

二 使用示例... 82

SpringBoot入门 (十二) 定时任务... 88

一 SpringBoot 提供的定时任务... 87

二 SpringBoot集成Quartz. 90

返回目录

下一篇

SpringBoot入门 (一) HelloWorld

一 什么是springboot

  springboot是一个全新的框架,它设计的目的简化spring项目的初始环境的搭建和开发,主要有以下几个特点:

  1、简化初始配置 ,可与主流框架集成;

  2、内置Servlet容器,无需在打War包;

  3、使用了Starter(启动器)管理依赖并版本控制;

  4、大量的自动配置,简化开发,方便集成第三方;

  5、提供准生产环境运行时的监控,如指标,健康,外部配置等;

  6、无需XML配置,减少冗余代码 。

  未使用springboot时,如果我们要搭建一个springweb项目环境,我们需要配置web.xml及各种xml的配置文件来集成其他第三方的框架,而这些springboot已经帮我们做了集成,我们不再需要去配置。

二 入门实例

  创建springboot项目有2中方式,1种是通过ide来创建,一种是官方网站创建(https://start.spring.io创建完成后会将工程下载到本地)然后导入ide即可,本文我们通过idea创建项目。

  1 在idea的工具栏 file-->new project 如下图

会弹出选择工程类型的框,如下图

这个时候我们有2种选择,一种是使用Spring Initializr创建,一种是使用Maven或者Gradle创建。这两种方式的不同的地方是使用方法二创建的是空项目,我们需要自己去修改添加所需要的依赖,方式一在创建的过程中,我们可以在ide中直接选择需要的依赖且会生成一个项目的根启动类。本文我们使用Spring Initializr来创建项目。点击上图的Next

输入Group、Artifact、Package的对应的信息,再点击next

这是我们可以选择我们的项目要运行的springboot的版本和需要的依赖包(本文我们只选择依赖web),然后点击Next

这是弹框会显示我们项目的名称及项目的路径,点击Finish。初始时会下载springboot默认依赖的一些jar包,需要一会时间,我们等待它下载完成。之后我们可以看到项目的结构

pom.xml内容如下,2.0.8版本不是一个发布版本,在我们的实际项目中,最好还是要引用release版本的。

<?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>

<groupId>org.allen.demo</groupId>

<artifactId>springboot-helloworld</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>

<name>springboot-helloworld</name>

<description>Demo project for Spring Boot</description>

<parent>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-parent</artifactId>

<version>2.0.8</version>

<relativePath/> <!-- lookup parent from repository -->

</parent>

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<java.version>1.8</java.version>

</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>

</plugin>

</plugins>

</build>

</project>

生成的启动类,一个可执行的main方法。

package org.wl.demo;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class HelloWorldApplication {

public static void main(String[] args) {

SpringApplication.run(HelloWorldApplication.class, args);

}

}

默认在类上边使用了@SpringBootApplication注解,这个注解是一个符合注解,相当于同时使用

@SpringBootConfiguration  指定类为配置类

@EnableAutoConfiguration  开启自动配置

@ComponentScan 指定扫描路径

下来创建一个可以访问的控制器,使用@RestController注解,它是一个复合注解,相当于同时使用了@Controller和@ResponseBody注解

package org.wl.demo.web;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

public class HelloController {

@RequestMapping("/hello")

public String hello(){

return "hello world";

}

}

执行main方法,启动项目,在控台可可以看到启动的日志信息

[           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''

[           main] org.wl.demo.HelloWorldApplication     : Started HelloWorldApplication in 3.591 seconds (JVM running for 4.784)

内嵌的tomcat服务器已经启动,启动端口是8080,更路径是'',我们访问控制器的hello方法

这样一个web项目环境就搭建成功了,使用起来还是很简单方便的。

在上边的日志信息中,默认启动端口是8080,访问根路径是‘’,如果我们希望修改的话也是可以的,只需要在application.properties中修改

修改端口,从默认的8080修改为8090

server.port=8090

修改根路径 修改前是‘’,修改后为‘/helloworld’

server.servlet.context-path=/helloWorld

返回目录

上一篇

下一篇

SpringBoot入门 (二) 属性文件读取

  在上一篇中介绍了在idea中创建springboot工程及使用web工程输出“helloworld”到前端页面,本文学习在springboot工程中读取属性文件中的属性值。

一 自定义属性

  在application.properties文件中添加属性配置项

myapp.name=helloworld

  可以使用@Value 注解来读取,在之前的helloworld工程的控制器中添加属性并读取,如下

@RestController

public class HelloController {

@Value("${myapp.name}")

private String name;

@RequestMapping("/hello")

public String hello(){

System.out.println("myapp name :" + name);

return "helloworld";

}

}

@Value 注解会去application.properties文件中配置属性为myapp.name的属性,并将值赋值给name属性。访问hello方法,可以看到控制台上打印出来的信息

  自定义的属性比较少时使用@Value读取还是比较方便的,如果我们的属性比较多的时候,比如我们要读取数据库连接池配置的很多的属性值,再这样读取的话,就显得不是很美观了。这种情况我们一般定义一个对象,把属性字段作为对象的属性来接收,在属性文件中在增加一个属性

myapp.age=2

  我们定义一个对象MyAppProperties来接收属性值

@Component

@ConfigurationProperties(prefix = "myapp")

public class MyAppProperties {

private String name;

private int age;

@Override

public String toString() {

return "MyAppProperties{" +

"name='" + name + '\'' +

", age=" + age +

'}';

}

//省略getter和setter方法

}

@Component 表示指定当前类为实例,以便可以在spring中使用
@ConfigurationProperties(prefix = "myapp") 表示把application中的myapp开头的属性自动赋值给当前对象的属性,比如把myapp.name的值赋值给name属性,访问hello方法可以看到。

二 自定义属性配置文件

有的时候,我们需要把自己的属性配置信息写在自己的配置文件中,以便和系统的application.properties分开,我们可以在resources目录下新建自己的属性文件my.properties,并添加属性
my.name=helloworld2

my.age=22

定义读取属性的对象类,使用 @PropertySource("classpath:my.properties") 来指定我们当前类要读取的属性文件

@Component

@PropertySource("classpath:my.properties")

@ConfigurationProperties(prefix
= "my")

public class MyAppProperties2 {

private String name;

private int age;

@Override

public String toString() {

return "MyAppProperties2{" +

"name='" + name +
'\'' +

", age=" + age +

'}';

}

// 省略getter和setter方法

}

  修改控制器hello方法测试

@RestController

public class HelloController {

@Resource

private MyAppProperties myAppProperties;

@Resource

private MyAppProperties2 myAppProperties2;

@RequestMapping("/hello")

public String hello(){

System.out.println(myAppProperties.toString());

System.out.println(myAppProperties2.toString());

return "helloworld";

}

}

  访问hello方法,可以看到控制台输出的打印信息

返回目录

上一篇

下一篇

SpringBoot入门 (三) 日志配置

上一篇博文记录了再springboot项目中读取属性文件中配置的属性,本文学习在springboot项目中记录日志。

  日志记录在项目中是很常见的一个功能了,对排查问题有很大帮助,也可以做分类分析及统计。SpringBoot内部使用的是Commons Logging做日志的记录,但是对其他的日志框架也提供了默认的配置,如:Java util Logging,Log4j2,Logback,每一种情况下日志记录器都预先配置为使用控制台输出和可选的文件输出。

日志格式及内容

  如果我们的SpringBoot项目使用的是Starters启动器,默认使用LogBack做日志记录。如我们启动项目时在控制台上看到的

输出的信息有以下几点
日期和时间:精确到毫秒,如 2019-01-24 14:03:14.260

日志级别:日志级别有ERROR,WARN,INFO,DEBUG,TRACE,如上边的INFO

进程号:如上边的 10348

分割线:如上边的 ---

线程名:如上边用[]括起来的

日志名:一般使用的是代码的类名

日志信息:我们在代码中要输出的内容

日志输出

  Spring Boot中默认配置了ERROR、WARN和INFO级别的日志输出到控制台,我们也是可以修改的日志级别的,比如我们修改为debug级别,有两种方式

1 使用命令 $ java -jar myapp.jar --debug

2 在application.properties中添加配置debug=true 开启debug,此配置只会对核心Logger(如内嵌的tomcat容器、hibernate、spring)有效,但是我们自己应用的日志并不会输出为DEBUG级别,需要自己配置,如 logger.level.root=debug

日志级别

  可以通过logger.level.*=LEVEL来改变对应日志级别,如

1

2

3

4

5

6

7

8

logging.level.root=warn

logging.level.org.springframework.web=info

logging.level.org.hibernate=error

######修改mubatis日志级别

logging.level.org.mybatis=info

#mapper接口为debug级别

logging.level.mybatis mapper接口所在包=debug

输出文件

  SpringBoot默认只在控制台输出日志信息,如果想把日志信息写入文件中记录,需要在application.properties中配置,如

1

2

3

4

5

6

7

8

##文件存放路径

logging.path=/usr/local/log

##写入的文件

logging.file=myapp.log

##日志文件大小,超过大小时会在新的文件中记录

logging.file.max-size=10Mb

##日志保存时间 天

logging.file.max-history=15

集成log4j

  SpringBoot在高版本中已经不支持log4j了,支持的是log4j2。在pom.xml中引入需要的log4j2的jar包

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<exclusions>

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-logging</artifactId>

</exclusion>

</exclusions>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-log4j2</artifactId>

</dependency>

由于web依赖包中已经有了log的依赖,所以我们选去掉web中的log依赖,避免jar包冲突。

  然后配置log4j2的属性,在官方文档中可以看到集成的日志文件的命名如下

  在resources目录下创建一个log4j2-spring.xml文件

<?xml
version="1.0" encoding="utf-8"?>

<configuration>

<properties>

<!-- 文件输出格式 -->

<property name="PATTERN">%d{yyyy-MM-dd
HH:mm:ss,SSS} |-%-5level [%thread] %c [%L] -| %msg%n</property>

</properties>

<appenders>

<!--添加一个控制台追加器-->

<Console name="Console"
target="system_out">

<PatternLayout
pattern="${PATTERN}" />

</Console>

<!--添加一个文本追加器,文件位于E:\\logs目录下,名为HelloWorld.log-->

<File name="File"
fileName="E:\\logs\\HelloWorld.log">

<PatternLayout>

<pattern>${PATTERN}</pattern>

</PatternLayout>

<Policies>

<SizeBasedTriggeringPolicy size="10 MB" />

</Policies>

</File>

</appenders>

<loggers>

<root level="info">

<appenderref
ref="File" />
       <!--为了直观,在控制天中也输出info级别的日志-->

<appenderref
ref="Console" />

</root>

<!--把org.springframework包下的所有日志输出到log文件,additivity="false"表示不输出到控制台-->

<Logger
name="org.springframework" level="debug"
additivity="true">

<appenderRef
ref="File" />

</Logger>

</loggers>

</configuration>

  正式项目中,我们需要对上述xml文件中的配置信息做些修改。写一个控制器访问,看看输出的信息

@RestController

public class HelloController {

private static final Logger logger = LoggerFactory.getLogger(HelloController.class);

@RequestMapping("/hello")

public String hello(){

logger.info("使用log4j输出日志!");

return "helloworld";

}

}

控制台和生成的log文件中输出的信息一致

返回目录

上一篇

下一篇

SpringBoot入门 (四) 数据库访问之JdbcTemplate

JDBC回顾

  最早是在上学时接触的使用JDBC访问数据库,主要有以下几个步骤:

1 加载驱动 Class.forName(Driver)

2 获取数据库连接 conn = DriverManager.getConnection(url, user,password)

3 创建一个statement对象来访问操作数据库 statement = conn.createStatement();

4 执行SQL,访问操作数据库 rs = statement.execute(sql)

5 得到结果集,业务处理

6 关闭连接,释放资源 statement.close(); conn.close();

使用jdbc访问并操作数据库时比较麻烦,后来也做了一定的封装,主要是对statement的创建前和sql执行后进行的封装。

二 JdbcTemplate使用示例

  目前我们使用的主流的开源的数据库访问框架主要有Hibernate,Mybatis,SpringJdbc。SpringJdbc时spring对JDBC封装后的一个ORM框架,JdbcTemplate就是SpringJdbc对外提供的一个访问数据库的接口类,我们通过它可以很容易的实现数据库的访问操作。但是需要我们自己根据业务手写相关的SQl,然后执行。

在spring boot项目中引入依赖,本文练习中使用的时MySql数据库

1

2

3

4

5

6

7

8

9

<dependency>

       <groupId>org.springframework.boot</groupId>

       <artifactId>spring-boot-starter-jdbc</artifactId>

</dependency>

        

  <dependency>

     <groupId>mysql</groupId>

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

  </dependency>

 

在application.properties中配置数据库连接相关信息

1

2

3

4

5

6

7

8

9

#数据库配置

#连接

spring.datasource.url=jdbc:mysql://localhost:3306/test

#账号

spring.datasource.username=root

#密码

spring.datasource.password=123456

#驱动

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

完成以上配后,在启动springboot项目时,会自动把数据源信息注入到JdbcTemplate中,我们只需要在使用的地方注入JdbcTemplate就可以了

定义要操作数据库的相关方法接口

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

public interface UserDao {

    /**

     * 添加用户

     */

    int insert(UserInfo user);

    /**

     * 根据ID删除用户

     */

    int delete(Long id);

    /**

     * 修改用户

     */

    int update(UserInfo user);

    /**

     * 根据ID查询用户

     */

    UserInfo
queryById(Long id);

    /**

     * 查询所有用户

     *
@return

     */

    List<UserInfo>
queryAll();

    /**

     * 分页查询用户

     *
@param start
开始位置

     *
@param size
要查询的数据条数

     *
@return

     */

    List<UserInfo>
pagedQuery(int
 start, int size);

}

1

<span style="font-size:
18px">
接口的实现</span>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

@Service

public class UserDaoImpl implements UserDao {

    @Autowired

    private JdbcTemplate jdbcTemplate;

    @Override

    public int insert(UserInfo user) {

      String
insertSql = "insert into t_user(name, age) values(?, ?)";

      return jdbcTemplate.update(insertSql, user.getName(),
user.getAge());

    }

    @Override

    public int delete(Long id) {

        String
deleteSql = "delete from t_user where id = ?";

        return jdbcTemplate.update(deleteSql, id);

    }

    @Override

    public int update(UserInfo user) {

        String
updateSql = "update t_user set name = ?, age = ? where id =
?";

        return jdbcTemplate.update(updateSql, user.getName(),
user.getAge(), user.getId());

    }

    @Override

    public UserInfo queryById(Long id) {

        String
sql = "select * from t_user where id = ?";

        return jdbcTemplate.queryForObject(sql, new Object[]{id}, new BeanPropertyRowMapper<UserInfo>(UserInfo.class));

    }

    @Override

    public List<UserInfo> queryAll() {

        String
sql = "select * from t_user";

        return jdbcTemplate.query(sql, new BeanPropertyRowMapper(UserInfo.class));

    }

    @Override

    public List<UserInfo> pagedQuery(int start, int size) {

        String
sql = "select * from t_user limit ?, ?";

        return jdbcTemplate.query(sql, new Object[]{start, size}, new BeanPropertyRowMapper(UserInfo.class));

    }

}

 

可以看到,使用JdbcTemplate时我们完全省去了使用JDBC时的创建连接,释放资源等操作。不同数据库的分页查询不一样,MySql中用的limit关键字,第一个参数代表获取数据的起点,第二个是要查询的数量,如:limit 5,5  我们查询出来的是第6行到10行 共5条数据

测试,使用Junit Test

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

@RunWith(SpringRunner.class)

@SpringBootTest

public class SpringbootJdbctemplateApplicationTests
{

    @Autowired

    private UserDaoImpl userDaoImpl;

    @Test

    public void testInsert(){

        UserInfo
user = new
 UserInfo("kg", 21);

        userDaoImpl.insert(user);

    }

    @Test

    public void testDelete(){

        long id = 2;

        userDaoImpl.delete(id);

    }

    @Test

    public void testUpdate(){

        long id = 1;

        UserInfo
user = new
 UserInfo();

        user.setId(id);

        user.setName("kobe");

        user.setAge(3);

        userDaoImpl.update(user);

    }

    @Test

    public void testQueryById(){

        long id = 1;

        UserInfo
user = userDaoImpl.queryById(id);

    }

    @Test

    public void testQueryAll(){

        List<UserInfo>
list = userDaoImpl.queryAll();

    }

    @Test

    public void testPagedQuery(){

        List<UserInfo>
list = userDaoImpl.pagedQuery(1, 2);

    }

}

在启动项目时,可以看到在控制台上输出的日志信息中有以下内容

连接池配置

  在我们没有配置数据库连接池时,springboot默认给我们提供的Hikari连接池。如果我们像使用其他的,我们可以自己配置,比如我们想使用阿里的Druid连接池,我们需要引入依赖

1

2

3

4

5

<dependency>

            <groupId>com.alibaba</groupId>

            <artifactId>druid</artifactId>

            <version>1.1.10</version>

        </dependency>

为了测试方便,我这里直接用@Value注解来获取配置信息

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

@SpringBootConfiguration

public class DataSourceConfig {

    @Value("${spring.datasource.driver-class-name}")

    private String DRIVER;

    @Value("${spring.datasource.url}")

    private String URL;

    @Value("${spring.datasource.username}")

    private String USER;

    @Value("${spring.datasource.password}")

    private String PASSWORD;

    @Bean

    public JdbcTemplate jdbcTemplate(){

        JdbcTemplate
jdbcTemplate = new
 JdbcTemplate();

        jdbcTemplate.setDataSource(dataSource());

        return jdbcTemplate;

    }

    @Bean

    public DataSource dataSource(){

        System.out.println("初始化数据源start。。。");

        DruidDataSource
dataSource = new
 DruidDataSource();

        dataSource.setDriverClassName(DRIVER);

        dataSource.setUrl(URL);

        dataSource.setUsername(USER);

        dataSource.setPassword(PASSWORD);

        //连接池的其他的属性。。。

        dataSource.setMaxActive(5);

        //...

        System.out.println("初始化数据源end。。。");

        return dataSource;

    }

}

 @SpringBootConfiguration 这个注解说明当前类是一个配置类,需要被特殊处理,在扫描时发现有@Bean
注解,会把当前方法返回的类Bean注入的容器中,方法名会别标记为注入的Bean的别名,如上JdbcTemplate使用的DataSource就从默认的Hikari修改为Druid了,启动项目可以看到控制台日志信息

返回目录

上一篇

下一篇

SpringBoot入门 (五) 数据库访问之spring data
jpa

本文记录学习使用spring data
jpa访问数据库

什么是Spring Data JPA

  JPA(Java Persistence API)是Sun官方提出的Java持久化规范。它为Java开发人员提供了一种对象、关联映射工具来管理Java应用中的关系数据。主要是为了简化现有的持久化开发工作和整合ORM技术,对不同的ORM框架提供统一的规范标准。

  Spring Data JPA 是Spring基于Hibernate框架和JPA(Java Persistence API)规范的基础上封装的一套JPA应用框架,它提供了增、删、改、查等一些常用得方法供开发者调用,可以轻松实现实现对数据的访问和操作。

简单示例

  要使用spring data jpa,我们首先需要在pom.xml中引入所需要的依赖jar包

1

2

3

4

5

6

7

8

9

<dependency>

  <groupId>org.springframework.boot</groupId>

      <artifactId>spring-boot-starter-data-jpa</artifactId>

    </dependency>

        <dependency>

          <groupId>mysql</groupId>

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

        </dependency>

  在application.properties文件配置数据源连接信息

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#驱动

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://localhost:3306/test

#用户

spring.datasource.username=root

#密码

spring.datasource.password=123456

#ddl create:不论数据库表有没有都是创建,update:当表存在时更新,不存在是创建

spring.jpa.properties.hibernate.hbm2ddl.auto=update

#方言

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

#输出SQL

spring.jpa.show-sql=true

#对SQL格式化

spring.jpa.properties.hibernate.format_sql=true

  创建一个实体对象,完成与数据库表之间的映射

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

@Entity

@Table(name="t_user")

public class UserInfo {

    @Id

    @GeneratedValue(strategy
= GenerationType.AUTO)

    private Long id;

    @Column(name
= "name", nullable = true, length = 30)

    private String name;

    @Column(name="age",
nullable = true, length = 3)

    private Integer age;

    public UserInfo() {

    }

    public UserInfo(String name, Integer age) {

        this.name
= name;

        this.age
= age;

    }

    @Override

    public String toString() {

        return "UserInfo{" +

                "id=" + id +

                ",
name='"
 + name + '\'' +

                ",
age="
 + age +

                '}';

    }

    //省略getter和setter

}

@Entity 说明当前类为一个实体类,默认属性name可以不输入,默认就是当前类名
@Table(name="t_user") 说明当前类映射的数据库表是t_user
@Id 说明该字段是表的主键
@GeneratedValue(strategy = GenerationType.AUTO) 说明主键的生成策略是 GenerationType.*,*有AUTO、INDENTITY、SEQUENCE 和 TABLE可供选择
@Column(name = "name", nullable = true, length = 30) 说明当前字段在数据库表中对应的列属性,name:表中的列名,nullable:允许为空,length:长度不超过30

  前边已经说过了,Spring Data JPA 已经给我们提供了具体的接口供调用,所以只需要继承他的方法来使用就可以了,创建接口并继承JpaRepository

1

2

3

public interface UserRepository extends JpaRepository<UserInfo, Long> {

}

  在IDEA中打开类图(如上图),可以看到我们继承的JpaRepository 通过继承关系已经有了增删该查(CrudRepository)和分页排序(PagingAndSortingRepository),所以我们的UserRspository也就有了对应的方法

测试 

  使用Junit做个简单的测试

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

@RunWith(SpringRunner.class)

@SpringBootTest

public class JpaApplicationTests {

    @Resource

    private UserRepository userRepository;

    /**

     * 保存用户信息

     */

    @Test

    public void save() {

        UserInfo user = new UserInfo("卢俊义", 3);

        userRepository.save(user);

    }

    /**

     * 根据ID删除

     */

    @Test

    public void delete() {

        long id = 3;

        userRepository.deleteById(id);

    }

    /**

     * 修改

     */

    @Test

    public void update() {

        UserInfo user = new UserInfo();

        user.setId(Long.valueOf(3));

        user.setName("老卢");

        user.setAge(5);

        userRepository.saveAndFlush(user);

    }

    /**

     * 根据ID查询

     */

    @Test

    public void selectById() {

        long id = 3;

        UserInfo user = userRepository.findById(id).get();

        System.out.println(user);

    }

    /**

     * 查询所有

     */

    @Test

    public void selectAll() {

        List<UserInfo> userList = userRepository.findAll();

        System.out.println(userList);

    }

    /**

     * 根据Id倒序查询

     */

    @Test

    public void selectAllOrder() {

        Sort sort = new Sort(Sort.Direction.DESC, "id");

        List<UserInfo> userList = userRepository.findAll(sort);

        System.out.println(userList);

    }

    @Test

    public void pageSelect() {

        int currentPage = 1;

        int pageSize = 2;

        //分页查询

     Pageable page = PageRequest.of(currentPage, pageSize);

     Page<UserInfo> pageObject = userRepository.findAll(page);

      //分页 ID倒序

     Sort sort = new Sort(Sort.Direction.DESC, "id");

 Pageable pageOrder = PageRequest.of(currentPage, pageSize, sort);

 Page<UserInfo> pageObject2 = userRepository.findAll(pageOrder);

    }

}

返回目录

上一篇

下一篇

SpringBoot入门 (六) 数据库访问之Mybatis

本文记录学习在SpringBoot中使用Mybatis。

什么是Mybatis

  MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录(官方定义);

官方使用示例代码如下:

1

2

3

4

5

6

7

8

9

10

String resource = "org/mybatis/example/mybatis-config.xml";

InputStream inputStream = Resources.getResourceAsStream(resource);

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();

try {

  BlogMapper mapper = session.getMapper(BlogMapper.class);

  Blog blog = mapper.selectBlog(101);

} finally {

  session.close();

}

通过示例程序可以看到它的执行过程主要有以下几个步骤:

  1 应用程序启动时,根据配置文件生成一个SqlSessionFactory;

  2 通过SqlSessionFactory的到一个SqlSession;

  3 SqlSession内部通过Excutor执行对应的SQl;

  4 返回处理结果;

  5 释放资源;

SpringBoot集成Mybatis

  Mybatis使用有2中方式,一种是使用注解,即在接口定义的方法上通过注解写入要执行的SQL和要完成的数据映射;一种是使用xml配置文件即在xml文件中写相关SQL和完成表与实体的映射。本文我们使用xml文件。

  首先需要引入SpringBoot集成Mybatis的依赖jar包,这里我们只引入了Spring与Mybatis集成的包,springboot默认会自动帮我们引入其他依赖的jar包。

1

2

3

4

5

<dependency>

            <groupId>org.mybatis.spring.boot</groupId>

            <artifactId>mybatis-spring-boot-starter</artifactId>

            <version>1.3.2</version>

        </dependency>

在application.properties文件中配置数据库连接信息和指定mybatis的接口对应的xml

1

2

3

4

5

6

7

#datasoure

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://localhost:3306/test

spring.datasource.username=root

spring.datasource.password=123456

#mybatis

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

如上,我们的mapper xml文件放在reources目录下的mybatis文件夹下

写一个接口,定义我们要完成的功能

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

@Mapper

public interface UserMapper {

    /**

     * 根据ID删除

     */

    int deleteByPrimaryKey(Long id);

    /**

     * 新增

     */

    int insert(UserInfo record);

    /**

     * 根据ID查询

     */

    UserInfo selectByPrimaryKey(Long id);

    /**

     * 修改

     */

    int updateByPrimaryKey(UserInfo record);

    /**

     * 查询所有

     */

    List<UserInfo> selectAll();

}

@Mapper 该注解说名当前类是一个Mybatis的Mapper接口类,交给spring管理,令一种方式是使用注解 @MapperScan(basePackages="...") 说明basePackages及其子包下的接口都交给spring管理,我们多数情况下都会使用@MapperScan这个注解,这样我们就不用在每一个Mapper接口上写注解了。
在xml文件中完成SQl,实现具体的方法,如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

<?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="org.allen.demo.dao.UserMapper"
 >

  <resultMap
id="BaseResultMap"
 type="org.allen.demo.domain.UserInfo" >

    <id
column="id"
 property="id" jdbcType="BIGINT" />

    <result
column="age"
 property="age" jdbcType="INTEGER" />

    <result
column="name"
 property="name" jdbcType="VARCHAR" />

  </resultMap>

  <sql
id="Base_Column_List"
 >

    id, age,
name

  </sql>

  <select
id="selectByPrimaryKey"
 resultMap="BaseResultMap" parameterType="java.lang.Long" >

    select

    <include
refid="Base_Column_List"
 />

    from t_user

    where id =
#{id,jdbcType=BIGINT}

  </select>

  <select
id="selectAll"
 resultMap="BaseResultMap">

    select

    <include
refid="Base_Column_List"
 />

    from t_user

  </select>

  <delete
id="deleteByPrimaryKey"
 parameterType="java.lang.Long" >

    delete from
t_user

    where id =
#{id,jdbcType=BIGINT}

  </delete>

  <insert
id="insert"
 parameterType="org.allen.demo.domain.UserInfo" useGeneratedKeys="true" keyProperty="id">

    insert into
t_user (id, age, name)

    values
(#{id,jdbcType=BIGINT}, #{age,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR})

  </insert>

  <update
id="updateByPrimaryKey"
 parameterType="org.allen.demo.domain.UserInfo" >

    update
t_user

    set age =
#{age,jdbcType=INTEGER},

      name
= #{name,jdbcType=VARCHAR}

    where id =
#{id,jdbcType=BIGINT}

  </update>

</mapper>

 从这个xml文件,我们可以看到以下几点:

1 namespace="org.allen.demo.dao.UserMapper" 通过namespace来指定将当前xml文件的SQL实现的对应的接口Mapper

2 通过 resultMap 完成实体类与数据库表字段的映射

3 xml中的SQL语句都必须写在<select> <insert> <update><delete>内部

4 每一个标识的id都必须与接口中的方法名保持一直

5 insert 中使用的 useGeneratedKeys 设置是否使用JDBC的getGenereatedKeys方法获取主键并赋值到keyProperty设置的领域模型属性中。如果设置了true,我们就可以在insert完成后直接通过当前操作的实体类获取数据库主键。

测试

  使用Junit做测试

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

@RunWith(SpringRunner.class)

@SpringBootTest

public class MybatisApplicationTests
{

    @Resource

    private UserMapper userMapper;

    @Test

    public void save() {

        UserInfo
user = new
 UserInfo();

        user.setName("world");

        user.setAge(2);

        userMapper.insert(user);

        System.out.println("保存后的ID:" + user.getId());

    }

    @Test

    public void delete() {

        long id = 5;

        userMapper.deleteByPrimaryKey(id);

    }

    @Test

    public void update() {

        UserInfo
user = new
 UserInfo();

        user.setName("world");

        user.setAge(5);

        long id = 3;

        user.setId(id);

        userMapper.updateByPrimaryKey(user);

    }

    @Test

    public void selectById() {

        long id = 3;

        userMapper.selectByPrimaryKey(id);

    }

    @Test

    public void selectAll() {

        List<UserInfo>
userList = userMapper.selectAll();

    }

    

}

返回目录

上一篇

下一篇

SpringBoot入门 (七) Redis访问操作

本文记录学习在SpringBoot中使用Redis。

什么是Redis

  Redis 是一个速度非常快的非关系数据库(Non-Relational Database),它可以存储键(Key)与 多种不同类型的值(Value)之间的映射(Mapping),可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,还可以使用客户端分片来扩展写性能。Redis主要有以下几个优点:

  1 性能极高,它每秒可执行约 100,000 个 Set 以及约 100,000 个 Get 操作;

  2 丰富的数据类型,Redis 对大多数开发人员已知的大多数数据类型提供了原生支持,这使得各种问题得以轻松解决;

  3 原子性,因为所有 Redis 操作都是原子性的,所以多个客户端会并发地访问一个 Redis 服务器,获取相同的更新值;

  4 丰富的特性,Redis 是一个多效用工具,有非常多的应用场景,包括缓存、消息队列(Redis 原生支持发布/订阅)、短期应用程序数据(比如 Web 会话、Web 页面命中计数)等。

  目前我们常用的Value的数据类型有String(字符串),Hash(哈希),List(列表),Set(集合),Zset(有序集合)。

SpringBoot集成Redis

  SpringBoot提供了对Redis的集成的模块,包是spring-boot-starter-data-redis,它依赖于
spring-data-redis 和 lettuce,lettuce是一个线程安全的redis客户端。

  在pom.xml中引入依赖

1

2

3

4

<dependency>

   <groupId>org.springframework.boot</groupId>

     <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>  

  在application.properties中配置Redis的连接信息

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

# redis 配置

## redis数据库索引(默认0)

spring.redis.database=0

## redis 服务器地址

spring.redis.host=localhost

## redis 服务器端口

spring.redis.port=6379

## redis数据库密码(默认为空)

spring.redis.password=

## redis连接池最大连接数(使用负数表示没有显示,默认8)

spring.redis.lettuce.pool.max-active=8

## 连接池最大阻塞等待时间(使用负值表示没有限制,默认-1)

spring.redis.lettuce.pool.max-wait=-1

## 连接池中的最大空闲连接 默认 8

spring.redis.lettuce.pool.max-idle=8

## 连接池中的最小空闲连接 默认 0

spring.redis.lettuce.pool.min-idle=0

  有了上边这两个步骤之后,我们启动服务后,SpringBoot就会帮我们连接到Redis服务器,并给我们提供了一个RedisTemplate类供我们使用操作Redis。我们在需要用的地方注入RedisTemplate就可以轻松完成操作。

测试

  使用Junit做测试,在我们的单元测试类中注入RedisTemplate

1

2

3

4

5

6

7

8

@RunWith(SpringRunner.class)

@SpringBootTest

public class RedisApplicationTests {

    @Autowired

    private RedisTemplate redisTemplate;

}

  String字符串操作,value是一个字符串,通过delete(key)删除

1

2

3

4

5

6

7

8

9

@Test

    public void testString() throws InterruptedException {

        //字符串

    ValueOperations
strOps = redisTemplate.opsForValue();

    strOps.set("name", "梓&艺");

    System.out.println("字符串结果name:"+strOps.get("name"));

    //删除key

    redisTemplate.delete("name");

}

  对象操作,value是一个对象

1

2

3

4

5

6

7

8

@Test

  public void testObject() throws InterruptedException {

     ValueOperations
objOps = redisTemplate.opsForValue();

     City
city = new
 City(1, "X`A", "西安");

     objOps.set("city",
city);

     System.out.println(objOps.get("city").toString());

        redisTemplate.delete("city");

}

  设置key的过期时间,在set时加上过期时间,通过hasKey(key)来判断key是否还存在

1

2

3

4

5

6

7

8

9

10

@Test

public void testExpire(){

  //过期

   ValueOperations
objOps1 = redisTemplate.opsForValue();

    City city1 = new City(1, "BJ", "北京");

    objOps1.set("expire",
city1, 2000, TimeUnit.MILLISECONDS);

 System.out.println(objOps1.get("expire").toString());

   Thread.sleep(2000);

     System.out.println(redisTemplate.hasKey("expire")); 

}

  hash哈希操作

1

2

3

4

5

6

@Test

  public void testHash() {

     HashOperations
hashOps = redisTemplate.opsForHash();

     hashOps.put("hash","hashKey","hashValue");

     System.out.println(hashOps.get("hash", "hashKey"));

   }

  在上边的代码中可以看出,Hash Set的时候就是在哈希表 Key 中的域(Field)的值设为 value。如果 Key 不存在,一个新的哈希表被创建并进行 Hash set 操作;如果域(Field)已经存在于哈希表中,旧值将被覆盖。

  List列表操作

1

2

3

4

5

6

7

8

@Test

public void testList() {

ListOperations<String,
String> listOps = redisTemplate.opsForList();

listOps.leftPush("list","梓");

listOps.leftPush("list","&");

listOps.leftPush("list","艺");

System.out.println(listOps.leftPop("list"));

    }

  列表操作时我们通过 leftPush
或者 rightPush 来将数据存入列表中,通过 leftPop 或者rightPop将数据取出来,我们可以利用它来实现一个队列。

  Set集合操作

1

2

3

4

5

6

7

8

9

@Test

public void testSet() {

 SetOperations<String,
String> setOps = redisTemplate.opsForSet();

 setOps.add("set","梓");

 setOps.add("set","&");

 setOps.add("set","&");

 setOps.add("set","艺");

 System.out.println(setOps.members("set"));

    }

  Set是一个没有顺序的集合,key相同时,如果value已经存在了,后边进入的会把前边的覆盖掉

  ZSet有序集合操作

1

2

3

4

5

6

7

8

9

10

@Test

  public void testZSet() {

   ZSetOperations
zSetOps = redisTemplate.opsForZSet();

   zSetOps.add("zSet", "梓", 1);

   zSetOps.add("zSet", "&", 2);

   zSetOps.add("zSet", "艺", 3);

   zSetOps.add("zSet", "zi", 1);

   zSetOps.add("zSet", "yi", 3);

   System.out.println(zSetOps.rangeByScore("zSet", 1, 3));

    }

  add 方法的3个参数分别是key,value,数据插入位置。ZSet中存储的数据都是有顺序的,输出时顺序按照存储时设置的从小到大,如果遇到key相同,Value和顺序一样的,后边的会把前边的覆盖掉,range方法后边的2个参数时插入的位置。如上的输出

返回目录

上一篇

下一篇

SpringBoot入门 (八) Cache使用

本文记录学习在SpringBoot中使用Cache。

为什么要使用缓存

  缓存是一个数据交换的缓冲区,在一些条件下可以替代数据库。举个例子:我们有一个查询的业务,访问数据的频率特别高,且每次访问时的查询条件都一样,数据库的数据一直保存不变,这样我们每次查询出来的结果都是一样的。为了降低高频率访问数据库给数据库带来的压力,我们可以在第一次访问后把数据缓存起来,以后再做相同查询时只去缓存中取数据,而不用取数据库再做查询。

使用Cache

  SpringBoot对缓存做了支持以供我们方便快速的使用,我们只需要引入相关依赖就可以了。在项目启动时会自动根据配置进行识别和初始化操作。

  在pom.xml文件中引入对Cache支持的依赖

1

2

3

4

<dependency>

 <groupId>org.springframework.boot</groupId>

 <artifactId>spring-boot-starter-cache</artifactId>

</dependency>

要使用Cache,我们必须在启动时开启Cache,

@EnableCaching 说明开启缓存

1

2

3

4

5

6

7

8

9

@SpringBootApplication

@EnableCaching

public class CacheApplication {

  public static void main(String[] args) {

     SpringApplication.run(CacheApplication.class,
args);

    }

}

  Spring Cache有3个比较重要的注解@Cacheable,@CachePut,@CacheEvict,我们一个一个说明:

1 @Cacheable

  这个注解表明对操作进行缓存,它可以放在类上,表明对当前类中所有的方法的结果进行缓存;也可以放在方法上,表明对当前方法的结果进行缓存。它有3个参数:

  Value 缓存的名称

  Key 缓存的键,可以为空,如果指定要按照 SpEL 表达式编写;如果不指定,则缺省按照方法的所有参数进行组合

  Condition 缓存触发的条件,返回值是true和false,只有满足条件的情况(true)才会加入缓存,默认为空,既表示全部都加入缓存,支持 SpEL表达式。

  实现代码

@RestController

public class CacheController {

@RequestMapping("/hello")

@Cacheable(value="helloCache",
key = "#name", condition
= "#name.length()>2")

public String hello(@RequestParam(value="name", required
= true) String name){

     System.out.println("没有走缓存,去数据库查询数据");

     return "hello,"+name;

    }

}

  我们的方法中有一个参数,模拟数据库查询时的条件,如上,当我们的参数name的长度大于2时,参数加入的缓存中。如果我们请求http://localhost:8080/hello?name=ch,不管我们请求多少次控制台中都会打印出‘没有走缓存,去数据库查询数据’,说明缓存没有触发,如果我们请求http://localhost:8080/hello?name=cache,第一次请求时控制台会打印出‘没有走缓存,去数据库查询数据’,之后不会再有,说明在方法执行前会先去缓存中看是否有结果,有则返回结果,没则执行请求方法,如第一次请求时执行后已经对结果进行了缓存,我们再请求时会直接去缓存中取结果,不会再去执行方法请求数据库了。

2 @CachePut

  它与@Cacheable类似,都是说明要做缓存,而且属性Key,Value,Condition的用法都一样,也可以使用在类或者方法上,区别是它每次都会执行实际方法(如查询数据库)

  代码实现

1

2

3

4

5

6

@RequestMapping("/hello1")

    @CachePut(value="helloCache",
key = "#name", condition
= "#name.length()>2")

    public String hello1(@RequestParam(value="name",
required = true) String name){

        System.out.println("没有走缓存,去数据库查询数据");

        return "hello,"+name;

    }

  当我们请求http://localhost:8080/hello1?name=cache时,不管我们请求第几次,都会在控制台输出‘没有走缓存,去数据库查询数据’。如果数据库数据发生了变化,则会用新的数据替换之前的同value同Key缓存的数据。

3 CacheEvict

  它是用来清除缓存的,同样是可以标记在类和方法上,标记在类上时说明所有方法都会清除缓存。Key,Value,Condition属性用法与Cacheable一样,还有2个属性allEntries和beforeInvocation。

  allEntries表示是否删除缓存中的所有属性,默认是false,表示不,true表示是。当被指定为true时,会忽略掉指定的Key而删除所有。

  beforeInvocation表示删除缓存的时机,默认是false,表示在方法执行完后才删除缓存,true表示在方法执行前就先删除缓存。在方法执行过程中如果发生了异常,默认设置下(false)将不能删除掉缓存。

  代码实现

@RequestMapping("/hello2")

@CacheEvict(value="helloCache",
key="#name", condition="#name.length()>2", allEntries
= true, beforeInvocation = true)

public String hello2(@RequestParam(value="name",
required = true) String name){

 System.out.println("CacheEvict
清除缓存");

 return "hello,"+name;

    }

  使用同样的参数如cache,先请求执行hello方法进行数据缓存,在执行hello2进行删除缓存操作,然后再执行hello1方法会发现在控制台上又输出了‘没有走缓存,去数据库查询数据’,说明执行hello2方法时已经将缓存删除掉了。

注:SpELSpring Expression Language)是一个支持运行时查询和操作对象图的强大的表达式语言,其语法类似于统一 EL,但提供了额外特性,显式方法调用和基本字符串模板函数。

 

返回目录

上一篇

下一篇

SpringBoot入门 (九) MQ使用

本文记录学习在Spring Boot中使用MQ。

什么是MQ

  MQ全称(Message Queue)又名消息队列,是一种异步通讯的中间件。它的作用类似于邮局,发信人(生产者)只需要将信(消息)交给邮局,然后由邮局再将信(消息)发送给具体的接收者(消费者),具体发送过程与时间发信人可以不关注,也不会影响发信人做其它事情。目前常见的MQ有activemq、kafka、rabbitmq、zeromq、rocketmq等。

  使用MQ的优点主要有:

  1 方法的异步执行 使用MQ可以将耗时的同步操作通过以发送消息的方式进行了异步化处理,减少了由于同步而等待的时间;

  2 程序之间松耦合 使用MQ可以减少了服务之间的耦合性,不同的服务可以通过消息队列进行通信,只要约定好消息的内容格式就行;

  JMS(Java Message Service)即java消息服务,是一个Java平台中关于面向消息中间件(MOM)的 API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。JMS的消息机制有2种模型,一种是1对1(Point to Point)的队列的消息,这种消息,只能被一个消费者消费;另一种是一对多的发布/订阅(Topic)消息,一条消息可以被多个消费者消费。ActiveMq是对JMS的一个实现。

二 SpringBoot集成Active MQ

  官网下载一个服务程序,解压后直接启动服务就可以了,下载地址:http://activemq.apache.org/activemq-5158-release.html

  SpringBoot也对Active MQ提供了支持,我们使用时引入具体的依赖即可,修改pom.xml文件,添加依赖

1

2

3

4

<dependency>

 <groupId>org.springframework.boot</groupId>

 <artifactId>spring-boot-starter-activemq</artifactId>

</dependency>

  在application.properties文件中配置Active MQ服务器的连接信息

1

2

3

4

5

spring.activemq.broker-url=tcp://localhost:61616

spring.activemq.user=admin

spring.activemq.password=admin

#消息模式 true:广播(Topic),false:队列(Queue),默认时false

#spring.jms.pub-sub-domain=true

完成以上配置信息后,当我们在启动SpringBoot项目时,会自动帮我们完成初始化操作,并提供一个JmsMessagingTemplate,提提供了我们常用发送消息的各种方法供我们使用。我们只需要在使用的地方注入JmsMessagingTemplate即可使用。

发送队列消息

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@RunWith(SpringRunner.class)

@SpringBootTest

public class ActivemqApplicationTests
{

@Autowired

private JmsMessagingTemplate jmsMessagingTemplate;

@Test

public void testQueueMsg(){

 //创建名称为zyQueue的队列

 Queue queue = new ActiveMQQueue("zyQueue");

 //向队列发送消息

 jmsMessagingTemplate.convertAndSend(queue,"这是一个队列消息!");

    }

}

  消息的接收方,监听消息队列,当队列中有消息时就可以获取到消息

@Component

public class Consumer {

private static DateFormat df =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss

,sss");

    /**

     *
destination
目标地址即队列

     */

     @JmsListener(destination
= "zyQueue")

    public void receiveMessage(String
text){

 System.out.println("接收队列消息时间:"+
df.format(new
 Date()) +", 接收到消息内容:"+text);

    }

}

  执行测试方法发送消息可以看到,控制台输出的消费者接受到消息

队列消息只能有一个消费者,如果有多个消费者同时监听一个队列时,只能有一个拿到消息,我们测试,修改发送方法,循环发送10调消息

@Test

    public void testQueueMsg(){

        //创建名称为zyQueue的队列

        Queue
queue = new
 ActiveMQQueue("zyQueue");

        //向队列发送消息

        for (int i=0;i<10;i++) {

jmsMessagingTemplate.convertAndSend(queue,"这是第"+i+"个队列消息!");

        }

    }

  在Consumer 类中再添加一个消费者,监听队列zyQueue

@JmsListener(destination
= "zyQueue")

public void receiveMessage(String
text){

System.out.println("接收队列消息时间:"+
df.format(new
 Date()) +", 接收到消息内容:"+text);

    }

    @JmsListener(destination
= "zyQueue")

    public void receiveMessage1(String
text){

System.out.println("1接收队列消息时间:"+
df.format(new
 Date())

+",接收到消息内容:"+text);

    }

执行发送消息,看到控制台输出的结果,2个消费者平分了这10条消息

  如果希望监听同一个队列的多个消费者都能接收到所有消息,我们就只能发送Topic消息了,我们修改application.properties中的

#消息模式 true:广播(Topic),false:队列(Queue),默认时false

spring.jms.pub-sub-domain=true

  我们在Consumer 类中添加两个消费者来监听zyTopic队列,接受消息

@JmsListener(destination
= "zyTopic")

public void receiveTopicMessage1(String
text){

System.out.println("消费者1接收消息时间:"+ df.format(new Date()) +", 接收到消息内容:"+text);

    }

@JmsListener(destination
= "zyTopic")

public void receiveTopicMessage2(String
text){

System.out.println("消费者2接收消息时间:"+ df.format(new Date()) +", 接收到消息内容:"+text);

    }

  执行发消息方法,可以看到控制台输出的内容,2个消费者都完整的接收到了5条消息

我们在测试发送消息时修改了属性文件中的配置信息,才可以发送对应的类型的消息,这是由于SpringBoot中默认的是队列消息(查看源码可以知道,监听器默认使用的DefaultJmsListenerContainerFactory),如果我们想在不修改配置信息的情况下可以同时发送Queue和Topic消息怎么办呢,我们需要手动的更改初始的配置类,分别针对Queue和Topic消息提供JmsListenerContainerFactory

  新建一个配置类,如下

@SpringBootConfiguration

public class ActiveMqConfig {

    @Bean("queueListenerFactory")

    public JmsListenerContainerFactory<?>
queueListenerFactory(ConnectionFactory connectionFactory){

        DefaultJmsListenerContainerFactory
factory = new
 DefaultJmsListenerContainerFactory();

        factory.setConnectionFactory(connectionFactory);

        //设置消息模型为队列

        factory.setPubSubDomain(false);

        return factory;

    }

    

    @Bean("topicListenerFactory")

    public JmsListenerContainerFactory topicListenerFactory(ConnectionFactory
connectionFactory){

        DefaultJmsListenerContainerFactory
factory = new
 DefaultJmsListenerContainerFactory();

        factory.setConnectionFactory(connectionFactory);

        //设置消息模型为队列

        factory.setPubSubDomain(true);

        return factory;

    }

}

  在容器启动时会针对两种消息类型,初始化得到两个不同的JmsListenerContainerFactory。下来再修改消费者类,在 @JmsListener 注解中指定 containerFactory,如

@JmsListener(destination
= "zyQueue", containerFactory
= "queueListenerFactory")

    public void receiveMessage(String
text){

        System.out.println("接收队列消息时间:"+
df.format(new
 Date()) +", 接收到消息内容:"+text);

    }

@JmsListener(destination
= "zyTopic", containerFactory
= "topicListenerFactory")

    public void receiveTopicMessage1(String
text){

        System.out.println("消费者1接收消息时间:"+ df.format(new Date()) +", 接收到消息内容:"+text);

    }

  Queue消息使用 queueListenerFactory,Topic消息使用 topicListenerFactory,然后注释掉属性文件中的消息模式配置就可以了。

返回目录

上一篇

下一篇

SpringBoot入门 (十) 发送邮件

本文记录学习在SpringBoot中发送邮件。

一 邮件发送过程

  发送邮件是一个我们在项目中经常会用到的功能,如在用户注册时发送验证码,账户激活等都会用到。完整的一个邮件发送过程主要包含以下几个步骤:

  1 发件人在用户邮件代理上写邮件内容及收件人的邮箱地址;

  2 用户邮件代理根据发件人填写的邮件信息,生成一封符合邮件格式的邮件;

  3 用户邮件代理把邮件发送到发信人的邮件服务器上;

  4 发件人的邮件服务器使用 SMTP 协议把这封邮件发送到收件人的邮件服务器上;

  5 收件人的邮件服务器收到邮件后,把这封邮件放到收件人在这个服务器上的信箱中;

  6 收件人使用用户邮件代理来收取邮件。

二 发送邮件示例

  以前发送邮件时,调用 JavaMail 的相关 API 来开发实现,需要我们自己去组装消息体,代码量比较大,实现起来也不是很容易;Spring 提供的JavaMailSender 对JavaMail的Api做了一定的封装,简化了发邮件的过程。JavaMailSender 提供了强大的邮件发送功能,可支持各种类型的邮件发送。Spring Boot在JavaMailSender 的基础上又做了进一步的封装,让我们的使用更加的简单。

  在pom.xml文件中引入需要得依赖

<dependency>

 <groupId>org.springframework.boot</groupId>

 <artifactId>spring-boot-starter-mail</artifactId>

</dependency>

  在application.properties中配置邮件服务相关信息(测试使用得是QQ邮件服务)

# Email

#邮件默认字符集

spring.mail.default-encoding=UTF-8

#邮件服务地址

spring.mail.host=smtp.qq.com

#邮件服务协议,可以不写(查看源码可知,没有时默认值是smtp )

#spring.mail.protocol=smtp

#端口

spring.mail.port=25

#账号

spring.mail.username=********@qq.com

#密码 使用第三方发送邮件时,需要开通授权,所以此处要写邮箱授权码

spring.mail.password=****************

完成了上边得配置后,在启动服务得时候,SpringBoot会根据我们得配置,为我们初始化好一个JavaMailSender类供我们调用,我们只需要在要使用得地方注入就可以了。

定义一个邮件服务接口,提供我们得邮件服务方法,其他业务中用到发邮件功能时,调用这个接口就可以了

public interface EmailSenderService {

    /**

     * 发送文本消息

     *
@param subject
邮件主题

     *
@param content
邮件内容

     * @param
to
收件人(可变参数,多个时用逗号隔开)

     */

    void sendTextMail(String subject, String content, String... to);

}

@Service

public class EmailSenderServiceImpl implements EmailSenderService {

    @Autowired

    private JavaMailSender javaMailSender;

    //发件人邮箱

    @Value("${spring.mail.username}")

    private String from;

    @Override

    public void sendTextMail(String
subject, String content, String... to) {

        SimpleMailMessage
mailMessage = new
 SimpleMailMessage();

        mailMessage.setFrom(from);

        mailMessage.setTo(to);

        mailMessage.setSubject(subject);

        mailMessage.setText(content);

        mailMessage.setSentDate(new Date());

        javaMailSender.send(mailMessage);

    }

}

  如上,我们在实现类中注入JavaMailSender后直接调用它send方法就可以完成发邮件。其他业务中用到发邮件业务了,直接调用EmailSenderService 接口得方法即可。我们完全也可以把JavaMailSender直接注入到我们得各个业务模块中去,在发邮件时直接调用JavaMailSender得send方法发邮件。但是还是推荐将邮件发送相关功能独立起来,便于维护。

  上边我们实现了发送一个文本邮件,有时候我们还需要在发送邮件时附带发送附件,这种也是支持的,我们添加接口及实现方法

/**

     * 发送带附件邮件

     *
@param subject
主题

     * @param
content
内容

     *
@param filePath
附件得本地路径

     *
@param fileName
附件名

     *
@param to
收件人

     */

    void sendAttachmentMail(String subject, String content, String
filePath, String fileName, String... to);

@Override

public void sendAttachmentMail(String
subject, String content, String filePath, String fileName, String... to) {

MimeMessage mimeMessage
=javaMailSender.createMimeMessage();

        try {

MimeMessageHelper helper
=new
 MimeMessageHelper(mimeMessage, true);

         helper.setFrom(from);

         helper.setTo(to);

         helper.setSubject(subject);

            helper.setText(content);

            //附件

            FileSystemResource
file = new
 FileSystemResource(new File(filePath));

            helper.addAttachment(fileName,
file);

            javaMailSender.send(mimeMessage);

        } catch (MessagingException e) {

            e.printStackTrace();

        }

    }

  如果我们发送邮件时,除了收件人之外,还想让某些人知道即抄送某些人,这种也是支持得,接口及实现方法如下:

1

2

3

4

5

6

7

8

/**

     * 发送一个邮件并抄送

     *
@param subject
主题

     * @param
content
内容

     *
@param ccList
抄送人(可以有多个)

     *
@param to
收件人(可以有多个)

     */

    void sendTextMail(String subject, String content,
List<String> ccList, String... to);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

@Override

    public void sendTextMail(String
subject, String content, List<String> ccList, String... to) {

        //收件人地址

        InternetAddress[]
addressesTo = new
 InternetAddress[to.length];

        if(to
!= null
 &&
to.length>0){

            for(int i=0;i<to.length;i++){

                InternetAddress
addressTo = null;

                try {

                    addressTo
= new
 InternetAddress(to[i]);

                    addressesTo[i]
= addressTo;

                } catch (AddressException e) {

                    e.printStackTrace();

                }

            }

        }

        //抄送人地址

        InternetAddress[]
addressesCc = new
 InternetAddress[ccList.size()];

        if(ccList
!= null
 && ccList.size()
> 0){

            for(int i=0;i<ccList.size();i++){

                String
ccAdd = ccList.get(i);

                InternetAddress
address = null;

                try {

                    address
= new
 InternetAddress(ccAdd);

                    addressesCc[i]
= address;

                } catch (AddressException e) {

                    e.printStackTrace();

                }

            }

        }

        MimeMessagePreparator
preparator = new
 MimeMessagePreparator()
{

            @Override

            public void prepare(MimeMessage
mimeMessage) throws
 Exception {

                mimeMessage.setFrom(from);

                mimeMessage.setSubject(subject);

                mimeMessage.setText(content);

                mimeMessage.setRecipients(Message.RecipientType.TO,
addressesTo);

                mimeMessage.setRecipients(Message.RecipientType.CC,
addressesCc);

            }

        };

        javaMailSender.send(preparator);

    }

  如上方法就实现了向多人发送邮件并且抄送多人

返回目录

上一篇

下一篇

SpringBoot入门 (十一) 数据校验

本文记录学习在SpringBoot中做数据校验。

一 什么是数据校验

  数据校验就是在应用程序中,对输入进来得数据做语义分析判断,阻挡不符合规则得数据,放行符合规则得数据,以确保被保存得数据符合我们得数据存储规则。

  在SpringMvc中做数据校验有2中方式:一种是 Spring 自带的验证框架,另外一种是利用 JSR 实现。JSR 是一个规范,提供了完整得一套 API,通过标注给对象属性添加约束。Hibernate Validator 就是 对JSR 规范所有注解的具体实现,以及一些附加的约束注解。用户还可以自定义约束注解。Hibernate Validator提供得注解如下:

  注解

   作用目标

  检查规则

@Length(min=, max=)

属性(String)

检查字符串长度是否符合范围

@Max(value=)

属性(以 numeric 或者 string 类型来表示一个数字)

检查值是否小于或等于最大值

@Min(value=)

属性(以 numeric 或者 string 类型来表示一个数字)

检查值是否大于或等于最小值

@NotNull

属性

检查值是否非空(not null)

@Future

属性(date 或 calendar)

检查日期是否是未来

@Pattern(regex="regexp",
flag=)

属性(string)

检查属性是否与给定匹配标志的正则表达式相匹配

@Range(min=, max=)

属性(以 numeric 或者 string 类型来表示一个数字)

检查值是否在最小和最大值之间(包括临界值)

@Size(min=, max=)

属性(array,collection,map)

检查元素大小是否在最小和最大值之间(包括临界值)

@AssertFalse

属性

检查方法的演算结果是否为 false(对以代码方式而不是注解表示的约束很有用)

@AssertTrue

属性

检查方法的演算结果是否为 true(对以代码方式而不是注解表示的约束很有用)

@Valid

属性(object)

对关联对象递归进行验证。如果对象是集合或数组,就递归地验证其元素;如果对象是 Map,则递归验证其值元素

@Email

属性(String)

检查字符串是否符合有效的 email
地址规范

@Past

属性(date 或 calendar)

检查日期是否是过去

二 使用示例

  SpringBoot对数据校验也做了支持,默认提供的参数校验依赖于 hibernate-validator来实现。使用 Hibernate
Validator校验数据,需要定义一个接收的数据模型,使用注解的形式描述字段校验的规则。通过前台页面提交form表单保存数据,后台做校验。

  在pom.xml文件中引入依赖

1

2

3

4

5

6

7

8

<dependency>

   <groupId>org.springframework.boot</groupId>

   <artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>

<dependency>

  <groupId>org.hibernate.validator</groupId>

  <artifactId>hibernate-validator</artifactId>

</dependency>

  写一个实体类,对相关属性字段通过注解添加校验规则

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class Person {

    @NotBlank(message
= "
不能为空")

    @Length(min
= 2, max = 20, message = "
长度要在2到20之间")

    private String name;

    @NotNull

    @Min(value
= 17, message = "
最小值为17")

    private Integer age;

    @NotEmpty

    @Email(message="邮件格式不正确")

    private String email;

    

  // 此处省略getter和setter 

}

  每个注解中得属性message是数据校验不通过时我们要给出得提示信息,如 @Email(message="邮件格式不正确") 当邮件格式校验不通过时,提示邮件格式不正确。

  控制器

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

@Controller

public class TestController {

    @GetMapping("/info")

    public String info(Model model){

        Person
person = new
 Person();

        model.addAttribute("person",
person);

        return "person_info.html";

    }

    @PostMapping("/save")

    public String save(@Valid Person person,
BindingResult result, Model model){

        if (result.hasErrors()) {

            model.addAttribute("person",
person);

            return "person_info.html";

        }    //数据保存。。。

        model.addAttribute("success","校验通过,数据已保存");

        return "success.html";

    }

}

  我们通过访问info方法,跳转到信息输入页面,提交数据时访问save方法,Person前得注解@Valid 说明当前对象要做数据校验,BindingResult 中会存储数据校验得结果,@Valid和BindingResult必须成对出现。如果校验不通过时即有错误信息,会进入if略记判断中,返回信息输入页面,展示错误提示信息。

  信息输入页面

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

<!DOCTYPE html>

<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>

    <meta
charset="UTF-8">

    <title>Title</title>

</head>

<body>

    <form
th:action="@{/save}"
 th:method="post" th:object="${person}">

        <table>

            <tr>

                <td>name:</td>

                <td><input
type="text"
 th:field="*{name}" th:value="${person.name}"/></td>

                <td
th:if="${#fields.hasErrors('name')}"
 th:errors="*{name}" th:style="'color:red'">Name Error</td>

            </tr>

            <tr>

                <td>age:</td>

                <td><input
type="text"
 th:field="*{age}" th:value="${person.age}"/></td>

                <td
th:if="${#fields.hasErrors('age')}"
 th:errors="*{age}" th:style="'color:red'">Age Error</td>

            </tr>

            <tr>

                <td>email:</td>

                <td><input
type="text"
 th:field="*{email}" th:value="${person.email}"/></td>

                <td
th:if="${#fields.hasErrors('email')}"
 th:errors="*{email}" th:style="'color:red'">Email Error</td>

            </tr>

            <tr>

                <td
th:colspan="2"><button
th:type="submit">Submit</button></td>

            </tr>

        </table>

    </form>

</body>

</html>

  th:object 要与后台接收校验得对象保持一致。访问info方法在页面随便输入一些信息,在提交数据,会看到校验结果

  如果各项数据都符合规则,则校验通过,跳转至success页面

返回目录

上一篇

返回第一篇

SpringBoot入门 (十二) 定时任务

本文记录在SpringBoot中使用定时任务。

  在我们的项目中,经常需要用到定时任务去帮我们做一些事情,比如服务状态监控,业务数据状态的更改等,SpringBoot中实现定时任务有2中方案,一种是自带的,我们只需要加上注解即可;另一种是集成Quartz来实现定时任务。

一 SpringBoot 提供的定时任务

  在SpringBoot的starter包中已经提供了对定时任务的支持,我们很容易实现定时任务。修改pom.xml文件,加入如下内容,引入依赖:

<dependency>

   <groupId>org.springframework.boot</groupId>

   
<artifactId>spring-boot-starter</artifactId>

</dependency>

  在启动类上加注@EnableScheduling
注解,开启定时任务

@SpringBootApplication

@EnableScheduling

public class TimingTaskApplication {

    public static void main(String[] args) {

        SpringApplication.run(TimingTaskApplication.class,
args);

    }

}

  创建定时任务类及要定时执行的方法

1

2

3

4

5

6

7

8

9

10

11

12

@Component

public class ScheduleTaskDemo {

    private int num1 = 0;

    private DateFormat df = new SimpleDateFormat("yyyy-MM-dd
HH:mm:ss");

    @Scheduled(cron
= "*/5 * * * * ?")

    public void test1(){

        System.out.println("这是test1方法第"+ (++num1) + "次执行,执行时间:"+df.format(new Date()));

    }

}

  @Component 注解 将当前类交给Spring容器管理

  @Scheduled 注解标明方法是一个定时执行的方法

  启动项目后可以看到控制台打印出的信息

  @Scheduled 注解有几个参数可以使用,每个参数的意义不一样,参数及执行规则说明如下:

  fixedRate
@Scheduled(fixedRate = 5000) 上一次开始执行时间点之后每5秒执行一次;

  fixedDelay
@Scheduled(fixedDelay = 5) 上一次执行完毕时间点之后每5秒执行一次;

  initialDelay
@Scheduled(initialDelay = 1, fixedDelay = 6) 第一次延迟1秒后执行,之后按照fixedRate的规则执行;

  cron @Scheduled(cron
= "*/5 * * * * ?") 一个表达式,一共有7位,一般只需要设置6为就可以了

  第一位:秒,取值范围是0-59;

  第二位:分,取值范围是0-59;

  第三位:时,取值范围0-23

  第四位:日,取值范围1-31

  第五位:月,取值范围1-12

  第六位:星期,取值范围1-71代表星期日,2代表星期一,7代表星期六;

  第七位:年,取值范围1970-099,可以不设置。

  cron表达式也可以在http://cron.qqe2.com进行在线生成。

二 SpringBoot集成Quartz

     Quartz是用Java编写的一个开源的任务调度框架。提供了丰富的Api供调用,支持集群环境和持久化,支持多种配置,与SpringBoot可以无缝集成。

  Quartz几个关键元素:

  Job  代表要执行的任务,是个接口,提供了一个方法 execute(JobExecutionContext context);

  JobDetail  代表具体的要执行的任务;

  Trigger  代表调度任务的触发器,配置调度任务的执行规则;

  Scheduler  代表调度容器,一个调度容器中可以注册多个JobDetail和Trigger。

  元素关系如下图:

  SpringBoot对Quartz已经提供了支持,我们可以很容易的使用,在pom.xml文件中引入依赖

1

2

3

4

<dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-quartz</artifactId>

        </dependency>

  在启动类上加注解 @EnableScheduling
,开启定时执行。

  创建一个要执行的任务继承QuartzJobBean,输出任务执行时间

1

2

3

4

5

6

7

8

9

10

public class MyTask1 extends QuartzJobBean {

    private DateFormat df = new SimpleDateFormat("yy-MM-dd
HH:mm:ss");

    @Override

    protected void executeInternal(JobExecutionContext
jobExecutionContext) throws
 JobExecutionException {

        System.out.println("MyTask1
执行时间:" + df.format(new Date()));

    }

}

  创建一个配置类,初始化MyTask1任务所需要的JobDetail和Trigger

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

@SpringBootConfiguration

public class QuartzConfig {

    @Bean

    public JobDetail myTask1JobDetail(){

        return JobBuilder.newJob(MyTask1.class).withIdentity("myTask1").storeDurably().build();

    }

    @Bean

    public Trigger myTask1Trigger(){

        SimpleScheduleBuilder
scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()

                //5秒执行一次

                .withIntervalInSeconds(5)

                .repeatForever();

        SimpleTrigger
trigger = TriggerBuilder.newTrigger()

                //指定触发器对应的JobDetail

                .forJob(myTask1JobDetail())

                .withIdentity("myTask1")

                .withSchedule(scheduleBuilder).build();

        return trigger;

    }

}

  启动项目,可以看到控制台输出的内容

    在上边的方法,我们使用的定时执行方法是在代码中写的固定值,这种局限性比较大,难以满足一些复杂的定制任务执行时间。我们使用Cron表达式来完成定时任务执行时间配置。

  创建任务类,继承QuartzJobBean

1

2

3

4

5

6

7

8

9

10

public class MyTask2 extends QuartzJobBean {

    private DateFormat df = new SimpleDateFormat("yy-MM-dd
HH:mm:ss");

    @Override

    protected void executeInternal(JobExecutionContext
jobExecutionContext) throws
 JobExecutionException {

        System.out.println("MyTask2
执行时间:" + df.format(new Date()));

    }

}

  在配置类QuartzConfig中创建任务所需的JobDetail和Trigger

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

@Bean

    public JobDetail myTask2JobDetail(){

        return JobBuilder.newJob(MyTask2.class).withIdentity("myTask2").storeDurably().build();

    }

    @Bean

    public Trigger myTask2Trigger(){

        //cron
表达式

        String
cronStr = "*/10 * * * * ?";

        //根据表达式设置ScheduleBuilder

        CronScheduleBuilder
cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronStr);

        CronTrigger
trigger = TriggerBuilder.newTrigger()

                //指定JobDetail

                .forJob(myTask2JobDetail())

                .withIdentity("myTask2")

                //指定ScheduleBuilder

                .withSchedule(cronScheduleBuilder)

                .build();

        return trigger;

    }

  重启项目,看到控制台输出内容,任务1每5秒执行一次,任务2每10秒执行一次

返回目录

备注:此文档借鉴其他博客的,如有问题请及时联系,谢谢