大家好,今天给大家分享一下SpringBoot框架以及如何整合Mybatis并且进行项目的分页查询。
一、背景介绍
随着项目的逐渐增大,会有许多的xml文件和各种复杂的bean依赖关系,所以就想要摆脱xml的束缚,以“约定优先配置”(convension over configuration)的思想来摆脱spring框架中的各种复杂配置。
在这个背景下,产生了springboot框架。它本身并不提供Spring框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于Spring框架的应用程序。也就是说,它并不是用来替代Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。
同时它集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),Spring Boot应用中这些第三方库几乎可以零配置的开箱即用(out-of-the-box),大部分的Spring Boot应用都只需要非常少量的配置代码,开发者能够更加专注于业务逻辑。
我们一开始学习都是使用的SSM框架,虽然SpringBoot比SSM简单方便,但这不代表我们一开始就应该学习SpringBoot,如果说SSM框架是自行车的话,那springboot就相当于摩托车。我们必须先学习SSM,了解那些bean的建立过程,了解xml中那些相关东西的配置,这样才能游刃有余的去使用SpringBoot。
至于为啥我们说SpringBoot好用呢?主要是基于以下几个优点:
1.Spring Boot Starter:依赖整合,开箱即用
2.自动配置:SpringBoot利用了Spring4对条件化配置的支持,合理推测应用所需的bean并自动化配置它们
3.摒弃繁琐的xml配置,使用application.properties自由组合,配置需要的属性。甚至web项目都不需要WEBAPP目录
4.提供了一系列大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等
5.打包方式自由,可以打jar包直接使用java命令启动,也可以打war包放在tomcat里启动
二、知识剖析
1.依赖
1)parent依赖
所谓parent依赖是指下面这个依赖,它是springboot所有依赖的parent,其他依赖会依赖此依赖,并且其他依赖可以省略version,版本会和parent的版本统一。
<!--项目中其他依赖会自动从parent中继承依赖版本-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
</parent>
点进spring-boot-starter-parent源码可以看到里面有很多默认的定义项,这就是springboot的默认配置以及可以开箱即用的原因,因为几乎所有东西都给你配置好了,除非你想修改。
2)依赖的传递
springboot的依赖并不是一个单纯的依赖,而是多个依赖整合起来的依赖包,相比我们自己添加依赖,其更方便快捷,而且可以避免版本冲突。
其中,它们使用了maven的依赖传递方案,Starter在自己的pom.xml文件中声明了多个依赖,当我们将某一个依赖添加到maven中时,starter的依赖将会自动地传递性解析。这些依赖本身可能也会有其他的依赖,一个Starter可能会传递性的引入几十个依赖,具体可以参考依赖源码。
2.项目结构
1)各个层级分配
首先上一张我自己的项目结构图
可以看到其他的都没什么,主要是有两处不一样,一个是多了一个配置类的文件夹,里面放的是一些配置类,相当于我们将xml文件的形式改为使用注解和java代码来实现。
第二个就是多了一个启动类,这个启动类的放置位置得在主文件夹之下,和其他文件夹平行,这样才能扫描到其他文件夹中的文件,并加载配置文件,这点需要额外注意。
2)application.properties文件
然后注意到我的resources里面有三个文件,我们首先来说第一个,也就是aplication.properties,
我们在介绍优点时说到了自动配置,开箱即用,这是因为所有依赖引入之后都有一个默认配置,
但是如果我们不想使用默认配置怎么办呢?最简单的方法就是在这个文件里面修改,当然,你也可以使用applicatiom.yml文件修改,道理大同小异。
#配置tomcat
#端口号,默认为8080
server.port=8080
#访问路径,默认为/
#server.context-path=/
server.tomcat.uri-encoding=utf-8
#输出日志文件,默认不输出
#logging.file=/log.txt
#修改日志级别,默认为INFO
#logging.level.root=DEBUG
#MySQL
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=4096
#hikari连接池最大连接数
spring.datasource.hikari.maximumPoolSize=100
#Spring Data JPA
spring.jpa.database=MYSQL
#设置为true可以在执行程序后在控制台看见sql语句
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the com.task.entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
#视图层控制
#定位thymeleaf模板目录
#spring.mvc.view.prefix=classpath:/templates/
#如果没有加thymeleaf依赖,则定位到static,加了就定位到/templates
spring.mvc.view.prefix=classpath:/
#给返回的模板添加后缀
spring.mvc.view.suffix=.html
#规定url上想直接访问静态资源必须带上/static,不能直接跳过此文件夹,且其他文件夹中的静态资源无法被访问
spring.mvc.static-path-pattern=/static/**
#将连接池切换为Hikari
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#将数据库中的NN_NN转换为驼峰命名法
mybatis.configuration.mapUnderscoreToCamelCase=true
#打印mybatis的SQL日志到控制台
logging.level.com.task.dao.UserDao=debug
#开启手动重启
spring.devtools.restart.trigger-file=trigger.txt
#设置所有端点不敏感
endpoints.sensitive=false
#设置redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.timeout=1000
几乎你所有需要修改的东西都可以在里面修改,方法就是使用包的前缀加上需要修改的属性,后面是你需要的值,至于哪些可以修改,一个是参考源码,一个是使用Actuator工具查看,这个今天先不聊。
3.注解
1)注解功能
传统的Spring做法是使用.xml文件来对bean进行注入或者是配置aop、事物,这么做有两个缺点:
①如果所有的内容都配置在.xml文件中,那么.xml文件将会十分庞大;如果按需求分开.xml文件,那么.xml文件又会非常多。总之这将导致配置文件的可读性与可维护性变得很低
②在开发中在.java文件和.xml文件之间不断切换,是一件麻烦的事,同时这种思维上的不连贯也会降低开发的效率
为了解决这两个问题,Spring引入了注解,通过"@XXX"的方式,让注解与Java Bean紧密结合,既大大减少了配置文件的体积,又增加了Java Bean的可读性与内聚性。
2)SpringBoot常用注解介绍
结合实际项目来看吧
首先是实体类,一个很简单的实体类,需要注意的就是@Entity注解,另外几个是JPA需要用到的注解
/声明实体类
@Entity
//绑定数据库的表格
@Table(name = "tbl_user")
public class User implements Serializable{
//指定表的主键
@Id
//注释定义了标识字段生成方式,后面的代表使用数据库的IDENTITY列来保证唯一
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String name;
private String password;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
然后是dao层,这里我没有使用一般的mapper.xml,而是使用直接写SQL语句在接口上方的方式,主要的注解就是@Mapper,然后是增删查改四个方法的注解@Insert @Delete @Select @Update,下面的那几个是动态查询,先不用深究
//标记dao层的mapper类
@Mapper
public interface UserDao {
// 按照id查找
@Select("select * from tbl_user where id=#{id}")
List<User> findById(User parm);
//查询全部正序
@Select("select * from tbl_user")
List<User> findAll();
//查询全部按照id逆序
@Select("select * from tbl_user order by id desc")
List<User> fineAllDesc();
//插入数据
@Insert("insert into tbl_user (name,password) values(#{name},#{password})")
//插入之后返回id
@Options(useGeneratedKeys=true,keyProperty="id")
int add(User parm);
//动态查询
// @Select("<script>select * from tbl_user " +
// "<trim prefix=\"WHERE\" prefixOverrides=\"AND\">" +
// "<if test=\"name!=null and name.length()>0\">AND name=#{name}</if>" +
// "<if test=\"id!=0 \">AND id=#{id}</if>" +
// "<if test=\"password!=null and password.length()>0\">AND password=#{password}</if>" +
// "</trim></script>" )
// List<User> findByUser(User parm);
@SelectProvider(type = UserDaoProvider.class,method = "findByUser")
List<User> findByUser(User parm);
class UserDaoProvider{
public String findByUser(User user){
return new SQL(){{
SELECT("*");
FROM("tbl_user");
if (user.getId()!=0)
WHERE("id=#{id}");
if(user.getName()!=null)
WHERE("name=#{name}");
if(user.getPassword()!=null)
WHERE("password=#{password}");
}}.toString();
}
// public String findByUser(User user){
// String sql="Select * from tbl_user ";
// if(user.getId()!=0){
// sql+="where id =#{id}";
// }
// if (user.getName()!=null){
// sql+="where name =#{name}";
// }
// if (user.getPassword()!=null){
// sql+="where password =#{password}";
// }
// return sql;
// }
public String updateByUser(User user){
return new SQL(){{
UPDATE("tbl_user");
if(user.getName()!=null)
SET("name=#{name}");
if(user.getPassword()!=null)
SET("password=#{password}");
WHERE("id=#{id}");
}}.toString();
}
}
//动态修改
// @Update("<script>UPDATE tbl_user " +
// "<trim prefix=\"SET\" suffixOverrides=\",\" suffix=\"WHERE id = #{id}\" > " +
// "<if test=\"name != null and name != '' \">name = #{name}, </if> " +
// "<if test=\"password != null and password != '' \">password=#{password}, </if> " +
// "</trim></script>")
// boolean UpdateByUser(User user);
@UpdateProvider(type = UserDaoProvider.class,method = "updateByUser")
boolean UpdateByUser(User user);
}
service层,简单的不说,另外几个缓存,事务,异步也不是必须的
/标记service层
@Service
//开启事务,标记在类上则类中全部方法都使用事务功能
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
@Resource
private UserDao userDao;
public User findUserByName(String name){
User user=null;
try{
user=userRepository.findByName(name);
}catch (Exception e){
e.printStackTrace();
}
return user;
}
@Cacheable(cacheNames = "findUserById",key = "#id")
public List<User> findUserById(long id){
System.out.println("没有用缓存,进入数据库查询");
User user=new User();
user.setId(id);
return userDao.findById(user);
}
}
controller层,这里可以说的比较多,比如@RestController等于@Controller+@ResponceBody,@GetMapping也是简写
/标记controller层
@Controller
//@RequestMapping(value = "/user")
public class UserController {
@Autowired
private UserService userService;
private static final Logger log=LoggerFactory.getLogger(UserController.class);
//相当于@RequestMapping(method=request.get)
@GetMapping(value = "/index")
public String index() {
System.out.println("123");
int i=1/0;
return "/index";
}
@GetMapping(value = "/html")
public String html() {
System.out.println("123");
return "redirect:/user/index";
}
@PostMapping( value = "/show")
//以json类型返回数据
@ResponseBody
public Map show(@RequestParam(value = "name") String name) {
System.out.println("123");
Map map=new HashMap();
User user = userService.findUserByName(name);
if (null != user)
map.put("data",user);
return map;
}
配置类
//以最高优先级加载
@Order(Ordered.HIGHEST_PRECEDENCE)
//定义配置类,可以替换xml文件,相当于xml中的beans
@Configuration
//开启事务管理
@EnableTransactionManagement(proxyTargetClass = true)
//扫描jpa的repository
@EnableJpaRepositories(basePackages = "com.task.repository")
//扫描实体类
@EntityScan(basePackages = "com/task/entity")
//扫描dao层
@MapperScan(basePackages = "com/task/dao")
//配置异步
@EnableAsync
public class JpaConfiguration {
//注册bean对象,相当于xml中的bean标签
@Bean
PersistenceAnnotationBeanPostProcessor persistenceAnnotationBeanPostProcessor(){
return new PersistenceAnnotationBeanPostProcessor();
}
}
启动类
/@Configuration、@EnableAutoConfiguration、@ComponentScan的集合,自动配置,自动整合
@SpringBootApplication
//扫描整个包
@ComponentScan("com.task")
//开启缓存功能
@EnableCaching
public class Entry {
public static void main(String[] args) {
SpringApplication.run(Entry.class,args);
}
}
其实配置类和启动类很多的注解可以省略,因为我的启动类放置在外面了,如果启动类放在配置类的文件夹里,就要保证这些扫描的注解都有。
三、常见问题
1.怎么整合Mybatis
2.怎么使用分页插件实现分页查询
四、解决方案
1.要整合Mybatis,首先得加入相关依赖,第一个是连接mysql的依赖,第二个是mybaitis依赖,第三个mybaitis的分页插件
<!-- 连接mysql所需依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<!-- 引入mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- mybatis分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
然后在配置文件中将mysql的相关信息填写进去,这个和SSM一样
#MySQL
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=4096
编写实体类,dao,service,controller,具体代码参考上面,启动就可以了
2.怎么实现分页:
首先,在第一个功能实现的基础上,我们将查询所有的service方法修改如下。
将findAll修改为分页查询,我这里形参设置了一个对象,其具体见下面,
就是哪一页开始查,一页几条数据,顺序还是逆序,具体数据在controller层传进来,因为是一个对象,所以前端传进来的可以是任意的属性,其他的没有传的就使用默认值,你也可以只使用页数这一个属性,其他改为不可修改直接写死。
这里就直接使用分页插件中的方法来帮助实现,首先是设置页数和每页的条数,由传进来的对象决定,
然后后面就紧跟查询语句,
其执行原理如下:
PageHelper.startPage会拦截下一个sql,也就是userDao.findAll()的SQL。
并且根据当前数据库的语法,把这个SQL改造成一个高性能的分页SQL,同时还会查询该表的总行数,具体可以看SQL日志。
PageHelper.startPage和userDao.findAll()最好紧跟在一起,中间不要有别的逻辑,否则可能出BUG。
Page<User> page:相当于一个list集合,findAll()方法查询完成后,会给page对象的相关参数赋值。
所以看起来好像findAll没有东西来接,实际上已经把数据存进page对象了。
public class PageModel {
//创建三个查询条件,直接对其进行初始化,如果接收到的是null就自动启用初始条件
private Integer pageNum=1;
private Integer pageSize=10;
private String order="asc";
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public String getOrder() {
return order;
}
public void setOrder(String order) {
this.order = order;
}
}
public Page<User> find(PageModel pageModel){
int pageNum=pageModel.getPageNum();
int pageSize=pageModel.getPageSize();
String order=pageModel.getOrder();
//pageNum=起始页,pageSize=每页数目
Page<User> page = PageHelper.startPage(pageNum,pageSize);
//拦截下一条语句,也就是下面这条findAll(),这条语句可以设置正序还是逆序排列,按照什么排列
if (order.equalsIgnoreCase("asc"))
userDao.findAll();
else
userDao.fineAllDesc();
//数据总条数,不能设置count为false,否则无法计算
page.getTotal();
System.out.println(page.getTotal());
//每页条数
page.size();
System.out.println(page.size());
//获得指定下标的第几条数据,0基
page.get(0);
System.out.println(page.get(0));
return page;
}
然后在controller中就可以接收到传过来的对象了
@GetMapping(value ="/list" )
@ResponseBody
public Map list(@ModelAttribute(value = "pageModel")PageModel pageModel){
Map map=new HashMap();
Page<User> userPage=null;
//分页查询
userPage=userService.find(pageModel);
//数据总条数
int total=(int)userPage.getTotal();
//数据每页条数
int pageSize=userPage.getPageSize();
//总页数
int pageCount=0;
//是否有下一页
boolean hasNextPage=false;
if(total%pageSize==0)
pageCount=total/pageSize;
else
pageCount=total/pageSize+1;
//判断需要查询的页数是否小于总页数,是就返回还有下一页
if(pageModel.getPageNum()<pageCount)
hasNextPage=true;
map.put("hasNextPage",hasNextPage);
map.put("data",userPage);
return map;
}
五、更多讨论
1.如何启动日志功能
logback是springboot的默认日志组件,出自log4j的作者,相比log4j其功能更强大。
之前在配置properties文件的时候设置过简单的日志输出,但是如果想设置更复杂的,则需要创建xml文件
2.官方建议日志文件命名为logback-spring.xml,现创建一个具体的日志配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- appender是配置输出终端,ConsoleAppender是控制台,name是自定义名 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 配置日志格式,这是一个比较通用的格式 -->
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<!-- 输出终端是滚动文件 -->
<appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 基于时间滚动,就是每天的日志输出到不同的文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 输出日志的目录文件名,window中默认分区为当前程序的硬盘分区,%d{yyyy-MM-dd}是当前日期 -->
<fileNamePattern>/logs/warn/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 最大保存99个文件,超出的历史文件会被删除 -->
<maxHistory>99</maxHistory>
</rollingPolicy>
<!-- 按照日志级别进行过滤 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 只收集WARN级别的日志,其他高级别和低级别的日志都放弃 -->
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<!-- 除了filter,其他 配置和上面一样, 只是name和文件路径不同-->
<appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/logs/error/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>99</maxHistory>
</rollingPolicy>
<!-- 阈值过滤器 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 收集ERROR及ERROR以上级别的日志 -->
<level>ERROR</level>
</filter>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<!-- root是根日志打印器,只有一个,负责整个系统的日志输出 -->
<root level="INFO">
<!-- 将上面三个输出终端配置到根打印器,将对整个系统 生效。 -->
<appender-ref ref="STDOUT" />
<appender-ref ref="WARN" />
<appender-ref ref="ERROR" />
</root>
<!-- logger是root的子打印器,可以有多个,输出name配置的包中的日志。 -->
<!-- com.task.repository是我的mybatis映射dao的包名,设置为debug可以打印mybatis的sql语句 -->
<logger name="com.task.repository" level="DEBUG" />
</configuration>
然后就会在相应磁盘位置创建log文件了
2.怎么实现热部署
我们一般在修改完项目重新启动时,会重新加载Jar包并重载代码,这在平时开发中是比较浪费时间的,所以我们此时可以添加一个开发者工具,它可以帮我们只重新加载代码而不去加载依赖。
首先导入开发者工具
<!--开发者工具,热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
注意:
1)修改pom.xml的依赖,也算是修改代码,也会重启,但不会重载你修改的依赖jar
2)修改前端代码不会触发重启
如果不想写一点就被重启,可以将启动重启改为手动触发重启
#开启手动重启
spring.devtools.restart.trigger-file=trigger.txt
此时我修改了resource中的此文件才会触发重启,而且必须要修改过代码才会触发,这样就可以实现手动触发了。
我们在打成jar包时,开发者工具是不会打进去的。
3.怎么进行单元测试?
Spring Boot的测试,和普通项目的测试类同,可以继续使用我们熟悉的测试工具。当然,这里的测试,主要还是后台代码的测试。
其实我们主要测试的还是dao层和service层,controller层的测试还不如直接启动用postman来测比较方便。
首先还是依赖的导入
<!--测试相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
在test文件夹建立测试类,
@RunWith(SpringJUnit4ClassRunner.class)
//classes后面是启动类.class
@SpringBootTest(classes = Entry.class)
//@WebAppConfiguration
public class HelloTest {
@Autowired
UserService userService;
@Resource
RedisCacheManager redisCacheManager;
@Test
public void test1(){
redisCacheManager.hset("user","name","wang");
System.out.println(redisCacheManager.hget("user","name"));
User user=new User();
user.setId(122);
user.setPassword("123");
user.setName("wangshsh");
redisCacheManager.hset("user","1",user);
System.out.println(redisCacheManager.hget("user","1"));
}
注意这里几个注解的使用。
六、参考文献
https://blog.csdn.net/column/details/15339.html?&page=2
本文只是对springboot的一个简单介绍,具体可参考上面的这个博客,有很详细的介绍。