SpringBoot基础知识

145 阅读13分钟

SpringBoot 集成的时候可能遇到的问题

如果无法启动Tomcat需要注意Springpom.xml文件中的依赖是否存在以下配置:

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

导入Module的时候分两步

1. 把需要导入的Module拷贝到项目目录中
2. command + ; 导入模块的时候, 添加的是pom.xml配置文件即可

SpringBoot是提供了web的快速开发的支持包括

- Spring   提供java开发的底层容器和基础支持IoC/AOP/JDBC/ORM等等
- Tomcat   web应用服务器(Servlet容器)
    1、负责接收和反馈外部请求的连接器Connector
    2、负责处理请求的容器Container
    3、管理servlet应用的生命周期
    4、把客户端请求的url映射到对应的servlet
    5、与Servlet程序合作处理HTTP请求
- JDK      
- Annotation  注解支持
- Servlet  动态生成web内容 
- JMS   Java Messaging Service, 它便于消息系统中的Java应用程序进行消息交换,并且通过提供标准的产生、发送、接收消息的接口简化企业应用的开发
- JavaMail 处理电子邮件相关的编程接口

SpringBoot 和 Tomcat 是通过一个叫 DispatcherServlet 进行交互

获取请求参数

1.1 原始手段

@RequestMapping("/hello")  
public String hello(HttpServletRequest req) {  
    String name = req.getParameter("name");  
    String ageStr = req.getParameter("age");  

    int age = Integer.parseInt(ageStr);  

    System.out.println("hello spring -- " + age);  
    return "OK";  
}

1.2 基于sb接收

// 只需要在接口中放入参数名称即可... 
@RequestMapping("/hello")  
public String hello(String name, String age) {  

    System.out.println("hello spring -- " + age);  
    return "OK";  
}

不管是GET请求还是POST请求, 只要在方法中保持接收参数一致即可

请求参数只要能和参数对应上就可以获取成功, 如果映射不上则就会获取不到

// @RequestParam(name = "name") String userName, 这种是可以把请求中的name映射成userName
// 使用了这个注解, 默认是必须要传递的, 否则会报错, 但可以通过设置 @RequestParam(name ="kk", required = false)
public String hello(@RequestParam(name = "name") String userName, String age) {  
    System.out.println("hello spring -- " + age);  
    return "OK";  
}

1.3 复杂实体的请求

http://localhost:8080/hello?name=kk&age=10&address.city=bj&address.province=hn

会有这么写请求的么? 

需要在User实体类中再增加一个 即可
private Address address;  

1.4 数组类的参数

// 这里的hobby 要和请求参数中的形参保持一致
// 如果要封装到list中  要使用 @RequestParam List<String> hobby
public String hello(String[] hobby) {  
    System.out.println("hello spring -- " + Arrays.toString(hobby));  
    return "OK";  
}

1.5 日期参数

// 定义好时间格式
public String hello(@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") LocalDateTitme dateTime) {  

1.6 Json参数

postMan 中 传递json使用 raw 最后侧选择json

image.png

sb中接收json参数

public String hello(@RequestBody User user)

1.7 路径参数

@RequestMapping("/hello/{id}")  
public String hello(User user, @PathVariable Integer id) {
    // {}里面的内容表示是路径, 然后通过@PathVariable 和 id进行绑定
    // {}里的和 注解中指定的还是要保持一致
}

如果是多个路径参数

@RequestMapping("/hello/{id}/{name}")  
public String hello(@PathVariable Integer id, @PathVariable String name) {
    
}

响应数据

@ResponseBody

类型: 方法注解, 类注解
位置: Controller方法上/类上
作用: 将方法返回值直接响应, 如果返回值类型是对象/集合, 将会转为json格式
说明: @RestController = @Controller + @ResponseBody

有了上面这玩意, 就啥也不用做, 直接组织return的数据结构即可

通常返回数据都会有一个固定格式

{
    "code": 1, 
    "msg":"操作成功",
    "data":...
}

public class Result {
    private Integer code;
    
    private String msg;
    
    Private Object data;

}

解析xml文件需要 dom4j, 工具类是XMLParseUtils(自己写吧, 解析自己的xml)


// 获取文件路径
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();

// 解析
List<Emp> empList = XmlParseUtils.parse(file, Emp.class);

// 处理
empList.stream().forEach(emp -> {

})

// 封装到Result
return Result.success(emplist);

静态页面资源文件放到resources下的static目录下, 访问这个目录下的页面时, 不用加static http://lcoalhost:8080/emp.index

分层架构

三层架构的划分

Controller: 控制层, 接收请求, 响应数据

Service: 业务逻辑层

Dao: 数据访问层(Data Access Object), 访问数据操作, 增, 删, 改, 查

分层架构的基本逻辑, 请求到达Controller --> Service处理业务逻辑 --> 向Dao层要数据 --> Service返回数据给Controller --> Controller再返回给请求

Dao层
    EmpDao --- interface
        public List<Emp> listEmp();
        
    impl
        EmpDaoA implements EmpDao
            @Override 
            public List<Emp> listEmp() {
                xxxx
                return empList;
            }
            

Service层
    EmpService --- interface
        public List<Emp> listEmp();
        
    impl
        EmpServiceA implenents EmpService
             private EmpDao empDao = new EmpDao();
             
             @Override
             public List<Emp> listEmp() {
                 List<Emp> empList = empDao.listEmp();
                 // 这个List 是Dao层的数据
                 return empList;
             }
             
Controller层
private EmpService empService = new EmpServiceA();

public Result list() {
    List<Emp> empList = empService.listEmp();
    
    return Result.success(empList);
}

分层解耦

初步分层所带来的问题

上面这种使用三层架构有问题就是他们互相引用, Controller层需要处理业务逻辑 private EmpService empService = new EmpServiceA()直接持有了Service层的对象, 如果业务层有变动或者类发生了变化, 那么控制层也需要变动, 这两个对象就有了耦合, 解除耦合就是不能由控制层直接去实例化Service, 而是交给第三个对象来做, 这么做的好处

1. 控制层只需要EmpService对象, 他不关心是谁来实例化出来的
2. 第三方容器对象来持有Service对象后, 不管是ServiceA来实例化的, 还是ServiceB来实例化, 那么控制器都是无感知的
3. 对象交给第三方管理牵扯的问题是控制反转, 第三方怎么把Service对象交给控制器牵扯的问题是依赖注入

控制反转 IOC

Inversion Of Control简称IOC也就是控制反转

对象的创建控制权由程序自身转移到外部, 这种思想被称为控制反转

IOC相关的注解有四种
@Component    不符合下面三种的, 比如工具类这些
@Controller   标注在控制器上的
@Service      标注在service上的
@Repository   标注在数据访问上的, 因为与mybatis结合, 用的少

在使用注解的时候, 还可以指定名字, 如果不指定, 则是类名首字母小写  一般也不指定
@Component("bName")

注解想要生效, 就需要被@ComponentScan扫描, 虽然没有显示配置, 实际上已经包含在了启动声明注解@SpringBootApplication中, 默认扫描范围是启动类所在包及其子包, 想要修改的话, 可以在入口类中使用@ComponentScan({"dao", "com.example"})这种会覆盖默认的, 一般也不会这么做, 不符合规范, 使用@Import方式

导入普通类/配置类/ImportSelector接口实现类


@Import({TokenParser.class, HeaderConfig.class})
@SpringBootApplication
public class SpringApplication {

}

// ImportSelector接口实现类
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.kk.className"};
    }
}

// 导入实现类
@Import({MyImportSelector.class})

一般是否要注入, 第三方最清楚, 所以一般第三方都会提供一个注解 (推荐方式)

@Enablexxx注解, 来封装@Import
// 比如这个第三方叫 Config, 
// 那么他会提供一个叫 EnableConfig的类

然后使用的时候只需要 
@EnableConfig // 使用这种方式即可
@SpringBootApplication
public class SpringBootTest {

}

依赖注入 DI

Dependency Injection, 简称`DI`, 容器为应用程序提供运行时, 所依赖的资源, 称之为依赖注入

@Autowired注解, 默认是按照类型进行的, 如果存在多个类型相同的bean, 就会报错, 有三总解决方法

@Primary

    @Primary  // 有这个注解的才会生效
    @Service
    public class ServiceA {}

@Qualifier

    @Autowired
    @Qualifier("serviceA") // 配合上面的来指定, 需要的是哪个bean, 注意这里的s是小写指的是bean的名字
    private EmpService empService;

@Resource
    使用这个的话, 就不用@Autowired@Resource(name="serviceB")
    private EmpService empService;

Bean对象

IOC容器中创建, 管理的对象, 称之为bean, 是java中的一种规范
    1. Bean必须生成public class类。
    2. 所有属性必须封装,Bean类的属性都为private
    3. 属性值应该通过一组[方法](getXxx 和 setXxx)来访问和修改
    4. Bean类必须有一个空的构造函数(默认构造函数)
    

三层架构的优化

1. Service 和 Dao层交给容器类管理, 使用注解
    @Component
    public Class EmpDaoA Implements EmpDao{}
    
    @Component // 交给IOC容器, 成为bean
    public Class EmpServiceA implents EmpService{
        private EmpDao empDao;
    }
    
2. 控制层和Service注入运行时, 依赖的对象

    public class EmpController {
        @AutoWired
        private EmpService empService;
        
        xxx
    }
    
    EmpServiceA
    @Component
    public Class EmpServiceA implents EmpService{
        @AutoWired // 运行时, IOC容器会怼该类型的bean对象, 并赋值给该变量 依赖注入
        private EmpDao empDao;
        
        xxx
    }

数据库设计

数据库设计中有三种现象

一对一: 是要做表拆分, 还是一个主键关联到一个外键(唯一不可重复)

比如个人信息表: 包含姓名, 性别, 出生时间, 年龄, 地址.... , 可能经常用到的就是前面几项, 这个时候如果拆分两张表, 效率会有所提升
个信息表    用户id 主键 
详细信息表  外键id  唯一不可重复

一对多: 多的一方添加一个外键, 关联到一的主键

多对多: 拆分出第三张的表

比如学生和课表的关系, 一个学生可以选择多门课程, 一门课程也可以对应多个学生
    学生表  学生id  
    课程表  课程id
    关系表  学生id  课程id

多表查询

笛卡尔积: 关联多张表的时候, 如果不指定条件, 那么会出现N多数据

比如表A 5条, 表B 10条数据
select * from A, B;  
这个时候会有50条数据, 表A的数据每一个都会跟表B的数据进行关联

内联(join): 两张表求交集

隐式内联
select * from tab_a , tab_b where tab_a.id = tab_b.bid

显示内联
select * from tab_a (inner) join tab_b on tab_a.id = tab_b.bid

外联(left join & right join): 以left/right 全部数据也包括左右两张表的交集

子查询: 在一个sql语句中又包含了别的查询

select * fro tab_a where column1 = (select * from tab_b where xxxx)

操作符包括 >, <, in, not in

事务

事务开始: start transaction; / begin;

设置回滚点:  savepoint xxx  // 为了实现子任务
事务回滚: rollback;  rollback to xxx;
事务提交: commit;

索引

- 没有索引的时候, 会查询是全表扫描, 数据量大就会很慢
- 添加索引之后, 会大大提高搜索效率, 索引就会维护一个树形结构(类似) - 二叉树 然后同时跟原始数据进行关联
- 索引提高了查询效率, 但是对insert, update, delete并没有那么好, 修改了数据之后需要更新索引表
- 索引会额外的占用空间, 虽然并不大
- MySql 支持: Hash, B+Tree(多路平衡搜索树), Full-Text, 默认是B+Tree

MyBatis

一款优秀的持久层框架(DAO), 用于简化JDBC的操作, 可通过xml或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录, 插件MybatisX

MyBatis基本使用

  1. 添加依赖
<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

// mysql的驱动包 , 自动会导入?
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <version>runtime</version>
</dependency>
  1. 连接数据库设置
链接到数据库的4要素
    1. 驱动: com.mysql.cj.jdbc.Driver
        spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
        
    2. url: jdbc:mysql://localhost:3306/mybatis
        spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
        mybatis是数据库的名字
        
    3. username: 用户名
        spring.datasource.username=root
        
    4. password: 密码
        spring.datasource.password=1234
    
上面这四项需要再 application.properties 中配置
  1. 使用MyBatis注解
只需要定义接口, 框架底层会自动实现

@Mapper  // 运行时会自动生成该接口的实现类对象(代理对象), 并将该对象交给IOC管理(已经是一个Bean对象了), 那么可以通过DI使用
public interface UserMapper {
    @Select("select * from user")
    public List<User> list();
}

默认情况下, 在idea下面 写上面的sql语句是不会有提示的, 被视为字符串, 想要有提示
    1. 选中sql语句
    2. 右键 --> Inject Language of refreence
    3. 找到MySQL选项
    4. 在idea中配置MySQL数据库链接(否则表名会爆红)

配置数据库连接

image.png

JDBC(Java DataBase Connectivity)

JDBC只是sun公司提供的的一套规范API, 具体实现由各大数据库厂商来实现

数据库连接池

1. 数据库连接池是个容器, 负责分配, 管理数据库链接(Connection)
2. 允许应用程序重复使用一个现有的数据库链接, 而不是重新建立
3. 释放空闲时间超过最大空闲时间的连接, 来避免因为没有释放连接而引起的数据库连接遗漏

数据库连接池产品
    C3P0 
    DBCP
    Druid  德鲁伊, 阿里巴巴开源的数据库连接池
    Hikari(sb默认) 追光者 翻译为光
        com.zaxxer.hikari.HikariDataSource

如果想切换连接池到Druid

1. 添加Druid依赖(添加这个就行了...)
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>runtime</version>
    </dependency>
    
2. 设置数据库连接池

再次运行程序的时候, 数据库连接池的提示应该是 com.alibaba.druid.pool.DruidDataSource

Tips: 在类文件中, 右键 --> Diagrams --> Show Diagrams可以查看继承图

Lombok: 一个实体类定义之后, 需要去重写get和set方法/toString/构造方法之类的, 而他只需要一个注解就可以了, 这玩意儿还有一个插件, 新版本应该都安装了

@Data
public class User {
    xxx;
    xxx;
    xxx;
    
}

主要注解包括
@Getter/@Setter
@ToString
@EqualsAndHashCode
@Data // 这个就相当于上面四个
@NoArgsConstructor // 生成无参构造
@AllArgsConstructor // 除static修饰的字段之外的有参构造

添加依赖
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

一般使用的规则

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    xxx;
    xxx;
    xxx;
    
}

MyBatis操作

删除操作


// 这里的删除需要指定where的数据, 那么就需要在调用delete的时候把需要删除的id传递进来
// #{} 是MyBatis的固定写法, 里面的id 和方法里的参数要保持一致
@Mapper 
public interface EmpMapper {
    @Delete("delete from emp where id = #{id}") // 会进行预编译sql语句 性能高, 防止sql注入
    public void delete(Integer id) // 这里的语句是有返回值的, 返回的数据表示影响的条目数
    
或者使用 ${id} 会进行拼接
}

测试类的书写


@Autowired
private EmpMapper empMapper;

@Test
public void testDelete() {
    empMapper.delete(20);
}

开启mybatis日志, 默认这个日志是没有开启的, 在application.properties配置


// 这种配置很多..

# 显示mybatis的日志到控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

sql执行一条语句的时候

1. 通过连接池链接到数据库, 用户名/密码验证, 通过之后去查询权限
2. 是否有缓存, 现在貌似默认是关闭的? 
3. 语法语义检查
4. 优化
5. 执行

如果使用预编译, 则可以提高效率, 如果命中缓存直接就会拿到优化后的sql去执行, 执行的时候把参数带上

sql注入指的是跟预编译相对立的这种,直接把参数放入到sql语句中


这种就是直接把参数放入到语句中
select count(*) from user where username="kkk" password="ddd";
如果这里在填入参数的时候使用sql注入
select count(*) from user where username="kkk" password="" or '1' = 1"
这里的or是一个或语句, 后面 1 = 1 永远成立


预编译是采用占位符的形式
select count(*) from user where username= ? password= ?;
占位符中的 ? 在执行的时候会把参数放入

插入操作

// value前面的是数据库的里的字段, 要跟数据库保持一致
// value后面呢的是要插入的数据是emp实体, 要跟实体里的属性名保持一致
@Insert("insert into emp(username, name, gender.....) values (#{username}, #{name}, #{gender}...)")
public void insert(Emp emp);

主键返回, 获取到新插入数据的主键值需要额外的一个注解

@Options(keyProperty="id", userGeneratedKeys=true) // 需要获取主键值, 然后赋值给实体里的id属性
@Insert("insert into emp(username, name, gender.....) values (#{username}, #{name}, #{gender}...)")
public void insert(Emp emp);

更新操作

@Update("update emp set username=#{uname}, name=#{name}... where id=#{id}")
public void update(Emp emp);

查询操作


方案一:
下面那两种操作知道就行了, 不怎么用, 还得是MyBatis配置, 自动把 a_column 转为 aColumn
# 开启MyBatis的驼峰命名自动映射开关
mybatis.configuration.map-underscore-to-camel-case=true

方案二:
// 这里的 dept_id 数据库中的   deptId 表示是实体中的  需要对应起来
@Select("select id, name, ..." + "dept_id deptId, crate_time createTime ... where id=#{id}")
public Emp getById(Integer id);


方案三:
或者使用 @Results注解 有点麻烦

// 有几个不同的字段就要有几个 @Result()
@Results({
    @Result(column="depet_id", property="deptId"),
    @Result(column="create_time", property="crateTime"),
})
@Select("select * from emp where id = #{id}")
public Emp getById(Integer id);

条件查询

select * from emp where name like "%kk%" and gender = 2 and entryDate between '' and '' orderby update_time desc;

// ${} 这个是拼接作用 #{}是替换, 这里%%不能用#{}
@Select("select * from emp where name like '%${name}%' and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

// 这里如果使用${} 不会生成预编译的sql, 所以使用 concat函数进行链接
@Select("select * from emp where name like concat('%', #{name}, '%') and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc")
public List<Emp> list(String name, Short gender, LocalDate begin, LocalDate end);

XML映射文件

  1. XML映射文件名与Mapper接口名称一致, 并且他们要在同搞一个包下
EmpMapper 这个在java目录下的 com.kk.mapper 下
    
EmpMapper.xml 这要在resource目录下的 com.kk.mapper, 跟上面的保持一致
  1. XML映射文件的namespace属性要和Mapper接口的全限定名称保持一致
@Mapper 
public interface EmpMapper{
    public List<Emp> list(String anme, Short gender, LocalDate begin, LocalDate end);
}

// 全限定名称是指带上包的名称
<mapper namespace="com.kk.mapper.EmpMapper">
    <select id="list" resultType="com.kk.pojo.Emp">
        select * from emp where name like concat('%', '#{name}', '%') and gender=#{gender} and entrydate between #{begin} and #{end} order by update_time desc
    </select>
</mapper>
  1. XML映射文件中sql语句的id与Mapper接口中方法名一致, 并保持返回类型一致
xml中的 id 应该是指的是方法名称
返回是指 resultType, 也要全限定名, 这个是指单条数据的类型

动态SQL

跟随用户的输入或外部条件而变化的SQL语句, 我们称之为动态SQL, 主要就是条件部分, 主要就是各种标签<if> <foreach> <sql> <include>

if和where标签

<mapper namespace="com.kk.mapper.EmpMapper">
    <select id="list" resultType="com.kk.pojo.Emp">
        select * from emp 
            <where> // 如果下面有一个匹配就会生成where语句, 会自动的清除掉and连接符
                <if test="name != null">
                    name like concat('%','#{name}','%')
                </if>
                <if test="gender != null">
                    and gender = #{gender}
                </if>
                
                <if test="begin != null and end != null">
                    and entrydate between #{begin} and #{end}
                </if>
            <where>
        
        order by update_time desc
    </select>
</mapper>

set标签

这个是用来进行更新的

<mapper namespace="com.kk.mapper.EmpMapper">
    <select id="update2">
        update emp 
            <set> // 会自动清除掉,
                <if test="name != null"> name = #{useranme},</if>
                <if test="gender != null"> gender = #{gender},</if>
                <if test="deptId != null"> dept_id = #{deptId},</if>
                
            </set>
        where id = #{id}
    </update>
</mapper>

foreach标签

collection: 遍历的集合, // 传递的参数
item: 遍历出来的元素 // 变量名
separator: 分隔符
open: 开始前拼接的sql片段  // 括号
close: 结束后拼接的sql片段

delete from emp where id in (1, 2, 3);

<mapper namespace="com.kk.mapper.EmpMapper">
    <select id="deleteByIds">
        delete from emp where id in
        <foreach collection="ids" item="id" separator="," open="(" close=")">
        #{id}
        </foreach>
    </update>
</mapper>

sql和include标签

主要是为了提高代码效率

如果操作的数据库中的sql语句有一部分是相同的, 那么就可以使用sql语句进行定义, 然后使用include引入

// 定义
<sql id="commonSelect">
    select id, name, gender .... from emp
</sql>

// 引入
<select id="list" resultType="com.kk.Emp">
    <include refid="commonSelect"/>
    where 
        xxxx
    order by update_time desc
</select>

// 引入
<select id="getById" resultType="com.kk.Emp">
    <include refid="commonSelect"/>
    where id=#{id}
</select>

choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员精选的 Blog)。

<select id="findActiveBlogLike"
     resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>