44444
使用Mybatis实现数据库编程---在一个项目中,除了关联表,绝大部分的数据表都至少需要实现以下功能
插入1条数据
批量插入数据
根据id删除某一条数据
根据若干个id批量删除某些数据
根据id修改数据
统计当前表中数据的数量
根据id查询数据详情
查询当前表中的数据列表
一、在配置类中使用@MapperScan指定接口文件所在的根包,在项目的根包下创建config.MybatisConfiguration配置类(添加了
@Configuration注解的类),在此类上配置@MapperScan注解
二、在配置文件中通过mybatis.mapper-locations属性来指定XML文件所在的位置,在application.properties中添加配置
mybatis.mapper-locations=classpath:mapper/*.xml,然后在src/main/resources下创建mapper文件夹
xml version="1.0" encoding="UTF-8"?>
-//mybatis.org//DTD Mapper 3.0//EN" "" target="_blank">http://mybatis.org/dtd/mybatis-3-mapper.dtd"" target="_blank">>
三、创建实体类,在项目根包下创建实体类pojo.entity.Album,在类中声明与数据表对应的各属性。在编写pojo类之前,先在项目
中添加依赖
在任何pojo类上,可以添加@Data注解,则Lombok框架会自动在编译期生成getter、setter、hashcode、equals方法
另外任何pojo类,都应该实现Serializable接口。
四、创建接口,并声明抽象方法。在项目的根包下创建mapper.AlbumMapper接口,并在接口中添加"插入1条数据"的抽象方法
public interface AlbumMapper{
int insert(Album album);
}
五、添加XML文件,并在此文件中配置抽象方法映射的SQL语句。在src/main/resources/mapper下,
六、创建测试类,编写并执行测试方法,在src/test/java下的根包下,创建mapper.AlbumMapperTests类,在类上添加
@SpringBootTest注解,并在类中编写测试方法
MySQL数据类型与Java类中的数据类型的对应关系-----
tinyint/smallint/intInteger
bigintLong
char/varchar/text系列String
date_timeLocalDateTime
关于抽象方法的声明--
返回值类型:如果执行的SQL语句是增、删、改类型的,返回值类型始终使用int,表示"受影响的行数",不建议使用void
方法的名称:参考阿里编码规范,且不推荐重载
参数的列表:根据需要执行的SQL语句中的参数来设计,如果参数较多且具有相关性,则推荐封装
密码加密----用户注册时(管理员添加新的账号信息时)的密码必须经过某种算法进行编码,并将得到的结果存储到数据库,而不允许
将原始密码直接存储到数据库中。原始密码通常可以称之为密码的原文,或称之为明文密码,经过编码处理的结果可
称之为密文。
如果直接存储明文密码,容易导致用户账号被窃取的安全问题,基于当下主流的网络技术,这类安全问题通常是企业
内部管理问题导致的!简单来说,对密码进行加密处理,主要防的就是内部员工(例如能够直接接触到数据库服务器的
员工,无论是进入机房或是远程登录)
对明文密码进行编码处理时,不可以使用任何加密算法!因为所有加密算法都是可以逆向运算的,即根据算法类型、加密
参数、密文,可以通过运算得到原文。通常,加密算法只是用于保障数据在传输过程中的安全性!
对于需要存储下来的密码,且只需要验证原密码是否匹配,不需要(不允许)通过逆向运算根据密文得到原文时,应该使
用消息摘要算法对密码原文进行编码处理
消息摘要算法的典型特征:
消息相同时,摘要必然相同
使用固定的某种消息摘要算法,无论消息的长度多少,摘要的长度是固定的
消息不同时,摘要极大概率不会相同,理论上必然存在N个不同的消息,通过编码得到的摘要是完全相同的
由于摘要的种类也非常多,设计得非常好的算法会使得这种概率非常低
所有消息摘要算法都是不可以逆向运算的
消息摘要算法的结果通常会使用十六进制数来表示,通常,运算结果相对简单的消息摘要算法的结果长度也有32位
十六进制数组成,还原成二进制数就是128个二进制数组成,这样的算法称之"128位算法",同理,如果某个
结果是由64个十六进制数组成,还原成二进制数就是256个二进制数,则称之为"256位算法"
在Spring Boot项目中,已有DigestUtils工具类中的md5DigestAsHex()方法提供了MD5算法进行编码
Lombok框架--@Data:添加在类上,可以在编译期生成getter、setter、hashcode、toString、equals方法等,使用此注解,必须保证
当前类的父类存在无参构造方法
@Setter:可以加在属性上,也可以添加在类上
@Getter:同@Setter
@EqualsAndHashCode:添加在类上,生成规范的equals和hashcode方法
@ToString:添加在类上
@Slf4j:添加在类上添加,日志注解,可以添加在测试类上
Slf4j日志---在Spring Boot项目中,基础依赖项(spring-boot-starter)中已经包含了日志的相关依赖项
在添加了Lombok依赖项后,可以在类上添加@Slf4j注解,则Lombok框架会在编译期生成名为log的变量,可调用此变量
的方法来输出日志
显示级别:根据日志内容的重要程度,从不重要到重要依次为————
trace:跟踪信息,可能包含不一定关注,但是包含了程序执行流程的信息
debug:调试信息,可能包含一些敏感内容,比如关键的数据的值
info :一般信息
warn :警告信息
error:错误信息
在Spring Boot项目中,默认的日志显示级别为【info】,将只会显示application.properties中配置
logging.level.根包=日志显示级别 来设置当前显示级别
在开发实践中,应该使用trace或debug级别的日志来输出与流程相关的、涉及敏感数据的日志,使用info输出一般的、
被显示在控制台也无所谓的信息,使用warn和error输出更加重要的需要关注的日志
输出日志时,通常建议使用void trace(String message,Object... args)方法(也有其他级别日志的同样参数列表的方法)
在第1个参数String message中可以使用{}作为占位符,表示某变量的值。后面可以跟N个值,类似于Vue的插值
Profile配置-同一个项目,在不同的环境中,需要的配置值可能是不同的,例如日志的显示级别、连接数据库的配置参数等,由Spring
框架提供,在Spring Boot中更是简化了此项操作,它允许使用application-自定义名称.properties作为Profile配置文件
的文件名
这类配置文件默认不会加载,在applicaton.properties中通过spring.profiles.active=自定义名称 来激活配置文件
当主从配置文件配置冲突时,以从文件配置为准(范围越小越优先)
YAML配置----是一种编写配置文件的语法,表现为使用.yml作为扩展名的配置文件,Spring框架默认不支持此类配置文件,而Spring
Boot的基础依赖项中已经包含解析此类文件的依赖项,所以在Spring Boot项目可以直接使用此类配置文件
在Spring Boot项目中,使用.properties和.yml配置是等效的
插入数据时获取自动编号的id--
如果表中的id是自动编号的,在
的id值,并由Mybatis自动赋值到参数对象的对应的属性id上
批量插入多条数据----代码如下
INSERT INTO pms_album(
name,description,sort
)VALUES
(#{album.name},#{album.description},#{album.sort})
根据id更新一条数据--代码如下,Mybatis的
UPDATE pms_album
WHERE id=#{album.id}
查询----每个
查询的一般写法--建议使用
SELECT
FROM pms_category
WHERE id=#{id}
id,name,parent_id,depth,keywords,sort,icon,enable,is_parent,is_display
关于Service-----在项目中,应该使用Service组件来处理业务相关的代码,以此来设计业务流程、业务逻辑,以保证数据的完整性、
有效性。
通常,Service应该由接口和实现类来组成,实现类上要加@Service注解
Service抽象方法的声明原则————
返回值类型:仅以操作成功为前提来设计,操作失败用通过抛出异常来表示,异常应继承自RuntimeException
方法名称 :自定义的、规范的,无其它约束
参数列表 :根据客户端提交的请求参数来设计,如果参数较多,且具有相关性,则应该封装
添加相册的业务--先制定业务规则:相册名称必须是唯一的
处理添加相册请求----开发控制器相关代码时,需要项目中添加spring-boot-starter-web依赖项,此依赖项包含了
spring-boot-starter依赖项
关于处理异常----在服务器端项目中,如果某个抛出的异常始终没有被处理,则默认会向客户端响应500状态代码
所以在服务器端项目中,必须对异常进行处理,因为如果不处理,软件的使用者可能不清楚出现异常的原因,也不
知道如何调整请求参数来解决此问题,甚至可能反复尝试提交错误的请求(刷新页面),对于服务器端而言,也是浪费
了一些性能
Spring MVC框架统一处理异常的机制----
每种类型的异常只需要编写1段相关的处理代码即可,当使用这种机制时,在各处理请求的方法中,将不再使用try-catch语句
块来包裹可能抛出异常的方法并处理,则控制器类中处理请求的方法都将是抛出异常的(虽然不必在代码中显示的throws),
这些异常会被Spring MVC框架捕获并尝试调用统一处理异常的方法如下:
注解 :必须添加@ExceptionHandler注解
访问权限 :public
返回值类型:参考处理请求的方法
方法名称 :自定义
参数列表 :必须有1个异常类型的参数,表示Spring MVC框架调用处理请求的方法时捕获的异常,并且,可以按需添加
HttpServletRequest、HttpServletResponse等少量特定类型的参数,不可以随意添加其他参数
一般会将统一处理异常的代码放在专门的类中,并在此类上添加@RestControllerAdvice注解,此类中特定的方法将作用于整个
项目任何处理请求的过程
限制请求方式----以获取数据为主要目的的用get,其他用post
在RequestMapping(value="/reg",method=RequestMethod.POST)设置该方法只能允许post请求
Spring MVC框架已经定义了限制请求方式的注解,只能添加在方法上:
@GetMapping :把请求方式限制为get
@PostMapping:把请求方式限制为post
@PutMapping:把请求方式限制为put
@DeleteMapping:把请求方式限制为delete
@PatchMapping :把请求方式限制为patch
关于RESTful-----是一种软件的设计风格(不是规定,也不是规范)
典型表现:一定是响应正文的,服务器端处理完请求后将响应数据,不会由服务器响应页面到客户端
通常会将具有唯一性的请求参数设计到url中,成为url的一部分
严格区分4种请求方式,在许多业务系统其实并不这样设计,增加数据使用post、删除数据
使用delete、修改数据使用put、查询数据使用get
Spring MVC框架很好的支持了RESTful风格的设计,当需要在url中使用变量值时,可以使用{自定义名称}
作为占位符,同时如果想拿到传过来的参数,可以在参数列表添加@PathVariable注解,并声明对应
的变量来接收,通常会将占位符中的自定义名称和方法的参数名称保持一致,如果因为某些原因
无法保持一致,则需要配置@PathVariable("参数"),此注解参数与占位符一致即可
在开发实践中,可以将处理请求的方法的参数类型设计为期望的类型,例如将id设计为Long类型的
但是,如果这样设计,必须保证请求中的参数值是可以被正确转换为Long类型的,否则会出现400错误
为了尽量保证匹配的准确性、保证参数值可以正常转换,在设计占位符时,可以在占位符名称右侧添加冒号
并在冒号右侧使用正则表达式来限制占位符的值的格式
// http://localhost:8080/album/9527/delete
@RequestMapping("/{id:[0-9]+}/delete")
public String delete(@PathVariable Long id){
String message = "尝试删除id值为"+id+"的相册";
log.debug(message);
return message;
}
一旦使用正则表达式后,多个不冲突的占位符的设计是允许共存在的,例如:
// http://localhost:8080/album/hello/delete
@RequestMapping("/{id:[a-z]+}/delete")
public String delete(@PathVariable String name){
String message = "尝试删除id值为"+id+"的相册";
log.debug(message);
return message;
}
另外,没有使用占位符的设计,与使用了占位符的设计,也是允许共存的,例如:
// http://localhost:8080/album/test/delete//精准匹配优先级是最高的
@RequestMapping("/test/delete")
public String delete(@PathVariable Long id){
String message = "尝试删除id值为"+id+"的相册";
log.debug(message);
return message;
}
最后,关于RESTful风格的url设计,如果没有明确的要求,或没有更好的选择,可以设计为:
获取数据列表 :/albums
根据id获取数据 :/albums/1
根据id对数据执行某种操作:/albums/1/delete
关于Knife4j框架-----是一款基于Swagger 2的在线API文档框架,使用Knife4j框架需要:
添加依赖,注意:本次使用的Knife4j必须基于Spring Boot的版本在2.6之前(2.6及更高的版本不可用)
需要在主配置文件中添加配置
knife4j.enable=true
添加配置类
package cn.tedu.jsd2207csmall.product.config;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
/**
* Knife4j配置类
*
* @author [email protected]
* @version 0.0.1
*/
@Slf4j
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
/**
* 【重要】指定Controller包路径
*/
private String basePackage = "cn.tedu.jsd2207csmall.product.controller";
/**
* 分组名称
*/
private String groupName = "product";
/**
* 主机名
*/
private String host = "http://java.tedu.cn";
/**
* 标题
*/
private String title = "酷鲨商城在线API文档--商品管理";
/**
* 简介
*/
private String description = "酷鲨商城在线API文档--商品管理";
/**
* 服务条款URL
*/
private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0";
/**
* 联系人
*/
private String contactName = "Java教学研发部";
/**
* 联系网址
*/
private String contactUrl = "http://java.tedu.cn";
/**
* 联系邮箱
*/
private String contactEmail = "[email protected]";
/**
* 版本号
*/
private String version = "1.0.0";
@Autowired
private OpenApiExtensionResolver openApiExtensionResolver;
public Knife4jConfiguration() {
log.debug("创建配置类对象:Knife4jConfiguration");
}
@Bean
public Docket docket() {
String groupName = "1.0.0";
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.host(host)
.apiInfo(apiInfo())
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions(groupName));
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.termsOfServiceUrl(termsOfServiceUrl)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(version)
.build();
}
}
完成后,重新启动项目,可以通过 http://localhost:8080/doc.html 查看在线API文档并可使用其中的调用功能等
在使用Knife4j时,应该使用相关注解,将API文档配置得更加易于阅读和使用:
@Api:添加在控制器类上,通过此注解的tags属性,可以指定模块名称,并且可以在模块名称前自行
添加数字序号,以实现排序效果,框架会根据各控制器类的@Api注解的tags属性值进行升序
@ApiOperation:添加在处理请求的方法上,通过此注解的value属性,可以指定业务名称
@ApiOperationSupport:添加在处理请求的方法上,通过此注解的order属性(数值型),可以指定
排序序号,框架会根据此属性值升序排列
通常指定order属性为三位数,且1开头代表insert操作
2开头代表delete操作
3开头代表update操作
4开头代表select操作
9开头代表废弃的操作
@ApiModelProperty:添加在DTO、VO类的属性上,配置属性的名称和是否可以为空,也可以加在响应结果上
@ApiModelProperty(value="相册名称",required=true)
@ApiImplicitParam:添加在处理请求的方法上,配置不是pojo类型的参数
@ApiImplicitParam(name="id",value="相册id",required=true,dataType="long")
@ApiImplicitParams:添加在处理请求的方法上,配置多个不是pojo类型的参数
@ApiImplicitParams({数组里面放多个@ApiImplicitParam注解})
安装Node.js-----搭建前端项目的仓库管理器,下载Node.js后并安装
npm -v查看是否安装成功
npm config set registry https://registry.npmmirror.com配置npm源
npm config get registry查看npm源
安装VUE Cli-----本项目的前端项目将使用"Vue脚手架"项目来开发,必须先在本机安装Node.js,然后在cmd中输入
npm install -g @vue/cli安装vue脚手架
vue -V检查vue脚手架是否安装完成
如果卡住了,Ctrl+C强行终止,并再次尝试安装
创建Vue脚手架项目---在创建项目之前,先确定当前操作位置,例如:C:\Users\tarena,此操作位置决定项目创建位置
通过vue create +项目名称即可创建项目,例如:vue create jsd-csmall-web-client,按回车等待
接下来选择项目的参数① Manually select features
② Babel、Router、Vuex
③ 2.x
④ In package.json
然后把创建好的项目剪切到常用的文件夹,然后在idea中打开,在终端面板中npm run serve启动项目,然后
根据提示的url在浏览器中打开可以看到默认的页面,前端项目没有终止按钮,只能在终端面板Ctrl+C
配置前后端端口--避免每次启动项目端口不一样发生冲突,在后端的从配置文件中配置后端端口server.port=9080
在前端项目的package.json中在serve值的后面手动指定端口为 -- 9000
关于视图组件----以.vue为拓展名的文件称之为"视图组件",将负责页面的显示,包括编写样式、JavaScript代码,一定程度上,与
传统项目中的.html文件的作用是相似的,主要由3部分组成:
:用于设计页面元素,即页面需要显示什么,此标签有且只有一个直接子标签
:用于设计样式规则,即CSS
:用于编写JavaScript程序
默认的页面效果--与运行效果相关的常用文件有:
src/App.vue:是项目中唯一的.html文件默认绑定的视图组件,可理解为项目的入口视图组件,并且,此页面
中设计的内容是始终显示的,在此视图中,有
视图组件来显示"
src/router/index.js:是默认的路由配置文件,此文件中的routes常量配置了路径与视图组件的对应关系,决定了
关于routes的配置:此属性是数组类型的,其中的各元素通常称之为一个个的"路由对象",每个路由对象中主要
配置path和component属性,即配置路径与视图组件的对应关系,关于component,如果某个
视图是类似"主页"定位,推荐通过import语句导入,其它视图推荐使用import()函数导入
Vue脚手架项目的结构-----[.idea] :是通过idea编辑项目时,由idea创建的文件夹,不需要人为干预
[node_modules]:当前项目的依赖项文件夹,不应该人为干预,在使用Git等管理工具时,通常并不会将
此文件夹提交到Git服务器,从Git服务器下载得到的项目也不会包含此文件夹及内容,
如果没有这个文件夹,则项目无法编译、运行。可以通过执行npm install命令安装
项目所需的依赖项
[public] :静态资源文件夹,此文件夹下的内容是可以直接被访问的,不会经过项目的编译过程,
通常会在此文件下存放css、js、图片文件等
favicon.ico:当前网站的图标文件,此文件是固定文件名的
index.html :当前项目中仅有的唯一.html文件
[src] :项目的源代码文件夹
[assets]:静态资源文件夹,与public文件夹不同,此处的静态资源应该是不随程序
运行而变化的
[components]:被其它视图导入、调用的视图组件的文件夹
[views] :视图组件文件夹
[router]:路由配置文件的文件夹
index.js:默认的路由配置文件
[store] :存储全局的量的文件所在的文件夹
index.js:默认的存储全局的量的文件
App.vue :直接绑定到了index.html的视图组件
main.js :项目的主配置文件
.gitignore :用于配置"提交Git时忽略哪些文件、文件夹"
babel.config.js:暂不关心
jsconfig.json :暂不关心
LICENSE :作为Git的开源项目应该包含"许可协议",此文件就是"许可协议"文件
package.jsion :当前项目的配置文件,类似Maven项目中的pom.xml文件,不建议手动修改
package-lock.json:锁定的当前项目配置文件,此文件不允许手动编辑,即使编辑了,也会自动还原
安装并配置ELEMENT UI----在终端窗口中执行npm i element-ui -S
需要在main.js中添加配置:
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
嵌套路由----在设计前端的视图组件时,如果根级视图(通常是App.vue)使用了
来完成显示,而另一个视图组件中也使用了
中配置嵌套路由。可以给路由对象配置children属性,此属性配置方式与routes完全一样
安装axios---npm i axios -S
然后在main.js中添加配置:
import axios from 'axios';
Vue.prototype.axios = axios;
跨域访问----客户端与服务端不在同一台主机上,在默认情况下,不允许发送跨域访问请求
在Spring Boot项目中,需要使用配置类实现WebMvcConfigurer接口,并重写addCorsMappings()方法进行配置
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("*")
.allowCredentials(true)
.maxAge(3600);
}
@RequestBody----在Spring MVC项目中(包括添加了spring-boot-starter-web依赖项的Spring Boot项目),可以在处理请求的方法
的参数列表中,在某参数上添加@RequestBody注解。如果请求参数添加了@RequestBody注解,则客户端提供的
请求参数必须是对象格式。
如果请求参数没添加@RequestBody注解,则客户端提供的请求参数必须是FormData格式
使用qs------是一款可以将对象格式的数据转换为FormData格式的框架,npm i qs -S
然后在main.js中添加配置:
import qs from 'qs';
Vue.prototype.qs=qs;
let formData = this.qs.stringify(this.ruleForm)
响应JSON格式字符串--在Spring Boot项目中,只需要自定义类型,在此类型中设计响应结果中包含的数据属性,并在处理请求、处理
异常的方法上使用此类型作为返回值类型即可,在类上加@Data注解
检查请求参数----所有请求参数都应该对数据的基本格式进行检查,此类检查可以在客户端直接进行,但是,对于服务器端而言,客户
端的检查结果应该视为"不信任的",因为可能某些请求是绕过了客户端的检查的,客户端软件可能存在被篡改,
客户端软件可能不是最新版本,与服务器期望的并不一致,以及其它原因
所以服务器端必须在接收到请求参数的第一时间,就对请求参数的基本格式进行检查
使用Validation--来检查请求参数的基本格式,先添加依赖项
在控制器中,对于封装类型的请求参数,应该先在请求参数之前添加@Valid或@Validated注解,表示将需要对此请求
参数的格式进行检查,然后在此封装的类型中,在需要检查的属性上,添加检查注解,例如:
@NotNull(message = "必须提交相册名称")非空检测,并自定义错误提示
当请求参数可能出现多种错误时,也可以选择"快速失败"的机制,它会使得框架只要发现错误,就停止检查其他规则
这需要在配置类ValidationConfiguration中进行配置如下:
//Builder模式
//链式语法
@Bean
public Validator validator(){
return Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
当处理请求的方法的参数时未封装的(例如Long id),检查时,需要:
在当前控制器类上添加@Validated注解
在需要检查的请求参数上添加检查注解@Range(min = 1,message = "删除相册失败,id无效")
关于检查注解----@NotNull :非空检测
@Range :整数数值范围检测
@NotEmpty:不允许长度为0的字符串
@NotBlank:不允许全部为空白字符
@Pattern :此注解有regexp属性,配置正则表达式
除了@NotNull注解以外,其他注解均不检查请求参数为null的情况
VUE生命周期方法-----created(){}刚刚创建出来
mounted(){}挂载,准备就绪,推荐使用
基于Spring JDBC的事务管理---事务(Transaction),是关系型数据库中一种能够保障多个写操作(增、删、改)要么全部成功,要么
全部失败的机制
在编程式事务管理过程中,需要先开启事务(BEGIN),然后执行数据操作,当全部完成,需要提交
事务(COMMIT),如果失败,则回滚事务(ROLLBACK)
在基于Spring JDBC的项目中,只需要使用声明式事务即可,只需要在业务方法上添加
@Transactional注解,即可使得方法有事务性
Spring JDBC实现事务管理大致是:
开启事务:Begin
try{
执行事务方法:即数据访问操作
提交事务:Commit
}catch(RuntimeException e){
回滚事务:Rollback
}
所以,Spring JDBC在事务管理中,默认将基于RuntimeException进行回滚,可以通过@Transactional的
rollbackFor或rollbackForClassName属性来修改,例如:
@Transactional(rollback = {NullPointerException.class,IndexOutOfBoundsException.class})
@Transactional(rollbackForClassName = {"java.lang.NullPointerException",...})
还可以通过noRollbackFor或noRollbackForClassName属性来配置对于哪些异常不回滚
其实,@Transactional注解还可以添加在:
实现类上 :作用于当前类中所有业务方法
实现类业务方法:作用于添加了注解的业务方法
接口上 :作用于实现了此接口的类中的所有业务方法
接口业务方法 :作用于实现了此接口的类中的重写的当前业务方法
如果都添加了,且配置了相同的参数,则范围越小越优先,推荐添加在接口或接口中的业务方法上
添加在测试方法上可以自动回滚
关于获取受影响行数--对于写操作,一般都要在业务方法获取受影响的行数,并判断是否与预期一致,如果不一致就抛出异常
int rows = categoryMapper.insert(category);
if(rows != 1){
String message = "添加类别失败,服务器繁忙,请稍后再尝试";
log.debug(message);
throw new ServiceException(ServiceCode.ERR_INSERT,message);
}
关于Spring Security框架-----主要解决了认证与授权相关的问题,先添加Spring Boot Security依赖项
关于Spring Security配置类---在Spring Boot项目中,创建config.SecurityConfiguration配置类,需要继承自
WebSecurityConfigurerAdapter,并重写configure(HttpSecurity http)方法进行配置
protected void configure(HttpSecurity http) throws Exception {
// 白名单
String[] urls = {
"/doc.html",
"/**/*.js",
"/**/*.css",
"/swagger-resources",
"/v2/api-docs"
};
// 将防止伪造跨域攻击的机制给禁用
http.csrf().disable();
// super.configure(http);
http.authorizeRequests() //管理请求授权
.mvcMatchers(urls)
.permitAll() //直接许可
.anyRequest() //除了以上配置过的其它所有请求
.authenticated(); //要求是"已通过认证的"
http.formLogin(); //启用登录表单
}
关于伪造的跨域攻击--即CSRF主要是基于服务器端对浏览器的信任,在多选项卡的浏览器中,如果在X选项卡中登录,在Y选项卡中
的访问也会被视为"已登录"
在Spring Security框架中默认开启了防止伪造的跨域攻击的机制,其基本做法就是需要在POST请求中要求
客户端提交其随机生成的一个UUID值
关于登录账号----默认情况下,Spring Security框架提供了默认的用户名user和启动时随机生成的UUID密码,如果需要自定义登录
账号,可以自定义类,实现UserDetailsService接口,重写接口中的如下方法:
UserDetails loadUserByUsername(String username);
Spring Security框架在处理认证时,会自动根据提交的用户名(用户在登录表单中输入的用户名)来调用以上方法
以上方法应该返回匹配的用户详情(UserDetails类型的对象),接下来,Spring Security会自动根据用户详情
(UserDetails对象)来完成认证过程,例如判断密码是否正确
在根包下创建security.UserDetailsServiceImpl类,在类上添加@Service注解,实现接口并重写方法:
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if("root".equals(s)){
UserDetails userDetails = User.builder()
.username("root")
.password("1234")
.disabled(false)
.accountLocked(false)
.accountExpired(false)
.credentialsExpired(false)
.authorities("暂时给出的假权限标识")
.build();
log.debug("{}",userDetails);
return userDetails;
}
return null;
}
在Spring Security配置类中添加密码编码器(也可以添加BCrypt编码器,对应上面也要修改):
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
使用前后端分离的登录模式----使用Service处理登录认证,调用AuthenticationManager的authenticate()方法处理认证,可以通过重写
配置类中的authenticationManagerBean()方法,并添加@Bean注解来得到,如下:
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void login(AdminLoginDTO adminLoginDTO) {
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
Authentication authentication
= new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
authenticationManager.authenticate(authentication);
简历技术描述参考----【了解/掌握/熟练掌握】开发工具的使用,包括:Eclipse、IntelliJ IDEA、Git、Maven
【了解/掌握/熟练掌握】Java语法
【理解/深刻理解】面向对象编程思想
【了解/掌握/熟练掌握】Java SE API,包括:String、日期、IO、反射、线程、网络编程、集合、异常等
【了解/掌握/熟练掌握】HTML、CSS、JavaScript前端技术
【了解/掌握/熟练掌握】前端相关框架技术及常用工具组件,包括:jQuery、Bootstrap
Vue脚手架、Element UI、axios、qs、富文本编辑器等
【了解/掌握/熟练掌握】MySQL的应用
【了解/掌握/熟练掌握】DDL、DML的规范使用
【了解/掌握/熟练掌握】数据库编程技术,包括:JDBC、数据库连接池(commons-dbcp、commons-dbcp2、Hikari、druid)
及相关框架技术,例如:Mybatis Plus等
【了解/掌握/熟练掌握】主流框架技术的规范使用,例如:SSM(Spring,Spring MVC, Mybatis)
Spring Boot、Spring Validation、Spring Security等
【理解/深刻理解】Java开发规范(参考阿里巴巴的Java开发手册)
【了解/掌握/熟练掌握】基于RESTful的Web应用程序开发
【了解/掌握/熟练掌握】基于Spring Security与JWT实现单点登录
关于登录的判断标准--在Spring Security框架中,对于登录的判断标准是:在SecurityContext中是否存在Authentication对象,如果
存在,Spring Security框架会根据Authentication对象识别用户的身份、权限等,如果不存在,视为未登录
在默认情况下,Spring Security框架也是基于Session来处理用户信息的
关于Session-----为了能够识别客户端身份,当某客户端第1次向服务器端发起请求时,服务器端将向客户端响应一个JSESSIONID数据
(其本质是一个UUID数据),在客户端后续的访问中会自动携带此JSESSIONID,以至于服务器端能够识别客户端的身份
同时在服务器端还有一个Map结构的数据,此数据使用JSESSIONID作为key,所以每个客户端在服务器端都有一个与之
对应的在此Map中的value,也就是Session数据
缺点:不适合长时间保存数据,因为Session是内存中的数据,并且所有来访的客户端在服务器端都有对应的Session
数据,就必须存在Session清除机制,因为如果长期不清除,随着来访的客户端越来越多,将占用越来越多的
内存,通常会将Session设置为15分钟或最多30分钟清除
默认不适用于集群或分布式系统,因为Session是内存中的数据,所以默认情况下,Session只存在于与客户端
交互的那台服务器上,如果使用了集群,客户端每次请求的服务器都不是同一台服务器,则无法有效的识别
客户端的身份。【可以通过共享Session机制解决】
Token---票据、令牌
由于客户端种类越来越多,目前主流的识别用户身份的做法都是使用Token机制,Token可以理解为"票据",例如现实生活中
的火车票,某客户端第1次请求服务器,或执行登录请求,即可视为购买火车票行为,当客户端成功登录,相当于成功购买
了火车票,客户端的后续访问应该携带Token,相当于乘坐火车需要携带购票凭证,则服务器端可以识别客户端的身份,相当
于火车站及工作人员可以识别携带了购买凭证的乘车人
与Session最大的区别在于:Token是包含可识别的有效信息的,长时间保存用户信息
JWT-----JSON WEB TOKEN,是一种使用Json格式来组织数据的Token,不能放隐私数据,先添加依赖项
void generate() {
Date date = new Date(System.currentTimeMillis() + 2 * 60 * 1000);
Map
claims.put("id", 9527);
claims.put("username", "liucangsong");
String jwt = Jwts.builder()
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
.setClaims(claims)
.setExpiration(date)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
System.out.println(jwt);
}
void parse() {
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OTUyNywiZXhwIjoxNjY3ODc4ODEzLCJ1c2VybmFtZSI6ImxpdWNhbmdzb25nIn0.c_ZkBOHr2NAUSt2z0918jrNV-lbzPabFu1ClRviWuC0";
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
Object id = claims.get("id", Long.class);
Object username = claims.get("username", String.class);
System.out.println(id + "," + username);
}
登录成功时返回JWT---在处理登录时,当用户登录成功,应该向客户端返回JWT数据,以至于客户端下次提交请求时,可以携带JWT
来访问服务器端,需要自定义过滤器来接收客户端携带的JWT数据
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
public static final int JWT_MIN_LENGTH = 113;
public JwtAuthorizationFilter() {
log.debug("创建过滤器对象JwtAuthorizationFilter");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.debug("开始执行过滤");
String jwt = request.getHeader("Authorization");
log.debug("获取客户端携带的JWT:{}", jwt);
// 检查是否获取到了基本有效的JWT
if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) {
// 对于无效的JWT,直接放行,交由后续的组件进行处理
log.debug("获取到的JWT被视为无效,当前过滤器将放行······");
filterChain.doFilter(request, response);
return;
}
// 尝试解析JWT
log.debug("获取到的JWT被视为有效,准备解析······");
Claims claims = Jwts.parser()
.setSigningKey("abcdefg")
.parseClaimsJws(jwt)
.getBody();
String username = claims.get("username", String.class);
// 处理权限信息
List
GrantedAuthority authority = new SimpleGrantedAuthority("这是一个假权限");
authorities.add(authority);
// 创建Authentication对象
Authentication authentication = new UsernamePasswordAuthenticationToken(username,null,authorities);
// 将Authentication对象存入到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 过滤器链继续向后传递,即放行
filterChain.doFilter(request, response);
}
}
为了保证此过滤器能够在Spring Security过滤器之前执行,还应该在SecurityConfiguration中,先自动装配
此过滤器对象,然后将过滤器对象添加到Spring Security过滤器之前
关于复杂请求的PreFlight-----PreFlight(预检),当客户端提交的请求自定义了请求头,且请求头中的属性不是常规属性时,就会
触发预检机制,这类请求会被视为复杂请求,浏览器会自动向对应的url提交一个options类型的请求
如果此请求被正常响应,才可以正常提交原本的请求,否则视为预检失败,提示跨域错误
http.cors()启用cors过滤器,此过滤器可以对复杂请求的预检放行
使用配置文件自定义JWT参数---在配置文件中添加配置后,当加载时,会将这些配置读取到Spring框架内置的Environment对象中,
另外,操作系统的配置和JVM配置也会自动读取到Environment中,且配置文件中的配置的优先级是
最低的(会被覆盖),使用@Value注解读取值,其实是从Environment中读取的,并不是直接从配置
文件中读取的,所以添加自定义配置时,命名值得斟酌以免发生冲突
# JWT相关配置
csmall.jwt.secret-key=abcdefg
csmall.jwt.duration-in-minute=14400
@Value("${csmall.jwt.secret-key}")
private String secretKey;
@Value("${csmall.jwt.duration-in-minute}")
private long durationInMinute;
将查询到的多个结果封装到一个集合/数组属性中-----
单点登录----SSO(Single Sign On),表现为客户端只需要在某1个服务器上通过认证,其它服务器也可以识别此客户端的身份
实现手段主要有2种:
使用Session机制,并共享Session,添加依赖项
spring-boot-starter-data-redis结合spring-session-data-redis
使用Token机制
各服务器需要有同样的解析JWT的代码
Spring框架--作用:主要解决了创建对象、管理对象的问题
当项目中需要使用Spring框架时,需要添加的依赖项是:spring-context
创建对象的做法:
在任何配置类(添加了@Configuration)中,自定义方法,返回某种类型的(你需要的)对象,并在方法上添加@Bean注解
此方式创建出来的对象,在Spring容器中的名称就是方法名称
此方法应该是public
此方法的返回值类型,是你期望Spring框架管理的数据的类型
此方法的参数列表,建议为空
此方法的方法体,应该是自行设计的,没有要求
配置组件扫描,并在组件类上添加组件注解
此方式创建出来的对象,在Spring容器中的名称默认是将类名首字母改为小写
例如:类名是AdminController,则对象在Spring容器中的名称为adminController
此规则仅适用于类名的第1个字母大写,且第2个字母小写的情况,如果不符合此规则
则对象在Spring容器中的名称就是类名
可以通过组件注解的参数来指定名称
在任何配置类上,添加@ComponentScan,当加载此配置类时,就会激活组件扫描
可以配置@ComponentScan的参数,此参数应该是需要被扫描的根包(会扫描所配置的包,及其所有子孙包)
且此注解参数的值是数组类型的
例如:
@ComponentScan("cn.tedu")
如果没有配置@ComponentScan的参数中的根包,则组件扫描的范围就是当前类的包及其子孙包
需要在各组件类上添加组件注解,才会被创建对象,常见的组件注解有:
@Component:通用注解
@Controller:控制器类的注解
@RestController:仅添加Spring MVC框架后可使用
@ControllerAdvice:仅添加Spring MVC框架后可使用
@RestControllerAdvice:仅添加Spring MVC框架后可使用
@Service:Service这种业务类的注解
@Repository:处理数据源中的数据读写的类的注解
以上4种组件注解在Spring框架作用范围之内是完全等效的
在Spring框架中,还有@Configuration注解,也是组件注解的一种
但是Spring对此注解的处理更加特殊(Spring框架对配置类使用了代理模式)
对于这2种创建对象的做法,通常:
如果是自定义的类,优先使用组件扫描的做法来创建对象
如果不是自定义的类,无法使用组件扫描的做法,只能在配置类中通过@Bean方法来创建对象
当Spring成功的创建了对象后,会将对象保存在Spring应用程序上下文(ApplicationContext)中
后续,当需要这些对象时,可以从Spring应用程序上下文中获取!
由于Spring应用程序上下文中持有大量对象的引用,所以,Spring应用程序上下文也通常被称之为"Spring容器"
Spring MVC框架--响应结果:默认情况下,处理请求的方法的返回值将表示"处理响应结果的视图组件的名称,及相关的数据",在
Spring MVC中,有一种内置的返回值类型"ModelAndView",不是前后端分离的做法
在处理请求的方法上,可以添加@ResponseBody注解,当添加此注解后,处理请求的方法的返回值将
表示响应的数据,不再由服务器端决定视图组件,也叫做响应正文,这是前后端分离的做法
@ResponseBody注解可以添加在方法和控制器类上
控制器类需要添加@Controller注解,才是控制器,或者也可以改为添加@RestController,此注解是由
@Controller和@ResponseBody组合而成的,所以添加@RestController后,当前控制器类中所有处理
请求的方法都是响应正文的
当控制器处理完请求需要响应正文时,Spring MVC会根据方法的返回值类型,来决定使用某个
MessageConverter来讲返回值转换为响应到客户端的数据
处理异常:添加了@ExceptionHandler注解的方法,就是处理异常的方法。
处理异常的方法到底处理哪种异常,由@ExceptionHandler注解参数或方法的参数中的异常类型来决定
如果@ExceptionHandler注解没有配置参数,由方法的参数中的异常类型决定,
如果@ExceptionHandler注解配置了参数,由以注解参数中配置的类型为准!
处理异常的方法可以声明在控制器类,将只作用于当前控制器类中的方法抛出的异常!
通常,建议将处理异常的方法声明在专门的类中,并在此类上添加@ControllerAdvice注解
当添加此注解后,此类中特定的方法(例如处理异常的方法)将作用于每次处理请求的过程中!
如果处理异常后的将"响应正文",也可以在处理异常的方法上添加@ResponseBody注解,
或在当前类上添加@ResponseBody,或使用@RestControllerAdvice取代@ControllerAdvice和@ResponseBody
Spring Boot框架-----作用:主要解决了统一管理依赖项与简化配置相关的问题
注解----注解所属框架作用
@ComponentScanSpring添加在配置类上,开启组件扫描。
如果没有配置包名,则扫描当前配置类所在的包,
如果配置了包名,则扫描所配置的包及其子孙包
@ComponentSpring添加在类上,标记当前类是组件类,可以通过参数配置Spring Bean名称
@ControllerSpring添加在类上,标记当前类是控制器组件类,用法同@Component
@ServiceSpring添加在类上,标记当前类是业务逻辑组件类,用法同@Component
@RepositorySpring添加在类上,标记当前类是数据访问组件类,用法同@Component
@ConfigurationSpring添加在类上,仅添加此注解的类才被视为配置类,通常不配置注解参数
@BeanSpring添加在方法上,标记此方法将返回某个类型的对象,
且Spring会自动调用此方法,并将对象保存在Spring容器中
@AutowiredSpring添加在属性上,使得Spring自动装配此属性的值
添加在构造方法上,使得Spring自动调用此构造方法
添加在Setter方法上,使得Spring自动调用此方法
@QualifierSpring添加在属性上,或添加在方法的参数上,
配合自动装配机制,用于指定需要装配的Spring Bean的名称
@ScopeSpring添加在组件类上,或添加在已经添加了@Bean注解的方法上,
用于指定作用域,注解参数为singleton(默认)时为“单例”,注解参数为prototype时为“非单例”
@LazySpring添加在组件类上,或添加在已经添加了@Bean注解的方法上,
用于指定作用域,当Spring Bean是单例时,注解参数为true(默认)时为“懒加载”,注解参数为false时为“预加载”
@ValueSpring添加在属性上,或添加在被Spring调用的方法的参数上,用于读取Environment中的属性值
@ResourceSpring此注解是javax包中的注解,
添加在属性上,使得Spring自动装配此属性的值,
通常不推荐使用此注解
@ResponseBodySpring MVC添加在方法上,标记此方法是“响应正文”的,
添加在类上,标记此类中所有方法都是“响应正文”的
@RestControllerSpring MVC添加在类上,标记此类是一个“响应正文”的控制器类
@RequestMappingSpring MVC添加在类上,也可以添加在处理请求的方法上,
通常用于配置请求路径
@GetMappingSpring MVC添加在方法上,是将请求方式限制为GET的@RequestMapping
@PostMappingSpring MVC添加在方法上,是将请求方式限制为POST的@RequestMapping
@DeleteMappingSpring MVC添加在方法上,是将请求方式限制为DELETE的@RequestMapping
@PutMappingSpring MVC添加在方法上,是将请求方式限制为PUT的@RequestMapping
@RequestParamSpring MVC添加在请求参数上,可以:
1. 指定请求参数名称
2. 要求必须提交此参数
3. 指定请求参数的默认值
@PathVariableSpring MVC添加在请求参数上,用于标记此参数的值来自URL中的占位符,如果URL中的占位符名称与方法的参数名称不同,需要配置此注解参数来指定URL中的占位符名称
@RequestBodySpring MVC添加在请求参数上,用于标记此参数必须是对象格式的参数,如果未添加此注解,参数必须是FormData格式的
@ExceptionHandlerSpring MVC添加在方法上,标记此方法是处理异常的方法,可以通过配置注解参数来指定需要处理的异常类型,如果没有配置注解参数,所处理的异常类型取决于方法的参数列表中的异常类型
@ControllerAdviceSpring MVC添加在类上,标记此类中特定的方法将作用于每次处理请求的过程中
@RestControllerAdviceSpring MVC添加在类上,是@ControllerAdvice和@ResponseBody的组合注解
@MapperScanMybatis添加在配置类上,用于指定Mapper接口的根包,Mybatis将根据此根包执行扫描,以找到各Mapper接口
@MapperMybatis添加在Mapper接口上,用于标记此接口是Mybatis的Mapper接口,如果已经通过@MapperScan配置能够找到此接口,则不需要使用此注解
@ParamMybatis添加在Mapper接口中的抽象方法的参数上,用于指定参数名称,当使用此注解指定参数名称后,SQL中的#{} / ${}占位符中的名称必须是此注解指定的名称,通常,当抽象方法的参数超过1个时,强烈建议在每个参数上使用此注解配置名称
@SelectMybatis添加在Mapper接口的抽象方法上,可以通过此注解直接配置此抽象方法对应的SQL语句(不必将SQL语句配置在XML文件中),用于配置SELECT类的SQL语句,但是,非常不推荐这种做法
@InsertMybatis同上,用于配置INSERT类的SQL语句
@UpdateMybatis同上,用于配置UPDATE类的SQL语句
@DeleteMybatis同上,用于配置DELETE类的SQL语句
@TransactionalSpring JDBC推荐添加在业务接口上,用于标记此接口中所有方法都是事务性的,或业务接口中的抽象方法上,用于此方法是事务性的
@SpringBootApplicationSpring Boot添加在类上,用于标记此类是Spring Boot的启动类,每个Spring Boot项目应该只有1个类添加了此注解
@SpringBootConfigurationSpring Boot通常不需要显式的使用,它是@SpringBootApplication的元注解之一
@SpringBootTestSpring Boot添加在类上,用于标记此类是加载Spring环境的测试类
@ValidSpring Validation添加在方法的参数上,标记此参数需要经过Validation框架的检查
@ValidatedSpring Validation添加在方法的参数上,标记此参数需要经过Validation框架的检查;添加在类上,并结合方法上的检查注解(例如@NotNull等)实现对未封装的参数的检查
@NotNullSpring Validation添加在需要被检查的参数上,或添加在需要被检查的封装类型的属性上,用于配置“不允许为null”的检查规则
@NotEmptySpring Validation使用位置同@NotNull,用于配置“不允许为空字符串”的检查规则
@NotBlankSpring Validation使用位置同@NotNull,用于配置“不允许为空白”的检查规则
@PatternSpring Validation使用位置同@NotNull,用于配置正则表达式的检查规则
@RangeSpring Validation使用位置同@NotNull,用于配置“数值必须在某个取值区间”的检查规则
@ApiKnife4j添加在控制器类上,通过此注解的tags属性配置API文档中的模块名称
@ApiOperationKnife4j添加在控制器类中处理请求的方法上,用于配置业务名称
@ApiOperationSupportKnife4j添加在控制器类中处理请求的方法上,通过此注解的order属性配置业务显示在API文档中时的排序序号
@ApiModelPropertyKnife4j添加在封装的请求参数类型中的属性上,用于配置请求参数的详细说明,包括:名称、数据类型、是否必须等
@ApiImplicitParamKnife4j添加在控制器类中处理请求的方法上,用于配置请求参数的详细说明,包括:名称、数据类型、是否必须等
@ApiImplicitParamsKnife4j添加在控制器类中处理请求的方法上,如果需要通过@ApiImplicitParam注解配置的参数超过1个,则必须将多个@ApiImplicitParam注解作为此注解的参数
@ApiIgnoreKnife4j添加在请求参数上,用于标记API文档中将不关心此参数
@EnableGlobalMethodSecuritySpring Security添加在配置类上,用于开启全局的方法级别的权限控制
@PreAuthorizeSpring Security添加在方法上,用于配置权限
@AuthenticationPrincipalSpring Security添加在方法的参数上,且此参数应该是Security上下文中的认证信息中的当事人类型,用于为此参数注入值
@DataLombok添加在类上,将在编译期生成此类中所有属性的Setter、Getter方法,及hashCode()、equals()、toString()方法
@SetterLombok添加在类上,将在编译期生成此类中所有属性的Setter方法,也可以添加在类的属性上,将在编译期生成此属性的Setter方法
@GetterLombok添加在类上,将在编译期生成此类中所有属性的Getter方法,也可以添加在类的属性上,将在编译期生成此属性的Getter方法
@EqualsAndHashcodeLombok添加在类上,将在编译期生成基于此类中所有属性的hashCode()、equals()方法
@ToStringLombok添加在类上,将在编译期生成基于此类中所有属性的toString()方法
@NoArgConstructorLombok添加在类上,将在编译期生成此类的无参数构造方法
@AllArgsConstructorLombok添加在类上,将在编译期生成基于此类中所有属性的全参构造方法
Redis---是一款基于内存来读写数据的NoSQL非关系型数据库,Redis访问的数据都在内存中,其实Redis也会自动的处理持久化
但是正常读写都是在内存中执行的
NoSQL:不涉及SQL语句,也可理解为No Operation(不操作)
非关系型数据库:不关心数据库中存储的是什么数据,几乎没有数据种类的概念,更不存在数据与数据之间的关联
通常,在项目中,Redis用于实现缓存
优点:读取数据的效率会高很多
能够一定程度上保障缓解数据库的查询压力,提高数据库的安全性
缺点:需要关注数据一致性问题,即Redis中的数据与MySQL中的数据是否一致,如果不一致,是否需要处理
Redis的基本操作-----在Windows操作系统中,通过.msi安装包来安装的Redis,会自动注册Redis服务,开机会自动启动Redis,所以
Redis处于随时可用的状态
redis传统数据类型:
string 字符串
list 列表
hash 对象
set 集合
zset 有序集合
常用命令:
redis-cli 登录redis控制台
exit 退出redis
ping 检查redis是否可用
set key value 存值
get key 取值
flushall 清除所有数据
del key 删除指定数据
dbsize 查看数据总数
keys pattern 根据模式查找key,通常不建议使用
添加依赖项:
在配置类RedisConfiguration中做出如下配置:
@Bean
public RedisTemplate
RedisTemplate
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
return redisTemplate;
}
数据一致性问题的思考----当使用Redis缓存数据,如果数据库中的数据发生了变化,此时,Redis中的数据会暂时与数据库中的
数据并不相同,通常称之为"数据一致性问题",一般有两种做法:
及时更新Redis缓存数据
放任数据不一致的表现,直至更新Redis数据为止
如果及时更新Redis缓存数据,其优点是Redis缓存中的数据基本上是准确的,其缺点在于可能需要频繁
的更新Redis缓存数据,本质上是反复读MySQL、反复写Redis的操作,如果读取Redis数据的频率根
本不高,则会形成浪费,并且更新缓存的频率太高也会增加服务器的压力,所以,对于增、删、改
频率非常高的数据,可能不太适用此规则
放任数据不一致的表现,其缺点很显然就是数据可能不准确,其优点是没有给服务器端增加任何压力
其实并不是所有的数据都必须时时刻刻都要求准确的,某些数据即使不准确,也不会产生恶劣后果
例如热门话题排行榜,定期更新即可,或者某些数据即使准确,也没有太多实际意义,例如热门时段
的火车票、飞机票等,就算在列表中显示了正确的余量,也不一定能够成功购买
通常的规律:
使用Redis缓存的数据符合一定标准-
数据的增、删、改的频率非常低,查询频率更高。这时无论采取即时还是定期更新策略都是可行的
例如电商平台中的商品类别
对数据的准确性要求不高的数据,例如某些列表或榜单,使用定期更新策略,更新周期根据数据
变化的频率及其价值来决定
Redis缓存数据的操作-----通常,推荐将Redis的读写数据操作进行封装
在根包下创建repo.IBrandRedisRepository接口
在repo包下创建impl.BrandRedisRepositoryImpl实现类,实现以上接口
开机之后马上进行缓存读写操作----缓存预热,在启动项目的时候就将缓存数据加载到Redis缓存中
在Spring Boot项目中,自定义组件类,实现ApplicationRunner接口,此接口中的run()方法
会在启动项目之后立即执行,可以通过此机制实现缓存预热
周期性更新缓存的操作----计划任务,设定某种规则(通常是与时间相关的规则),当满足规则时,自动执行任务,并且,此规则可
能是周期性的满足,则任务也会周期性的执行
在Spring Boot项目中,需要在配置类上添加@EnableScheduling注解,以开启计划任务,否则,当前项目
中所有计划任务都是不允许执行的
在任何组件类中,自定义方法,在方法上添加@Scheduled注解,则此方法就是计划任务,通过此注解的
参数可以配置计划任务的执行规则(参数:fixedRate = 500, fixedDelay = 500)
计划任务在项目启动时就会执行第一次
Mybatis的缓存机制---Mybatis框架默认是有2套缓存机制的,分别称之一级缓存和二级缓存
一级缓存也称之为"会话(Session)缓存",默认是开启的,且无法关闭
一级缓存必须保证多次的查询操作满足:同一个SqlSession、同一个Mapper、执行相同的SQL查询、使用相同的参数
测试代码:SqlSession sqlSession = sqlSessionFactory.openSession()
一级缓存会因为以下任意一种原因而消失:
手动清除缓存:sqlSession.clearCache()
当前执行了任何写操作
二级缓存也称之为"namespace缓存",是作用于某个namespace的,具体表现为:无论是否为同一个SqlSession
只要执行的是相同的Mapper的查询,且查询参数相同,就可以应用二级缓存
在使用Spring Boot与Mybatis的项目中,二级缓存默认是全局开启的,但各namespace默认并未开启
如果需要在namespace中开启二级缓存,需要在XML文件中添加
则表示当前XML中所有查询都开启了二级缓存
需要注意:使用二级缓存时,需要保证查询结果的类型实现了Serializable接口
另外,还可以在
此属性的默认值为true,表示"使用缓存"
当应用二级缓存后,在日志上会提示[Cache Hit Ratio],表示"当前namespace缓存命中率"
与一级缓存相同,只需要发生任何写操作,都会自动清除缓存数据
Mybatis在查询数据时,会优先尝试从二级缓存中查询是否存在缓存数据,如果命中,将直接返回
如果未命中,则尝试从一级缓存中查询是否存在缓存数据,如果命中,将返回
如果仍未命中,将执行数据库查询
【通常,Mybatis的一级和二级缓存用处都不大】
周期性更新缓存的操作----定时任务@Scheduled(cron = "x x x x x x x")
各个x依次表示秒、分、时、日、月、周、[年],各值都可以使用通配符,*代表任意值,?代表不关心具体值
L代表最后一个值
?只能用于日和周(星期几),
月和周都可以使用英文的前三个字母表示
以上各值,还可以使用"x/x"格式的值,例如在分钟对应的位置1/5,表示
当分钟值为1时,间隔5分钟执行一次
按需加载缓存----某些不常用业务开机时不加载缓存,则在需要数据时,先从缓存查,若没有再查数据库
查到后存入缓存,再返回【通常不建议缓存查不到就去查数据库】
Spring AOP------面向切面的编程,并不是Spring框架特有的技术,只是Spring框架很好的支持了AOP
AOP主要用于:日志、安全、事务管理、自定义的业务规则
例如:统计所有Service中的业务方法的执行耗时,添加依赖项
在根包下创建aop.TimerAspect类作为切面类,添加@Aspect注解
由于是通过Spring来实现AOP,所以此类应该交由Spring管理,还需添加@Component注解,并在类中自定义
方法,且通过注解来配置方法何时执行
// @Around注解表示"包裹",通常也称之为"环绕",方法传pjp参数
// @Before注解表示在Service业务方法之前执行,方法无参数
// @After注解表示在Service业务方法之后执行,方法无参数
// @AfterReturning注解表示在返回结果之后,方法传JoinPoint和返回值
// @AfterThrowing注解表示在抛出异常之后,方法传JoinPoint和异常对象
// @Around注解中的execution内部配置表达式,以匹配上需要哪里执行切面代码
// 表达式中,星号(*)是通配符,可匹配1次任意内容
// 表达式中,2个连接的小数点(..)也是通配符,可匹配0~n次,只能用于包名和参数列表
@Around("execution(* cn.tedu.jsd2207csmall.product.service.*.*(..))")
// ↑ 此星号表示需要匹配的方法的返回值类型
// ↑ -------------- 根包 -------------- ↑
// ↑ 类名
// ↑ 方法名
// ↑↑ 参数列表
public Object timer(ProceedingJoinPoint pjp) throws Throwable {
log.debug("{}", pjp.getTarget());
long start = System.currentTimeMillis();
// 注意:必须获取调用此方法的返回值,作为当前切面方法的返回值
// 注意:必须要抛出异常,不可以使用try...catch捕获
Object result = pjp.proceed(); // 相当于执行了匹配的方法,即业务方法
long end = System.currentTimeMillis();
log.debug("执行耗时:{}毫秒", end - start);
return result;
}
连接点(JoinPoint):数据处理过程中的某个时间节点,可能是调用了方法,或抛出了异常
切入点(PointCut):选择一个或多个连接点的表达式
通知(Advice):选择到的连接点执行的代码
切面(Aspect):是包含了切入点和通知的模块
this.$router.push(url)--脚手架项目中跳转页面的语句
富文本编辑器----先在项目中安装npm i wangeditor -S
在main.js中添加配置import wangEditor from 'wangeditor'
Vue.prototype.wangEditor = wangEditor
在需要使用富文本编辑器的视图中,先在视图的设计中,使用某个标签来表示需要显示富文本编辑器的区域
通常,使用
然后,在data中声明对应的属性:
export default {
data() {
return {
editor: {}, // 富文本编辑器
并且,准备一个用于初始化富文本编辑器的函数:
initWangEditor() {
this.editor = new this.wangEditor('#wangEditor');
this.editor.create();
}
最后,在视图刚刚加载时,就调用此初始化函数:
mounted() {
this.initWangEditor(); // 初始化富文本编辑器
}
使用Mybatis拦截sql语句自动添加gmt_create和gmt_modified字段值----
先创建类实现接口,在类中写入业务代码:
@Slf4j
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class InsertUpdateTimeInterceptor implements Interceptor {
/**
* 自动添加的创建时间字段
*/
private static final String FIELD_CREATE = "gmt_create";
/**
* 自动更新时间的字段
*/
private static final String FIELD_MODIFIED = "gmt_modified";
/**
* SQL语句类型:其它(暂无实际用途)
*/
private static final int SQL_TYPE_OTHER = 0;
/**
* SQL语句类型:INSERT
*/
private static final int SQL_TYPE_INSERT = 1;
/**
* SQL语句类型:UPDATE
*/
private static final int SQL_TYPE_UPDATE = 2;
/**
* 查找SQL类型的正则表达式:INSERT
*/
private static final String SQL_TYPE_PATTERN_INSERT = "^insert\\s";
/**
* 查找SQL类型的正则表达式:UPDATE
*/
private static final String SQL_TYPE_PATTERN_UPDATE = "^update\\s";
/**
* 查询SQL语句片段的正则表达式:gmt_modified片段
*/
private static final String SQL_STATEMENT_PATTERN_MODIFIED = ",\\s*" + FIELD_MODIFIED + "\\s*=";
/**
* 查询SQL语句片段的正则表达式:gmt_create片段
*/
private static final String SQL_STATEMENT_PATTERN_CREATE = ",\\s*" + FIELD_CREATE + "\\s*[,)]?";
/**
* 查询SQL语句片段的正则表达式:WHERE子句
*/
private static final String SQL_STATEMENT_PATTERN_WHERE = "\\s+where\\s+";
/**
* 查询SQL语句片段的正则表达式:VALUES子句
*/
private static final String SQL_STATEMENT_PATTERN_VALUES = "\\)\\s*values?\\s*\\(";
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 日志
log.debug("准备拦截SQL语句……");
// 获取boundSql,即:封装了即将执行的SQL语句及相关数据的对象
BoundSql boundSql = getBoundSql(invocation);
// 从boundSql中获取SQL语句
String sql = getSql(boundSql);
// 日志
log.debug("原SQL语句:{}", sql);
// 准备新SQL语句
String newSql = null;
// 判断原SQL类型
switch (getOriginalSqlType(sql)) {
case SQL_TYPE_INSERT:
// 日志
log.debug("原SQL语句是【INSERT】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendCreateTimeField(sql, LocalDateTime.now());
break;
case SQL_TYPE_UPDATE:
// 日志
log.debug("原SQL语句是【UPDATE】语句,准备补充更新时间……");
// 准备新SQL语句
newSql = appendModifiedTimeField(sql, LocalDateTime.now());
break;
}
// 应用新SQL
if (newSql != null) {
// 日志
log.debug("新SQL语句:{}", newSql);
reflectAttributeValue(boundSql, "sql", newSql);
}
// 执行调用,即拦截器放行,执行后续部分
return invocation.proceed();
}
public String appendModifiedTimeField(String sqlStatement, LocalDateTime dateTime) {
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_MODIFIED, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("原SQL语句中已经包含gmt_modified,将不补充添加时间字段");
return null;
}
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern whereClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_WHERE, Pattern.CASE_INSENSITIVE);
Matcher whereClauseMatcher = whereClausePattern.matcher(sql);
// 查找 where 子句的位置
if (whereClauseMatcher.find()) {
int start = whereClauseMatcher.start();
int end = whereClauseMatcher.end();
String clause = whereClauseMatcher.group();
log.debug("在原SQL语句 {} 到 {} 找到 {}", start, end, clause);
String newSetClause = ", " + FIELD_MODIFIED + "='" + dateTime + "'";
sql.insert(start, newSetClause);
log.debug("在原SQL语句 {} 插入 {}", start, newSetClause);
log.debug("生成SQL: {}", sql);
return sql.toString();
}
return null;
}
public String appendCreateTimeField(String sqlStatement, LocalDateTime dateTime) {
// 如果 SQL 中已经包含 gmt_create 就不在添加这两个字段了
Pattern gmtPattern = Pattern.compile(SQL_STATEMENT_PATTERN_CREATE, Pattern.CASE_INSENSITIVE);
if (gmtPattern.matcher(sqlStatement).find()) {
log.debug("已经包含 gmt_create 不再添加 时间字段");
return null;
}
// INSERT into table (xx, xx, xx ) values (?,?,?)
// 查找 ) values ( 的位置
StringBuilder sql = new StringBuilder(sqlStatement);
Pattern valuesClausePattern = Pattern.compile(SQL_STATEMENT_PATTERN_VALUES, Pattern.CASE_INSENSITIVE);
Matcher valuesClauseMatcher = valuesClausePattern.matcher(sql);
// 查找 ") values " 的位置
if (valuesClauseMatcher.find()) {
int start = valuesClauseMatcher.start();
int end = valuesClauseMatcher.end();
String str = valuesClauseMatcher.group();
log.debug("找到value字符串:{} 的位置 {}, {}", str, start, end);
// 插入字段列表
String fieldNames = ", " + FIELD_CREATE + ", " + FIELD_MODIFIED;
sql.insert(start, fieldNames);
log.debug("插入字段列表{}", fieldNames);
// 定义查找参数值位置的 正则表达 “)”
Pattern paramPositionPattern = Pattern.compile("\\)");
Matcher paramPositionMatcher = paramPositionPattern.matcher(sql);
// 从 ) values ( 的后面位置 end 开始查找 结束括号的位置
String param = ", '" + dateTime + "', '" + dateTime + "'";
int position = end + fieldNames.length();
while (paramPositionMatcher.find(position)) {
start = paramPositionMatcher.start();
end = paramPositionMatcher.end();
str = paramPositionMatcher.group();
log.debug("找到参数值插入位置 {}, {}, {}", str, start, end);
sql.insert(start, param);
log.debug("在 {} 插入参数值 {}", start, param);
position = end + param.length();
}
if (position == end) {
log.warn("没有找到插入数据的位置!");
return null;
}
} else {
log.warn("没有找到 ) values (");
return null;
}
log.debug("生成SQL: {}", sql);
return sql.toString();
}
@Override
public Object plugin(Object target) {
// 本方法的代码是相对固定的
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
// 无须执行操作
}
/**
*
获取BoundSql对象,此部分代码相对固定
*
*
注意:根据拦截类型不同,获取BoundSql的步骤并不相同,此处并未穷举所有方式!
*
* @param invocation 调用对象
* @return 绑定SQL的对象
*/
private BoundSql getBoundSql(Invocation invocation) {
Object invocationTarget = invocation.getTarget();
if (invocationTarget instanceof StatementHandler) {
StatementHandler statementHandler = (StatementHandler) invocationTarget;
return statementHandler.getBoundSql();
} else {
throw new RuntimeException("获取StatementHandler失败!请检查拦截器配置!");
}
}
/**
* 从BoundSql对象中获取SQL语句
*
* @param boundSql BoundSql对象
* @return 将BoundSql对象中封装的SQL语句进行转换小写、去除多余空白后的SQL语句
*/
private String getSql(BoundSql boundSql) {
return boundSql.getSql().toLowerCase().replaceAll("\\s+", " ").trim();
}
/**
*
通过反射,设置某个对象的某个属性的值
*
* @param object 需要设置值的对象
* @param attributeName 需要设置值的属性名称
* @param attributeValue 新的值
* @throws NoSuchFieldException 无此字段异常
* @throws IllegalAccessException 非法访问异常
*/
private void reflectAttributeValue(Object object, String attributeName, String attributeValue) throws NoSuchFieldException, IllegalAccessException {
Field field = object.getClass().getDeclaredField(attributeName);
field.setAccessible(true);
field.set(object, attributeValue);
}
/**
* 获取原SQL语句类型
*
* @param sql 原SQL语句
* @return SQL语句类型
*/
private int getOriginalSqlType(String sql) {
Pattern pattern;
pattern = Pattern.compile(SQL_TYPE_PATTERN_INSERT, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_INSERT;
}
pattern = Pattern.compile(SQL_TYPE_PATTERN_UPDATE, Pattern.CASE_INSENSITIVE);
if (pattern.matcher(sql).find()) {
return SQL_TYPE_UPDATE;
}
return SQL_TYPE_OTHER;
}
}
然后在Mybatis配置类中注册拦截请求的方法:
@PostConstruct // 此注解添加在方法上,表示此方法是Spring Bean的生命周期方法中的初始化方法,会在创建对象、自动装配之后,自动执行
public void addInterceptor() {
log.debug("开始注册Mybatis拦截器");
InsertUpdateTimeInterceptor interceptor = new InsertUpdateTimeInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
后端项目打包----先安装打包插件
然后在maven控制台clean-->test-->package得到jar包,然后在终端进入jar包目录,输入指令java -jar 包名
启动此项目
前端项目打包----npm run build,得到dist文件夹,然后把dist放入tomcat,启动tomcat