本笔记标题不代表对应课程标题
项目架构分类
创建多模块、多环境项目
搭建数据库与持久层
持久层框架:Mybatis,特点是支持XML形式管理、支持动态SQL
在项目中添加Mybatis框架依赖、在配置文件中配置Mybatis
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
#Mybatis
mybatis.mapper-locations=classpath:mapper/*.xml
mapper文件夹都放到了dao模块,配置文件是在service模块中,service模块依赖dao层,所以应该是上层能扫描到下层的classpath
@Mapper: 注解,Mybatis提供,作用是项目在启动的时候将Dao层的数据封装成一个实体类
<mapper namespace="类路径"></mapper>进行相关的关联
mybatis.type-aliases-package=com.bilibili.dao
实现热部署
打开Idea设置,搜索compile,勾选Compile independent modules in parallel
Ctr + Alt + Shift + /,选择registry,勾选compiler.document.save.enabled和compiler.automake.allow.when.app.running
在项目启动项里边,即Edit Configurations...配置启动的配置,在On ‘update’ action
和 On frame deactivation
选择成 Update classes and resources
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.7.9</version>
</dependency>
添加配置项
spring.devtools.restart.enabled=true
GET和POST方法可以使用相同的路径(相同资源名称URI)
// 可以同时使用
@GetMapping("/users")
...
@PostMapping("/users")
public String postData(@RequestBody Map<String, Object> data){
Integer[] idArray = dataMap.keySet().toArray(new Integer[0]);
Array.sort(idArray);
int nextId = idArray[idArray.length-1] + 1;
dataMap.put(nextId, data);
return "post seccess";
}
@RequestBody 注解:SpringBoot会将数据data进行自动的封装,封装成以JSON的格式传输进来 (将前端传的值进行封装,封装成一个JSON类型)
// PUT 方法
@PutMapping("/put")
public String putData(@RequestBody Map<Stirng, Object> data){
Integer id = Integer.valuseOf(String.valueOf(data.get("id")));
Map<String, Object> containedData = data.get(id);
if(containedGata == null){
Intager[] idArray = data.keySet().toArray(new Integer[0]);
Arrays.sort(idArray);
int nextId = idArray[idArray.length - 1] + 1;
dataMap.put(nextId, data);
}else{
dataMap.put(id,data);
}
}
通用功功能:加解密工具(AES、RSA、MD5)
通用配置:JSON信息转换配置、全局异常处理配置
@Configuration 声明是一个配置类
@Order 注解:@Order(Ordered.HIGHEST_PRECEDENCE),表明执行顺序,最高
实现java.io.Serializable这个接口是**为序列化,serialVersionUID 用来表明实现序列化类的不同版本间的兼容性(即在版本升级时反序列化仍保持对象的唯一性)。如果你修改了此类, 要修改此值。否则以前用老版本的类序列化的类恢复时会出错。
生成的用户令牌,保存在本地的LocalStorage里边
JWT (JSON Web Token):是一个规范,用于在空间受限环境下安全传递“声明”
JWT由三部分组成:
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
// 为了获取Token,Token传给前端之后,保存在LocalStorage里边,下次(后边)前端在请求接口的时候,从LocalStorage里边拿到Token,一般放在请求头里,所有的接口统一放到请求头里边,这样就不区分接口具体要传哪些参数。
String token = requestAttributes.getRequest().getHeader("token");
Long userId = TokenUtil.verifyToken(token);
tip:
1、提示找不到Service层的Bean(即无法自动注入bean),解决方案:
@SpringBootApplication(scanBasePackages = {"com.imooc.bilibili.api", "com.bilibili.service"})
2、提示找不到Dao层的Bean(即无法自动注入@Mapper注解标示的类),解决方案:
@MapperScan(basePackages = "com.bilibili.dao")
以上均是在启动类添加
3、XML文件提示数据库的表和字段找不到,解决方案:在Database中配置数据库链接
Java中的四种内部类:静态内部类、成员内部类、局部内部类、匿名内部类
参考:https://blog.csdn.net/xiaowanzi_zj/article/details/121893431
redis中的setnx,如果set not exist ,返回1,否则返回0,linux创建多级目录门mkdir -p,递归-r,git查看不同 git -diff
坑位:动态SQL中使用参数作为变量,需要@Param注解,即使只有一个参数
// 接口方法
List<UserInfo> getUserInfoByUserIds(Set<Long> userIdList);
<select id="getUserInfoByUserIds" resultType="com.bilibili.domain.UserInfo">
select
*
from
t_user_info
where
1=1
<if test="userIdList != null and userIdList.size > 0">
and userId in
<foreach collection="userIdList" item="userId" index="index" open="(" close=")" separator=",">
#{userId}
</foreach>
</if>
</select>
在xml文件中提示userIdList找不到,原因是使用了idea自带的jdk,没有修改为jdk8,jdk8编译时采取了强制保持方法参数变量名(使用jdk8,去掉注解还是报错,具体待究)
正确示例
List<UserInfo> getUserInfoByUserIds(@Param("userIdList") Set<Long> userIdList);
添加注解后就不再提示找不到了参考文档:https://blog.csdn.net/ningmeng666_c/article/details/115440720
虽然$存在SQL注入安全问题,但是有时候确实要使用,比如说传入列名或者表名时,就需要加上@Param注解,例如: 接口方法:
List<User> getUsersOrderByParam(@Param("order_by") String order_by);
12
xml文件:
<select id="getUsersOrderByParam" resultMap="BaseResultMap" parameterType="String">
SELECT * FROM T_USER
<if test="order_by != null and order_by != ''">
ORDER BY ${order_by} DESC
</if>
</select>
@RequestParam注解也表示必填的选项
JSONObject类使用技巧
JSONObject 为阿里的fastJson包里边的一个类,它实现了一个Map类,可以当成一个Map类,里边内置了一些好用的方法,所以不适用Map类,使用JSONObject类
public PageResult<UserInofo> pageListInfo(JSONObject para){
// 可以直接获取Integer等类型,如果是Map,只能是String或Object类型,获得Map里的值之后还要进行转换,使用JSONObject已经实现可以直接转换
//
Integer no = param.getInteger("no");
Integer size = params.getInteger("size");
params.put("start", (no-1)*size);
params.put("limit", size);
Integer total = userDao.pageCountUserInfo(param);
}
// UserDao 类
//从上一层自动生成方法的实现如下
Integer pageCountUserInfos(JSONObject params);
//使用上边的JSONObject在Mybatis标签里或相关的xml语句时,传入参数类型时要写很长,不方便。在这里可以手动改成Map类型.因为JSONObject实现了Map类,可以直接转换,这样就可以在参数类型里写成java.util.Map类型
Integer pageCountUserInfos(Map<String, Object> params)
使用 ${...} 代替 #{...}
// 使用${}不能防止sql注入,不推荐使用
select * from table where column like '%${value}%'
使用sql拼接字符串
select * from table where column like concat('%', #{value}, '%')
把'%#{value}%'改为"%"#{value}"%"
select * from table where column like "%"#{value}"%"
使用标签
<select id="aaa" resultType="com.example.entity.ABCEntity"
parameterType="com.example.entity.ABCEntity">
<bind name = "pattern1" value = "'%' + _parameter.name + '%'" />
SELECT * FROM table
<where>
<if test="column != null and name != ''">
name like #{pattern1}
</if>
</where>
</select>
5、代码中拼接后注入
select * from table where column like #{sqltext}
条目 | 订阅发布模式 | 观察者模式 |
---|---|---|
角色 | 发布者(Producer)、订阅者(Consumer)、代理人(Broker) | 观察者(Observe)、主体(Subject) |
耦合性 | 发布者和订阅者者之间完全解耦,他们彼此不知道对方,完全通过代理人来执行事项 | 观察者和主题之间是松耦合的关系,他们之间没有代理人 |
总结 | 订阅发布模式≠观察者模式 |
RocketMQ使用知识,详情查看 wiki笔记 10-11章节内容
@Value注解,是Springboot提供引入变量的一种方式
// 自定义的属性定义在application-test.properties配置文件中
// 可以从配置中获取属性进行初始化
@Value("${rocketmq.name.server.address}")
private String nameServerAddr;
RedisTemplate 提供一些方法,有时候可以自己也可以封装,如果有时候觉得使用起来比较麻烦,会封装一些RedisUtil工具类,本教程使用原生的
DefaultMQProducer 是RocketMQ自带的实体类
@Autowired注解报错,提示找不到类,可以替换成@Recorce注解
控制用户对系统资源(URI)的操作
RBAC权限控制模型(Role-Based Access Control):基于角色的权限控制
RBAC模型的层级:RABC0、RABC1、RABC2、RABC3
关键字:用户、角色、资源、权限、操作
数据库表设计:角色表、用户角色表、元素操作权限表、角色元素操作权限关联表、页面菜单权限表、角色页面菜单权限关联表
t_auth_element_operation表里边的elementCode字段(根元素对应的唯一编码,)在写的时候要保持前后端一致,比如数据里值为postVideoButton,前端也要使用postVideoButon组件(或者说是按钮)来跟数据库的记录保持对应,这样前端才能通过唯一编码确定对应的是哪一个按钮,拿到按钮之后在根据拿到的数据进行判断这个按钮是否可以被点击或者备操作。
@Param(mybatis提供的),因为参数是列表类型,并且参数名不是list所以前边添加@Param注解
// 在 Dao / Mapper 层
List<AuthRoleElementOperation> getRoleElementOperationsByRoleIds(@Param("roleIdSet" Map<Long> roleIdSet));
如果使用List ,Set这种集合式的形式进行传参的话,一定 要用@Param注解来命名一下相关的名称,为了方便在循环标签里使用
概念:是一种约定流程的编程。
关键字:约定(AOP的核心),把固定的流程抽出来做成一种约定流程,插入个性化的工具
典型案例:数据库事务包括打开数据库,设置属性,执行sql语句,没有异常则提交事务,有一场则回滚事务,最后关闭数据库连接。
术语:
两种实现方式
Object[] orgs = joinPoint.getArgs();获得切到的方法的参数
Object[] args = jointPoint.getArgs();
for(Object arg : args){
if(arg instanceof UserMoment){
UserMoment usermoment = (UserMoment)arg;
String type = userMoment.getType();
if(roleCodeSet.contains(AuthRoleConstant.ROLE_LV0 && !"0".equals(type))){
throw new ConditionException("参数异常");
}
}
}
引入依赖
加入配置
#fastdfs,是一个tracher列表,可为多个,用逗号分隔开
fdfs.tracker-list=39.107.54.108:22222,39.107.54.108:22222
MultipartFile:SpringBoot封装的一个实体类,使用过程中和普通文件没有太大区别,两者是可以进行转换的,底层原理是用二进制流进行转换
// 文件上传之后返回一个路径, StorePath是FastFileStorageClient封装好的一个类,存储的是文件上传后的路径所有信息
StorePath storPath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileType, metaDataSet);
return storPath.getPath();// getFullPath()获取全路径
// FastDfs提供的,支持断点上传的工具类,里边提供的appendFile()方法,分片上传,会在断了之后再次上传,有可能重复,使用modifyFile只会在断了的位置修复继续上传,不会造成重复上传
@Autowired
private AppendFileStorageClient appendFileStorageClient;
安装FsatDFS按照网址(https://github.com/happyfish100/fastdfs/wiki)的方式,不要按照给的文档,并且记得开放22122(或者直接关闭防火墙)这个端口号测试上传的时候,提示拒绝链接,是因为没有启动软件,按照网址下方的启动进行设置即可
本地FastDFS地址http://192.168.248.139/8888(后边跟上传返回的id,直接访问22122端口,会提示禁止下载)
配置nginx之后,记得要重启
前后端分离这种情况,一般都是前端对大文件进行分片(前端拿到文件之后,进行分片,保存在本地内存中,再调用后端及的接口进行一片片的上传,为了理解流程,本教程后端写了一下流程),上传一个分片之后,会分会一个路径,后边根据该路径进行上传
对文件进行MD5加密,来获取一个字符串,为后边的秒传做准备
分片上传,第一个是上传(返回一个存储路径),后边的是修改上传文件内容,从而达到上传文件的目的
Long类型转换为String类型,使用String.valueOf(uploedeSize),不要使用其他方法,推荐使用该方法,该方法的好处是避免出现空指针异常
increment方法自增1
redisTemplate.opsValue().increment(uploadedNoKey);
redisTemplate提供一个删除列表里边的所有值的key都删除
redisTemplate.delete(keyList)// 批量删除redis里边的值
File.createTempFile() 创建临时文件
// 文件类型转换 MultipartFile -> File
public File multipartFileToFile(MultipartFile multipartFile) throws Exception {
String originalFileName = multipartFile.getOriginalFilename();
String[] fileName = originalFileName.split("\\.");
File file = File.createTempFile(fileName[0], "." + fileName[1]);
multipartFile.transferTo(file);
return file;
}
一般分片大小为2-5M
RandomAccessFile类,java提供的文件工具类,支持随机访问(即可以去跳转到任意位置)
// r 代表赋予读的权限
RandomAccessFile randomAccessFile = new RandomAccessFile(file,"r");
// 跳转到置顶i的位置
randomAccessFile.seek(i);
...
// 最后记得要关闭
randomAccessFile.close();
// 获取文件MD5加密后的字符串
public static String getFileMD5(MultipartFIle file) throws Exception{
InputStream fis = file.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int byteReade;
while((byteReade = fis.read(buffer)) > 0){
baos.write(buffer, 0, byteRead);
}
fis.close();
return DigestUtils.md5Hex(baos.toByteArray())
}
不要把资源的绝对路径暴露出去,通过后端接口进行一次封装
在进行传输的时候,可以对流进行AES加密,前端在进行解密(解密通过代码混淆的方式),这样被别人获取到流也无法打开
因为是通过流的方式进行传输,接口不需要返回类型(void),会通过Http响应的输出流里边
获取文件信息
FileInfo fileInfo = fastFileStorageClient.queryFileInfo(DEFAULT_GROUP, url);
请求头信息
// 获得的是一个枚举类型
Enumeration<String> headerNames = request.getHeaderNames();
Map<String, Object> headers = new HashMap<>();
while(headerNames.hasMoreElements()){
String header = headerName.nextElement();
headers.put(header, request.getHeader(header));
}
对range进行切片之后,第一个数据是为控的数据,所以起始位置begain = Long.parseLong(range[1]);而不是range[0];
当在controller层传入的参数是一个列表(集合),哪怕只有一个参数,也要添加@Param("参数名"),否则会提示找不到。参考链接:https://blog.csdn.net/Lao3shen/article/details/125222585
WebSocket简介:
弹幕系统架构设计
整合及使用
引入依赖
配置WebSocketConfig
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
// 作用:主要是用来发现WebSocket服务的
return new ServerEndpointExporter();
}
}
创建WebSocket服务类WebSocketService.java
@Component
// 被下边的注解标注,就说明是一个和WebSocket相关的服务类了,里边的路径值随便定义,访问的时候是用自定义的路径即可
@ServerEndpoint("/imserver")
public class WebSocketService{
private final Logger = LoggerFactory.getLogger(this.class);
// AtomicInteger java提供的一个原子操作(为了保证线程安全)的相关类
private static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0);
// ConcurrentHashMap 是一个线程安全的Map
// 每个客户端链接都会生成一个WebSocketService,所以WebSocketService是一个多例模式,多例模式下是用@Autowire注入的话会产生问题
private static final ConcurrentHashMap<String, WebSocketService> WEBSOCKET_MAP = new ConcurrentHashMap<>();
// Session 是服务端和客户端的一个会话,WebSocket里边包含一个Session
private Session session;
private String sessionId;
// WebSocket 提供的一个注解,当调用成功时,就调用@OnOpen注解表示的方法
@OnOpen
public void openConnection(Session session){
this.sessionId = session.getId();
this.session = session;
if(WEBSOCKET_MAP.containsKey(sessionId)){
WEBSOCKET_MAP.remove(sessionId);
// this 代表当前的WebSocketService
WEBSOCKET_MAP.put(sessionId, this);
}else{
WEBSOCKET_MAP.put(sessionId, this);
ONLINE_COUNT.getAndIncrement;
}
// ONLINE_COUNT.get() 会返回一个原始的int类型
logger.info("用户连接成功" + sessionId + ",当前在线人数为:" + ONLINE_COUNT.get());
try{
this.sendMessage("0")
}catch(Exception e){
logger.error("连接异常!");
}
}
@OnClose
public void closeConnection(){
if(WEBSOCKET_MAP.containKey(sessionId)){
WEBSOCKET_MAP.remove(sessionId);
ONLINE_COUNT.getAndDecrement();
}
logger.info("用户退出:" + sessionId + ",当前在线人数为:" + ONLINE_COUNT.get());
}
@OnMessage
public void onMessage(String message){
}
@OnError
public void onError(Throwable error){
}
public void sendMassege(String message) throws IOException{
// 调用会话现成的getBasicRemote()方法获得Basic实体类,在调用发送信息的方法
this.session.getBasicRemote().sentText(message);
}
因为WebSocket是多例模式,所以在WebSocketService类里注入RedisTemplat时,系统会认为已经注入一个RedisTemplat了,就不再注入,会导致注入的RedisTemplate为空(NULL);
@Autowire
private RedisTemplat redisTemplat;
...
// redisTemplat 会为空
redisTemplat.opsForValue().get("kkk");
修改方法为(WebSocketService.java类)
privat static ApplicationContext APPLICATION_CONTEXT;
public static void setApplicationContext(ApplicationContext applicationContext){
WebSocketService.APPLICATION_CONTEXT = applicationContext;
}
...
RedisTemplate<String, String> redisTemplate = (RedisTemplate)WebSocketService.APPLICATION_CONTEXT.getBean("redisTemplate");
// redisTemplate 就不再为空了
redisTemplate.opaForValue().get("kkk");
在启动类中添加:
// 把app放入到WebSocketService的方法中,使得相当于有了一个全局上下文变量
WebSocketService.setApplicationContext(app);
tips:在XML文件中,由于><(大于、小于符号)容易与标签符号混淆,所以需要使用特殊符号进行表示,如: <![CDATA[>=]]>
表示大于等于符号 ,用于区别标签的内容,加入一些声明避免一些额外会发生的东西。
<if test="createTime != null and createTime != '' ">
createTime <![CDATA[>=]]> #{createTime}
</if>
// 把String类型的massage(json格式的信息)转换成Object类型
Danmu danmu = JSONObject.parseObject(massage, Danmu.class)
@PathParam 注解有WebSocket提供,为了获取链接后边拼的参数的
@ServerEndpoint("/imserver/{token}")
...
@OnOpen
public void openConnection(Session session, @PathParam("token") String token){
try{
userId = TokenUtil.verifyToken(token);
}catch(Exception ignored){}
}
// JSONObject.toJSONString(list)将list转换成json格式
redisTemplate.opsForValue().set(key, JSONObject.toJSONString(list));
// 将json格式的String转换成相应的类
String value = redisTemplate.opsForValue().get(key);
list = JSONArray.parseArray(value, Danmu.class);
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。