0、写在最前面的话:jdk版本必须是8!!!jdk版本必须是8!!!jdk版本必须是8!!!
一、Spring Framework Runtime
Test
Spring提供的测试工具, 可以整合JUnit测试, 简化测试环节,对应spring-test.jar。
Core Container
Spring的核心组件, 包含了Spring框架最基本的支撑。
-
Beans:Spring 负责创建类对象并管理对象,对应spring-beans.jar。
-
core:核心类,对应spring-core.jar。
-
context:上下文参数,获取外部资源或管理注解,对应spring-context.jar。
-
SpEL:Spring表达式语言,对应spring-expression.jar。
spring启动需要的4个jar包。
-
Aop:面向切面编程, 对应spring-aop.jar。
-
Aspects:切面Aop依赖的包
-
Instrumentation:Spring 对服务器的代理接口
-
Messaging:信息体系结构和协议支持
Data Access/Integartion
Spring封装数据访问层相关内容
-
JDBC:Spring对jdbc的封装,当需要使用spring连接数据库时使用,对应spring-jdbc.jar.
-
ORM:spring整合第三方orm框架需要使用的jar包,例如Hibernate框架,对应spring-orm.jar。
-
OXM:Spring对于object/xml映射的支持,可以让JAVA与XML之间来回切换
-
JMS:为简化jms api的使用而做的简单封装
-
Transactions:用于事务管理,对应Spring-tx.jar。
Web
Spring完成web相关功能时需要。
spring boot运行
二、Spring中支持扩展的设计模式:
1、观察者模式在 Spring 中的应用
// Event事件
public class DemoEvent extends ApplicationEvent {
private String message;
public DemoEvent(Object source, String message) {
super(source);
}
public String getMessage() {
return this.message;
}
}
// Listener监听者
@Component
public class DemoListener implements ApplicationListener<DemoEvent> {
@Override
public void onApplicationEvent(DemoEvent demoEvent) {
String message = demoEvent.getMessage();
System.out.println(message);
}
}
// Publisher发送者
@Component
public class DemoPublisher {
//自动装配,自动地为这个属性赋值
@Autowired
private ApplicationContext applicationContext;
public void publishEvent(DemoEvent demoEvent) {
this.applicationContext.publishEvent(demoEvent);
}
}
@Autowired原理:
举例:@Autowired
private BookService bookService;
1)、先按照类型去容器中找到对应的组件;bookService = ioc.getBean(BookService.class)
①、找到一个:找到就赋值
②、没找到就报异常
③、按照类型可以找到多个?找到多个如何装配上?
a、类型一样就按照变量名为ID继续匹配
Ⅰ、匹配上就装配
Ⅱ、没有匹配?报错
原因:因为我们按照变量名作为id继续匹配的
使用@Qualifier指定一个新的id
找到就匹配
2、模板模式在 Spring 中的应用
① Spring Bean 的创建过程,可以大致分为两大步:对象的创建和对象的初始化。
② 对象的创建是通过反射来动态生成对象,而不是 new 方法。不管是哪种方式,说白了,总归还是调用构造函数来生成对象,没有什么特殊的。
模板模式在Spring Bean中创建使用示例:
public class DemoClass {
//...
public void initDemo() {
//...初始化..
}
}
// 配置:需要通过init-method显式地指定初始化方法
<bean id="demoBean" class="com.xzg.cd.DemoClass" init-method="initDemo"></bean>
这种初始化方式有一个缺点,初始化函数并不固定,由用户随意定义,这就需要 Spring 通过反射,在运行时动态地调用这个初始化函数。而反射又会影响代码执行的性能
③ Spring 针对对象的初始化过程,还做了进一步的细化,将它拆分成了三个小步骤:初始化前置操作、初始化、初始化后置操作。其中,中间的初始化操作就是我们刚刚讲的那部分,初始化的前置和后置操作,定义在接口 BeanPostProcessor 中。BeanPostProcessor 的接口定义如下所示:
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object var1, String var2) throws BeansException;
Object postProcessAfterInitialization(Object var1, String var2) throws BeansException;
}
三、MyBatis如何权衡易用性、性能和灵活性?
1、Mybatis 和 ORM 框架介绍
① MyBatis 是一个 ORM(Object Relational Mapping,对象 - 关系映射)框架
② MyBatis 依赖 JDBC 驱动,所以,在项目中使用 MyBatis,除了需要引入 MyBatis 框架本身(mybatis.jar)之外,还需要引入 JDBC 驱动(比如,访问 MySQL 的 JDBC 驱动实现类库 mysql-connector-java.jar)。将两个 jar 包引入项目之后,我们就可以开始编程了。使用 MyBatis 来访问数据库中用户信息的代码如下所示:
// 1. 定义UserDO
public class UserDo {
private long id;
private String name;
private String telephone;
// 省略setter/getter方法
}
// 2. 定义访问接口
public interface UserMapper {
public UserDo selectById(long id);
}
// 3. 定义映射关系:UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org/DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.xzg.cd.a87.repo.mapper.UserMapper">
<select id="selectById" resultType="cn.xzg.cd.a87.repo.UserDo">
select * from user where id=#{id}
</select>
</mapper>
// 4. 全局配置文件: mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8" />
<property name="username" value="root" />
<property name="password" value="..." />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>
</configuration>
在使用 MyBatis 的实现方式中,类字段与数据库字段之间的映射关系、接口与 SQL 之间的映射关系,是写在 XML 配置文件中的,是跟代码相分离的,这样会更加灵活、清晰,维护起来更加方便。
2、如何利用职责链与代理模式实现MyBatis Plugin?
例如:假设我们需要统计应用中每个 SQL 的执行耗时,如果使用 MyBatis Plugin 来实现的话,我们只需要定义一个 SqlCostTimeInterceptor 类,让它实现 MyBatis 的 Interceptor 接口,并且,在 MyBatis 的全局配置文件中,简单声明一下这个插件就可以了。具体的代码和配置如下所示
@Intercepts({
@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
public class SqlCostTimeInterceptor implements Interceptor {
private static Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object target = invocation.getTarget();
long startTime = System.currentTimeMillis();
StatementHandler statementHandler = (StatementHandler) target;
try {
return invocation.proceed();
} finally {
long costTime = System.currentTimeMillis() - startTime;
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
logger.info("执行 SQL:[ {} ]执行耗时[ {} ms]", sql, costTime);
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
System.out.println("插件配置的信息:"+properties);
}
}
<!-- MyBatis全局配置文件:mybatis-config.xml -->
<plugins>
<plugin interceptor="com.xzg.cd.a88.SqlCostTimeInterceptor">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
我们只重点看下 @Intercepts 注解这一部分。
我们知道,不管是拦截器、过滤器还是插件,都需要明确地标明拦截的目标方法。
@Intercepts 注解实际上就是起了这个作用。其中,@Intercepts 注解又可以嵌套 @Signature 注解。一个 @Signature 注解标明一个要拦截的目标方法。如果要拦截多个方法,我们可以像例子中那样,编写多条 @Signature 注解。
@Signature 注解包含三个元素:type、method、args。其中,type 指明要拦截的类、method 指明方法名、args 指明方法的参数列表。通过指定这三个元素,我们就能完全确定一个要拦截的方法。
默认情况下,MyBatis Plugin 允许拦截的方法有下面这样几个:
四、Spring使用规则:
0、开发顺序:先写Dao -> Service -> Controller
单元测试:Service
调试Dao层:Mybatis打印sql
1、请求方法设置
@RestController:用来声明一个 Controller 类,加载到 Spring Boot 上下文;
@RequestMapping:指定当前类中所有方法在 URL 中的访问路径的前缀;
@Post/Get/PutMapping:定义当前方法的 HTTP Method 和访问路径。
@RestController类中的所有方法只能返回String、Object、Json等实体对象,不能跳转到模版页面。
@RequestMapping是一个用来处理请求地址映射的注解,表示支持GET、POST、PUT、DELETE所有请求
@GetMapping、@PostMapping、@PutMapping、@DeleteMapping
@RequestMapping(value = "/user/1",method = RequestMethod.GET)
表示get请求用户id等于1
controller/TestController.java
package com.dukaiwen.wiki.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@RequestMapping("/hello")
public String hello(){
return "Hello World!";
}
}
2、请求状态type=Method Not Allowed, status=405表示没有这个接口
3、@SpringBootApplication注解一般放在项目的一个启动类上,用来把启动类注入到容器中,用来定义容器扫描的范围,用来加载classpath环境中一些bean。
@SpringBootApplication 注解会自动开启包路径扫描,并启动一系列的自动装配流程(AutoConfig)。
如果我们想加载额外的扫包路径,只用添加 ComponentScan 注解并指定 path 即可。
4、使用@ComponentScan来扫描包(限制在同一个包内才能扫描到,不同包可以使用@ComponentScan({"com.dukaiwen","com.test"})),让我们直接创建,无需以自己引入
5、波浪文字处理a|t+Enter实现添加到字典中
6、配置端口号:在resources中application.properties文件中配置端口号
server.port = 8880
系统识别端口号位置:resources中config文件下application.properties、application.yml也可以识别,注意:单个SpringBoot不会读bootstrap配置,要SpringCloud架构下的SpringBoot应用才会读,bootstrap一般用于动态配置,线上可以实时修改实时生效的配置,一般可配合上nacos配置中心使用
7、全局配置文件application.properties中
server.port=8880
test.hello=Hello
用@Value("${test.hello:TEST}"),冒号后面默认值(优先读配置文件,没有就用默认值),防止生产配有配置值导致服务起不来问题
controller/TestController.java
@Value("${test.hello:TEST}")
private String testHello;
@GetMapping("/hello")
public String hello(){
return "Hello World!" + testHello;
}
8、集成热部署【点击小锤子🔨】
在pom.xml中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
补充知识点:内部可以不用加版本号,因为有父项目中版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
9、Mybatis
依赖引入
pom.xml
<!-- 集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!-- 集成mysql连接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
配置数据源
application.properties
spring.datasource.url=jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com/wikidev?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
spring.datasource.username=wikidev
spring.datasource.password=wikidevABC123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
10、数据表对象层
在domain,或entity,或POJO,总之这一层实体类就是和数据库表一一映射
domain/Test.java
package com.jiawa.wiki.domain;
public class Test {
private Integer id;
private String name;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
11、持久层叫Mapper层,即广为人知的Dao层。因为后续要用官方代码生成器,其生成的代码就是XXXMapper
mapper/TestMapper.java(interface)
package com.jiawa.wiki.mapper;
import com.jiawa.wiki.domain.Test;
import java.util.List;
public interface TestMapper {
public List<Test> list();
}
12、sql脚本存放位置
resources/mapper/TestMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.jiawa.wiki.mapper.TestMapper" >
<select id="list" resultType="com.jiawa.wiki.domain.Test">
select `id`, `name`, `password` from `test`
</select>
</mapper>
1)<mapper namespace="com.jiawa.wiki.mapper.TestMapper" >中namespace对应的mapper/TestMapper.java(interface)文件的package名称+interface名称
2)<select id="list" resultType="com.jiawa.wiki.domain.Test">中id对应的mapper/TestMapper.java(interface)文件的public List<Test> list()的list方法
3)<mapper namespace="com.jiawa.wiki.mapper.TestMapper" >对应的是domain中的实体类
4) select `id`, `name`, `password` from `test`就是sql语法
13、添加mapper扫描注解
在config/WikiApplication.java
@MapperScan("com.jiawa.wiki.mapper")
1)对应的是mapper/TestMapper.java(interface)文件的package名称
14、配置mybatis所有Mapper.xml所在的路径
在application.properties中
# 配置mybatis所有Mapper.xml所在的路径
mybatis.mapper-locations=classpath:/mapper/**/*.xml
15、Service层:所有逻辑都放在Service
service/TestService.java
package com.jiawa.wiki.service;
import com.jiawa.wiki.domain.Test;
import com.jiawa.wiki.mapper.TestMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class TestService1 {
@Resource
private TestMapper testMapper;
public List<Test> list(){
return testMapper.list();
}
}
使用@Service注解,将这个Service交给Spring来管理
使用@Resource注解,将TestMapper数据源引入
自定义一个公共方法list返回数据
16、controller层中新增一个
controller/TestController.java
@Resource
private TestService testService;
@GetMapping("/test/list")
public List<Test> list(){
return testService.list();
}
@getMapping设置路由
@Resource返回service
17、打包工具依赖
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
18、mybatis-generator依赖引入
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
</dependencies>
</plugin>
1)新建配置文件
/resources/generator/generator-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
<!-- 自动检查关键字,为关键字增加反引号 -->
<property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!--覆盖生成XML文件-->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
<!-- 生成的实体类添加toString()方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 不生成注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com:3306/wikidev?serverTimezone=Asia/Shanghai"
userId="wikidev"
password="wikidevABC123">
</jdbcConnection>
<!-- domain类的位置 -->
<javaModelGenerator targetProject="src\main\java"
targetPackage="com.jiawa.wiki.domain"/>
<!-- mapper xml的位置 -->
<sqlMapGenerator targetProject="src\main\resources"
targetPackage="mapper"/>
<!-- mapper类的位置 -->
<javaClientGenerator targetProject="src\main\java"
targetPackage="com.jiawa.wiki.mapper"
type="XMLMAPPER"/>
<!--<table tableName="demo" domainObjectName="Demo"/>-->
<!--<table tableName="ebook"/>-->
<!--<table tableName="category"/>-->
<!--<table tableName="doc"/>-->
<!--<table tableName="content"/>-->
<!--<table tableName="user"/>-->
<table tableName="ebook_snapshot"/>
</context>
</generatorConfiguration>
修改数据库连接
/resources/generator/generator-config.xml
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com:3306/wikidev?serverTimezone=Asia/Shanghai"
userId="wikidev"
password="wikidevABC123">
</jdbcConnection>
对某张表进行生成数据持久层
/resources/generator/generator-config.xml
<table tableName="ebook_snapshot"/>
调用方法
service/TestService
@Resource
private TestMapper testMapper;
public List<Test> list() {
return testMapper.selectByExample(new TestExample());
}
19、Service、controller层修改内容
比如直接替换文件中Demo、demo替换【选中Match Case、不要选中Words】
20、controller请求配置(类似配置父子路由)
controller/DemoController.java
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private DemoService demoService;
@GetMapping("/list")
public List<Demo> list() {
return demoService.list();
}
}
21、myBatis-generator配置
1)pom.xml
<!-- mybatis generator 自动生成代码插件 -->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.0</version>
<configuration>
<configurationFile>src/main/resources/generator/generator-config.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
</dependencies>
</plugin>
2)在resources/generator新建generator-config.xml【配置不同的表生成不同的文件】
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
<!-- 自动检查关键字,为关键字增加反引号 -->
<property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<!--覆盖生成XML文件-->
<plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
<!-- 生成的实体类添加toString()方法 -->
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 不生成注释 -->
<commentGenerator>
<property name="suppressAllComments" value="true"/>
</commentGenerator>
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://rm-uf6470s9615e13hc4no.mysql.rds.aliyuncs.com:3306/wikidev?serverTimezone=Asia/Shanghai"
userId="wikidev"
password="wikidevABC123">
</jdbcConnection>
<!-- domain类的位置 -->
<javaModelGenerator targetProject="src\main\java"
targetPackage="com.jiawa.wiki.domain"/>
<!-- mapper xml的位置 -->
<sqlMapGenerator targetProject="src\main\resources"
targetPackage="mapper"/>
<!-- mapper类的位置 -->
<javaClientGenerator targetProject="src\main\java"
targetPackage="com.jiawa.wiki.mapper"
type="XMLMAPPER"/>
<!--<table tableName="demo" domainObjectName="Demo"/>-->
<table tableName="ebook"/>
<!--<table tableName="category"/>-->
<!--<table tableName="doc"/>-->
<!--<table tableName="content"/>-->
<!--<table tableName="user"/>-->
<!-- <table tableName="ebook_snapshot"/>-->
</context>
</generatorConfiguration>
3)在Idea中新增配置
22、返回格式统一
1)新建resp/CommonResp.java
package com.jiawa.wiki.resp;
public class CommonResp<T> {
/**
* 业务上的成功或失败
*/
private boolean success = true;
/**
* 返回信息
*/
private String message;
/**
* 返回泛型数据,自定义类型
*/
private T content;
public boolean getSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getContent() {
return content;
}
public void setContent(T content) {
this.content = content;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("ResponseDto{");
sb.append("success=").append(success);
sb.append(", message='").append(message).append('\'');
sb.append(", content=").append(content);
sb.append('}');
return sb.toString();
}
}
2)使用示例
controller/EbookController.java
@GetMapping("/list")
public CommonResp list(String name) {
CommonResp<List<Ebook>> resp = new CommonResp<>();
List<Ebook> list = ebookService.list(name);
resp.setContent(list);
return resp;
}
service/EbookService.java【Criteria:相当与Where条件】
public List<Ebook> list(String name){
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
criteria.andNameLike("%"+name+"%");
return ebookMapper.selectByExample(ebookExample);
}
23、快速生成一个变量
Ctrl+A|t+v
24、封装请求参数
注意:①将请求所有参数封装成一个类
②toString主要是用来打日志
1)新建req/EbookQueryReq.java
package com.jiawa.wiki.req;
public class EbookQueryReq {
private Long id;
private String name;
private Long categoryId2;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getCategoryId2() {
return categoryId2;
}
public void setCategoryId2(Long categoryId2) {
this.categoryId2 = categoryId2;
}
@Override
public String toString() {
return "EbookQueryReq{" +
"id=" + id +
", name='" + name + '\'' +
", categoryId2=" + categoryId2 +
"} " + super.toString();
}
}
2)Controller层示例
EbookController.java
@GetMapping("/list")
public CommonResp list(EbookQueryReq req) {
CommonResp<List<Ebook>> resp = new CommonResp<>();
List<Ebook> list = ebookService.list(req);
resp.setContent(list);
return resp;
}
3)Service层示例
EbookService.java【Spring会自动将参数映射到类属性】
public List<Ebook> list(EbookQueryReq req){
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
criteria.andNameLike("%"+req.getName()+"%");
return ebookMapper.selectByExample(ebookExample);
}
25、封装返回参数
1)新建resq/EbookQueryResp.java
package com.jiawa.wiki.resp;
public class EbookQueryResp {
private Long id;
private String name;
private Long category1Id;
private Long category2Id;
private String description;
private String cover;
private Integer docCount;
private Integer viewCount;
private Integer voteCount;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getCategory1Id() {
return category1Id;
}
public void setCategory1Id(Long category1Id) {
this.category1Id = category1Id;
}
public Long getCategory2Id() {
return category2Id;
}
public void setCategory2Id(Long category2Id) {
this.category2Id = category2Id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCover() {
return cover;
}
public void setCover(String cover) {
this.cover = cover;
}
public Integer getDocCount() {
return docCount;
}
public void setDocCount(Integer docCount) {
this.docCount = docCount;
}
public Integer getViewCount() {
return viewCount;
}
public void setViewCount(Integer viewCount) {
this.viewCount = viewCount;
}
public Integer getVoteCount() {
return voteCount;
}
public void setVoteCount(Integer voteCount) {
this.voteCount = voteCount;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", name=").append(name);
sb.append(", category1Id=").append(category1Id);
sb.append(", category2Id=").append(category2Id);
sb.append(", description=").append(description);
sb.append(", cover=").append(cover);
sb.append(", docCount=").append(docCount);
sb.append(", viewCount=").append(viewCount);
sb.append(", voteCount=").append(voteCount);
sb.append("]");
return sb.toString();
}
}
2)Service层示例
service/EbookService.java【Spring会自动将参数映射到类属性】
public List<EbookQueryResp> list(EbookQueryReq req){
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
criteria.andNameLike("%"+req.getName()+"%");
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
// 持久层返回List<Ebook>需要转成List<EbookResp>再返回controller
List<EbookQueryResp> respList = new ArrayList<>();
for (Ebook ebook : ebookList) {
EbookQueryResp ebookQueryResp = new EbookQueryResp();
// 通过spring工具方法实现类的拷贝
BeanUtils.copyProperties(ebook, ebookQueryResp);
respList.add(ebookQueryResp);
}
return respList;
}
3)Controller层示例
EbookController.java【在controller层不要出现domain实体ebook】
@GetMapping("/list")
public CommonResp list(EbookQueryReq req) {
CommonResp<List<EbookQueryResp>> resp = new CommonResp<>();
List<EbookQueryResp> list = ebookService.list(req);
resp.setContent(list);
return resp;
}
26、制作CopyUtil封装BeanUtils
1)新增util/CopyUtil.js
package com.jiawa.wiki.util;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
public class CopyUtil {
/**
* 单体复制
*/
public static <T> T copy(Object source, Class<T> clazz) {
if (source == null) {
return null;
}
T obj = null;
try {
obj = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
BeanUtils.copyProperties(source, obj);
return obj;
}
/**
* 列表复制
*/
public static <T> List<T> copyList(List source, Class<T> clazz) {
List<T> target = new ArrayList<>();
if (!CollectionUtils.isEmpty(source)){
for (Object c: source) {
T obj = copy(c, clazz);
target.add(obj);
}
}
return target;
}
}
2)使用
单体复制用法
EbookController.java
CopyUtil.copy(ebook, EbookQueryResp.class);
列表复制用法
EbookController.java
List<EbookQueryResp> respList = CopyUtil.copyList(ebookList, EbookQueryResp.class);
return respList;
27、编译报错问题处理【maven中Lifecycle中点击clean】,完成后再编译
28、解决跨域问题
1)新建config/CorsConfig.java
package com.jiawa.wiki.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true)
.maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
}
}
29、动态sql处理
service/EbookService.java
if(!ObjectUtils.isEmpty(req.getName())){
criteria.andNameLike("%"+req.getName()+"%");
}
30、SpringBoot过滤器的使用
filter/LogFilter.java【增加@Component这个注解,Spring就会去扫描。我们的容器就会拿到这个过滤器】
package com.jiawa.wiki.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
public class LogFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 打印请求信息
HttpServletRequest request = (HttpServletRequest) servletRequest;
LOG.info("------------- LogFilter 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());
long startTime = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}
注:filterChain.doFilter(servletRequest, servletResponse);类似await
31、格式化
Ctrl+A|t+L
32、日志折行
33、SpringBoot拦截器的使用【和拦截器不太一样,它前面和后面是分开的,调用业务方法是自己去写】
1)过滤器【拦截器是Spring框架特有的,常用于登录校验,权限校验,请求日志打印】
interceptor/LoginInterceptor.java
package com.jiawa.wiki.interceptor;
import com.alibaba.fastjson.JSON;
import com.jiawa.wiki.resp.UserLoginResp;
import com.jiawa.wiki.util.LoginUserContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);
@Resource
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LoginInterceptor 开始 -------------");
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
// OPTIONS请求不做校验,
// 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
if(request.getMethod().toUpperCase().equals("OPTIONS")){
return true;
}
String path = request.getRequestURL().toString();
LOG.info("接口登录拦截:,path:{}", path);
//获取header的token参数
String token = request.getHeader("token");
LOG.info("登录校验开始,token:{}", token);
if (token == null || token.isEmpty()) {
LOG.info( "token为空,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
Object object = redisTemplate.opsForValue().get(token);
if (object == null) {
LOG.warn( "token无效,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
LOG.info("已登录:{}", object);
LoginUserContext.setUser(JSON.parseObject((String) object, UserLoginResp.class));
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// LOG.info("LogInterceptor 结束");
}
}
注:preHandle返回true往后走,返回false结束
2)过滤器配置
config/SpringMvcConfig.java
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
LoginInterceptor loginInterceptor;
@Resource
ActionInterceptor actionInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/test/**",
"/redis/**",
"/user/login",
"/category/all",
"/ebook/list",
"/doc/all/**",
"/doc/vote/**",
"/doc/find-content/**",
"/ebook-snapshot/**",
"/ebook/upload/avatar",
"/file/**"
);
registry.addInterceptor(actionInterceptor)
.addPathPatterns(
"/*/save",
"/*/delete/**",
"/*/reset-password");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/file/**").addResourceLocations("file:D:/file/wiki/");
}
}
34、系统流程
过滤器范围更大,过滤器在容器tomcat中,接口一进来会先到容器 ——>然后容器会发到应用
我们的SpringBoot它就是一个应用,一个web应用,所以再进到web应用我们的拦截器就拿到了
,再执行业务逻辑,最后拦截器先结束,然后过滤器再结束
35、SpringBootAOP的使用
配置AOP,打印接口耗时、请求参数、返回参数
先加依赖
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
新增aspect/LogAspect.java
package com.jiawa.wiki.aspect;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.jiawa.wiki.util.RequestContext;
import com.jiawa.wiki.util.SnowFlake;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/** 定义一个切点 */
@Pointcut("execution(public * com.jiawa.*.controller..*Controller.*(..))")
public void controllerPointcut() {}
@Resource
private SnowFlake snowFlake;
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
RequestContext.setRemoteAddr(getRemoteIp(request));
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
/**
* 使用nginx做反向代理,需要用该方法才能取到真实的远程IP
* @param request
* @return
*/
public String getRemoteIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
public void pointcut() {
}
@Around(value = "pointcut()")
public Object dealHandler(ProceedingJoinPoint point) throws Throwable {
Object[] args = point.getArgs();
long startTime = System.currentTimeMillis();
String methodName = point.getSignature().getName();
String className = point.getTarget().getClass().getSimpleName();
LoggerClient.info("start {}.{} request , arg:{}", className, methodName, JSONObject.toJSONString(args));
try {
Object result = point.proceed();
long endTime = System.currentTimeMillis();
LoggerClient.info("finish {}.{} request, result:{} cost {}ms", className, methodName, JSONObject.toJSONString(result), (endTime - startTime));
return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
LoggerClient.info("finish {}.{} request, but an exception occurs, exception:{}, cost {}ms", className, methodName, Throwables.getStackTraceAsString(e), (endTime - startTime));
throw e;
}
}
注意:
1)使用AOP要用@Aspect注解、@component表示你要把这个类交给Spring来管理
2)切点:@Pointcut,针对所有的controller都进到这个方法
3)前置通知:@Before,就是执行业务代码之前,我们要去做的事情,就是去打印日志
4)连接点:joinPoint,通过连接点拿到入参,然后将入参打印
5)排除字段:敏感字段或太长的字段不显示
6)环绕:@Around,围绕业务前后执行,打印开始时间、过滤内容、返回结果、耗时
7)执行内容:Object result = proceedingJoinPoint.proceed();
8)aspect切面:切点@Pointcut+通知@Before、@Around、@After
36、使用PageHelper实现前后端分页【帮我们实现了sql中查总数count+查当前页数据limit】
pom.xml
<!-- pagehelper 插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.13</version>
</dependency>
使用
service/EbookService.java
public List<EbookQueryResp> list(EbookQueryReq req){
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if(!ObjectUtils.isEmpty(req.getName())){
criteria.andNameLike("%"+req.getName()+"%");
}
// 分页
PageHelper.startPage(1,3);
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数"+pageInfo.getTotal());
LOG.info("总页数"+pageInfo.getPages());
List<EbookQueryResp> respList = CopyUtil.copyList(ebookList, EbookQueryResp.class);
return respList;
}
注意:分页必须放在查询语句的下面一行,防止分页作用于其他查询语句导致本条查询分页失效
37、打印数据库的日志
application.properties
# 打印所有的sql日志:sql, 参数, 结果
logging.level.com.jiawa.wiki.mapper=trace
38、封装分页请求参数
req/PageReq.java
package com.jiawa.wiki.req;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
public class PageReq {
@NotNull(message = "【页码】不能为空")
private int page;
@NotNull(message = "【每页条数】不能为空")
@Max(value = 1000, message = "【每页条数】不能超过1000")
private int size;
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("PageReq{");
sb.append("page=").append(page);
sb.append(", size=").append(size);
sb.append('}');
return sb.toString();
}
}
使用
EbookQueryReq.java
public class EbookQueryReq extends PageReq {
}
使用
EbookService.java
PageHelper.startPage(req.getPage(), req.getSize());
39、封装分页返回参数
补充泛型使用:不确定类型使用泛型
resp/CommonResp
public class CommonResp<T> {
/**
* 返回泛型数据,自定义类型
*/
private T content;
}
封装返回参数:
resp/PageResp.java
package com.jiawa.wiki.resp;
import java.util.List;
public class PageResp<T> {
private long total;
private List<T> list;
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("PageResp{");
sb.append("total=").append(total);
sb.append(", list=").append(list);
sb.append('}');
return sb.toString();
}
}
使用
service/EbookService.java
public PageResp<EbookQueryResp> list(EbookQueryReq req){
EbookExample ebookExample = new EbookExample();
EbookExample.Criteria criteria = ebookExample.createCriteria();
if(!ObjectUtils.isEmpty(req.getName())){
criteria.andNameLike("%"+req.getName()+"%");
}
PageHelper.startPage(req.getPage(), req.getSize());
List<Ebook> ebookList = ebookMapper.selectByExample(ebookExample);
PageInfo<Ebook> pageInfo = new PageInfo<>(ebookList);
LOG.info("总行数"+pageInfo.getTotal());
LOG.info("总页数"+pageInfo.getPages());
// 列表复制
List<EbookQueryResp> respList = CopyUtil.copyList(ebookList, EbookQueryResp.class);
PageResp<EbookQueryResp> pageResp = new PageResp();
pageResp.setTotal(pageInfo.getTotal());
pageResp.setList(respList);
return pageResp;
}
controller/EbookController.java
@GetMapping("/list")
public CommonResp list(EbookQueryReq req) {
CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
PageResp<EbookQueryResp> list = ebookService.list(req);
resp.setContent(list);
return resp;
}
40、保存功能
controller/EbookController.java
@PostMapping("/save")
public CommonResp save(@Valid @RequestBody EbookSaveReq req){
CommonResp resp = new CommonResp<>();
ebookService.save(req);
return resp;
}
注意:希望通过注解校验post请求的body,需要用到@Valid注解
在request实体类添加注解进行校验,例如用@NotNull进行判空校验
例如:
@Data
public class MyRequest {
@NotNull(message = "id 不能为空")
private Integer id;
@NotNull(message = "name 不能为空")
private String name;
}
补充:
@Data : 注解在类上, 为类提供读写属性, 此外还提供了 equals()、hashCode()、toString() 方法
@Getter/@Setter : 注解在类上, 为类提供读写属性
@ToString : 注解在类上, 为类提供 toString() 方法
@Slf4j : 注解在类上, 为类提供一个属性名为 log 的 log4j 的日志对象
@Log4j : 注解在类上, 为类提供一个属性名为 log 的 log4j 的日志对象
service/EbookService.java
/**
* 保存
* @param req
*/
public void save(EbookSaveReq req){
Ebook ebook = CopyUtil.copy(req,Ebook.class);
if(ObjectUtils.isEmpty(req.getId())){
// 新增
ebookMapper.insert(ebook);
}else{
// 更新
ebookMapper.updateByPrimaryKey(ebook);
}
}
41、雪花算法精度丢失问题
注意!注意!注意!雪花算法ID是Long类型,前端是number类型,精度上不一样,Long类型超过一定长度后,前端接收到的值会不准确。会导致新增的记录不能编辑、删除。
日期获取时间戳
String dateTime = "2021-01-01 08:00:00";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println(sdf.parse(dateTime).getTime());
util/SnowFlake
@Component
public class SnowFlake { ... }
后期使用通过注入写法,所以用@Component将SnowFlake交给spring来管理
使用方法:@Resource是jdk自带的,@Autowired是Spring自带的
service/EbookService
@Resource
private SnowFlake snowFlake;
/**
* 保存
* @param req
*/
public void save(EbookSaveReq req){
Ebook ebook = CopyUtil.copy(req,Ebook.class);
if(ObjectUtils.isEmpty(req.getId())){
// 新增
ebook.setId(snowFlake.nextId());
ebookMapper.insert(ebook);
}else{
// 更新
ebookMapper.updateByPrimaryKey(ebook);
}
}
42、删除功能
controller/EbookController
@DeleteMapping("/delete/{id}")
public CommonResp delete(@PathVariable Long id){
CommonResp resp = new CommonResp<>();
ebookService.delete(id);
return resp;
}
service/EbookService
public void delete(Long id){
ebookMapper.deleteByPrimaryKey(id);
}
43、集成Validation做参数校验
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
使用
req/PageReq
public class PageReq {
@NotNull(message = "【页码】不能为空")
private int page;
@NotNull(message = "【每页条数】不能为空")
@Max(value = 1000, message = "【每页条数】不能超过1000")
private int size;
}
使用@Valid 开启校验规则
controller/EbookController
@GetMapping("/list")
public CommonResp list(@Valid EbookQueryReq req) {
CommonResp<PageResp<EbookQueryResp>> resp = new CommonResp<>();
PageResp<EbookQueryResp> list = ebookService.list(req);
resp.setContent(list);
return resp;
}
44、统一异常处理
controller/ControllerExceptionHandler
package com.jiawa.wiki.controller;
import com.jiawa.wiki.exception.BusinessException;
import com.jiawa.wiki.resp.CommonResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* 统一异常处理、数据预处理等
*/
@ControllerAdvice
public class ControllerExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BindException.class)
@ResponseBody
public CommonResp validExceptionHandler(BindException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("参数校验失败:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
commonResp.setSuccess(false);
commonResp.setMessage(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return commonResp;
}
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public CommonResp validExceptionHandler(BusinessException e) {
CommonResp commonResp = new CommonResp();
LOG.warn("业务异常:{}", e.getCode().getDesc());
commonResp.setSuccess(false);
commonResp.setMessage(e.getCode().getDesc());
return commonResp;
}
/**
* 校验异常统一处理
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public CommonResp validExceptionHandler(Exception e) {
CommonResp commonResp = new CommonResp();
LOG.error("系统异常:", e);
commonResp.setSuccess(false);
commonResp.setMessage("系统出现异常,请联系管理员");
return commonResp;
}
}
@ControllerAdvice注解可以对Controller做一些统一的异常处理,或者数据的预处理
@ExceptionHandler(value = BindException.class)注解可以对BindException做统一的异常处理
我们只需要在controller中写这样一个类,不需要去用它,是SpringBoot框架自己去扫描@ControllerAdvice注解,由框架来使用
45、IntelliJ IDEA根据文件名查找文件
连续按两次shift
46、生成string方法
我们按住alt+insert键生成toString方法,toString方法主要是用来打印日志,选择super.toString
47、表实体是在domain文件中,BO、VO都可以复制domain中的实体
48、查询排序
service/CategoryService
public List<CategoryQueryResp> all() {
CategoryExample categoryExample = new CategoryExample();
// 正序排列
categoryExample.setOrderByClause("sort asc");
List<Category> categoryList = categoryMapper.selectByExample(categoryExample);
// 列表复制
List<CategoryQueryResp> list = CopyUtil.copyList(categoryList, CategoryQueryResp.class);
return list;
}
保存和更新小字段和大字段,如果更新条数为0则插入数据
service/DocService
// 更新
docMapper.updateByPrimaryKey(doc);
int count = contentMapper.updateByPrimaryKeyWithBLOBs(content);
if (count == 0) {
contentMapper.insert(content);
}
49、查找用户名是否重复
service/UserService
User userDB = selectByLoginName(req.getLoginName());
if (ObjectUtils.isEmpty(userDB)) {
// 新增
user.setId(snowFlake.nextId());
userMapper.insert(user);
} else {
// 用户名已存在
throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
}
50、自定义异常
exception/BusinessException
public class BusinessException extends RuntimeException{
private BusinessExceptionCode code;
public BusinessException (BusinessExceptionCode code) {
super(code.getDesc());
this.code = code;
}
public BusinessExceptionCode getCode() {
return code;
}
public void setCode(BusinessExceptionCode code) {
this.code = code;
}
/**
* 不写入堆栈信息,提高性能
*/
@Override
public Throwable fillInStackTrace() {
return this;
}
}
exception/BusinessExceptionCode
public enum BusinessExceptionCode {
USER_LOGIN_NAME_EXIST("登录名已存在"),
LOGIN_USER_ERROR("用户名不存在或密码错误"),
VOTE_REPEAT("您已点赞过"),
;
private String desc;
BusinessExceptionCode(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
使用
service/UserService
// 用户名已存在
throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
因为在controller/ControllerExceptionHandler文件中由@ControllerAdvice,SpringBoot框架自己去扫描@ControllerAdvice注解,由框架来使用,最终Spring会统一处理
51、跟新用户信息:
service/UserService
user.setLoginName(null);
user.setPassword(null);
userMapper.updateByPrimaryKeySelective(user);
每次更新用户信息用户名和密码不需要修改,所以用户名和密码设置为null
updateByPrimaryKeySelective表示如果user里面的属性有值,我才去更新,没有值我就不更新这个字段
52、密码加密处理
service/UserService
req.setPassword(DigestUtils.md5DigestAsHex(req.getPassword().getBytes()));
53、集成redis
pom.xml
<!--整合redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis配置地址
application.properties
# redis配置
spring.redis.host=r-uf6ljbcdaxobsifyctpd.redis.rds.aliyuncs.com
spring.redis.port=6379
spring.redis.password=Redis000
使用
controller/UserController
@Resource
private RedisTemplate redisTemplate;
// redis信息存入
Long tokenKey = snowFlake.nextId();
redisTemplate.opsForValue().set(tokenKey.toString(), JSONObject.toJSONString(userLoginResp), 3600 * 24, TimeUnit.SECONDS);
// redis信息删除
redisTemplate.delete(token);
54、接口增加登录校验
interceptor/LoginInterceptor
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LoginInterceptor.class);
@Resource
private RedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LoginInterceptor 开始 -------------");
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
// OPTIONS请求不做校验,
// 前后端分离的架构, 前端会发一个OPTIONS请求先做预检, 对预检请求不做校验
if(request.getMethod().toUpperCase().equals("OPTIONS")){
return true;
}
String path = request.getRequestURL().toString();
LOG.info("接口登录拦截:,path:{}", path);
//获取header的token参数
String token = request.getHeader("token");
LOG.info("登录校验开始,token:{}", token);
if (token == null || token.isEmpty()) {
LOG.info( "token为空,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
Object object = redisTemplate.opsForValue().get(token);
if (object == null) {
LOG.warn( "token无效,请求被拦截" );
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
LOG.info("已登录:{}", object);
LoginUserContext.setUser(JSON.parseObject((String) object, UserLoginResp.class));
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LoginInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// LOG.info("LogInterceptor 结束");
}
}
使用
config/SpringMvcConfig
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
LoginInterceptor loginInterceptor;
@Resource
ActionInterceptor actionInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/test/**",
"/redis/**",
"/user/login",
"/category/all",
"/ebook/list",
"/doc/all/**",
"/doc/vote/**",
"/doc/find-content/**",
"/ebook-snapshot/**",
"/ebook/upload/avatar",
"/file/**"
);
registry.addInterceptor(actionInterceptor)
.addPathPatterns(
"/*/save",
"/*/delete/**",
"/*/reset-password");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/file/**").addResourceLocations("file:D:/file/wiki/");
}
}
55、线程本地变量使用【类似前端localStorage】
util/RequestContext
public class RequestContext implements Serializable {
private static ThreadLocal<String> remoteAddr = new ThreadLocal<>();
public static String getRemoteAddr() {
return remoteAddr.get();
}
public static void setRemoteAddr(String remoteAddr) {
RequestContext.remoteAddr.set(remoteAddr);
}
}
线程本地变量remoteAddr、可以创建多个线程本地变量,线程之间不会互相干扰
使用
service/DocService
// 远程IP+doc.id作为key,24小时内不能重复
String ip = RequestContext.getRemoteAddr();
56、redis存取封装
util/RedisUtil
@Component
public class RedisUtil {
private static final Logger LOG = LoggerFactory.getLogger(RedisUtil.class);
@Resource
private RedisTemplate redisTemplate;
/**
* true:不存在,放一个KEY
* false:已存在
* @param key
* @param second
* @return
*/
public boolean validateRepeat(String key, long second) {
if (redisTemplate.hasKey(key)) {
LOG.info("key已存在:{}", key);
return false;
} else {
LOG.info("key不存在,放入:{},过期 {} 秒", key, second);
redisTemplate.opsForValue().set(key, key, second, TimeUnit.SECONDS);
return true;
}
}
}
validateRepeat检验redies,没有值添加,有值更新
使用
service/DocService
redisUtil.validateRepeat("DOC_VOTE_" + id + "_" + ip, 3600 * 24)
57、SpringBoot定时任务示例
启用定时任务
config/WikiApplication
@EnableAsync
public class WikiApplication {
}
job/TestJob
@Component
public class TestJob {
private static final Logger LOG = LoggerFactory.getLogger(TestJob.class);
/**
* 固定时间间隔,fixedRate单位毫秒
*/
@Scheduled(fixedRate = 1000)
public void simple() throws InterruptedException {
SimpleDateFormat formatter = new SimpleDateFormat("mm:ss");
String dateString = formatter.format(new Date());
Thread.sleep(2000);
LOG.info("每隔5秒钟执行一次: {}", dateString);
}
/**
* 自定义cron表达式跑批
* 只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过
*/
@Scheduled(cron = "*/1 * * * * ?")
public void cron() throws InterruptedException {
SimpleDateFormat formatter = new SimpleDateFormat("mm:ss SSS");
String dateString = formatter.format(new Date());
Thread.sleep(1500);
LOG.info("每隔1秒钟执行一次: {}", dateString);
}
}
所有定时器都是同一个线程
只有等上一次执行完成,下一次才会在下一个时间点执行,错过就错过
58、日志流水号的使用
日志位置
logback-spring.xml
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
LOG_ID就是日志流水号
在线程入口处设置日志流水号LOG_ID
aspect/LogAspect.java
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 增加日志流水号
MDC.put("LOG_ID", String.valueOf(snowFlake.nextId()));
}
59、webSocket使用示例
依赖引入
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置
config/WebSocketConfig
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
开启websocket服务
websocket/WebSocketServer【类似controller接收客户端请求】
@Component
@ServerEndpoint("/ws/{token}")
public class WebSocketServer {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 每个客户端一个token
*/
private String token = "";
private static HashMap<String, Session> map = new HashMap<>();
/**
* 连接成功
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
map.put(token, session);
this.token = token;
LOG.info("有新连接:token:{},session id:{},当前连接数:{}", token, session.getId(), map.size());
}
/**
* 连接关闭
*/
@OnClose
public void onClose(Session session) {
map.remove(this.token);
LOG.info("连接关闭,token:{},session id:{}!当前连接数:{}", this.token, session.getId(), map.size());
}
/**
* 收到消息
*/
@OnMessage
public void onMessage(String message, Session session) {
LOG.info("收到消息:{},内容:{}", token, message);
}
/**
* 连接错误
*/
@OnError
public void onError(Session session, Throwable error) {
LOG.error("发生错误", error);
}
/**
* 群发消息
*/
public void sendInfo(String message) {
for (String token : map.keySet()) {
Session session = map.get(token);
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
LOG.error("推送消息失败:{},内容:{}", token, message);
}
LOG.info("推送消息:{},内容:{}", token, message);
}
}
}
推送服务
service/DocService
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
String logId = MDC.get("LOG_ID");
wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
60、使用异步化解耦点赞通知功能
config/WikiApplication开启异步化支持,另起一个线程执行后续内容
@EnableAsync
public class WikiApplication {
}
使用
service/DocService
/**
* 点赞
*/
public void vote(Long id) {
// 推送消息
Doc docDb = docMapper.selectByPrimaryKey(id);
String logId = MDC.get("LOG_ID");
wsService.sendInfo("【" + docDb.getName() + "】被点赞!", logId);
// rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");
}
service/WsService
@Async
public void sendInfo(String message, String logId) {
MDC.put("LOG_ID", logId);
webSocketServer.sendInfo(message);
}
注意@Async必须写到方法上【最近原则,类似js中async】
单一职责,这个方法只负责发送数据
service类上必须加上@service
61、事务处理
应用场景:
同时对两张表有增删改的操作,就要考虑加事务,否则会造成数据不准确。当然也有不加事务的场景,不能一概而论
使用事务,直接添加@Transactional,spring就会自动处理事务
【先处理sql,再处理redis】
@Transactional
public void save(DocSaveReq req) {
...
docMapper.insert(doc);
contentMapper.insert(content);
}
注意:同一个类里面A去调用B,B加事务注解不生效
62、使用rocketmq解耦【替代多线程逻辑】
(1)需要启动一个ma服务器
(2)我们SpringBoot就是MQ的客户端
(3)客户端(发送方)可以王topic主题里面发送一个消息,服务端(接收方/消费方)可以监听这个主题
下载依赖
pom.xml
<!-- RocketMQ-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
rocketmq配置地址
application.properties
# rocketmq配置
rocketmq.name-server=127.0.0.1:9876
rocketmq.producer.group=default
组默认写default,需要后续监听
【发送MQ】
service/DocService
rocketMQTemplate.convertAndSend("VOTE_TOPIC", "【" + docDb.getName() + "】被点赞!");
逻辑处理
【消费MQ】
rocketmq/VoteTopicConsumer
@Service
@RocketMQMessageListener(consumerGroup = "default", topic = "VOTE_TOPIC")
public class VoteTopicConsumer implements RocketMQListener<MessageExt> {
private static final Logger LOG = LoggerFactory.getLogger(VoteTopicConsumer.class);
@Resource
public WebSocketServer webSocketServer;
@Override
public void onMessage(MessageExt messageExt) {
byte[] body = messageExt.getBody();
LOG.info("ROCKETMQ收到消息:{}", new String(body));
webSocketServer.sendInfo(new String(body));
}
}
注意:@RocketMQMessageListener(consumerGroup = "default", topic = "VOTE_TOPIC")
consumerGroup与配置中consumerGroup对应,topic与mq发送的topic对应
62、复杂报表统计方案:
中间表统计:定时将业务表数据汇总到中间表,报表数据从中间表获取
63、打包分配二级域名位置
application.yml
server:
port: 8080
servlet:
context-path: /dist
jean上健康告警配置
:8080/dist/monitor.html
修改后重新刷新maven
63、@Override用法
加了此注解的方法,表示此方法是一个覆写的方法,如果不满足覆写会报错。
在日常的编程中,一般出现在ServiceImpl实现类的方法上。java在进行接口实现的时候,要实现接口中定义的所有方法,也就是通过实现类重写方法。加上此注解,会帮助我们检测方法重写的正确性,例如:方法名是否与接口中一致(也就是是否可以在接口中找到次方法定义),方法的引用参数是否正确等等。。。
64、业务错误捕获
service
try {
throw new BizException(ResponseEnum.NO_SUCCESS);
}
catch (BizException bx) {
throw bx;
}
catch (Exception ex) {
throw ex;
}
controller
try {
// service方法调用
}catch (BizException bx){
LogUtils.warn(String.format("业务异常【%s】【%s】");
response.setCode(bx.getCode());
response.setMessage(bx.getMessage());
}catch (Exception ex){
LogUtils.error(String.format("系统异常【%s】【%s】");
response.setCode(ResponseEnum.SYSTEM_ERROR.code);
response.setMessage(ResponseEnum.SYSTEM_ERROR.message);
}
65、compareTo比较
payInfo.getPayAmount().compareTo(
BigDecimal.valueOf(payResponse.getOrderAmount()))
== 0
66、直接取配置中的内容
@ConfigurationProperties(prefix = "alipay")
67、md5加密
//MD5摘要算法(Spring自带) + 指定编码格式
user.setPassword(DigestUtils.md5DigestAsHex(
user.getPassword().getBytes(StandardCharsets.UTF_8)
));
68、判断数据库是否写入成功
//写入数据库
int resultCount = userMapper.insertSelective(user);
if (resultCount == 0) {
return ResponseVo.error(ERROR);
}
69、单元测试完成回滚,不对数据库进行污染
@Transactional
public class UserServiceImplTest extends MallApplicationTests {
}
70、公共返回json
@Data
public class ResponseVo<T> {
private Integer status;
private String msg;
private T data;
private ResponseVo(Integer status, String msg) {
this.status = status;
this.msg = msg;
}
private ResponseVo(Integer status, T data) {
this.status = status;
this.data = data;
}
public static <T> ResponseVo<T> successByMsg(String msg) {
return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), msg);
}
public static <T> ResponseVo<T> success(T data) {
return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), data);
}
public static <T> ResponseVo<T> success() {
return new ResponseVo<>(ResponseEnum.SUCCESS.getCode(), ResponseEnum.SUCCESS.getDesc());
}
public static <T> ResponseVo<T> error(ResponseEnum responseEnum) {
return new ResponseVo<>(responseEnum.getCode(), responseEnum.getDesc());
}
public static <T> ResponseVo<T> error(ResponseEnum responseEnum, String msg) {
return new ResponseVo<>(responseEnum.getCode(), msg);
}
public static <T> ResponseVo<T> error(ResponseEnum responseEnum, BindingResult bindingResult) {
return new ResponseVo<>(responseEnum.getCode(),
Objects.requireNonNull(bindingResult.getFieldError()).getField() + " " + bindingResult.getFieldError().getDefaultMessage());
}
}
71、公共返回错误码
@Getter
public enum ResponseEnum {
ERROR(-1, "服务端错误"),
SUCCESS(0, "成功"),
PASSWORD_ERROR(1,"密码错误"),
USERNAME_EXIST(2, "用户名已存在"),
PARAM_ERROR(3, "参数错误"),
EMAIL_EXIST(4, "邮箱已存在"),
NEED_LOGIN(10, "用户未登录, 请先登录"),
USERNAME_OR_PASSWORD_ERROR(11, "用户名或密码错误"),
PRODUCT_OFF_SALE_OR_DELETE(12, "商品下架或删除"),
PRODUCT_NOT_EXIST(13, "商品不存在"),
PROODUCT_STOCK_ERROR(14, "库存不正确"),
CART_PRODUCT_NOT_EXIST(15, "购物车里无此商品"),
DELETE_SHIPPING_FAIL(16, "删除收货地址失败"),
SHIPPING_NOT_EXIST(17, "收货地址不存在"),
CART_SELECTED_IS_EMPTY(18, "请选择商品后下单"),
ORDER_NOT_EXIST(19, "订单不存在"),
ORDER_STATUS_ERROR(20, "订单状态有误"),
;
Integer code;
String desc;
ResponseEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
}
72、表单验证
form/UserLoginForm.java
@Data
public class UserLoginForm {
@NotBlank
private String username;
@NotBlank
private String password;
}
73、公共异常捕获
exception/RuntimeExceptionHandler.java
@ControllerAdvice
public class RuntimeExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseBody
// @ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseVo handle(RuntimeException e) {
return ResponseVo.error(ERROR, e.getMessage());
}
@ExceptionHandler(UserLoginException.class)
@ResponseBody
public ResponseVo userLoginHandle() {
return ResponseVo.error(ResponseEnum.NEED_LOGIN);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ResponseVo notValidExceptionHandle(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
Objects.requireNonNull(bindingResult.getFieldError());
return ResponseVo.error(ResponseEnum.PARAM_ERROR,
bindingResult.getFieldError().getField() + " " + bindingResult.getFieldError().getDefaultMessage());
}
}
首先,ControllerAdvice
本质上是一个Component
,因此也会被当成组建扫描,一视同仁,扫扫扫。
@ControllerAdvice
配合@ExceptionHandler
实现全局异常处理
74、 忽略大小写
user.getPassword().equalsIgnoreCase
75、session设置过期时间
server:
servlet:
session:
timeout: 120
76、设置拦截器
UserLoginInterceptor.java
@Slf4j
public class UserLoginInterceptor implements HandlerInterceptor {
/**
* true 表示继续流程,false表示中断
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle...");
User user = (User) request.getSession().getAttribute(MallConst.CURRENT_USER);
if (user == null) {
log.info("user=null");
throw new UserLoginException();
// response.getWriter().print("error");
// return false;
// return ResponseVo.error(ResponseEnum.NEED_LOGIN);
}
return true;
}
}
InterceptorConfig.java
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserLoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/error", "/user/login", "/user/register", "/categories", "/products", "/products/*");
}
}
77、@PathVariable注解使用
@GetMapping("/products/{productId}")
public ResponseVo<ProductDetailVo> detail(@PathVariable Integer productId) {
return productService.detail(productId);
}
78、金额处理
BigDecimal cartTotalPrice = BigDecimal.ZERO;
cartTotalPrice = cartTotalPrice.add(cartProductVo.getProductTotalPrice());
79、测试用例(执行先后顺序)
@Before
@Test
@After
80、sql执行判断成功通过row
@Override
public ResponseVo delete(Integer uid, Integer shippingId) {
int row = shippingMapper.deleteByIdAndUid(uid, shippingId);
if (row == null || theCount <= 0) {
return ResponseVo.error(ResponseEnum.DELETE_SHIPPING_FAIL);
}
return ResponseVo.success();
}
81、数据处理方法可以的单独抽离
private OrderVo buildOrderVo(Order order, List<OrderItem> orderItemList, Shipping shipping) {
OrderVo orderVo = new OrderVo();
BeanUtils.copyProperties(order, orderVo);
List<OrderItemVo> OrderItemVoList = orderItemList.stream().map(e -> {
OrderItemVo orderItemVo = new OrderItemVo();
BeanUtils.copyProperties(e, orderItemVo);
return orderItemVo;
}).collect(Collectors.toList());
orderVo.setOrderItemVoList(OrderItemVoList);
if (shipping != null) {
orderVo.setShippingId(shipping.getId());
orderVo.setShippingVo(shipping);
}
return orderVo;
}
82、list转map
Map<Long, List<OrderItem>> orderItemMap = orderItemList.stream()
.collect(Collectors.groupingBy(OrderItem::getOrderNo));
以直接引用已有Java类或对象的方法或构造器。方法引用
83、启动类扫包路径
@SpringBootApplication
@EnableJpaAuditing
@ComponentScan(basePackages = {"com.geekbang"})
@EnableTransactionManagement
//用于扫描Dao @Repository
@EnableJpaRepositories(basePackages = {"com.geekbang"})
//用于扫描JPA实体类 @Entity,默认扫本包当下路径
@EntityScan(basePackages = {"com.geekbang"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@SpringBootApplication
@EnableJpaAuditing
@ComponentScan(basePackages = {"com.geekbang"})
@EnableTransactionManagement
//用于扫描Dao @Repository
@EnableJpaRepositories(basePackages = {"com.geekbang"})
//用于扫描JPA实体类 @Entity,默认扫本包当下路径
@EntityScan(basePackages = {"com.geekbang"}
84、返回空数组
Collections.emptyList()
85、跨域设置
controller
@CrossOrigin(origins = "*")
86、识别子类型
@Autowired
@Qualifier("taskExecutor")
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
87、接口参数获取为null
检查每个参数的类型是否争取
88、复制时间处理
result.settime(DateUtil.date2String(time, DateUtil.DATE_PATTERN_YYYY_MM_DD_HH_MM_SS));
89、properties-转yaml