注:本篇笔记较长,所有涉及到知识仅作为入门的简单了解,帮助扩宽知识面,以便在能用上的场景可以有大体解决方向
本篇项目的giee:https://gitee.com/ywq869819435/springboot-hodgepodge-primer

快速创建Spring Boot

1.在官网下载

不好用还是需要导入idea

2.idea创建

创建一个新项目


Group: 表示什么样项目类型(com【表示公司】.公司名)

Artifact: 项目名称

Type: 选择工程类型

Packaging: 选择导出包的形式 这里是jar包

Name:项目名称 Description:描述

选择一些常用的组件

这里选择Web的Spring Web、SQL的Spring Data JDBC、MySQL Driver

项目名称以及存储路径,之后完成创建

设置导入依赖以及配置

file->setting

勾掉图中这个,才可以导入依赖成功

之后配置application文件

spring:datasource:url: jdbc:mysql://127.0.0.1:3306/ecp-test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true&useSSL=false&allowMultiQueries=trueusername: rootpassword: 123456driver-class-name: com.mysql.jdbc.Driver

之后运行即可

Maven简单了解

不使用maven的jar导入

首先从网上下载需要的jar包,下载放入项目的lib下面,然后加到项目的classpath下面的

依赖冲突

a->b表示a包要依赖b包才能导入

A->B->C->D1 ,E->F->D2 这时候有存在依赖冲突D1,D2只是版本上的不同,这时候我们就需要手动删除某些jar非常麻烦,当使用maven去处理的时候会选择路径最短的依赖,但是如果想保留D1也是可以的,在pom.xml文件中对应的依赖加入如下方式(以某个依赖包为列)来去除指定依赖

<dependency><grounpld>org.apache.hadoop</grounpld><artifactld>zookeeper</artifactld><version>3.3.1</version><exclusions><exclusion><groupld>jline</groupld><artifactld>jline</artifactld></exclusion></exclusions>
</dependency>

依赖管理文件

  • pom文件为基本的依赖管理文件
  • resouces 资源文件
    • statics 静态资源
    • templates 模板资源
    • application.yml 配置文件
  • myspringbootApplication程序的入口。

spring boot的基本依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><!--继续的默认的jar包(父项目)的坐标信息--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.1.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><!--本项目的坐标信息--><groupId>com.mycompany</groupId><artifactId>myspringboot</artifactId><version>0.0.1-SNAPSHOT</version><name>myspringboot</name><description>Demo project for Spring Boot</description><!--项目开发者属性,如指定jdk版本--><properties><java.version>1.8</java.version></properties><!--本项目导入的依赖--><dependencies><!--指定数据库连接(JDBC)方式的依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId></dependency><!--指定项目是就java web 项目--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--mysql的驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!--单元测试的依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><!--去除某些指定依赖--><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency></dependencies><!--启动先加载内容--><build><!--某些插件--><plugins><!--maven自动打包插件--><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

其中spring-boot-starter-web-parent不仅包含spring-boot-starter,还自动开启了web功能。

功能演示

举个栗子,比如你引入了Thymeleaf的依赖,spring boot 就会自动帮你引入Spring Template Engine(模板依赖),当你引入了自己的Spring Template Engine,spring boot就不会帮你引入。它让你专注于你的自己的业务开发,而不是各种配置。

@RestController
public class HelloController {@RequestMapping("/")public String index() {return "Greetings from Spring Boot!";}}

启动SpringbootFirstApplication的main方法,打开浏览器localhost:8080,浏览器显示:

Greetings from Spring Boot!

神奇之处(相比SSM)

  • 你没有做任何的web.xml配置。
  • 你没有做任何的sping mvc的配置; springboot为你做了。
  • 你没有配置tomcat ;springboot内嵌tomcat.
  • 因为Spring Boot的自动装配为你节省了很多繁琐的配资文件的编写

Spring Boot特点

约定大于配置

约定大于配置是一种开发原则,就是减少人为的配置,直接用默认的配置就能获得我们想要的结果,eg:默认连接池,默认端口,默认配置文件名称,默认日志文件名称

约定好工程目录结构、配置文件位置【可以有多个配置文件对应不同的环境】、启动类的固定位置

  • application-test.yml:测试环境
  • application-dev.yml:开发环境
  • application-rc.yml:预生产环境
  • application-prod.yml:生产环境

application.yml配置内容

spring:profiles:active: dev #当有多个配置文件的时候,表明选用那个配置文件(查找里面有dev的yml文件,首先选择dev的配置属性,若dev里没有则在application.yml配置文件取)

application-dev.yml

spring:datasource: #配置数据源driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/ecp-test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true&useSSL=false&allowMultiQueries=true&serverTimezone=UTCusername: rootpassword: 123456server:port: 8080 #设置端口号

总结约定:

  1. maven机构目录

    1. 默认的resources文件夹下是放资源文件
    2. resources 下的static放静态资源文件
    3. templates放动态的资源文件
    4. 默认的生成的class 文件存在target目录下
  2. 我们的主启动类必须置于业务代码的最外层,否则我们写的一些类不能被扫描注册到bean
  3. 我们的配置文件命名必须是application
  4. 日子文件必须是logback.xml或者logback-spring.xml(推荐后者,因为后者加载晚于启动类,可以取到全部类的变量)
  5. spring boot 内嵌tomcat且默认端口号为8080

工程目录介绍

项目包idea的配置maven的相关配置项目所有文件内容项目内容java代码内容包名项目名业务分类包启动类(一定置项目名下)resourcesstatic[存放一些静态资源,如图片]templates[动态资源,比如访问的资页面]application.yml[配置文件]targer是编译后的文件......pom.xml [maven配置文件]工程用的jar包

由于Spring Boot的Bean的扫描原则是根据启动类自上而下的扫描,所以启动类必须在业务包的最外层

自动配置

内置tomcat在导入依赖包中有体现

启动类代码:[自动配置的入口]

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

注解跟着原码进入

@SpringBootApplication

@Target({ElementType.TYPE}) //注解的使用范围
@Retention(RetentionPolicy.RUNTIME) //注解的生成周期
@Documented //用它来生成工具文档
@Inherited //表示标注类型可以被继承
//这个四个是java的原注解
@SpringBootConfiguration //配置类不需要了解
@EnableAutoConfiguration //进去这里查看
@Import({AutoConfigurationImportSelector.class})
//导入那些组件的选择器
@Import({Registrar.class}) //自动配置包

跟随着注解的减少最后只剩下一个非原注解的之后就会发现[ctrl + F8查看方法返回值]一个扫描注解的方法。这些自动配置就是常说的约定

去除掉某些默认配置类的方法

@SpringBootApplication(exclude - {...}) //去除自动配置某个配置

YML基础与配置扩展

三种配置格式比较

properties的优先级比yml要高一些

等价格式演示:

<server><port>8090</port><servelet><context-path>/myspringboot</context-path></servelet>
</server>
server.port=8090
server.servelet.context-path= /myspringboot

使用properties的时候可能会出现中文乱码,这是idea的问题,需要改一下设置如下

yml:

server:port: 8090 #设置端口号serverlet:context-path: /myspringboot #配置上下文路径

相比来yml层级关系明显,以数据为中心

语法

  1. 冒号后一定有一个空格,下一级就按回车
  2. 对大小写敏感
  3. 使用缩进代表层级关系[不使用table,一般是2-4个空格]

扩展属性配置

server:port: 8080 #设置端口号serverlet:context-path: /myproject #配置上下文路径[命名可以改变],后面没有对应的配置默认找index.htmllogging: #设置日志level:  #打印等级root: infoperson: #自定义name: ywqage: 21readBook: 《java》,《C++》

获取yml中的值的方式

可以接受list的方式,用逗号隔开

方法一:

用value获取单个值

@Value("${server.port}")
private Integer port;@Test
void getValue(){System.out.println(port);
}

方法二:

导入依赖

<dependency><groupId> org.springframework.boot </groupId><artifactId> spring-boot-configuration-processor </artifactId><optional> true </optional>
</dependency>

写注解

@ConfigurationProperties(prefix = "person")
@Component

输出的方法

private Person person;@Test
void getValue(){System.out.println(person);
}

一般会把自定义的这种值单独放一个配置文件里方便管理,获取的时候多加一个参数

myperson.yml

person: #自己设置的一个name: ywqage: 21readBook: 《java》,《C++》
//指定改实体类获取值的路径
@PropertySource(value = {"classpath:myperson.yml"})
@ConfigurationProperties(prefix = "person")
@Component

两种方式各有优劣,自行选择

Spring Boot集成工具

集成Druid连接池

在springboot里面默认支持的三种连接池:dbcp、tomcat[boot1.5之前默认用这个]、hikari

【数据访问】

Druid连接池简介

是阿里巴巴开源的一个数据源,主要用于java数据库连接池,相比于Spring推荐的默认数据库连接池,在市场上占绝对优势;Druid数据源由于有强大的监控特性、可拓展性等特点被广泛使用。即使HikariCP的性能比Druid优秀,但是因为Druid包括许多维度的统计和分析功能,所以大家都选择使用Druid的更多

查看默认连接池

在不更改任何连接池配置文件的前提下查看默认连接池,使用Test单元测试查看

@SpringBootTest
class MyspringbootApplicationTests {@Autowiredprivate DataSource dataSource;@Testvoid contextLoads() throws SQLException {Connection connection = dataSource.getConnection();System.out.println(connection);}
}

默认连接池为HikarProxy

连接Druid连接池

导入依赖

有两种依赖一个druid和spring-boot-start-druid【spring-boot-start-xxx都是springboot提供的包,而xxx-spring-boot-start则是xxx专门为spring boot开发的包,如果是springboot的包应该选用带springboot的】两种不同的依赖包

<!--Druid连接池的依赖-->
<dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version>
</dependency>

引进带start的包可以免去一些配置文件的配置,不带的就需要手动配置许多文件

配置Druid

配置yml
spring:#配置druid连接池type: com.alibaba.druid.pool.DruidDataSourcedruid:initial-size: 10#初始化连接池大小max-active: 100#最大连接数min-idle: 10#最小连接数max-wait: 60001#连接超时等待时间pool-prepared-statements: false#是否开启PSCache[某个缓存处理]time-between-eviction-runs-millis: 60000#配置间隔多久进行一次检查是否有需要关闭的链接min-evictable-idle-time-millis: 300000#配置一个连接在池中存在最小存在时间filter: stat,wall,log4j2#配置一些扩展插件[监控统计、防SQL注入、日志]

配置后重启查看是否配置成功

可以看到已经生效。

com.mysql.cj.jdbc.Driver版本5之前的路径不一样,之后要用这个

配置config文件

在业务包下建一个config包然后创建对应的配置java文件

/*** @Description 连接池配置类* @Author ywq*/
@Configuration
public class DruidConfiguration {private final Logger logger = LoggerFactory.getLogger(DruidConfiguration.class);@Beanpublic ServletRegistrationBean druidServlet() {ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");//IP白名单servletRegistrationBean.addInitParameter("allow","*");//IP黑名单(共同存在时,deny优于allow)servletRegistrationBean.addInitParameter("deny","192.168.1.100");//控制台管理用户名和密码servletRegistrationBean.addInitParameter("loginUsername","admin");servletRegistrationBean.addInitParameter("loginPassword","admin");//是否能够重置数据,禁用HTML页面上的“Reset All”功能servletRegistrationBean.addInitParameter("resetEnable","false");return servletRegistrationBean;}@Beanpublic FilterRegistrationBean filterRegistrationBean(){FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());//监控一些Url请求filterRegistrationBean.addUrlPatterns("/*");filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");return filterRegistrationBean;}
}

配置完成后查看druid监控界面

http://localhost:8080/druid/index.html

集成MyBatis/Hibernate

MyBatis与Hibernate

MyBatis优势:

1. 可以进行更为细致的sql优化,可以减少查询字段2. 容易掌握,而Hibernate门槛较高
3. 占有绝大部分的中国软件市场

Hibernate优势:

  1. DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射
  2. 对对象的维护和缓存要比Mybatis好,对增删查改的对象的维护要方便
  3. 数据库移植性很好,Mybatis的数据库移植性不好,不同的数据库需要写不同sql

【数据访问】

声明式事物[Transactional]

两者都带有对应sql 的事物机制,即当程序执行异常的时候进行数据的回滚,就是在有这个声明的开始,整个运行本次程序过程中只要中间任意一步出现异常就会让其运行到改变全部改回原来的状态,即回到没有运行的状态。

使用也很简单,只要加上注解@Transactional就可以了,可以设置其接受的异常范围

eg:

 @Transactional(rollbackFor = Exception.class)

配置MyBatis

导入依赖

这里仍选择Spring boot starter的包(含有自动配置)

<!--导入MyBatis依赖-->
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.1</version>
</dependency>

配置yml

mybatis:#mapper.xml所在位置mapperLocations: classpath:mybatis/mapper/**/*Mapper.xmlconfiguration:mapUnderscoreToCamelCase: true  #大小写驼峰转换call-setters-on-nulls: true #设置查询结果为null的时候是否返回jdbcTypeForNull: VARCHAR #当没有指定传入参数的时候默认为varchar

配置好后就可以进行mybatis编写sql

<?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="com.mycompany.myspringboot.user.dao.UserMapper"><select id="queryUserNameList" resultType="String">SELECTuser_nameFROMt_sys_user</select>
</mapper>

注意当使用一些特写字符的时候用这个<![CDATA[ ]]>装起来,这个会按文本形式处理

还有注意关于各个层级之间的注解写好。

Mybatis写sql的标签

主要标签如下

  1. select对应注解@Select
  2. 对应注解@Update
  3. 对应注解@Insert
  4. 对应注解@Delete
  5. :在某些条件根据入参有无决定时可使用,以避免1=1这种写法,也会根据是否为where条件后第一个条件参数自动去除and
  6. :类似于java中的条件判断if,没有标签
  7. 标签
  8. :可以对数组、Map或实现了Iterable接口(如List、Set)的对象遍历。可实现in、批量更新、批量插入等。
  9. :映射结果集
  10. :映射结果类型,可是java实体类或Map、List等类型。

配置Hibernate(了解)

导入依赖

<!--添加mysql连接类和连接池类:-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency><!--mysql驱动-->
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope>
</dependency>

配置yml

spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8username: rootpassword: 123456jpa:hibernate:ddl-auto: update  # 第一次简表create  后面用updateshow-sql: true

注意,如果通过jpa在数据库中建表,将jpa.hibernate,ddl-auto改为create,建完表之后,要改为update,要不然每次重启工程会删除表并新建。

完成以上步骤后,实验一个例子

创建实体类

通过@Entity 表明是一个映射的实体类, @Id表明id, @GeneratedValue 字段自动生成

@Entity
public class Account {@Id@GeneratedValueprivate int id ;private String name ;private double money;//...  省略getter setter
}
Dao层

数据访问层,通过编写一个继承自 JpaRepository 的接口就能完成数据访问,其中包含了几本的单表查询的方法,非常的方便。值得注意的是,这个Account 对象名,而不是具体的表名,另外Interger是主键的类型,一般为Integer或者Long

public interface AccountDao  extends JpaRepository<Account,Integer> {}
Web层

在这个栗子中我简略了service层的书写,在实际开发中,不可省略。新写一个controller,写几个restful api来测试数据的访问。

@RestController
@RequestMapping("/account")
public class AccountController {@AutowiredAccountDao accountDao;@RequestMapping(value = "/list", method = RequestMethod.GET)public List<Account> getAccounts() {return accountDao.findAll();}@RequestMapping(value = "/{id}", method = RequestMethod.GET)public Account getAccountById(@PathVariable("id") int id) {return accountDao.findOne(id);}@RequestMapping(value = "/{id}", method = RequestMethod.PUT)public String updateAccount(@PathVariable("id") int id, @RequestParam(value = "name", required = true) String name,@RequestParam(value = "money", required = true) double money) {Account account = new Account();account.setMoney(money);account.setName(name);account.setId(id);Account account1 = accountDao.saveAndFlush(account);return account1.toString();}@RequestMapping(value = "", method = RequestMethod.POST)public String postAccount(@RequestParam(value = "name") String name,@RequestParam(value = "money") double money) {Account account = new Account();account.setMoney(money);account.setName(name);Account account1 = accountDao.save(account);return account1.toString();}}

通用Mapper集成

是什么

通用mapper是为了方便开发的时候去避免写一些简单单表增删改查,里面有集成的增删改查方法。属于mybatis的扩展

导入依赖

<!--导入通用Mapper-->
<dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>2.1.5</version>
</dependency>

修改dao和entity代码

对应的Dao层Mapper继承Mapper[tk包的],Mapper<表明对应的实体类>

@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User> {List<String> queryUserNameList();
}

对应的entity代码需要注解表明和主键

@Table(name = "t_sys_user")
public class User {@Idprivate String userId;private String userName;
}

都修改完成后,运行结果如下:

当使用的通用mapper之后需要更改MapperScan的包为

import tk.mybatis.spring.annotation.MapperScan;

PageHelper集成

是什么

PageHelper是分页工具,在开发过程中经常使用

导入依赖

<!--导入PageHelper[分页工具]-->
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.2.10</version>
</dependency>

配置yml

pagehelper:helper-dialect: mysql #数据库类型[数据库分页原理不同,mysql是limit,sql server是top]reasonable: true #分页合理化support-methods-arguments: true #是否支持接口参数来传递分页参数,默认falsepage-size-zero: true #当设置为true的时候,如果pageSize设置为0(或RowBounds的limit=0,就不执行分页)

修改Controller和Service代码

Controller:

@GetMapping("/queryUserNameList")
public PageInfo<String> queryUserNameList(@RequestParam(defaultValue = "0") Integer pageNum, @RequestParam(defaultValue = "0") Integer pageSize){return userService.queryUserNameList(pageNum,pageSize);
}

@RequestParam设置默认参数值

Service:

public PageInfo<String> queryUserNameList(Integer pageNum, Integer pageSize){//设置分页的页数和页码,一定要放在查询前,且仅对一条sql进行分页PageHelper.startPage(pageNum,pageSize);List<String> list = userMapper.queryUserNameList();//返回一些分页的信息PageInfo<String> pageInfo = new PageInfo<>(list);return pageInfo;
}

Lombok集成

简介

在项目开发的阶段往往存在实体类的属性的变化以及字段的增删等,这时候存在多次实体类变动,我们就要频繁的对其对应的getter和setter进行修改,这时候就有了这个Lombok插件,通过注解为我们省去手写的getter和setter,是一个编译级别的插件,在编译源码的时候自动帮我们生成对应方法

引入Lombok

导入依赖报错后,需要通过idea->file->settting->pulsing下载对应插件

<!--导入Lombok插件-->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.10</version><scope>provided</scope>
</dependency>

当导入依赖报错后需要安装如下插件:

修改实体类注解

@Table(name = "t_sys_user")
@Setter
@Getter
public class User {@Idprivate String userId;private String userName;
}

也可以直接写@Data,这里会自动提供get、set、toString、equals的重写

相关注解

@Data

需要导入Lombok依赖或者idea下载次插件,主要作用是提高代码的简洁,使用这个注解可以省去代码中大量的get()、 set()、 toString()、equals、hashCode、canEqua方法;

@Setter

注解在属性或者实体类上,提供set方法

@Getter

注解在属性或者实体类上,提供get方法

@EqualsAndHashCode

注解在类上,提供对应的equals和hashCode方法

@AllArgsConstructor

使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数

@NoArgsConstructor

使用后创建一个无参构造函数

@NonNull

如果给参数加上这个注解,参数为null会抛出空指针异常

@Value

与@Data类似,区别在于它会把所有的成员变量默认定义为private final修饰,并且不会生成set方法

测试注解

@Slf4j
@RestController
@RequestMapping("/lombokTest")
public class LombokTest {@GetMapping("/query")//参数前可以有多个注解public String query(@NonNull @RequestParam("name") String name){log.info("用户姓名是" + name);return log.toString();}
}

当有传入name参数的时候

当不传入name参数的时候

原理

不能改变java代码运行机制,只是编译源码的时候去对应的语法树中找到代码里写的注解然后匹配语法规则生成相应的源码,这样编译完成后自动生成一些代码。

注意问题

  1. idea2017以前的版本不支持lombok
  2. 使用Lombok时,编译器可能会报错,报错就需要安装Lombok插件,选择星级最高即可
  3. 参数的处理往往都根据项目需求来进行,该用Value就用,该手写的还是要手写

当下载插件还是不可以编译的时候,开启idea启用注解编程,勾选上表示开启

使用Spring Cache集成redis

缓存作用

缓和较慢存储的高频请求,缓解数据库压力,提升响应速率。

Spring Cache + redis优势

spring cache是Spring对缓存的封装,适用于EHCache、Redis、Guava等缓存技术。主要是可以使用注解的方式来处理缓存,eg:单使用Redis进行缓存,查询数据,如果查到需要写一些判断,而如果使用Spring Cache注解来处理,则可以省去这些判断。【spring cache之间数据不互通,而redis是分布的缓存,可以互通数据】

引入Spring Cache + Redis

导入Spring Cache依赖

依赖本号最后带有RELEASE表示当前稳定的版本

<!--导入Spring Cache-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId><version>2.2.2.RELEASE</version>
</dependency>

测试Spring Cache

这里的value表示对应的缓冲区的名称,key是存储对应内容【这里是缓存当前的方法名对应的返回结果】

@Cacheable(value = "mycache",key = "#root.methodName")
@GetMapping("/queryUserNameList")
public PageInfo<String> queryUserNameList(@RequestParam(defaultValue = "0") Integer pageNum, @RequestParam(defaultValue = "0") Integer pageSize){System.out.println("进入查询+++++++++++++++++++++++++++++++++++++++++++++++++");return userService.queryUserNameList(pageNum,pageSize);
}

当连续执行两次后,后台只打印了一次“进入查询”就表示生效

注意:需要在启动类上加开启缓存的注解@EnableCaching否则不会生效

导入Redis依赖

当不指定版本的时候会在这里找可用的版本信息

2.1.5之前版本的redis的连接池是jedis,之后就用了lettuce

<!--导入Redis依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.2.5.RELEASE</version>
</dependency>
<!--引入redis连接池-->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>

配置yml

spring:datasource: #配置数据源driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/walk_bookstore?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true&useSSL=false&allowMultiQueries=true&serverTimezone=UTCusername: rootpassword: 123456#配置druid连接池type: com.alibaba.druid.pool.DruidDataSourcedruid:#初始化连接池大小initial-size: 10#最大连接数max-active: 100#最小连接数min-idle: 10#连接超时等待时间max-wait: 60001#是否开启PSCache[某个缓存处理]pool-prepared-statements: false#配置间隔多久进行一次检查是否有需要关闭的链接time-between-eviction-runs-millis: 60000#配置一个连接在池中存在最小存在时间min-evictable-idle-time-millis: 300000#配置一些扩展插件[监控统计、防SQL注入、日志]filter: stat,wall,log4j2cache:type: REDIS  #选择处理缓存的工具redis:host: 127.0.0.1port: 6379 #默认是这个端口号#password: 没有设置密码不需要配置这个database: 8 #使用几号redis库timeout: 600 #连接池最大的连接数,若使用负值表示没有限制lettuce:pool:max-active: 50 #连接池最大数max-wait: -1 #连接池最大阻塞等待时间,若使用负值表示没有限制max-idle: 8 #连接池中的最大空闲连接min-idle: 0 #连接池中的最小空闲连接

查看结果

配置完重启后发现缓存已经存入Redis中

运行删除缓存

@CacheEvict(value = "mycache", allEntries = true, beforeInvocation = true)
@GetMapping("/deleteCache")
public String deleteCache(){System.out.println("进入删除++++++++++++++++");return "删除成功";
}

之后再看redis刷新那个缓存就没有了

Spring Cache的其他注解

@CacheEvict
  1. value = 要删除的缓冲区名,也可以写上对应key去删除具体的缓冲区的那个模块
  2. allEntries = true 表示清楚这个缓存模块下的所有缓存
  3. beforeInvocation = true 表示在这个方法执行之前先执行删除缓存,防止方法报错导致删除失败

Redis中get值显示16进制问题

原因:

spring-data-redis的RedisTemplate<K,V>模板类在操作redis时默认使用JdkSerializationRedisSerializer来进行序列化

解决:

使用其他类序列化redis的key和value,因为RedisTemplate默认是用字节方式序列化,可以用泛型解决RedisTemplate,也可以直接使用StringRedisTemplate进行Redis的相关操作。

自己写一个redis的configuration然后重写关于序列化的地方

集成Swagger2在线调试

为什么?

在传统的开发模式中,每个系统准备一份接口文档,方便各个团队之间的交流。但是由于软件的发展,需求功能越来越多,导致接口数量也越来越多,开发的人也越来越杂,维护接口文档就变成了一个很大的负担。

简介

Swagger是一款通过注解的方式生成Restful APIs交互界面的工具

优点:

  1. 减少了我们文档的编写
  2. 同时能够实现接口测试,接口修改的同时能够维护文档,可以很好的避免信息不一致

缺点:

  1. 代码侵入严重,在源代码的基础上要写更多的注解

导入依赖

swagger2核心jar包和UI界面包

<!--导入Swagger2核心包-->
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version>
</dependency>
<!--导入UI界面包-->
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version>
</dependency>

配置Swagger2Config

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;import java.util.HashSet;@Configuration
@EnableSwagger2
@ConditionalOnBean(Swagger2Config.class)
public class Swagger2Config {@Autowiredprivate AppConfig appConfig;/*** 全局设置Content Type,默认是application/json* 如果想只针对某个方法,则注释掉改语句,在特定的方法加上下面信息* @ApiOperation(consumes="application/x-www-form-urlencoded")*/public static final HashSet<String> consumes = new HashSet<String>() {{add("application/x-www-form-urlencoded");}};@Bean(value = "commonApi")public Docket commonApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("通用公共类接口").select().apis(RequestHandlerSelectors.basePackage("com.mycompany.myspringboot.controller.common")).paths(PathSelectors.any()).build()//.securityContexts(Lists.newArrayList(securityContext())).securitySchemes(Lists.<SecurityScheme> newArrayList(apiKey())).consumes(consumes);}@Bean(value = "systemApi")public Docket systemApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("系统权限管理接口").select().apis(RequestHandlerSelectors.basePackage("com.mycompany.myspringboot.controller.system")).paths(PathSelectors.any()).build()//.securityContexts(Lists.newArrayList(securityContext())).securitySchemes(Lists.<SecurityScheme> newArrayList(apiKey())).consumes(consumes);}@Bean(value = "ecpBaseDataApi")public Docket ecpBaseDataApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("基础数据管理").select().apis(RequestHandlerSelectors.basePackage("cn.com.bgyfw.ecp.controller.config"))//扫描包.paths(PathSelectors.any()).build()//.securityContexts(Lists.newArrayList(securityContext())).securitySchemes(Lists.<SecurityScheme> newArrayList(apiKey())).consumes(consumes);}@Bean(value = "businessBaseDataApi")public Docket businessBaseDataApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("用户信息模块").select().apis(RequestHandlerSelectors.basePackage("com.mycompany.myspringboot.user.controller"))//扫描包.paths(PathSelectors.any()).build()//.securityContexts(Lists.newArrayList(securityContext())).securitySchemes(Lists.<SecurityScheme> newArrayList(apiKey())).consumes(consumes);}@Bean(value = "scheduleApi")public Docket scheduleApi() {return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).groupName("外部调用定时同步测试接口").select().apis(RequestHandlerSelectors.basePackage("cn.com.bgyfw.ecp.controller.synch")).paths(PathSelectors.any()).build()//.securityContexts(Lists.newArrayList(securityContext())).securitySchemes(Lists.<SecurityScheme> newArrayList(apiKey())).consumes(consumes);}/*** 添加摘要信息*/private ApiInfo apiInfo() {// 用ApiInfoBuilder进行定制return new ApiInfoBuilder()//标题.title("swagger2Demo文档")//简介.description("")//服务条款.termsOfServiceUrl("")//作者个人信息.contact(new Contact(appConfig.name, null, null)).version("版本号:" + appConfig.version).build();}
}

这里对应的值要在application.yml里配置

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;/*** @author ywq* @version v1.0.0* @date 2020-06-23 17:37*/
@Data
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppConfig {public String name;public String version;public boolean checkAccessToken;public boolean autoToken;//下载路径public String downloadPath;//上传路径public String uploadPath;
}

配置完成后就可以开始使用注解

常用注解

@Api

用在请求的类上,表示对类的说明,描述Controller的作用

tags=“说明该类的作用,可以在UI界面上看到的注解”

value=“该参数没什么意义,在UI界面上也看到,所以不需要配置”

@Api(tags = "用户信息模块")

@ApiOperation

用在请求的方法或者接口上,说明方法的用途、作用

value=“说明方法的用途、作用”

notes=“方法的备注说明”

@ApiOperation(value = "用户名查询", notes = "如果不传任何分页参数就不分页,分页必须传pageNum,pageSize",httpMethod = "GET")

@ApiImplicitParams

用在请求的方法上,表示一组参数说明

@ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
name:参数名
value:参数的汉字说明、解释
required:参数是否必须传
paramType:参数放在哪个地方
· header --> 请求参数的获取:@RequestHeader
· query --> 请求参数的获取:@RequestParam
· path(用于restful接口)–> 请求参数的获取:@PathVariable
· body(不常用)
· form(不常用)
dataType:参数类型,默认String,其它值dataType=“Integer”
defaultValue:参数的默认值

@ApiImplicitParams({@ApiImplicitParam(name = "pageNum",value = "第几页", dataType = "int",defaultValue = "0"),@ApiImplicitParam(name = "pageSize",value = "一页几条", dataType = "int",defaultValue = "0")
})

还有一些其他的注解可以去了解,查看当前结果

其他部分注解简介

  • @ApiParam:单个参数描述
  • @ApiModel:用对象来接收参数
  • @ApiProperty:用对象接收参数时,描述对象的一个字段
  • @ApiResponse:HTTP响应其中1个描述
  • @ApiResponses:HTTP响应整体描述
  • @ApiIgnore:使用该注解忽略这个API
  • @ApiError :发生错误返回的信息

logback集成

简介

区分日志门面和具体的日志框架

slf4j就是简单的日志门面框架,只提供接口,没有具体的实现。具体的日志功能有具体的日志框架去实现[logback,log4j],使用Slf4j【门面】有个很大的好处,当你想切换其他日志框架的时候,原来的代码几乎不用更改。

logback与log4j比较

更快的执行速度,更少的内存

导入依赖

spring boot本身都有导入很多的日志框架依赖,当你需要更新版本的时候才去导入新的日志依赖

配置yml

my:log:path: t:/myspringboot

logback-spring-dev.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration  scan="true" scanPeriod="60 seconds"><contextName>logback</contextName><!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 --><property name="log.path" value="G:/logs/pmp" /><!--0. 日志格式和颜色渲染 --><!-- 彩色日志依赖的渲染类 --><conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" /><conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" /><conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" /><!-- 彩色日志格式 --><property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/><!--1. 输出到控制台--><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息--><filter class="ch.qos.logback.classic.filter.ThresholdFilter"><level>debug</level></filter><encoder><Pattern>${CONSOLE_LOG_PATTERN}</Pattern><!-- 设置字符集 --><charset>UTF-8</charset></encoder></appender><!--2. 输出到文档--><!-- 2.1 level为 DEBUG 日志,时间滚动输出  --><appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 正在记录的日志文档的路径及文档名 --><file>${log.path}/web_debug.log</file><!--日志文档输出格式--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern><charset>UTF-8</charset> <!-- 设置字符集 --></encoder><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 日志归档 --><fileNamePattern>${log.path}/web-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文档保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文档只记录debug级别的 --><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>debug</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- 2.2 level为 INFO 日志,时间滚动输出  --><appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 正在记录的日志文档的路径及文档名 --><file>${log.path}/web_info.log</file><!--日志文档输出格式--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern><charset>UTF-8</charset></encoder><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 每天日志归档路径以及格式 --><fileNamePattern>${log.path}/web-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文档保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文档只记录info级别的 --><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>info</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- 2.3 level为 WARN 日志,时间滚动输出  --><appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 正在记录的日志文档的路径及文档名 --><file>${log.path}/web_warn.log</file><!--日志文档输出格式--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern><charset>UTF-8</charset> <!-- 此处设置字符集 --></encoder><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${log.path}/web-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文档保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文档只记录warn级别的 --><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>warn</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- 2.4 level为 ERROR 日志,时间滚动输出  --><appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 正在记录的日志文档的路径及文档名 --><file>${log.path}/web_error.log</file><!--日志文档输出格式--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern><charset>UTF-8</charset> <!-- 此处设置字符集 --></encoder><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${log.path}/web-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文档保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文档只记录ERROR级别的 --><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!--<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。<logger>仅有一个name属性,一个可选的level和一个可选的addtivity属性。name:用来指定受此logger约束的某一个包或者具体的某一个类。level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。如果未设置此属性,那么当前logger将会继承上级的级别。addtivity:是否向上级logger传递打印信息。默认是true。<logger name="org.springframework.web" level="info"/><logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>--><!--使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:【logging.level.org.mybatis=debug logging.level.dao=debug】--><!--root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,不能设置为INHERITED或者同义词NULL。默认是DEBUG可以包含零个或多个元素,标识这个appender将会添加到这个logger。--><!-- 4. 最终的策略 --><!-- 4.1 开发环境:打印控制台--><springProfile name="dev"><logger name="com.sdcm.pmp" level="debug"/></springProfile><root level="info"><appender-ref ref="CONSOLE" /><appender-ref ref="DEBUG_FILE" /><appender-ref ref="INFO_FILE" /><appender-ref ref="WARN_FILE" /><appender-ref ref="ERROR_FILE" /></root><!-- 4.2 生产环境:输出到文档<springProfile name="pro"><root level="info"><appender-ref ref="CONSOLE" /><appender-ref ref="DEBUG_FILE" /><appender-ref ref="INFO_FILE" /><appender-ref ref="ERROR_FILE" /><appender-ref ref="WARN_FILE" /></root></springProfile> --></configuration>

查看结果

Restful风格的接口

为了方便与前端各种各样的设备层进行通信,就有了这个统一的机制。

例举几个类型

  1. GET(SELECT): 从服务器取出资源(一项或多项)
  2. POST(CREATE): 在服务器创建一个资源.
  3. PUT(UPDATE): 在服务器更新资源(客户端提供改变后的完整资源)
  4. PATCH(UPDATE): 在服务器更新资源(客户端提供改变的属性)
  5. DELETE(DELETE): 从服务器删除资源
  6. OPTIONS: 获取信息,关于资源的那些属性是客户端可以改变的

附上代码示例

都是同样的路径,但是请求方式不一样

Controller:

@GetMapping(value = "/user")
public List<User> findUser(){return userService.getUserNameList();
}@PostMapping(value = "/user")
public int addUser(@RequestParam("name") String name){User user = new User();user.setUserId("202006241002");user.setPhone("123123123");user.setUserName(name);return userService.addUser(user);
}@PatchMapping(value = "/user/{userCode}")
public int updateUser(@PathVariable("userId") String userId,@RequestParam("name") String name){User user = new User();user.setPhone("1231234564");user.setUserId(userId);user.setUserName(name);return userService.updateUser(user);
}@DeleteMapping(value = "/user/{userCode}")
public int deleteUser(@PathVariable("userId") String userId){return userService.deleteUser(userId);
}

Service:

public List<User> getUserNameList(){List<User> list = userMapper.selectAll();return list;
}public int addUser(User user){return userMapper.insert(user);
}public int updateUser(User user){return userMapper.updateByPrimaryKey(user);
}public int deleteUser(String userId){return userMapper.deleteByPrimaryKey(userId);
}

查询成功

新增成功

修改成功(这个只修改一条用PATCH,注意url的不同),查看数据库也有改变

删除成功

异常处理

spring boot自身带有对应的异常处理,但是这个异常处理的返回信息提示非常不友好,直接反回Http状态码500(让页面崩溃的那种),而在实际过程中我们往往只需要返回一个错误提示就好,就是http的状态码为200,返回错误的信息就可以。这时候我们需要自己定义异常处理。

自定义异常处理

@ControllerAdvice定义统一的异常处理类,这样就不必在每个Controller中逐个定义AOP去拦截处理异常。

@ExceptionHandler用定义函数针对的异常类型,最后将Exception对象处理成自己想要的结果

理想结果,返回错误信息http请求状态码是200

编写异常处理

@ControllerAdvice
public class ResfulApiExceptionHandler {/*** 设置请求参数错误的时候返回的异常信息,这样处理http的状态码是200,不过返回信息是500* @param request* @param exception* @return*/@ExceptionHandler(value = MissingServletRequestParameterException.class)@ResponseBodypublic Map<String, Object> requestExceptionHandler(HttpServletRequest request,MissingServletRequestParameterException exception){Map<String, Object> error = Maps.newHashMap();error.put("status",500);error.put("messager","参数" + exception.getParameterName() + "错误");return error;}/*** 找不到具体错误的,就返回这个异常信息* @param request* @param exception* @return*/@ExceptionHandler(value = Exception.class)@ResponseBodypublic Map<String, Object> exceptionHandler(HttpServletRequest request,Exception exception){Map<String, Object> error = Maps.newHashMap();error.put("status",500);error.put("messager","系统错误,请联系管理员");return error;}
}

这时候故意写错一个请求参数返回结果为

故意写一个非请求参数的异常

@PatchMapping(value = "/user/{userId}")
public int updateUser(@PathVariable("userId") String userId,@RequestParam("name") String name){User user = new User();user.setPhone("1231234564");user.setUserId(userId);user.setUserName(name);if (name.length() > 5){int a = 1/0;   //!!!!!!!}return userService.updateUser(user);
}

返回结果是:

编写统一返回结果

AppResult

@Data
public class AppResult<T> {private int code;private String message;private T data;}

AppResultBuilder[构建体]

public class AppResultBuilder {public static <T> AppResult<T> successNoData(ResultCode code){AppResult<T> result = new AppResult<T>();result.setCode(code.getCode());result.setMessage(code.getMessage());return result;}public static <T> AppResult<T> success(T t,ResultCode code){AppResult<T> result = new AppResult<T>();result.setCode(code.getCode());result.setMessage(code.getMessage());result.setData(t);return result;}public static <T> AppResult<T> fail(ResultCode code,String error){AppResult<T> result = new AppResult<T>();result.setCode(code.getCode());result.setMessage(code.getMessage() + ": " + error);return result;}
}

ResultCode[枚举体]

package com.mycompany.myspringboot.config;public enum ResultCode {SUCCESS(200,"成功"),//成功FAIL(400,"失败"),//失败CHECK_FAIL(405,"数据检查异常"),//数据检查异常UNAUTHORIZED(401,"未认证(签名错误)"),//未认证(签名错误)NOT_FOUND(404,"接口不存在"),//接口不存在INTERNAL_SERVER_ERROR(500,"服务器内部错误"),//服务器内部错误OK(0,"成功"), //成功NOK(500,"失败"); //失败public int code;public String message;ResultCode(int code,String message) {this.code = code;this.message = message;}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}
}

嵌入业务代码

@PatchMapping(value = "/user/{userId}")
public AppResult updateUser(@PathVariable("userId") String userId, @RequestParam("name") String name){User user = new User();user.setPhone("1231234564");user.setUserId(userId);user.setUserName(name);if (name.length() > 5){//            int a = 1/0;return AppResultBuilder.fail(ResultCode.FAIL,"用户名长度大于5");}int cnt = userService.updateUser(user);return AppResultBuilder.success(cnt, ResultCode.SUCCESS);
}

结果如下:

自定义注解

使用自定义注解 + AOP进行开发

下面示范一个列子

步骤一:定义注解

自定义注解一定要加上java原注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestTypeHandler {String value();
}

步骤二:所在类加注解

注解对应包含要实现的那些类

@Component
@RequestTypeHandler("BIP2B341")
@Slf4j
public class RequireSaveServiceImpl extends AbstractRequestTypeHandler {@Override@ResponseBodypublic void handler(HttpServletRequest request, HttpServletResponse response) throws Exception{log.info("现在进入了报文下发接口");}
}
@Component
@RequestTypeHandler("BIP2B342")
@Slf4j
public class SeeLookServiceImpl extends AbstractRequestTypeHandler {@Override@ResponseBodypublic void handler(HttpServletRequest request, HttpServletResponse response) throws Exception{log.info("现在进入了报文审阅接口");}
}

步骤三:AOP拦截

[AOP拦截注解类解析进行bean注册实例化]

写了注解的请求类型处理器(选择要做那件事)

/*** 注解的请求类型处理器* 注解的类型和对应实现类放入map[注册到Bean工厂]来处理选择用哪个实现类*/
@Slf4j
@Component
public class RequestTypeHandlerContext implements ApplicationContextAware {@AutowiredApplicationContext applicationContext;private static final Map<String, Class> handlerMap = new HashMap<>(10);/*** 获取Bean是那个实现类* @param type* @return*/public AbstractRequestTypeHandler getHandlerInstance(String type){//获取Bean的对象Class clazz = handlerMap.get(type);if (clazz == null){log.error("本次业务编码对应接口未找到:{}",type);}return (AbstractRequestTypeHandler) applicationContext.getBean(clazz);}/*** 设置Bean,注册Bean,就是对应的Type给出对应的实现类* @param applicationContext* @throws BeansException*/@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException{Map<String,Object> beans = applicationContext.getBeansWithAnnotation(RequestTypeHandler.class);if (beans != null && beans.size() > 0){for (Object serviceBean : beans.values()){String payType = serviceBean.getClass().getAnnotation(RequestTypeHandler.class).value();handlerMap.put(payType,serviceBean.getClass());}}}
}

为了让所有的具体实现类都统一,写了如下的抽象方法

public abstract class AbstractRequestTypeHandler {abstract public void handler(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

让具体实现类去继承他

对应的Controller

/***  采用策略模式,避免过多的if-else*  提高扩展性,添加新的功能直接添加新的类,根据type选择对应的实现*/
@RestController
@AllArgsConstructor
@Slf4j
@RequestMapping("/HttpService")
public class HttpServiceController {private final RequestTypeHandlerContext requestTypeHandlerContext;@RequestMapping(value = "/httpserver")@Transactional(rollbackFor = Exception.class)public void requestProcessor(HttpServletRequest request, HttpServletResponse response) throws Exception{String type = request.getParameter("type");this.requestTypeHandlerContext.getHandlerInstance(type).handler(request,response);}
}

结果如下:

任务[异步、定时、邮件]

异步任务

创建异步任务

  1. 书写异步任务的配置类

@EnableAsync:表示的是开启异步任务(多线程)功能; 注意名称前缀

  1. 在需要开启线程的方法或者类上,加注解:@Async

eg:

@Slf4j
@Component
@EnableAsync //这个注解可以写在启动类,但是启动类东西有些多,放这里表明这个方法启动就好
public class AsyncTask {@Asyncpublic void sendMessage() {try {Thread.sleep(2000);}catch (InterruptedException e){e.printStackTrace();}log.info("选择进行短信群发");}
}
@Slf4j
@RestController
@RequestMapping("/userController")
public class ResfulUserController {@GetMapping(value = "/user")public List<User> findUser(){asyncTask.sendMessage();System.out.println("执行查询开始");return userService.getUserNameList();}
}

注意:

  1. 当异步任务在同一个Controller层下的时候是不可以生效的,因为同一层下是不会被Spring AOP拦截的

eg:

@Slf4j
@RestController
@RequestMapping("/userController")
public class ResfulUserController {@Autowiredprivate UserService userService;@GetMapping(value = "/user")public List<User> findUser(){this.sendMessage();System.out.println("执行查询开始");return userService.getUserNameList();}@Asyncpublic void sendMessage() {try {Thread.sleep(2000);}catch (InterruptedException e){e.printStackTrace();}log.info("选择进行短信群发");}
}

配置线程池

不配置会存在的问题

当访问量很多的时候,每个请求都要调用异步的方法,那就要拿到相应的数量的处理异步的线程,大量的线程会给服务器造成很大压力。

解决方法:

在配置类中,ThreadPoolTaskExecutor类是一个线程池。配置一些线程池的参数,确保高效的运转。

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor(){ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();// 核心线程池数量,方法executor.setCorePoolSize(7);// 最大线程数量executor.setMaxPoolSize(42);// 线程池的队列容量executor.setQueueCapacity(11);// 线程名称的前缀executor.setThreadNamePrefix("fyk-executor-");// 线程池对拒绝任务的处理策略executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize();return executor;}@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(){return new SimpleAsyncUncaughtExceptionHandler();}
}

定时任务

创建定时任务有两种写法,一种是基于注解,一种是基于接口【当cron的值需要从数据库中读取的时候就必须要用这种方式】,在以后遇到更频繁改变的定时任务的时候需要用相关的第三方框架:Quart2

基于注解的定时任务

  1. 启动类加注解@EnableScheduling【可以加在对应要用的地方】
  2. 需要定时任务的类上面加@Scheduled(cron = “0/5 * * * * ?”)
@EnableScheduling
@Component
@Slf4j
public class MyScheduled {@Scheduled(cron = "0/5 * * * * ?")public void task(){log.info("定时任务");}}

结果如下:

基于接口的定时任务

@Component
@Slf4j
@EnableScheduling
public class interfaceScheduled implements SchedulingConfigurer {@Mapper //为了方便随手写了dao层public interface CronMapper{@Select("SELECT cron FROM cron LIMIT 1")public String getCron();}@AutowiredCronMapper cronMapper;@Overridepublic void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {scheduledTaskRegistrar.addTriggerTask(// 1. 添加定时任务内容(Runnable)() -> System.out.println("执行动态定时任务:" + LocalDateTime.now().toLocalDate()),// 2. 设置执行周期(Trigger)triggerContext -> {// 2.1 从数据库获取执行周期String cron = cronMapper.getCron();// 2.2 合法性校验if (StringUtils.isEmpty(cron)){// 默认的定时方式}// 2.3 返回执行周期return new CronTrigger(cron).nextExecutionTime(triggerContext);});}
}

结果如下:

邮件任务

导入依赖

<!--导入邮件依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId>
</dependency>

配置文件

(password: eeffuilmbtkujehh #QQ邮箱开通第三方登录的授权码)

spring:mail:username: 869819435@qq.compassword: kathgcjopvzdbdfe  # 授权码host: smtp.qq.comproperties:mail:smtp:ssl:enable: true

邮箱确认开启smtp

QQ邮箱设置-账号设置开启

我的邮箱号授权码:kathgcjopvzdbdfe

测试结果

@Test
public void sendMaill(){SimpleMailMessage mailMessage = new SimpleMailMessage();mailMessage.setSubject("注意");mailMessage.setText("????");mailMessage.setFrom("869819435@qq.com");mailMessage.setTo("personal_mail_yang@163.com");//发送邮箱mailSender.send(mailMessage);
}

发送图片

@Test
public void sendMaill2() throws MessagingException {MimeMessage message = mailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message,true);helper.setSubject("注意");helper.setText("<b style='color:red'>???</b>",true);helper.setFrom("869819435@qq.com");helper.setTo("personal_mail_yang@163.com");helper.addAttachment("",new File("E:\\poo\\personal files\\实习资料\\学习\\LearnImage\\1368768-20190613220434628-1803630402.png"));mailSender.send(message);
}

防止重复提交本地锁

产生原因

存在服务器反应时间内仍接受到多次重复的请求,这时候会形成N个请求,每个请求都需要花时间,服务器压力只会越来越大。前端要控制,后端也要,数据库也要。以防万一。

实现方式

自定义注解+Spring AOP + Cache【AOP拦截】

导入依赖

<!--导入切面依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建Lock注解

String key() default

import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {/*** 默认的key*/String key() default "";
}

创建Lock拦截器(AOP)

CacheBuilder.new Builder()构建出缓存对象在具体的interceptor()方法上采用的是Around(环绕增强)

还有其他的注解表明在方法执行的什么时候运行拦截内容如图所示

import com.alibaba.druid.util.StringUtils;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.mycompany.myspringboot.user.annotation.LocalLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;/*** 配置拦截后动作内容* @author ywq*/
@Aspect
@Configuration
public class LocalMethodInterceptor {/*** 创建缓存* maximumSize(180) //最大缓存个数* expireAfterWrite(5, TimeUnit.SECONDS) //缓存5秒过期*/private static final Cache<String, Object> CACHE = CacheBuilder.newBuilder().maximumSize(180).expireAfterWrite(50, TimeUnit.SECONDS).build();/*** Around表明是环绕增强,触发条件为任意的公共方法以及有对应[路径]的注解* @param proceedingJoinPoint* @return*/@Around("execution(public * *(..)) && @annotation(com.mycompany.myspringboot.user.annotation.LocalLock)")public  Object interceptor(ProceedingJoinPoint proceedingJoinPoint){//获取切点MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();//获取拦截到的方法Method method = signature.getMethod();LocalLock localLock = method.getAnnotation(LocalLock.class);String key = getKey(localLock.key(),proceedingJoinPoint.getArgs());if (!StringUtils.isEmpty(key)){if (CACHE.getIfPresent(key) != null){//如果有这个key,则抛出不要重复提交throw new RuntimeException("请勿重复提交");}//放入缓存CACHE.put(key,key);}try{return proceedingJoinPoint.proceed();}catch (Throwable throwable){throwable.printStackTrace();throw new RuntimeException("服务器异常");}}/*** key的生成策略*/private String getKey(String keyExpress,Object[] args){for (int i = 0; i < args.length ; i++ ){keyExpress = keyExpress.replace("arg[" + i + "]",args[i].toString());}return keyExpress;}
}

注意抛出异常的时候,自己如果有重写异常抛出情况,就需要加上一个异常抛出类型Runtime的

import com.google.common.collect.Maps;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;
import java.util.Map;@ControllerAdvice
public class ResfulApiExceptionHandler {//.../*** 运行时异常* @param request* @param exception* @return*/@ExceptionHandler(value = RuntimeException.class)@ResponseBodypublic Map<String, Object> requestExceptionHandler(HttpServletRequest request,RuntimeException exception){Map<String, Object> error = Maps.newHashMap();error.put("status",500);error.put("messager", exception.getMessage());return error;}//...
}

添加注解

@LocalLock(key = “book:arg[0]”);意味着会将arg[0]替换成第一个参数的值,生成后的新key将被缓存起来

@PostMapping(value = "/user")
@LocalLock(key = "book:arg[0]")
public int addUser(@RequestParam("name") String name){User user = new User();user.setUserId("202006241002203");user.setPhone("123123123");user.setUserName(name);return userService.addUser(user);
}

防止重复提交分布式锁

产生原因

当服务器集群,单机的Cache就不能生效,也就是服务器之间的Cache不互通【其实解决方式就是用第三方缓存,redis等等】

【AOP可以说一个动态代理,对类实现预处理,如果符合预期要求就执行,不符合就不让执行】

实现方式

自定义注解 + Spring Aop + Redis

优化:根据注解获取指定唯一参数

创建Lock注解

import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {/*** redis锁的key  前缀*/String perfix() default "";
}

创建Lock拦截器

import com.alibaba.druid.util.StringUtils;
import com.mycompany.myspringboot.user.annotation.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;@Aspect
@Configuration
@Slf4j
public class RedisMethodInterceptor {@Autowiredprivate RedisTemplate<String, Object> template;@Around("execution(public * *(..)) && @annotation(com.mycompany.myspringboot.user.annotation.RedisLock)")public Object interceptor(ProceedingJoinPoint proceedingJoinPoint){ValueOperations<String, Object> opsForValue = template.opsForValue();MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();Method method = signature.getMethod();RedisLock lock = method.getAnnotation(RedisLock.class);String key = getKey(lock.perfix(),proceedingJoinPoint.getArgs());if (!StringUtils.isEmpty(key)){if (opsForValue.get(key) != null){log.error("表单重复提交了");throw new RuntimeException("请勿重复提交");}//设置键为key,值为key,时间为50,单位为秒opsForValue.set(key,key,50, TimeUnit.SECONDS);}try{return proceedingJoinPoint.proceed();}catch (Throwable throwable){throwable.printStackTrace();throw new RuntimeException("服务器异常");}}/*** key的生成策略*/private String getKey(String keyExpress,Object[] args){for (int i = 0; i < args.length ; i++ ){keyExpress = keyExpress.replace("arg[" + i + "]",args[i].toString());}return keyExpress;}}

添加注解

@PostMapping(value = "/user")
//    @LocalLock(key = "book:arg[0]")
@RedisLock(perfix = "redis")
public int addUser(@CacheParam(key = "name") @RequestParam("name") String name){User user = new User();user.setUserId("202006241002203");user.setPhone("123123123");user.setUserName(name);return userService.addUser(user);
}

优化获取key值

创建获取key注解

@Target({ElementType.PARAMETER,ElementType.METHOD}) //表明可以注解在方法和参数上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {/*** 获取字段名字 为指定唯一*/String key() default "";
}

创建getKey拦截器

import com.alibaba.druid.util.StringUtils;
import com.mycompany.myspringboot.user.annotation.CacheParam;
import com.mycompany.myspringboot.user.annotation.RedisLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;@Component
public class LocalKeyGenerator {/**** @param proceedingJoinPoint* @return*/public String getLockKey(ProceedingJoinPoint proceedingJoinPoint){MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();Method method = signature.getMethod();RedisLock redisLock = method.getAnnotation(RedisLock.class);final Object[] args = proceedingJoinPoint.getArgs();final Parameter[] parameters = method.getParameters();StringBuilder builder = new StringBuilder();//获取RedisLock里面指定的参数for (int i = 0; i < parameters.length ; i++){final CacheParam annotation = parameters[i].getAnnotation(CacheParam.class);if (annotation == null){continue;}builder.append(":").append(args[i]);}//获取实体里面包含的RedisLock里面指定的参数if (StringUtils.isEmpty(builder.toString())){final Annotation[][] parameterAnnotation = method.getParameterAnnotations();for (int i = 0; i < parameterAnnotation.length ;i++){final Object object = args[i];final Field[] fields = object.getClass().getDeclaredFields();for (Field field : fields){final CacheParam annotation = field.getAnnotation(CacheParam.class);if (annotation == null){continue;}field.setAccessible(true);builder.append(":").append(ReflectionUtils.getField(field,object));}}}return redisLock.perfix() + builder.toString();}
}

使用CacheParam注解

修改Lock连接器获取key方式

@Around("execution(public * *(..)) && @annotation(com.mycompany.myspringboot.user.annotation.RedisLock)")
public Object interceptor(ProceedingJoinPoint proceedingJoinPoint){ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();//        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();//        Method method = signature.getMethod();//        RedisLock lock = method.getAnnotation(RedisLock.class);String key = localKeyGenerator.getLockKey(proceedingJoinPoint);if (!StringUtils.isEmpty(key)){if (opsForValue.get(key) != null){log.error("表单重复提交了");throw new RuntimeException("请勿重复提交");}//设置键为key,值为key,时间为50,单位为秒opsForValue.set(key,key,50, TimeUnit.SECONDS);}try{return proceedingJoinPoint.proceed();}catch (Throwable throwable){throwable.printStackTrace();throw new RuntimeException("服务器异常");}
}

修改调用分布锁的Controller的参数注解

@PostMapping(value = "/user")
//    @LocalLock(key = "book:arg[0]")
@RedisLock(perfix = "redis")
public int addUser(@CacheParam(key = "name") @RequestParam("name") String name){User user = new User();user.setUserId("202006241002203");user.setPhone("123123123");user.setUserName(name);return userService.addUser(user);
}

入门SpringBoot集成常用框架以及常见处理方式(括宽知识面)相关推荐

  1. SpringBoot集成MyBatis-Plus框架

    1.说明 本文介绍Spring Boot集成MyBatis-Plus框架, 重点介绍需要注意的地方, 是SpringBoot集成MyBatis-Plus框架详细方法 这篇文章的脱水版, 主要是三个步骤 ...

  2. SpringBoot集成Log4j2框架

    1.说明 本文详细介绍Spring Boot集成Log4j2框架的方法, 基于已经创建好的Spring Boot工程, 由于Spring Boot默认使用的是Logback框架, 需要先排除掉Logb ...

  3. SpringBoot集成MyBatis-Plus框架详细方法

    1.说明 本文详细介绍Spring Boot集成MyBatis-Plus框架的方法, 使用MySQL数据库进行测试, 包括完整的开发到测试步骤, 从一开始的Spring Boot工程创建, 到MySQ ...

  4. 【Java从0到架构师】交错的日志系统、SpringBoot 集成日志框架

    交错的日志系统.SpringBoot 集成日志框架 交错复杂的日志系统① - 多个项目实现 SLF4J 门面 交错复杂的日志系统② - 统一底层实现为 Logback 交错复杂的日志系统③ - 统一底 ...

  5. SpringBoot 集成FluentMyBatis 框架之集成分页功能

    本文基于上一篇:SpringBoot 集成FluentMyBatis 框架之完善 SpringBoot 集成FluentMyBatis 框架之集成分页功能 FluentMyBatis 官方分页 官方提 ...

  6. SpringBoot集成Quartz框架

    SpringBoot集成Quartz框架 (一)集成环境: ​ Win10系统 ​ JDK版本:11.0.13 ​ SpringBoot版本:2.3.4.RELEASE ​ Quartz版本:2.3. ...

  7. SpringBoot - 集成Quartz框架:Couldn‘t acquire next trigger: Couldn‘t retrieve trigger: 不良的类型值 long : \x

    写在前面 SpringBoot 集成Quartz框架时,数据保存方式使用PostgreSQL进行数据库持久化. 报错如下: Couldn't acquire next trigger: Couldn' ...

  8. springboot md5加密_SpringSecurity入门-SpringBoot集成SpringSecurity

    前言 至今Java能够如此的火爆Spring做出了很大的贡献,它的出现让Java程序的编写更为简单灵活,而Spring如今也形成了自己的生态圈,今天咱们探讨的是Spring旗下的一个款认证工具:Spr ...

  9. 【dubbo】springboot集成dubbo框架

    1. dubbo介绍 dubbo是一款开源rpc框架,提供rpc调用诸多组件.支持服务注册与发现.服务负载均衡.服务容错.服务降级处理.服务失败尝试机制.服务监控等组件. 当我们项目拆分成微服务时,A ...

  10. spring入门之Spring 常用的三种注入方式

    Spring 常用的三种注入方式 Spring 通过 DI(依赖注入)实现 IOC(控制反转),常用的注入方式主要有三种:构造方法注入,set 方法注入,基于注解的注入. 一.通过构造方法注入 先简单 ...

最新文章

  1. memcpy,_tcscpy_s的使用
  2. 科大星云诗社动态20211202
  3. 运用Arc Hydro提取河网
  4. LVS学习笔记--DR模式部署
  5. HTML 上标题栏把右标题栏遮挡,如何编辑组件的样式(编辑样式)?
  6. (转)如何在MySql中记录SQL日志(例如Sql Server Profiler)
  7. (原创)一步一步学ZedBoard Zynq(一):ZedBoard的第一个工程Helloworld
  8. win10 电脑没声音 控制面板 realtek高清晰音频管理器没有解决方案
  9. SNP基因数据质控调研
  10. 一个读者大佬精心总结的阿里、腾讯、宇宙条大厂 Offer 面经和硬核面试攻略
  11. 浏览器运作原理笔记(来自up主objtube的卢克儿的视频)
  12. 序列化之Serialize
  13. MySQL备份恢复方案
  14. 揭秘!中国人一定要知道的北斗卫星系统
  15. 简单Tomcat和Nginx部署前端项目
  16. EDI助力家居行业实现供应链优化
  17. 多点解读贝泰妮:火速转战H股,女人的钱究竟有多好赚?
  18. java 调用matlab函数_java中调用Matlab的函数+注意事项
  19. C++中调用SPLUS对象经典例子
  20. 在下面的html标记中属于双边标记的有,兰大《网页与网站设计》20春平时作业1满分...

热门文章

  1. java dagger2_Dagger2用法整理
  2. 数据结构与算法分析——Hash表
  3. 解决QQ或TIM下载群文件网路失败或者网速贼慢的办法
  4. 【CAR笔记2】IGBT相关知识
  5. 原来这就是公文写作领导讲话稿万能模板(1)
  6. win10下虚拟机VMware安装PhoenixOS(凤凰OS)
  7. EDA技术与CPLD/FPGA开发应用实验教学
  8. 系统集成项目管理工程师高频考点(第五章)
  9. 群晖5.2php核心设置_只需四步, 黑群晖5.2 NAS 最简明搭建教程
  10. ffmpeg 再编译使用 ffmpeg-gl-transition