注:本篇笔记较长,所有涉及到知识仅作为入门的简单了解,帮助扩宽知识面,以便在能用上的场景可以有大体解决方向
本篇项目的giee:https://gitee.com/ywq869819435/springboot-hodgepodge-primer
不好用还是需要导入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=true
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
之后运行即可
首先从网上下载需要的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.hadoopgrounpld>
<artifactld>zookeeperartifactld>
<version>3.3.1version>
<exclusions>
<exclusion>
<groupld>jlinegroupld>
<artifactld>jlineartifactld>
exclusion>
exclusions>
dependency>
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.1.RELEASEversion>
<relativePath/>
parent>
<groupId>com.mycompanygroupId>
<artifactId>myspringbootartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>myspringbootname>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jdbcartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
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!
约定大于配置是一种开发原则,就是减少人为的配置,直接用默认的配置就能获得我们想要的结果,eg:默认连接池,默认端口,默认配置文件名称,默认日志文件名称
约定好工程目录结构、配置文件位置【可以有多个配置文件对应不同的环境】、启动类的固定位置
application.yml配置内容
spring:
profiles:
active: dev #当有多个配置文件的时候,表明选用那个配置文件(查找里面有dev的yml文件,首先选择dev的配置属性,若dev里没有则在application.yml配置文件取)
application-dev.yml
spring:
datasource: #配置数据源
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/ecp-test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
username: root
password: 123456
server:
port: 8080 #设置端口号
总结约定:
项目包
idea的配置
maven的相关配置
项目所有文件内容
项目内容
java代码内容
包名
项目名
业务分类包
启动类(一定置项目名下)
resources
static[存放一些静态资源,如图片]
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 - {...}) //去除自动配置某个配置
properties的优先级比yml要高一些
等价格式演示:
<server>
<port>8090port>
<servelet>
<context-path>/myspringbootcontext-path>
servelet>
server>
server.port=8090
server.servelet.context-path= /myspringboot
使用properties的时候可能会出现中文乱码,这是idea的问题,需要改一下设置如下
yml:
server:
port: 8090 #设置端口号
serverlet:
context-path: /myspringboot #配置上下文路径
相比来yml层级关系明显,以数据为中心
server:
port: 8080 #设置端口号
serverlet:
context-path: /myproject #配置上下文路径[命名可以改变],后面没有对应的配置默认找index.html
logging: #设置日志
level: #打印等级
root: info
person: #自定义
name: ywq
age: 21
readBook: 《java》,《C++》
可以接受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: ywq
age: 21
readBook: 《java》,《C++》
//指定改实体类获取值的路径
@PropertySource(value = {"classpath:myperson.yml"})
@ConfigurationProperties(prefix = "person")
@Component
两种方式各有优劣,自行选择
在springboot里面默认支持的三种连接池:dbcp、tomcat[boot1.5之前默认用这个]、hikari
【数据访问】
是阿里巴巴开源的一个数据源,主要用于java数据库连接池,相比于Spring推荐的默认数据库连接池,在市场上占绝对优势;Druid数据源由于有强大的监控特性、可拓展性等特点被广泛使用。即使HikariCP的性能比Druid优秀,但是因为Druid包括许多维度的统计和分析功能,所以大家都选择使用Druid的更多
在不更改任何连接池配置文件的前提下查看默认连接池,使用Test单元测试查看
@SpringBootTest
class MyspringbootApplicationTests {
@Autowired
private DataSource dataSource;
@Test
void contextLoads() throws SQLException {
Connection connection = dataSource.getConnection();
System.out.println(connection);
}
}
默认连接池为HikarProxy
有两种依赖一个druid和spring-boot-start-druid【spring-boot-start-xxx都是springboot提供的包,而xxx-spring-boot-start则是xxx专门为spring boot开发的包,如果是springboot的包应该选用带springboot的】两种不同的依赖包
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.10version>
dependency>
引进带start的包可以免去一些配置文件的配置,不带的就需要手动配置许多文件
spring:
#配置druid连接池
type: com.alibaba.druid.pool.DruidDataSource
druid:
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包然后创建对应的配置java文件
/**
* @Description 连接池配置类
* @Author ywq
*/
@Configuration
public class DruidConfiguration {
private final Logger logger = LoggerFactory.getLogger(DruidConfiguration.class);
@Bean
public 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;
}
@Bean
public 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优势:
1. 可以进行更为细致的sql优化,可以减少查询字段
2. 容易掌握,而Hibernate门槛较高
3. 占有绝大部分的中国软件市场
Hibernate优势:
【数据访问】
两者都带有对应sql 的事物机制,即当程序执行异常的时候进行数据的回滚,就是在有这个声明的开始,整个运行本次程序过程中只要中间任意一步出现异常就会让其运行到改变全部改回原来的状态,即回到没有运行的状态。
使用也很简单,只要加上注解@Transactional就可以了,可以设置其接受的异常范围
eg:
@Transactional(rollbackFor = Exception.class)
这里仍选择Spring boot starter的包(含有自动配置)
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.0.1version>
dependency>
mybatis:
#mapper.xml所在位置
mapperLocations: classpath:mybatis/mapper/**/*Mapper.xml
configuration:
mapUnderscoreToCamelCase: true #大小写驼峰转换
call-setters-on-nulls: true #设置查询结果为null的时候是否返回
jdbcTypeForNull: VARCHAR #当没有指定传入参数的时候默认为varchar
配置好后就可以进行mybatis编写sql
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">
SELECT
user_name
FROM
t_sys_user
select>
mapper>
注意当使用一些特写字符的时候用这个装起来,这个会按文本形式处理
还有注意关于各个层级之间的注解写好。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update # 第一次简表create 后面用update
show-sql: true
注意,如果通过jpa在数据库中建表,将jpa.hibernate,ddl-auto改为create,建完表之后,要改为update,要不然每次重启工程会删除表并新建。
通过@Entity 表明是一个映射的实体类, @Id表明id, @GeneratedValue 字段自动生成
@Entity
public class Account {
@Id
@GeneratedValue
private int id ;
private String name ;
private double money;
//... 省略getter setter
}
数据访问层,通过编写一个继承自 JpaRepository 的接口就能完成数据访问,其中包含了几本的单表查询的方法,非常的方便。值得注意的是,这个Account 对象名,而不是具体的表名,另外Interger是主键的类型,一般为Integer或者Long
public interface AccountDao extends JpaRepository<Account,Integer> {
}
在这个栗子中我简略了service层的书写,在实际开发中,不可省略。新写一个controller,写几个restful api来测试数据的访问。
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
AccountDao 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是为了方便开发的时候去避免写一些简单单表增删改查,里面有集成的增删改查方法。属于mybatis的扩展
<dependency>
<groupId>tk.mybatisgroupId>
<artifactId>mapper-spring-boot-starterartifactId>
<version>2.1.5version>
dependency>
对应的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 {
@Id
private String userId;
private String userName;
}
都修改完成后,运行结果如下:
当使用的通用mapper之后需要更改MapperScan的包为
import tk.mybatis.spring.annotation.MapperScan;
PageHelper是分页工具,在开发过程中经常使用
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>1.2.10version>
dependency>
pagehelper:
helper-dialect: mysql #数据库类型[数据库分页原理不同,mysql是limit,sql server是top]
reasonable: true #分页合理化
support-methods-arguments: true #是否支持接口参数来传递分页参数,默认false
page-size-zero: true #当设置为true的时候,如果pageSize设置为0(或RowBounds的limit=0,就不执行分页)
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;
}
在项目开发的阶段往往存在实体类的属性的变化以及字段的增删等,这时候存在多次实体类变动,我们就要频繁的对其对应的getter和setter进行修改,这时候就有了这个Lombok插件,通过注解为我们省去手写的getter和setter,是一个编译级别的插件,在编译源码的时候自动帮我们生成对应方法
导入依赖报错后,需要通过idea->file->settting->pulsing下载对应插件
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.10version>
<scope>providedscope>
dependency>
当导入依赖报错后需要安装如下插件:
@Table(name = "t_sys_user")
@Setter
@Getter
public class User {
@Id
private String userId;
private String userName;
}
也可以直接写@Data,这里会自动提供get、set、toString、equals的重写
需要导入Lombok依赖或者idea下载次插件,主要作用是提高代码的简洁,使用这个注解可以省去代码中大量的get()、 set()、 toString()、equals、hashCode、canEqua方法;
注解在属性或者实体类上,提供set方法
注解在属性或者实体类上,提供get方法
注解在类上,提供对应的equals和hashCode方法
使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数
使用后创建一个无参构造函数
如果给参数加上这个注解,参数为null会抛出空指针异常
与@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代码运行机制,只是编译源码的时候去对应的语法树中找到代码里写的注解然后匹配语法规则生成相应的源码,这样编译完成后自动生成一些代码。
当下载插件还是不可以编译的时候,开启idea启用注解编程,勾选上表示开启
缓和较慢存储的高频请求,缓解数据库压力,提升响应速率。
spring cache是Spring对缓存的封装,适用于EHCache、Redis、Guava等缓存技术。主要是可以使用注解的方式来处理缓存,eg:单使用Redis进行缓存,查询数据,如果查到需要写一些判断,而如果使用Spring Cache注解来处理,则可以省去这些判断。【spring cache之间数据不互通,而redis是分布的缓存,可以互通数据】
依赖本号最后带有RELEASE表示当前稳定的版本
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
<version>2.2.2.RELEASEversion>
dependency>
这里的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否则不会生效
当不指定版本的时候会在这里找可用的版本信息
2.1.5之前版本的redis的连接池是jedis,之后就用了lettuce
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<version>2.2.5.RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
spring:
datasource: #配置数据源
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/walk_bookstore?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
username: root
password: 123456
#配置druid连接池
type: com.alibaba.druid.pool.DruidDataSource
druid:
#初始化连接池大小
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,log4j2
cache:
type: REDIS #选择处理缓存的工具
redis:
host: 127.0.0.1
port: 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-data-redis的RedisTemplate
解决:
使用其他类序列化redis的key和value,因为RedisTemplate默认是用字节方式序列化,可以用泛型解决RedisTemplate,也可以直接使用StringRedisTemplate进行Redis的相关操作。
自己写一个redis的configuration然后重写关于序列化的地方
在传统的开发模式中,每个系统准备一份接口文档,方便各个团队之间的交流。但是由于软件的发展,需求功能越来越多,导致接口数量也越来越多,开发的人也越来越杂,维护接口文档就变成了一个很大的负担。
Swagger是一款通过注解的方式生成Restful APIs交互界面的工具
优点:
缺点:
swagger2核心jar包和UI界面包
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
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 {
@Autowired
private 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. 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. 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. 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. 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. 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;
}
配置完成后就可以开始使用注解
用在请求的类上,表示对类的说明,描述Controller的作用
tags=“说明该类的作用,可以在UI界面上看到的注解”
value=“该参数没什么意义,在UI界面上也看到,所以不需要配置”
@Api(tags = "用户信息模块")
用在请求的方法或者接口上,说明方法的用途、作用
value=“说明方法的用途、作用”
notes=“方法的备注说明”
@ApiOperation(value = "用户名查询", notes = "如果不传任何分页参数就不分页,分页必须传pageNum,pageSize",httpMethod = "GET")
用在请求的方法上,表示一组参数说明
@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")
})
还有一些其他的注解可以去了解,查看当前结果
slf4j就是简单的日志门面框架,只提供接口,没有具体的实现。具体的日志功能有具体的日志框架去实现[logback,log4j],使用Slf4j【门面】有个很大的好处,当你想切换其他日志框架的时候,原来的代码几乎不用更改。
更快的执行速度,更少的内存
spring boot本身都有导入很多的日志框架依赖,当你需要更新版本的时候才去导入新的日志依赖
my:
log:
path: t:/myspringboot
<configuration scan="true" scanPeriod="60 seconds">
<contextName>logbackcontextName>
<property name="log.path" value="G:/logs/pmp" />
<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}}"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debuglevel>
filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}Pattern>
<charset>UTF-8charset>
encoder>
appender>
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/web_debug.logfile>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
<charset>UTF-8charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/web-debug-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15maxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>debuglevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/web_info.logfile>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
<charset>UTF-8charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/web-info-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15maxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>infolevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/web_warn.logfile>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
<charset>UTF-8charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/web-warn-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15maxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warnlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/web_error.logfile>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
<charset>UTF-8charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/web-error-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15maxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERRORlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<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>
configuration>
为了方便与前端各种各样的设备层进行通信,就有了这个统一的机制。
都是同样的路径,但是请求方式不一样
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)
@ResponseBody
public 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)
@ResponseBody
public 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
@ResponseBody
public void handler(HttpServletRequest request, HttpServletResponse response) throws Exception{
log.info("现在进入了报文下发接口");
}
}
@Component
@RequestTypeHandler("BIP2B342")
@Slf4j
public class SeeLookServiceImpl extends AbstractRequestTypeHandler {
@Override
@ResponseBody
public void handler(HttpServletRequest request, HttpServletResponse response) throws Exception{
log.info("现在进入了报文审阅接口");
}
}
[AOP拦截注解类解析进行bean注册实例化]
写了注解的请求类型处理器(选择要做那件事)
/**
* 注解的请求类型处理器
* 注解的类型和对应实现类放入map[注册到Bean工厂]来处理选择用哪个实现类
*/
@Slf4j
@Component
public class RequestTypeHandlerContext implements ApplicationContextAware {
@Autowired
ApplicationContext 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
*/
@Override
public 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);
}
}
结果如下:
@EnableAsync:表示的是开启异步任务(多线程)功能; 注意名称前缀
eg:
@Slf4j
@Component
@EnableAsync //这个注解可以写在启动类,但是启动类东西有些多,放这里表明这个方法启动就好
public class AsyncTask {
@Async
public 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();
}
}
注意:
eg:
@Slf4j
@RestController
@RequestMapping("/userController")
public class ResfulUserController {
@Autowired
private UserService userService;
@GetMapping(value = "/user")
public List<User> findUser(){
this.sendMessage();
System.out.println("执行查询开始");
return userService.getUserNameList();
}
@Async
public void sendMessage() {
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
log.info("选择进行短信群发");
}
}
不配置会存在的问题
当访问量很多的时候,每个请求都要调用异步的方法,那就要拿到相应的数量的处理异步的线程,大量的线程会给服务器造成很大压力。
解决方法:
在配置类中,ThreadPoolTaskExecutor类是一个线程池。配置一些线程池的参数,确保高效的运转。
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public 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;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(){
return new SimpleAsyncUncaughtExceptionHandler();
}
}
创建定时任务有两种写法,一种是基于注解,一种是基于接口【当cron的值需要从数据库中读取的时候就必须要用这种方式】,在以后遇到更频繁改变的定时任务的时候需要用相关的第三方框架:Quart2
@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();
}
@Autowired
CronMapper cronMapper;
@Override
public 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.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
dependency>
(password: eeffuilmbtkujehh #QQ邮箱开通第三方登录的授权码)
spring:
mail:
username: [email protected]
password: kathgcjopvzdbdfe # 授权码
host: smtp.qq.com
properties:
mail:
smtp:
ssl:
enable: true
QQ邮箱设置-账号设置开启
我的邮箱号授权码:kathgcjopvzdbdfe
@Test
public void sendMaill(){
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setSubject("注意");
mailMessage.setText("????");
mailMessage.setFrom("[email protected]");
mailMessage.setTo("[email protected]");
//发送邮箱
mailSender.send(mailMessage);
}
发送图片
@Test
public void sendMaill2() throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message,true);
helper.setSubject("注意");
helper.setText("???",true);
helper.setFrom("[email protected]");
helper.setTo("[email protected]");
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.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
String key() default
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
/**
* 默认的key
*/
String key() default "";
}
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)
@ResponseBody
public 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
优化:根据注解获取指定唯一参数
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {
/**
* redis锁的key 前缀
*/
String perfix() default "";
}
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 {
@Autowired
private 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);
}
@Target({ElementType.PARAMETER,ElementType.METHOD}) //表明可以注解在方法和参数上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
/**
* 获取字段名字 为指定唯一
*/
String key() default "";
}
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();
}
}
修改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);
}