Spring Boot入门与数据访问

一、hello world

  使用 IDEA 来创建一个项目工程。如果是第一次打开 IDEA,可以选择 Create New Project 创建一个新工程。如果已经打开了 IDEA,在 File 菜单中选择 New Project,也能打开 New Project 对话框,使用 IDEA 创建一个 Spring Initializr 创建一个新项目。

  注意:Intializr Service URL 为 https://start.sprig.io。IDEA 将会需要连接网络,以查询 Spring Boot 的当前可用版本和组件列表,使用这种方式新建项目大体上也需要三个步骤。

1.选择类型

  可以使用默认选项,注意 Type 为 Maven Project,Java Version 为 8,Packaging 为 Jar 或 war。单击 Next 进入下一步。

2.选择 Spring Boot 版本和组件

  选择 Spring Boot 版本和 Spring Boot 组件,例如,在 Spring Boot Version 中选择 2.5.4 并勾选 Spring Web 项目组件,然后单击 Next 进入下一步。

3.输入项目名称

  选择存放路径后输人项目名称,并选择项目存放路径,这里使用 helloworld 作为项目的名称。

  单击 Finish,将创建一个初始化项目。这个项目不但有完整的目录结构,还有一个完整的 Maven 配置,并且生成了一个默认的主程序,几乎所有的准备工作都已经就绪,并且可以立即运行起来(虽然没有提供任何可用的服务)。这也是 Spring Boot 引以为傲的地方,即创建一个应用可以不用编写任何代码,只管运行即可。

maven 相关的依赖:

  使用 Maven,通过导人 Spring Boot 的 starter 模块,可以将许多程序依赖包自动导人工程中。使用 Maven 的 parent POM,还可以更容易地管理依赖的版本和使用默的配置,工程中的模块也可以很方便地继承它。

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

  这里只使用了一个依赖配置 spring-boot starter-web 和一个 parent 配置 spring-boot-sarter-parent,在工程的外部库(External Libraries)列表中,它将自动引人 springframework 依赖,以及 autoconfigure、logging、 slf4j、 jackson、tomcat 插件等,所有这些都是一个 Web 项目可能需要用到的东西(包括你已经考虑到的和没有考虑到的)。
  Spring Boot 的官方文档中提供了一个最简单的 Web 实例程序,这个实例只使用了几行代码。虽然简单,但实际上这已经可以算作是一个完整的 Web 项目了。

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

@RestController
public class HelloController {
    @RequestMapping("/")
    public String hello() {
        System.out.println("hello world!");
        return "hello world!!";
    }
}

  这个简单实例,首先是一个 Spring Boot 应用的程序人口,或者叫作主程序,其中使用了一个注解 @SpringBootApplication 来标注它是一个 Spring Boot 应用,main 方法使它成为一个主程序,目的是将在应用启动,在控制台可以看到如下日志表明项目启动成功。

Tomcat started on port(s): 8080 (http) with context path ''
Started DemoApplication in 3.941 seconds (JVM running for 8.812)

  从上面的输出中可以看出,Tomcat 默认开启了 8080 端口。要访问这个应用提供的服务,可以在浏览器的地址栏中输人 http://localhost:8080 这样就可以看到我们期望的输出字符:hello world !! 。
  其次,HelloController上的注解 @RestController 标注这个程序还是一个控制器,如果在浏览器中访问应用的根目录,它将调用 hello 方法,并输出字符串: hello world ! ,返回前端字符串 hello world !!

练习:

  1.创建 springboot 项并创建三个 Rest 接口,/name 返回自己的姓名,/age 返回自己的年龄,/hobby 返回自己的爱好。

参考代码:

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

@RestController
public class PublicController {

    @RequestMapping("/name")
    public String name() {
        return "张三";
    }

    @RequestMapping(value = "/age")
    public String age() {
        return "18";
    }

    @RequestMapping(value = "/hobby")
    public String hobby() {
        return "吃饭,睡觉,打豆豆";
    }
}

二、springboot 装配原理(自)

面试官:谈谈你对 Springboot 自动装配的了解?

  它是 starter 的基础,也是 Spring Boot 的核心,可以简单概括为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。

  (1)SpringBoot 核心注解 SpringBootApplication 是由几个注解组成的,其中比较重要的一个 EnableAutoConfiguration 注解

  (2)引导类上开启 @EnableAutoConfiguration,其内部通过 @import 注解引入 ImportSelector 方法

  (3)查找工程 jar 包中 META-INF/spring.factories 文件

  (4)装载内部的对象到容器

三、运行和发布

  在 idea 创建的项目依赖中可以看到如下打包插件,使用该插件可以将 springboot 项目快速打包发布。

<plugins>
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
</plugins>

  在配置了 spring-boot-maven-plugin 插件后,idea 的面板中多出相关的打包指令,双击 package 即可将项目打包。由于打包过程会测试相关的单元测试,可以点击跳过测试。
在这里插入图片描述
  当控制台输出:BUILD SUCCESS 时,表示项目已经成功打好对应的项目包。这时在 target 目录下便是对应的项目包。

  最后通过 java -jar 命令即可运行当前应用。

java -jar demo-0.0.1-SNAPSHOT.jar

  当需要覆盖配置文件中的一些属性时通常使用–加内容,如:

java -jar demo-0.0.1-SNAPSHOT.jar --server.port=1234

练习:

  1.将上一个练习的项目打包,并创建 bat 启动脚本使其启动在 80 端口,按步骤截图提交相应的操作到 word 文档。

四、配置文件

  关于 SpringBoot 配置,可以在工程的 resources 文件夹中创建一个 application.properties 或 aplication.yml 文件,这个文件会被发布在 classpath 中,并且被 Spring Boot 自动读取。这里推荐使用 application.yml 文件,因为它使用 yaml 的语法规则,提供了结构化及其嵌套的格式。例如,可以按如下所示配置上面的工程,将默认端口改为 80,并且将 Tomcat 的字符集定义为 UTF-8。

server:
  port: 80
  tomcat:
    uri-encoding: utf-8

  如果要使用 application.properties 文件,上面的配置就要改成如下所示的样子,其结果完全相同。

server.port=80
server.tomcat.uri-encoding=utf-8

  使用这个配置文件可以直接使用 Spring Boot 预定义的一些配置参数,关于其他配置参数的详细说明和描述在后面详细介绍。

  yaml 语法:

  • 缩进 yaml 使用一个固定的缩进风格表示数据层结构关系,Saltstack 需要每个缩进级别由两个空格组成。一定不能使用 tab 键
  • 冒号 yaml: mykey: my_value 每个冒号后面一定要有一个空格(以冒号结尾不需要空格,表示文件路径的模版可以不需要空格)

map 对象书写示例

friends:
  lastName: zhangsan
  age: 20

行内写法

friends: {lastName: zhangsan,age: 18}

五、多环境配置文件激活

  在实际开发过程中,配置文件一般分为开发模式(dev)和生产模式(pro),为了快速切换配置文件,springboot 提供了配置文件快速激活方案。即在application.properties 加入 spring.profiles.active=dev,即可以引用同目录下文件名为 application-dev.properties/yml 的配置文件。

  当然如果只想使用一个配置文件解决两种模式配置的切换也是可以的。如下则为单文件多 profile 模式:

application.yml

spring:
  profiles:
    active: pro

application-dev.yml

server:
  port: 8080

application-pro.yml

server:
  port: 80

  通过简单的一行配置即可改变配置文件的全部内容,将极大的方便项目部署到 linux 系统上时修改配置文件。

配置属性扩展

  为了满足各种业务需要,springboot 对 spring 的配置属性进行了大量的扩展,方便开发中获取自定义属性、随机数、变量引用等。在配置文件 application.properties 中加入如下代码:

com.didispace.blog.name=程序猿
com.didispace.blog.title=Spring Boot教程
com.didispace.blog.desc=${com.didispace.blog.name}正在努力写《${com.didispace.blog.title}》

# 随机字符串
com.didispace.blog.value=${random.value}
# 随机int
com.didispace.blog.number=${random.int}
# 随机long
com.didispace.blog.bignumber=${random.long}
# 10以内的随机数
com.didispace.blog.test1=${random.int(10)}
# 10-20的随机数
com.didispace.blog.test2=${random.int[10,20]}

  在启动程序的下级目录添加spring管理的一个bean

@Value("${com.didispace.blog.name}")
private String name;
@Value("${com.didispace.blog.title}")
private String title;
@Value("${com.didispace.blog.desc}")
private String desc;
@Value("${com.didispace.blog.value}")
private String value;
@Value("${com.didispace.blog.number}")
private Integer number;
@Value("${com.didispace.blog.bignumber}")
private Long bignumber;
@Value("${com.didispace.blog.test1}")
private Integer test1;
@Value("${com.didispace.blog.test2}")
private Integer test2;

六、静态资源

  springboot 项目微服务开发模式倡导使用前后端分离的开发模式,前端使用如 vue 脚手架等开发框架,开发完成后再打包前端项目到对应的 static 静态资源目录。

  对于一般的开发人员来说,将普通 html,css,JavaScript 等资源放在 static 静态资源目录下开发也是一种不错的选择。使用 html 开发能加强对前端渲染的认识,为将来的前后端独立开发打下坚实基础。

  一、默认静态资源映射

  Spring Boot 对静态资源映射提供了默认配置,Spring Boot 默认将 /** 所有访问映射到以下目录:

classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources

  如:在 resources目录下新建 public、resources、static 三个目录,并分别放入 a.jpg b.jpg c.jpg 图片。浏览器分别访问: http://localhost:8080/a.jpg http://localhost:8080/b.jpg http://localhost:8080/c.jpg 均能正常访问相应的图片资源。那么说明,Spring Boot 默认会挨个从 public、resources 和 static 里面找是否存在相应的资源,如果有则直接返回。
  二、自定义静态资源访问

  静态资源路径是指系统可以直接访问的路径,且路径下的所有文件均可被用户直接读取。 在 Springboot 中默认的静态资源路径有:classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/ 和 classpath:/public/ 从这里可以看出这里的静态资源路径都是在 classpath 中(也就是在项目路径下指定的这几个文件夹) 试想这样一种情况:一个网站有文件上传文件的功能,如果被上传的文件放在上述的那些文件夹中会有怎样的后果? 网站数据与程序代码不能有效分离; 当项目被打包成一个 .jar 文件部署时,再将上传的文件放到这个 .jar 文件中是有多么低的效率; 网站数据的备份将会很痛苦。 此时可能最佳的解决办法是将静态资源路径设置到磁盘的基本个目录。

  1、配置类

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        //将所有C:/aa/ 访问都映射到/aa/** 路径下
        registry.addResourceHandler("/images/**").addResourceLocations("file:C:/images/");
    }
}

  第二种方式

  配置 application.yml 的 spring.web.resources.static-locations。这种方式与第一种方式不同之处在于该目录 images 不需要在请求时指定对应的路径前缀。

spring:
  web:
    resources:
      static-locations: classpath:/static/,classpath:/public/,file:${web.upload-path}
web:
  upload-path: C:/images/

注意:

  web.upload-path:这个属于自定义的属性,指定了一个路径,注意要以 / 结尾;

  spring.web.resources.static-locations:在这里配置静态资源路径,前面说了这里的配置是覆盖默认配置,所以需要将默认的也加上否则 static、public 等这些路径将不能被当作静态资源路径,在这个最末尾的 file:${web.upload-path} 之所有要加 file:是因为指定的是一个具体的硬盘路径,其他的使用 classpath 指的是系统环境变量。

七、发送邮件(自)

  springboot 已经将发送邮件问题极大的简化。只需要小面几步简单的操作,邮件轻轻松松发到对面的手上。

  1.添加依赖

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

  2.配置服务器用户名授权码等

spring:
  mail:
    host: smtp.qq.com
    username: xxxxxxx@qq.com
    password: mxifcumctzjygf*j
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true

  3.直接注入 JavaMailSender 发送邮件,JavaMailSender 可以发送简单的文本邮件,带附件的邮件,以及富文本的邮件。

public class ApplicationTests {

    @Autowired
    private JavaMailSender mailSender;

    @Test
    public void sendSimpleMail() throws Exception {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom( "xxxxxxx@qq.com" );
        message.setTo( "xxxxxxx@qq.com" );
        message.setSubject( "主题:简单邮件" );
        message.setText( "测试邮件内容" );
        mailSender.send( message );
    }

    @Test
    public void sendAttachmentsMail() throws Exception {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper( mimeMessage, true );
        helper.setFrom( "xxxxxxx@qq.com" );
        helper.setTo( "xxxxxxx@qq.com" );
        helper.setSubject( "主题:有附件" );
        helper.setText( "有附件的邮件" );
        FileSystemResource file = new FileSystemResource( new File( "D:/壁纸/dsf.jpg" ) );
        helper.addAttachment( "附件-1.jpg", file );
        helper.addAttachment( "附件-2.jpg", file );
        mailSender.send( mimeMessage );
    }

    @Test
    public void sendInlineMail() throws Exception {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true );
        helper.setFrom( "xxxxxxx@qq.com" );
        helper.setTo( "xxxxxxx@qq.com" );
        helper.setSubject( "主题:嵌入静态资源" );
        helper.setText( "<html><body><img src=\"cid:weixin\" ></body></html>", true );
        FileSystemResource file = new FileSystemResource( new File( "D:/壁纸/dsf.jpg" ) );
        helper.addInline( "weixin", file );
        mailSender.send( mimeMessage );
    }
}

  注意:富文本,及包含 html 标签的文本,但是显示时需要想浏览器一样显示其样式。

八、jar 包与 war 选择

  springboot 官方建议使用 jar 包,但是由于使用 jsp 项目必须打成 war 包,打成 war 包后可以放入 tomcat 使用传统方式部署。

  手动将 jar 包方式修改为 war 包需要修改 pom.xml 如下:

<packaging>war</packaging>

  同时还要加入 ServletInitializer 类,如下:

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(HelloworldApplication.class);
    }
}

九、使用 Redis

  关系型数据库在性能上总是存在一些这样那样的缺陷,所以大家有时候在使用传统关系型数据库时,会与具有高效存取功能的缓存系统结合使用,以提高系统的访问性能。在很多流行的缓存系统中,Redis 是一个不错的选择。Redis 是一种可以持久存储的缓存系统,是一个高性能的 key-value 数据库,它使用键-值对的方式来存储数据。

  1.添加依赖

  要使用 Redis,需要在工程中加入 spring-boot-starter-data-redis:

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

  2.编写用户存取类

  Redis 提供了 string、hash、list、set 和 zset 几种数据类型可供存取,在下面实例中,我们将使用 string 即字符串的类型来演示数据的存取操作。对于 Redis,Spring Boot 没有提供像 JPA 那样相应的资源库接口,所以只能仿照上节中 Repository 的定义编写一个实体 User 的服务类,代码如下所示。这个服务类可以存取对象 User 以及由 User 组成的列表 List,同时还提供了一个删除的方法。所有这些方法都是使用 RedisTemplate 来实现的。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class UserRedis {
    @Autowired
    RedisTemplate redisTemplate;

    String projectName="springboot-redis";

    /**
     * 清空项目全部缓存
     */
    public  void deleteAll() {
        String keys = projectName + "_*";
        Set deleteKeys = redisTemplate.keys(keys);
        for (Object deleteKey : deleteKeys) {
            redisTemplate.delete(deleteKey);
        }
    }

    /**
    * 设置值
    * @param key 键
    * @param user 值
    */
    public  void put(String key, User user) {
        String keys = projectName + "_" + key;
        redisTemplate.opsForValue().set(keys, user);
    }

    /**
    * 获取值
    * @param key 键
    * @return 值
    */
    public  User get(String key) {
        String keys = projectName + "_" + key;
        Object o = redisTemplate.opsForValue().get(keys);
        return (User) o;
    }

    /**
     * 删除键
     * @param key 键
     */
    public  void delete(String key) {
        String keys = projectName + "_" + key;
        redisTemplate.delete(keys);
    }

    /**
     * 设置键的过期时间
     * @param key      键
     * @param time     时间
     * @param timeUnit 单位
     */
    public  void expire(String key, long time, TimeUnit timeUnit) {
        String keys = projectName + "_" + key;
        redisTemplate.expire(keys, time, timeUnit);
    }

}

  3.配置 RedisTemplate

  Redis 没有表结构的概念,所以要实现 MySQL 数据库中表的数据(即普通 Java 对象映射的实体数据)在 Redis 中存取,必须做一些转换, 使用 JSON 格式的文本作为 Redis 与 Java 普通对象互相交换数据的存储格式。这里使用 jackson 工具将类对象转换为 JSON 格式的文本进行存储,要取出数据时,再将 JSON 文本数据转化为 Java 对象。

  因为 Redis 使用了 key-value 的方式存储数据,所以存入时要生成一个唯一的 key,而要查询或者删除数据时,就可以使用这个唯一的 key 进行相应的操作。 保存在 Redis 数据库中的数据默认是永久存储的,可以指定一个时限来确定数据的生命周期,超过指定时限的数据将被 Redis 自动清除。

  另外,为了能正确调用 RedisTemplate,必须对其进行一些初始化工作,即主要对 它存取的字符串进行一个 JSON 格式的序列化初始配置,代码如下所示。

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 值采用jackson序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(jackson2JsonRedisSerializer);
        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

  注意:单独使用 jackson 时,需要导入 spring-boot-starter-web 依赖即可。

  4.启动和配置 redis 服务

  启动 redis 服务后,在工程的配置文件 aplication.yml 中配置连接 Redis 服务器连接等参数,代码如下所示。其 中 host 和 port 分别表示 Redis 数据库服务器的 IP 地址和开放端口,database 是对应的数据库索引。

spring:
  redis:
    host: localhost
    port: 6379
    database: 1

  5.单元测试

  测试程序创建一个部门对象并将其命名为“开发部",创建一个角色对 象并把它命名为 admin,创建一个用户对象并把它命名为 user,同时设定这个用户属于"开发部",并把 admin 这个角色分配给这个用户。接着测试程序使用类名等参数生成一个 key,并使用这个 key 清空原来的数据,然后用这个 key 存储现在这个用户的数据, 最后使用这个 key 查询用户,并将查到的信息打印出来。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class SpringbootRedisApplicationTests {
    @Autowired
    UserRedis userRedis;
    @Test
    void contextLoads() {
        userRedis.deleteAll();
        Department department = new Department();
        department.setName("开发部");
        Role role = new Role();
        role.setName(" admin");
        User user = new User();
        user.setName("user");
        user.setCreateTime(new Date());
        user.setDepartment(department);
        List<Role> roles=new ArrayList<>();
        roles.add(role);
        user.setRoles(roles);
        userRedis.put("1",user);
        userRedis.expire("1",1, TimeUnit.MINUTES);
        User userOut = userRedis.get("1");
        System.out.println(userOut);
    }
}

练习:

  1.在 spring boot 项目中不推荐使用后端渲染,在 static 目录下创建 index.html 完成 user 的增删改查,后台数据全部存到 redis。

参考代码:方式1

@RestController
@CrossOrigin
public class UserController {
    @Resource
    RedisTemplate<String, User> redisTemplate;

    @RequestMapping("list")
    public Map<String, Object> list() {
        Set<String> keys = redisTemplate.keys("user_*");
        List<User> users = new ArrayList<>();
        keys.forEach(k -> {
            users.add(redisTemplate.opsForValue().get(k));
        });
        users.sort(Comparator.comparingInt(User::getCode));
        Map<String, Object> map = new HashMap<>();
        map.put("data", users);
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("add")
    public Map<String, Object> add(User user) {
        redisTemplate.opsForValue().set("user_" + user.getCode(), user);
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("findByCode")
    public Map<String, Object> findByCode(@RequestParam Integer code) {
        User user = redisTemplate.opsForValue().get("user_" + code);
        Map<String, Object> map = new HashMap<>();
        map.put("data", user);
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("delete")
    public Map<String, Object> delete(@RequestParam Integer code) {
        redisTemplate.delete("user_" + code);
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;
    }
}

参考代码:方式2

@RestController
@CrossOrigin
public class UserController {
    @Resource
    RedisTemplate<String, User> redisTemplate;

    @RequestMapping("list")
    public Map<String, Object> list() {
        List<User> users = redisTemplate.opsForList().range("user", 0, -1);

        users.sort(Comparator.comparingInt(User::getCode));
        Map<String, Object> map = new HashMap<>();
        map.put("data", users);
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("add")
    public Map<String, Object> add(User user) {
        redisTemplate.opsForList().leftPush("user", user);
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("findByCode")
    public Map<String, Object> findByCode(@RequestParam Integer code) {
        Map<String, Object> map = new HashMap<>();
        List<User> users = redisTemplate.opsForList().range("user", 0, -1);
        for (int i = 0; i < users.size(); i++) {
            if (users.get(i).getCode().equals(code)) {
                map.put("data", users.get(i));
            }
        }
        map.put("message", "ok");
        map.put("success", true);
        return map;

    }

    @RequestMapping("delete")
    public Map<String, Object> delete(@RequestParam Integer code) {
        List<User> users = redisTemplate.opsForList().range("user", 0, -1);
        for (int i = 0; i < users.size(); i++) {
            if (users.get(i).getCode().equals(code)) {
                redisTemplate.opsForList().remove("user", 0, users.get(i));
            }
        }
        Map<String, Object> map = new HashMap<>();
        map.put("message", "ok");
        map.put("success", true);
        return map;
    }
}

十、分布式 session 共享(自)

  在分布式项目中,项目模块间 session 共享是一件非常重要的事。对此,springboot 不懈努力,从最开始的 session 存入数据库,到后来的 session 存入 redis。开发团队不断测试和优化,才有今天如此优秀的 spring-session-redis

  1.添加依赖

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

  2.开启 session 缓存注解

  @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)//session60*60秒过期

  就这样简单的几步,开发人员就可以将 session 存入 redis。我们操作是也只是简单的在 session 中存入或取值,该 session 内容变回自动缓存至 redis。当多个项目同时连接一个 redis 时,便可获取到其他项目的 session。

  注意:redis 缓存 session 时,会导致 session 默认的监听失去效果,如果需要使用 session 监听,必须配置 SessionEventHttpSessionListenerAdapter 并将对应 session 监听配置到该 bean 中。

@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 10800)//session60*60秒过期
public class WebConfig {
    @Bean
    public SessionEventHttpSessionListenerAdapter eventHttpSessionListenerAdapter() {
        List<HttpSessionListener> listeners = new ArrayList<>();
        listeners.add(new SessionDestroyListener());
        SessionEventHttpSessionListenerAdapter eventHttpSessionListenerAdapter = new SessionEventHttpSessionListenerAdapter(listeners);
        return eventHttpSessionListenerAdapter;
    }
}

  注意:如果两个应用分别运行在同一台服务器的不同端口时 session 是不能简单的通过浏览器共享的,因为不同端口的应用在浏览器看来是不同的服务(通常称为跨域),所自动携带 cookie 也将不能跨域使用。要想浏览器携带 cookie 必须让访问两个应用时必须使用相同的域,一般都要使用 nginx 负载均衡到两个应用上或使用 spring cloud 的统一网关。

十一、整合 mybatis

  虽然 springdata 足够智能和强大,但国内以腾讯阿里为首的 mybatis 大军仍然弃之不顾。为了满足相关人员的开发需求。springboot 对 mybatis 也有较好的支持。同时也简化了 mybatis 相关繁琐的配置。基本配置如下:

  1.加入依赖

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

  2.配置数据库连接信息和 mybatis 的 mapper.xml 位置

mybatis:
  mapper-locations: classpath:mappers/*.xml
spring:
  datasource:
    url: jdbc:mysql:///unifiedquery?characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

  3.在启动类上添加 mybatis 接口扫描路径

@SpringBootApplication
@MapperScan("cn.hx.dao")
public class QueryApplication {
    public static void main(String[] args) {
        SpringApplication.run(QueryApplication.class, args);
    }
}

  若需要显示 sql,只需如下简单配置即可。

logging:
  level:
    cn.hx.dao: debug

  cn.hx.dao 是 mybatis 接口的位置。

  练习:

  1.在 spring boot 项目中不推荐使用后端渲染,在 static 目录下创建 index.html 完成 user 的增删改查,orm 框架使用 mybatis。

十二、spring cache

  一般的 spring cache 使用 aop 的方式进行相关调用的缓存,使用这些缓存实现的话,只需导入相关缓存的依赖,并在配置类中使用 @EnableCaching 开启缓存即可:

  1.导入依赖

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

  2.使用注解

@CacheEvict(value = "DepartmentServiceImpl", key = "'queryAllByLimit'+#department.name+'_'+#department.id")
public Department queryAllByDepartment(Department department) {
    this.departmentDao.insert(department);
    return department;
}

@Cacheable(value = "DepartmentServiceImpl", key = "'queryById'+#id")
public Department queryById(Integer id) {
    return this.departmentDao.queryById(id);
}

@Cacheable(value = "DepartmentServiceImpl", key = "'queryAllByLimit' + #offset + '_' + #limit")
public List<Department> queryAllByLimit(int offset, int limit) {
    return this.departmentDao.queryAllByLimit(offset, limit);
}

@CacheEvict(value = "DepartmentServiceImpl", allEntries = true)
public Department insert(Department department) {
    this.departmentDao.insert(department);
    return department;
}


@CacheEvict(value = "DepartmentServiceImpl", allEntries = true)
public Department update(Department department) {
    this.departmentDao.update(department);
    return this.queryById(department.getId());
}

  3.application.yml 里面可以配置缓存失效时间

spring:  
  redis:
    database: 3
    host: 127.0.0.1
    port: 6379
    pool:
  cache:
    type: redis
    redis:
      time-to-live: 864000000s

十三、事务控制

  事务控制和缓存原理一样,使用方式也相近。只需要在 service 上添加 @Transactional(readOnly = true) 注解,即可将类里面的所有方法设置为只读。当有数据库修改或该方法必须事务时添加 @Transactional 注解。代码如下

@Service
@Transactional(readOnly = true)//全部方法只读事务
public class UserService {
    @Autowired
    private UserRepository userRepository;
    public List<User> list() {
        return userRepository.findAll();
    }

    @Transactional//事务控制该方法
    public User save(User user) {
        return userRepository.save( user );
    }
}

  当然,如果不是使用 jpa 作为持久层框架 @Transactional(readOnly = true) 一般不加,因为 jpa 的对象有一个持久化状态,处于该状态的对象在不小心修改其属性时会刷新到数据库,为了避免该问题,所以使用该注解。

十四、整合 jpa/spring data

  对于传统关系型数据库来说,Spring Boot 使用 JPA(Java Persistence API)资源库来实现对数据库的操作,使用 MySQL 也是如此。简单地说,JPA 就是为POJO (Plain Ordinary Java Obiject)提供持久化的标准规范,即将 Java 的普通对象通过对象关系映射(Obiect Relational Mapping,ORM)持久化到数据库中。JPA 底层使用的是 hibernate 但是对其进行相关的封装,同时也是 spring boot 在微服务开发领域最推荐的 ORM 框架。

  jpa与mysql依赖如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.46</version>
</dependency>

  1.配置连接信息

  在 MySQL 数据库服务器中创建一个数据库 springboot,数据库的表结构可以不用创建,在程序运行时将会按照实体的定义自动创建。在 Spring Boot 的配置文件 application.yml 中使用代码如下所示的配置,用来设置数据源和 JPA 的工作模式。

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql:///springboot?useSSL=false
  jpa:
    show-sql: true
    database-platform: org.hibernate.dialect.MySQL57InnoDBDialect
    hibernate:
      ddl-auto: update

  ddl-atuo:设置为 update,就是使用 Hibernate 来自动更新表结构的,即如果数据表不存在则创建表,或者如果修改了表结构,在程序启动时则执行表结构的同步更新,注意 hibernate 只会添加字段不会删除字段。

  database-platform:设置数据方言,一般根据数据库的版本选择,如:mysql5.7 以下的使用 MySQL5InnoDBDialect,如果方言选择有误,则相关创表和添加字段可能会报错。

  show-sql:用于设置是否打印 hibernate 打印的 SQL。

  2.实体建模

  首先创建一些普通对象,用来与数据库的表建立映射关系,接着演示如何使用 JPA 对数据库进行增删查改等存取操作。 假如现在有三个实体:部门、用户和角色,并且它们具有一定的关系,即一个用户只能隶属于一个部门, 一个用户可以拥有多个角色,它们的关系模型如下所示。
在这里插入图片描述
  Spring Boot 的实体建模与使用 Spring 框架时的定义方法一样, 同样比较方便的是使用了注解的方式来实现。

  部门实体的建模如下所示,其中注解 @Table 指定关联的数据库的表 名,注解 @Id 定义一条记录的唯一标识, 并结合注解 @GeneratedValue 将其设置其主键生成策略。部门实体只有两个字段:id 和 name。

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "department")
public class Department implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY )
    private Integer id;
    private String name;
}

  用户实体包含三个字段: id、name 和 createTime,用户实体建模代码如下所 示:

  其中注解 @ManyToOne 定义它与部门的多对一关系,并且在数据库表中用字段 department_id 来表示部门的 ID,注解 @ManyToMany 定义与角色实体的多对多关系,并且用中间表 user_ role 来存储它们各自的 ID,以表示它们的对应关系。日期类型的数据必须使用注解 @DateTimeFormat 来进行格式化,以保证它在存取时能提供正确的格式。

import org.springframework.format.annotation.DateTimeFormat;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Entity
@Table(name = "user")
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    @ManyToOne
    @JoinColumn(name = "department_id")
    private Department department;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "user_role",joinColumns ={@JoinColumn(name = "user_id")},inverseJoinColumns = {@JoinColumn(name = "role_id")})
    private List<Role> roles;
}

  角色实体建模比较简单,只要按设计的要求,定义 id 和 name 字段即可,当然同样 必须保证 id 的唯一性并将其设定为自动增长生成策略。角色实体的建模代码如下所示:

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name = "role")
public class Role implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY )
    private Integer id;
    private String name;
}

  3.实体持久化

  通过上面三个实体的定义,实现了使用 Java 的普通对象(POJO)与数据库表建立映射关系(ORM),接下来使用 JPA 来实现持久化。

  用户实体使用 JPA 进行持久化的代码如下所示。它是一个接口,并继承于 JPA 资源库 JpaRepository 接口,使用注解 @Repository 将这个接口也定义为一个资源库,使它能被其他程序引用,并为其他程序提供存取数据库的功能。
  使用相同的方法,可以定义部门实体和角色实体的资源库接口。接口同样继承于 JpaRepoitory 接口,只要注意使用的参数是各自的实体对象即可。

import cn.hx.springbootmysql.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
}

  这样就实现存取数据库的功能了。现在可以对数据库进行增删查改、进行分页查询和指定排序的字段等操作。

  或许你还有疑问,我们定义的实体资源库接口并没有声明一个方法,也没有对接口有任何实现的代码,甚至连一条 SQL 查询语句都没有写,这怎么可能?

  是的,使用 JPA 就是可以这么简单。我们来看看 JpaRepository 的继承关系,你也许会明白一些。 JpaRepository 继承于 PagingAndSortingRepository,它提供了分页和排序功能,PagingAndSortingRepository 继承于 CrudRepository,它提供了简单的增删查改功能。

  因为定义的接口继承于 JpaRepository,所以它传递性地继承上面所有这些接口,并拥有这些接口的所有方法,这样就不难理解为何它包含那么多功能了。这些接口提供的一些方法如下:

List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAllById(Iterable<ID> var1);
<S extends T> List<S> saveAll(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);

  JPA 还提供了一些自定义声明方法的规则,例如,在接口中使用关键字 findBy、 readBy、getBy 作为方法名的前缀,拼接实体类中的属性字段(首个字母大写),并可选 择拼接一些 SQL 查询关键字来组合成一个查询方法。例如,对于用户实体,下列查询 关键字可以这样使用:

And            例如findByldAndName (Long id, String name);
Or             例如findByIdOrName (Long id, String name);
Between        例如findByCreatedateBetween (Date start, Date end)
LessThan       例如findByCreatedateLessThan (Date start);
GreaterThan    例如findByCreatedateGreaterThan (Date start);
IsNull         例如findByNameIsNull0;
IsNotNull      例如findByNamelsNotNull0;
NotNull        与IsNotNull等价;
Like           例如findByNameLike (String name);
NotLike        例如findByNameNotLike ( String name);
OrderBy        例如findByNameOrderByIdAsc (String name );
Not            例如findByNameNot (String name);
In             例如findByNameIn (Collection<String>nameLis);
Notln          例如findByNameNotIn (Collection<String>nameList);

  为了测试jpa的相关语法,我们写了几个简单的抽象方法如下:

User findByNameLike (String name) ;
User readByName (String name) ;
List<User> getByCreateTimeLessThan(Date star);

  4.测试

  最后,编写一个测试程序,代码如下所示。测试程序首先初始化数据库,创建一个部门,命名为“开发部”,创建一个角色,命名为 admin,创建一个用户,命名为user,同时将它的所属部门设定为上面创建的部门,并将现有的所有角色都分配给这个用户。然后使用分页的方式查询所有用户的列表,并从查到的用户列表中,打印出用户的名称、部门的名称和第一个角 色的名称等信息。

import cn.hx.springbootmysql.entity.Department;
import cn.hx.springbootmysql.entity.Role;
import cn.hx.springbootmysql.entity.User;
import cn.hx.springbootmysql.repository.DepartmentRepository;
import cn.hx.springbootmysql.repository.RoleRepository;
import cn.hx.springbootmysql.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.util.Date;
import java.util.List;

@SpringBootTest
class SpringbootMysqlApplicationTests {
    @Autowired
    UserRepository userRepository;
    @Autowired
    DepartmentRepository departmentRepository;
    @Autowired
    RoleRepository roleRepository;

    @Test
    public void findById() {
        Department department = new Department();
        department.setName("开发部");
        departmentRepository.save(department);
        Role role = new Role();
        role.setName(" admin");
        roleRepository.save(role);
        User user = new User();
        user.setName("user");
        user.setCreateTime(new Date());
        user.setDepartment(department);
        List<Role>roles=roleRepository.findAll();
        user. setRoles(roles);
        userRepository.save(user);
        Pageable pageable= PageRequest.of(0, 10, Sort.by(Sort.Order.asc("id")));
        Page<User> page = userRepository. findAll (pageable) ;
        for (User out : page.getContent()) {
            System.out.println(out);
        }
    }
}

  这时如果在 MySQL 服务器中查看数据库 springboot,不但可以看到表结构都已经创建了, 还可以看到上面测试生成的数据。 这是不是很激动人心。

  在 Spring Boot 使用数据库,就是可以如此简单和有趣。到目前为止,我们不仅没有写过一条查询语句,也没有实现一个访问数据库的方法,但是已经能对数据库执行所有的操作,包括一般的增删查改和分页查询。

练习:

1.创建一个项目 user-system-jpa 并根据页面适当的接口,使这些接口完成用户、角色和资源的增删改查,但要测试完成如下功能。

一、用户、角色、资源的增删改查。

二、每个用户可以修改所属的角色。

三、每个角色可以被授予新权限和取消授权。

参考代码

user

@Entity
@Table
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String loginName;

    private String loginPassword;

    @ManyToOne
    @JoinColumn(name = "role_id")
    private Role role;

    @CreationTimestamp
    private Date createTime;

    @UpdateTimestamp
    private Date updateTime;
}

role

@Entity
@Table
public class Role implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String remark;

    @CreationTimestamp
    private Date createTime;

    @UpdateTimestamp
    private Date updateTime;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "role_resources", joinColumns = {@JoinColumn(name = "role_id")}, inverseJoinColumns = {@JoinColumn(name = "resources_id")})
    private List<Resources> resources;
}

Resources

@Entity
@Table
public class Resources implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private String url;

    @CreationTimestamp
    private Date createTime;

    @UpdateTimestamp
    private Date updateTime;

    private Integer pid;

    private Integer orderNumber;

    @Transient
    private List<Resources> list;
}

单元测试

@SpringBootTest
class UserSystemJpaApplicationTests {
    @Resource
    UserRepository userRepository;

    @Resource
    RoleRepository roleRepository;

    @Resource
    ResourcesRepository resourcesRepository;

    @Test
    void userAddUpdate() {
        User user = new User();
        user.setId(1);
        user.setLoginName("admin");
        user.setLoginPassword("123456");
        user.setName("李四");
        userRepository.save(user);
    }

    @Test
    void userSelect() {
        Optional<User> byId = userRepository.findById(1);
        if (byId.isPresent()) {
            System.out.println(byId.get());
        }
        List<User> all = userRepository.findAll();
        System.out.println(all);
        long count = userRepository.count();
        System.out.println(count);
    }

    @Test
    void roleAdd() {
        Role role = new Role();
        role.setName("普通用户");
        role.setRemark("就是看看");
        roleRepository.save(role);

    }

    @Test
    void resourcesAdd() {
        Resources resources = new Resources();
        resources.setName("用户删除");
        resourcesRepository.save(resources);
    }

    @Test
    void userUpdateRole() {
        Optional<User> byId = userRepository.findById(1);
        Optional<Role> byId1 = roleRepository.findById(2);
        if (byId.isPresent() && byId1.isPresent()) {
            User user = byId.get();
            user.setRole(byId1.get());
            userRepository.save(user);
            System.out.println("修改成功");
        }
    }

    @Test
    void roleUpdateResources() {
        Optional<Role> byId = roleRepository.findById(1);
        List<Resources> all = resourcesRepository.findAll();
        //all.remove(0);//删除它的某个资源
        if (byId.isPresent()) {
            Role role = byId.get();
            role.setResources(all);
            roleRepository.save(role);
            System.out.println("修改成功");
        }
    }

}

十五、spring data rest(自)

  springboot 官网在使用 springdata 时建议直接在 Controller 中调用 Repository,但是该建议在国内饱受争议。于是 spring data 提供一套标准的 rest 接口生成规范,也就是可以将 Repository 中的接口全部映射成 rest 接口以完成基本增删改查。由此,项目中便再无 Controller 和 Service。spring data rest 操作如下:

  1.加入依赖

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

  2.编写实体

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Entity
@Table(name = "menu_group")
@Data
public class Group implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String remark;

    @CreationTimestamp
    @Column(updatable = false)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    @UpdateTimestamp
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;

}

  3.编写Repository并加入映射注解

import cn.fotoit.jpa_rest.app1.entity.Group;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;

@RepositoryRestResource(path="group")
public interface GroupRepository extends JpaRepository<Group, Integer> {

    @RestResource(path = "findByName", rel = "findByName")
    Group findByName(String name);
//http://localhost:8080/user/search/findByName?name=张三

    @RestResource(path = "findByNameAndRemark", rel = "findByNameAndRemark")
    //将findByNameAndRemark方法暴露rest接口
    Group findByNameAndRemark(String name,String remark);

    @Override
    @RestResource(exported = false)//取消生成对应rest接口
    <S extends Group> S save(S s);

}

http://localhost:8080/group get 获取全部

http://localhost:8080/group?page=1&size=2 get 获取对应页

http://localhost:8080/group post json 添加 修改

http://localhost:8080/group/5 get 获取id为5的对象

http://localhost:8080/group/5 delete 删除id为5的对象

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

faramita_of_mine

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值