Spring Boot实践六

技术介绍

技术选型 功能说明
springboot 是一种基于 Spring 框架的快速开发应用程序的框架,它的主要作用是简化 Spring 应用程序的配置和开发,同时提供一系列开箱即用的功能和组件,如内置服务器、数据访问、安全、监控等,使开发者可以更加高效地构建和部署应用程序
Maven 快速的引入jar包进行开发,自动构建部署
tomcat 快速部署发布web 服务
junit 单元测试框架
mybatis 将Java对象与关系数据库进行映射,实现数据的持久化操作
redis 用作缓存。它的读写速度非常快,每秒可以处理超过10万次读写操作。高并发访问数据时直接走内存,和直接查询数据库相比,redis的高效性、快速性优势明显
mysql 关系型数据库
  • Springboot 将原有的 xml 配置,简化为 java 注解
  • 使用 IDE 可以很方便的搭建一个 springboot 项目,选择对应的 maven 依赖,简化Spring应用的初始搭建以及开发过程
  • springboot 有内置的 tomcat 服务器,可以 jar 形式启动一个服务,可以快速部署发布 web 服务
  • springboot 使用 starter 依赖自动完成 bean 配置,解决 bean 之间的冲突,并引入相关的 jar 包

mybatis的mapper文件是储存sql语句的一个xml文件,他替代了JDBC在类中写多条语句的问题,简化了步骤。

Mybatis和Redis缓存的区别在于:

  • Mybatis缓存是基于内存的,而Redis缓存是基于磁盘的。这意味着Mybatis缓存只能在单个应用程序实例中使用,而Redis缓存可以在多个应用程序实例之间共享。
  • Mybatis缓存是局部缓存,只能缓存查询结果,而Redis缓存可以缓存任何类型的数据,包括对象、列表、哈希表等。
  • Mybatis缓存是默认开启的,但需要手动配置,而Redis缓存需要安装和配置Redis服务器。
  • Mybatis缓存是基于时间和空间的限制,而Redis缓存可以设置过期时间和最大内存使用量。
  • Mybatis缓存是在应用程序内部实现的,而Redis缓存是在外部服务器上实现的,这意味着Redis缓存可以在多个应用程序之间共享,而Mybatis缓存只能在单个应用程序实例中使用。

Thymeleaf是一种Java模板引擎,它可以将HTML、XML、JavaScript等文件转换为可执行的模板。它可以与Spring框架无缝集成,使得开发者可以使用Thymeleaf来构建动态Web应用程序。

Tomcat是一个开源的Java Servlet容器,它可以运行Java Servlet和JavaServer Pages(JSP)应用程序。Tomcat是一个Web服务器,它可以处理HTTP请求和响应,并将它们传递给Java Servlet和JSP应用程序进行处理。

在开发Web应用程序时,通常会使用Tomcat作为Web服务器,而Thymeleaf可以作为模板引擎来生成动态的Web页面。因此,Thymeleaf和Tomcat可以一起使用来构建动态Web应用程序。

项目结构

 SpringBootRedis 工程项目结构如下:

  controller - Controller 层

  dao - 数据操作层

  model - 实体层

  service - 业务逻辑层

  Application - 启动类

  resources 资源文件夹

    application.properties - 应用配置文件,应用启动会自动读取配置

    generatorConfig.xml - mybatis 逆向生成配置(这里不是本文只要重点,所以不进行介绍)

    mapper 文件夹

      StudentMapper.xml - mybatis 关系映射 xml 文件

项目实现

step1:数据库

首先,我们基于mabatis+redis+mysql实现一个user类数据库增删改查的基本功能。

实体类User实现 Serializable 接口,因为 Spring 会将对象先序列化再存入 Redis

package com.example.demospringboot.bean;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
public class User implements Serializable {
    private int id;
    private String username;
    private String password;
}

dao层,定义UserMapper接口:

package com.example.demospringboot.dao;

import com.example.demospringboot.bean.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;

@Repository
@CacheConfig(cacheNames = "users")
public interface UserMapper {
    User findUserById(@Param("id") int id);
    User findUserByName(@Param("username") String username);
    String findPassword(String username);
    @Cacheable
    List<User> findAllUsers();
    void deleteUserById(@Param("id") int id);
    void deleteAllUsers();
    int insertUser(@Param("user") User user);
    void updateUserPassword(@Param("user") User user);
}

对应的demospringboot\src\main\resources\mybatis\UserMapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- mapper标签要指定namespace属性,和实际的mapper文件一致-->
<mapper namespace="com.example.demospringboot.dao.UserMapper">
    <select id="findUserById" resultType="com.example.demospringboot.bean.User">
        select * from t_user where id = #{id}
    </select>
    
    <select id="findUserByName" resultType="com.example.demospringboot.bean.User">
        select * from t_user where username = #{username}
    </select>
    
    <select id="findAllUsers" resultType="com.example.demospringboot.bean.User">
        select * from t_user
    </select>

    <delete id="deleteAllUsers" >
        delete from t_user
    </delete>

    <delete id="deleteUserById" parameterType="int">
        delete from t_user where id=#{id}
    </delete>

    <insert id="insertUser" parameterType="com.example.demospringboot.bean.User">
        insert into t_user(id,username,password) values(#{user.id},#{user.username},#{user.password})
    </insert>

    <update id="updateUserPassword" parameterType="com.example.demospringboot.bean.User">
        update t_user set password=#{user.password} where id=#{user.id}
    </update>

</mapper>

主启动类:

package com.example.demospringboot;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Repository;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.cache.annotation.EnableCaching;

@EnableCaching
@EnableAsync
@SpringBootApplication
@MapperScan(value = {"com.example.demospringboot.dao"})
public class DemospringbootApplication {

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

}

step2:Web页面

然后,我们基于springboot内嵌的tomcat,用Thymeleaf 模版实现一个存储用户账号密码的web界面如下:
Spring Boot实践六_第1张图片

实现userController:

package com.example.demospringboot.controller;

import com.example.demospringboot.bean.User;
import com.example.demospringboot.dao.UserMapper;

import com.sun.org.apache.bcel.internal.generic.ARETURN;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.ui.Model;

import javax.servlet.http.HttpSession;

@Controller
public class UserController {
    @Autowired
    UserMapper userService;

    @GetMapping(value = {"/login"})
    public String loginPage() {
        return "login";
    }

    //注册用户
    @GetMapping("gotoregister")
    public String register2Page(HttpSession session, Model model) {
        // 返回register.html
        return "register";
    }

    @PostMapping("register")
    public String RegisterUser(User user, Model model) {
        try {
            User userName = userService.findUserByName(user.getUsername());
            //没有用户可以进行注册
            if (userName == null) {
                if (user.getPassword().equals("") || user.getUsername().equals("")) {
                    model.addAttribute("tip", "请填写信息");
                    return "register";
                } else {
                    int ret = userService.insertUser(user);
                    if (ret > 0) {
                        model.addAttribute("tip", "注册成功,请返回登录页面进行登录");
                    }
                    return "register";
                }
            } else {
                model.addAttribute("tip", "用户已存在,请返回登录页面进行登录");
                return "register";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }

    @PostMapping("login")
    public String loginSuccess(User user, HttpSession session, Model model) {
        try {
            //先查找一下有没有该账号
            User userName = userService.findUserByName(user.getUsername());
            if (userName != null) {
                //如果有账号则判断账号密码是否正确
                String password = userService.findPassword(user.getUsername());
                if (password.equals(user.getPassword())) {
                    //添加到session保存起来
                    session.setAttribute("loginUser", user);
                    //重定向到@GetMapping("success")
                    return "redirect:/success";
                } else {
                    //如果密码错误,则提示输入有误
                    model.addAttribute("msg", "账号或者密码有误");
                    return "login";
                }
            } else {
                model.addAttribute("msg", "账号或者密码有误");
                return "login";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }

    @GetMapping("success")
    public String successPage(HttpSession session, Model model) {
        User loginUser = (User)session.getAttribute("loginUser");
        if (loginUser != null) {
            model.addAttribute("user", loginUser.getUsername());
            // 返回success.html
            return "success";
        } else {
            model.addAttribute("msg", "请登录");
            return "login";
        }
    }
}

对应的xml放置在demospringboot\src\main\resources\templates\目录下:

login.html:

<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登录注册界面</title>
    <link rel="stylesheet" href="../static/style.css">
</head>

<body>
<!-- 整体布局 -->
<div class="container right-panel-active">
    <!-- 登录框 -->
    <div class="container_from container_signin">
        <form class="form" id="form" method="post" th:action="@{/login}">
            <h2 class="form_title">欢迎登录</h2>
            <div class="row">
                <span>用户名:</span>
                <input type="text" name="username" placeholder="请输入您的账号" class="input">
            </div>
            <div class="row">
                <span>&emsp;码:</span>
                <input type="password" name="password" placeholder="请输入您的密码" class="input">
            </div>
            <div class="row">
                <span th:text="${msg}"></span>
            </div>
            <input type="submit" class="btn" value="登录"/>
        </form>
        <form class="form" method="get" id="form1" th:action="@{/gotoregister}">
            <label id="register" class="form-label" >没有账号?请点击
            <input class="btn" type="submit" value="注册"/>
        </form>
    </div>
</div>
<script src="../static/login.js"></script>
</body>
</html>

register.html:

<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登录注册界面</title>
    <link rel="stylesheet" href="../static/style.css">
</head>

<body>
<!-- 整体布局 -->
<div class="container right-panel-active">
    <!-- 注册框 -->
    <div class="container_from container_signup">
        <form class="from" method="post" id="from" th:action="@{/register}">
            <h2 class="form_title">注册账号</h2>
            <div class="row">
                <span>用户名:</span>
                <input type="text" id="username" name="username" placeholder="请输入账号" class="input">
            </div>
            <div class="row">
                <span>&emsp;码:</span>
                <input type="password" name="password" placeholder="请输入密码" class="input">
            </div>
            <!-- 提示注册信息${tip} -->
            <div class="row">
                <span th:text="${tip}"></span>
            </div>
            <input class="btn" type="submit" value="注册"/>
        </form>
    </div>
</div>
<script src="../static/login.js"></script>
</body>
</html>

success.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Thymeleaf Spring Boot Example</title>
</head>
<body>
<h1 th:text="'Welcome,'+${user}+'!'"></h1>
<a>You have successfully logged in !</a>
</body>
</html>

step3:多线程task

配置

# mysql 指定使用的数据库
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql5: spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# mybatis 指定mapper xml映射文件
mybatis.mapper-locations=classpath:mybatis/*.xml
# 打印mybatis的执行sql
# mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

# redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=100ms

# 线程池
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.shutdown.await-termination=false
spring.task.execution.shutdown.await-termination-period=
spring.task.execution.thread-name-prefix=task-


package com.example.demospringboot.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@EnableAsync
@Configuration
public class taskConfig {
    /**
     * 对象线程池定义
     */
    @Value("${spring.task.execution.pool.core-size}")
    private Integer corePoolSize;

    @Value("${spring.task.execution.pool.max-size}")
    private Integer maxPoolSize;

    @Value("${spring.task.execution.pool.queue-capacity}")
    private Integer queueCapacity;

    @Value("${spring.task.execution.pool.keep-alive}")
    private Integer keepAliveSeconds;

    @Value("task1-")
    private String threadNamePrefix1;

    @Value("task2-")
    private String threadNamePrefix2;

    // 使用@Bean 注解表明taskExecutor需要交给Spring进行管理
    // 未指定bean 的名称,默认采用的是"方法名"+"首字母小写"的配置方式
    @Bean
    public Executor taskExecutor1() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setThreadNamePrefix(threadNamePrefix1);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    @Bean
    public Executor taskExecutor2() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveSeconds);
        executor.setThreadNamePrefix(threadNamePrefix2);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}

package com.example.demospringboot.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.Random;
import java.util.concurrent.CompletableFuture;

@Slf4j
@Component
public class AsyncTasks {
    public static Random random = new Random();

    @Async("taskExecutor1")
    public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
        log.info("开始任务:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任务完成");
    }

    @Async("taskExecutor2")
    public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {
        log.info("开始任务:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任务完成");
    }
}

test

package com.example.demospringboot;

import com.example.demospringboot.dao.UserMapper;
import com.example.demospringboot.bean.User;
import com.example.demospringboot.task.AsyncTasks;


import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import java.util.concurrent.CompletableFuture;
import org.springframework.cache.CacheManager;

import java.util.List;

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback(value = false)
public class DemospringbootApplicationTests {
    @Autowired()
    private UserMapper userMapper;

    @Autowired
    private CacheManager cacheManager;

    @Test
    public void testUserMapper() throws Exception {
        // deleteAllUsers
        userMapper.deleteAllUsers();

        // insertUser 插入2条
        User user = new User();
        user.setId(100);
        user.setUsername("Jacky");
        user.setPassword("1000");
        userMapper.insertUser(user);
        user.setId(200);
        user.setUsername("Mike");
        user.setPassword("2000");
        userMapper.insertUser(user);

        // findUserById
        user = userMapper.findUserById(100);
        Assert.assertEquals("Jacky", user.getUsername());

        // updateUserPassword
        user.setPassword("1500");
        userMapper.updateUserPassword(user);
        Assert.assertEquals("1500", user.getPassword());

        // deleteUserById
        userMapper.deleteUserById(100);

        // findAllUsers
        List AllUsers = userMapper.findAllUsers();
        for (User u : AllUsers) {
            System.out.println(u);
        }
        //Assert.assertEquals(1, AllUsers.size());

        System.out.println("CacheManager type : " + cacheManager.getClass());

    }


    @Autowired
    private AsyncTasks asyncTasks;


    @Test
    public void testTasks() throws Exception {
        long start = System.currentTimeMillis();

        // 线程池1
        CompletableFuture task1 = asyncTasks.doTaskOne("1");
        CompletableFuture task2 = asyncTasks.doTaskOne("2");
        CompletableFuture task3 = asyncTasks.doTaskOne("3");

        // 线程池2
        CompletableFuture task4 = asyncTasks.doTaskTwo("4");
        CompletableFuture task5 = asyncTasks.doTaskTwo("5");
        CompletableFuture task6 = asyncTasks.doTaskTwo("6");

        // 一起执行
        CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();

        long end = System.currentTimeMillis();
        log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
    }

}


附录:常用注解

注解 说明 举例
@Mapper @Mapper注解是MyBatis框架中的一个注解,它的作用是将一个Java接口标记为一个MyBatis的Mapper,使得这个接口可以被MyBatis框架自动扫描并生成对应的实现类。
@MapperScan 如果多个接口都要变成实现类,那么需要在每个接口类上加上@Mapper注解,这样比较麻烦,我们通常会使用@MapperScan这个注解 @MapperScan(value = {"com.example.demospringboot"})
public class DemospringbootApplication {
@Repository @Repository注解便属于最先引入的一批,它用于将数据访问层 (DAO 层 ) 的类标识为 Spring Bean。具体只需将该注解标注在 DAO类上即可。 // 首先使用 @Repository 将 DAO 类声明为 Bean:
package bookstore.dao;
@Repository
public class UserDaoImpl implements UserDao{ …… }
@Bean 同时,为了让 Spring 能够扫描类路径中的类并识别出 @Repository 注解,需要在 XML 配置文件中启用Bean 的自动扫描功能,这可以通过context:component-scan/实现 // 其次,在 XML 配置文件中启动 Spring 的自动扫描功能:

……

……
@Component Spring 2.5 在 @Repository的基础上增加了功能类似的额外三个注解,它们分别用于软件系统的不同层次:
@Component 是一个泛化的概念,仅仅表示一个组件 (Bean) ,可以作用在任何层次。
通过在类上使用 @Repository、@Component、@Service 和 @Constroller 注解,Spring会自动创建相应的 BeanDefinition 对象,并注册到 ApplicationContext 中。这些类就成了 Spring受管组件。这三个注解除了作用于不同软件层次的类,其使用方式与 @Repository 是完全相同的。
@Service @Service 通常作用在业务层,但是目前该功能与 @Component 相同。
@Controller
@Constroller 通常作用在控制层,但是目前该功能与 @Component 相同。
@Scope 为了配合这四个注解,在标注 Bean 的同时能够指定 Bean 的作用域,Spring2.5 引入了 @Scope 注解。使用该注解时只需提供作用域的名称就行了 @Scope(“prototype”)
@Repository
public class Demo { … }
@Configuration 表明某个类是配置类 @Configuration
public class AppConfig {
@Bean
public MyBean myBean() {
// instantiate, configure and return bean …
}
}
@ResponseBody 将Controller的方法返回的对象,通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML数据
@RestController 相当于@ResponseBody + @Controller合在一起的作用;无法返回jsp页面,返回的内容就是Return 里的内容
@RequestMapping 用来处理请求地址映射的注解,可用于映射一个请求或一个方法,可以用在类或方法上;@RequestMapping 的 value 属性必须设值,通过当前请求的请求地址来匹配请求;
@PostMapping 处理post方式请求的映射
@GetMapping 处理get方式请求的映射;相当于@RequestMapping(method=RequestMethod.GET),它会将get映射到特定的方法上
@Entity 表示该类是一个实体类,在项目启动时会根据该类自动生成一张表,表的名称即@Entity注解中name的值,如果不配置name,默认表明为类名 @Entity(name = “t_book”)
@Data 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法 @Setter : 注在属性上,提供 set 方法
@Getter : 注在属性上,提供 get 方法
@EqualsAndHashCode : 注在类上,提供对应的 equals 和 hashCode 方法
@NoArgsConstructor 注在类上,提供类的无参构造 @AllArgsConstructor : 注在类上,提供类的全参构造
@Table 对应的数据表名。对象映射到数据库的数据表,通过它可以为实体指定表(table)
@Id 主键
@GeneratedValue 主键生成策略 @Entity
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue
private Integer id;

}
@Log4j/@Slf4j 注在类上,提供对应的 Logger 对象,变量名为 log
@Param 一般用于mybatis中mapper与map.xml中的参数对应。其他:
@Insert (insert into xxxx) 增
@Delete (xxxx) 删
@Update (xxxx)改
@Select (xxxx)查

@Select("SELECT * FROM t_user WHERE USERNAME= #{username}")
User findByName(@Param(“username”) String name);
@Async 在方法上加入这个注解,spring会从线程池中获取一个新的线程来执行方法,实现异步调用
@EnableAsync 表示开启对异步任务的支持,可以放在springboot的启动类上,也可以放在自定义线程池的配置类上
@value 用来注入外部化配置文件或者系统属性的值 @Value(“${example.property}”)
private String exampleProperty;

你可能感兴趣的:(JAVA语言,spring,boot,java,spring)