知识点总结
# 1. 静态资源及映射
当静态资源未存放在static目录下是无法直接访问的,在该项目中,初始创建的maven项目,添加的resource目录下没有static的文件夹
解决方案:
- 方案一:在resource目录下添加static文件夹,再存放静态资源;
- 方案二:添加静态资源映射
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 添加自定义的静态资源映射路径,第一个参数是访问路径前缀,第二个参数是文件存放路径
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:static/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:static/front/");
}
}
2
3
4
5
6
7
8
9
10
11
# 2. 拦截器和过滤器
# 2.1 拦截器
- 只有使用SpringMVC框架的工程才能使用拦截器,
- 只会拦截访问的控制器方法(即只对action请求起作用,不拦截静态资源),可以多次调用
- 不依赖于servlet容器
- 基于java的反射机制
- 可以访问action上下文、堆栈中的对象
- 可以获取IOC容器中的各个bean,在拦截器中注入一个service,可以调用业务逻辑
- 拦截器是在DispatcherServlet这个servlet中执行的,因此所有的请求最先进入Filter,最后离开Filter
应用场景:
- 登录验证,判断用户是否登录(瑞吉外卖)。
- 权限验证,判断用户是否有权限访问资源,如校验token
- 日志记录,记录请求操作日志(用户ip, 访问时间等),以便统计请求访问量。
- 处理cookie. 本地化、国际化、主题等。
- 性能监控,监控请求处理时长等
resources目录下的static的文件可能不再被视为静态资源,最好在添加资源管理器
# 2.2 过滤器
- 过滤器依赖于servlet容器,是servlet规范中的一部分,任何java web工程都可以使用
- 在url-pattern中配置了/*之后,可以对所有要访问的资源进行拦截
- 过滤器只能在容器初始化时被调用一次,在action的生命周期中
- 过滤器不能访问action上下文、堆栈里的对象
- 过滤器是基于函数回调
- 过滤器不能获取IOC容器中的各个bean
# 3. 前端接收参数
@RequestBody:用于接收前端传递给后端的json格式的数据
@RequestParam:用于接收前端传递给后端的key-value中的参数
如果参数前写了@RequestParam(xxx),那么前端必须有对应的xxx名字才行 前端路径同一个变量多个值传过来的时候要用@RequestParam eg:http://localhost:8080/dish/status/0?ids=1413384757047271425,1413385247889891330,1413342036832100354 ps:也可以不用list集合,直接一个String接过来,然后split方法用逗号分隔
- 直接接收:接收前端传递给后端的key-value里面的参数(如:分页查询请求路径中的page和pageSize和name)
eg:http://localhost:8080/category/page?page=1&pageSize=10 注意:用来接收的参数要和前端传过来的参数名称相同
- @PathVariable:从路径中获取参数
eg:http://localhost:8080/employee/1
# 4. 字符串拼接
使用{ }作为占位符
log.info("查询菜品信息,菜品id为"+id);
log.info("查询菜品信息,菜品id为{}",id);
2
# 5. Page对象
问题:为什么分页查询那里不需要搞个对象接收查询数据呢
**回答:**因为查询完毕之后内部自动赋值给Page对象,即把查到的数据放到自定义的pageinfo中。(Page里的records变量,存储的就是我们查询下出来的数据)
# 6. 由于js对long型数据的精度处理导致的问题
问题:在更新员工信息操作的时候会发现id不匹配 那是什么原因呢?
**分析:**这是因为js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致
**解决方案:**在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串
- 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- 在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
//优先使用自己定义的
converters.add(0,messageConverter);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 7. 公共字段自动填充
公共字段自动填充是mp给我们提供的一个功能,能大大的减少代码量提高效率
1.在实体类的属性上加上@TableFiled注解,指定自动填充策略 2.按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段开始自动填充[insert]...");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段开始自动填充[update]...");
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 8. ThreadLocal
其实在一开始的MyMetaObjectHandler中updateUser和createUser是写死的,因为我们无法获取HttpSession对象(即无法获取session)也就没办法获取登录用户,所以只能写死
客户端发送的每次http请求对,对应在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
- LoginCheckFilter的doFilter方法
- EmployeeController的update方法
- MyMetaObjectHandler的updateFill方法
什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值。
ThreadLocal常用方法:
- public void set(T value) : 设置当前线程的线程局部变量的值
- public T get() : 返回当前线程所对应的线程局部变量的值
- public void remove() : 删除当前线程所对应的线程局部变量的值
在瑞吉外卖项目中,通过在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取
# 9. 文件上传
文件上传也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程 Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller方法中声明一个MultipartFile类型的参数即可接收上传的文件
@PostMapping("/upload")
public Result<?> upload(MultipartFile file){
log.info("上传文件...");
//获取原始文件名
String originalName= file.getOriginalFilename();
//获取原始文件名的后缀,从点开始包括点
String suffix=originalName.substring(originalName.lastIndexOf("."));
//随机生成新的文件名
String fileName= UUID.randomUUID().toString()+suffix;
//创建一个目录对象
File dir=new File(BasePath);
//判断目录是否存在,不存在则创建
if (!dir.exists()){
dir.mkdir();
}
try {
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
file.transferTo(new File(BasePath+fileName));
}catch (IOException e){
e.printStackTrace();
}
return Result.success(fileName);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 10. 文件下载
文件下载也成为download是指将文件从服务器传输到本地计算机的过程,本质上就是服务端将文件以流的形式写回浏览器的过程
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//通过输入流读取文件
FileInputStream inputStream=new FileInputStream(new File(BasePath+name));
//通过输出流输出显示到浏览器
ServletOutputStream outputStream=response.getOutputStream();
response.setContentType("image/ipeg");
int len=0;
byte[] bytes=new byte[1024];
while ((len=inputStream.read(bytes))!=-1){
outputStream.write(bytes);
outputStream.flush();
}
//关闭资源
inputStream.close();
outputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 11. DTO(数据传输对象)
在添加菜品的时候的数据比较复杂 里面还有一个实体类的数据(口味表flavor) 所以我们在controller写参数的时候就不能简单地写其中一个实体类
解决方案: 写一个实体类封装我们页面提交的数据,DTO,全称为 Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输 其实就是当我们定义的实体类不能满足我们的开发时,可以自己封装对应的数据对象来满足数据传输要求
显然,分页查询范围Dish这种类型的数据是不对的,所以就可以用到前面提到的DTO
具体实现:
1.将原先的分页查询对象(dish)复制到新的分页查询对象(dishDto)中,复制的时候不复制records。
因为Page里面的record就是查出来的所有数据,而我们不想要第一个Dish查出来的(因为里面有一个不符合页面要求的数据) 所以就不拷贝这个数据。
2.将原先Page对象的records查出来的所有不带菜品分类名称的dish取出来处理一下加上菜品分类,再注入到DishDto所构造的分页查询中
3.最后返回新的分页查询对象
@GetMapping("/page")
public Result<Page<DishDto>> page(int page,int pageSize,String name){
log.info("分页查询:page-{} pageSize-{}",page,pageSize);
//构造分页构造器
Page<Dish> pageinfo=new Page<>(page,pageSize);
Page<DishDto> dishPageinfo=new Page<>(page,pageSize);
//构造条件构造器
QueryWrapper queryWrapper=new QueryWrapper<>();
//过滤
//首先要判断名字不为空才过滤
queryWrapper.like(StringUtils.isNotEmpty(name),"name",name);
//排序
queryWrapper.orderByDesc("update_time");
//查询
dishService.page(pageinfo,queryWrapper);
//对象拷贝,除了records,因为要先把categoryid处理成name,再放到dto的pageinfo中(把前者复制到后者,除了records)
BeanUtils.copyProperties(pageinfo,dishPageinfo,"records");
//把records取出来进行处理
List<Dish> records=pageinfo.getRecords();
List<DishDto> newRecords=new ArrayList<>();
for (Dish dish: records) {
//先取出id
Long categoryId=dish.getCategoryId();
//然后根据这个id查出name
Category category=categoryService.getById(categoryId);
String categoryName=category.getName();
//取出来之后放到DTO对象去,原来的数据也要拷贝过去
DishDto dishDto=new DishDto();
dishDto.setCategoryName(categoryName);
BeanUtils.copyProperties(dish,dishDto);
//最后把这些DTO对象放到list中
newRecords.add(dishDto);
}
//把records处理好之后,放到那个DTO的分页构造器上
dishPageinfo.setRecords(newRecords);
return Result.success(dishPageinfo);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 12. 事务注解
当一个方法设计多张表的操作,例如在添加菜品的时候还有设计添加口味所以要在该方法上添加事务注解
@Transactional
public void saveWithFlavor(DishDto dishDto){
//保存菜品的基本信息到菜品表
this.save(dishDto);
//保存口味到口味表
//但是由于口味只有name和value有值 所以缺少了菜品的id,因此我们应该添加上此id
Long dishId=dishDto.getId();
List<DishFlavor> list=dishDto.getFlavors();
for (DishFlavor dishFlavor: list) {
dishFlavor.setDishId(dishId);
}
dishFlavorService.saveBatch(list);
}
2
3
4
5
6
7
8
9
10
11
12
13
为了让该注解起作用还要在启动类上添加@EnableTransactionManagement