外卖
# 外卖
# 1.Spring封装的MD5
String password=DigestUtils.md5DigestAsHex("123456".getBytes());
# 2.路径通配符
/**表示匹配任意字符和任意层级
/*仅匹配当前层级的任意字符
# 3.使用@ControllerAdvice和@ExceptionHandler实现全局异常处理器
@ControllerAdvice可以全局配置所有controller,有该注解的类中的方法会应用到所有的controller
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
if (ex.getMessage().contains("Duplicate entry")) {
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 4.编写一个义务异常类
继承RuntimeException
public class CustomException extends RuntimeException {
public CustomException(String message) {
super(message);
}
}
2
3
4
5
# 5.修改对象转换器解决js处理精度损失问题
MappingJackson2HttpMessageConverter是springboot默认的消息转换器,使用@RequestBody注解时springboot会选择HttpMessageConverter接口的实现类来处理数据,调用read()方法读出并转化。
设置对象转换器,其中ObjectMapper是一个java对象和JSON之间序列化反序列化的处理类
@Component
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
在WebMvcConfigurer配置类中定义消息转换器,并把新定义对象转换器添加扩展其中
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
//扩展消息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器
mappingJackson2HttpMessageConverter.setObjectMapper(new JacksonObjectMapper());
converters.add(0,mappingJackson2HttpMessageConverter);
}
}
2
3
4
5
6
7
8
9
10
11
# 6.@PathVariable以及@RequestBody,使用Map接收请求体中的json数据
URI路径上的占位符参数(RESTFUL风格)
格式:"/user/51648"
使用注解@PathVariable接收
@PostMapping("/user/{id}")
public R<String> send(@PathVariable Long id) {}
2
路径参数
格式:"/page?page=1&pageSize=10"
直接在controller业务方法的形参接收,变量名要与路径参数变量一致,可以用实体类来接收,springboot会自动注入属性。
@GetMapping("/page")
public R<Page> page(Page<Category> pageinfo) {}
2
接收的前端对象数据放在请求体中
使用@RequestBody(一次请求只能由一个)接收,可以用实体类来接收,springboot会自动注入属性。
@PostMapping
public R<String> add(@RequestBody Category category) {}
2
如果没有合适的类来接收(类属性不匹配),那么使用Map来接收键值对
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session) {
String code = map.get("code").toString();
String phone = map.get("phone").toString();
}
2
3
4
5
# 7.公共字段自动填充@TableField和MetaObjectHandler
数据库表的某些共同属性字段在每次更新插入数据时都需要固定操作,比如创建时间,更新时间,更信用户id,MybatisPlus提供了@TableField注解和MetaObjectHandler进行自动填充装配。
在entity实体的填充属性中添加注解,INSERT表示插入时填充,UPDATE表示更新时填充
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill=FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
2
3
4
5
6
7
8
9
10
11
新建一个MetaObjectHandler实现类,并使用@Component或者@Configuration添加到spring容器中
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.getId());
metaObject.setValue("updateUser", BaseContext.getId());
}
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getId());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 8.ThreadLocal维护会话id
用户登录采用ThreadLocal保存用户信息,它是线程隔离的,每个请求发送到后端服务器由同一个线程负责处理业务,在某些地方获取不到session的情况下可以使用ThreadLocal进行存储信息
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setId(Long id) {
threadLocal.set(id);
}
public static Long getId() {
return threadLocal.get();
}
}
2
3
4
5
6
7
8
9
10
11
在过滤器位置保存下用户的id
# 9.过滤器
使用@WebFilter注解配置过滤器,urlpatterns设置为"/*"表示匹配过滤所有路径
使用spring工具类路径匹配器AntPathMatcher的match方法,可以忽略request获取到的上下文路径
如果匹配成功则doFilter放行,若不放行则通过response.getWriter().writer向前端打印字符串。
@WebFilter(filterName="loginCheckFilter" ,urlPatterns="/*")
public class LoginCheckFilter implements Filter {
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login"
};
boolean check = check(urls, request.getRequestURI());
if (check) {
filterChain.doFilter(request, response);
} else {
Object employee = request.getSession().getAttribute("employee");
if (employee != null) {
BaseContext.setId((Long) request.getSession().getAttribute("employee"));
filterChain.doFilter(request, response);
return ;
}
Object user = request.getSession().getAttribute("user");
if (user != null) {
BaseContext.setId((Long) request.getSession().getAttribute("user"));
filterChain.doFilter(request, response);
return;
}
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
}
}
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
38
# 10.文件上传下载
单独定义一个文件传输的controller,路径在spring.yml配置文件中设置键值对
# 上传
对form表单有如下要求:
- method="post" 采用post方式提交数据
- enctype="multipart/form-data" 采用multipart格式上传文件
- type="file" 使用input的file控件上传
后端Controller方法采用spring封装好的MultipartFile格式对象接收文件,并转存到服务器
注意这里如果后端转存的路径使用相对路径,则需要file.transferTo(file.getAbsoluteFile())转成绝对路径,该方法参数只能为绝对路径。
文件路径可以设置成linux的绝对路径/usr/local/img
# 下载
通过文件输入输出流回显到浏览器显示
文件输入流来获取文件位置进行读操作,数据读到一个byte[]数组中,然后文件输出流从数组中把数据写出来。
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {
//原始文件名
String originalFilename = file.getOriginalFilename();
String s = originalFilename.substring(originalFilename.lastIndexOf("."));
//UUID
String fileName= UUID.randomUUID().toString()+s;
File file1 = new File(basePath);
if (!file1.exists()) {
file1.mkdirs();
}
//将临时文件转存
try {
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
return R.success(fileName);
}
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
try {
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
ServletOutputStream outputStream = response.getOutputStream();
//设置响应的文件类型
response.setContentType("image/jpeg");
int len=0;
byte[] bytes = new byte[1024];
while ((len =fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes,0,len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
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
38
39
40
41
42
43
44
45
46
47
48
# 11.mybatis-plus数据库方法
MyBatis-Plus的Service服务常用方法
list():返回List<T>对象,相当于select * from
save(T object):insert添加插入一条T对象的记录
remove(queryWrapper):根据搜索条件删除该记录
removeByIds(List<Long>):删除id列表上所有id对应的记录
updateById(T object):根据object对象中的id字段匹配表记录,然后更新对象中的其他属性(如果object某个属性字段为空则不进行更新)
update(object,updateWrapper):根据updateWrapper搜索条件(相当于where)匹配记录,然后把object对象的属性更新到该条记录
getById(Long id):根据id查找表中的数据,返回一个实体对象
getOne(queryWrapper):根据搜索条件查询表,返回一个对象
count(queryWrapper):根据搜索条件查询表,返回记录的条数
# 12.LambdaUpdateWrapper和LambdaQueryWrapper
- LambdaUpdateWrapper
updateWrapper.eq相当于拼接where条件,第一个变量可以是boolean,第二个指明是类的哪个属性作为字段,第三个指明值。
updateWrapper.set相当于仅仅设置哪个字段,不更新全部。
整个sql语句拼接起来是update table set A=0 where 条件
LambdaUpdateWrapper<AddressBook> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(AddressBook::getUserId, BaseContext.getId());
updateWrapper.set(AddressBook::getIsDefault, 0);
2
3
- LambdaQueryWrapper
queryWrapper.orderByDesc表示根据哪个字段进行降序,一般可以根据更新时间
queryWrapper.orderByDesc模糊查询
使用queryWrapper1.in条件删除多个数据
queryWrapper1.in(SetmealDish::getSetmealId, ids); //ids是一个集合数组
setmealDishService.remove(queryWrapper1);
2
# 13.多表操作开启事务注解
某个类方法涉及到多表操作时,采用注解@Transactional开启事务回滚。
启动类上添加EnableTransactionManagement
@Slf4j
@SpringBootApplication
@ServletComponentScan //扫描过滤器
@EnableTransactionManagement
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
}
}
2
3
4
5
6
7
8
9
# 14.mybatis-plus分页拦截器
首先配置分页插件,交给Bean管理
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
2
3
4
5
6
7
8
9
然后构建分页构造器->查询构造器->调用Service的page()方法
其中分页构造器需要指明page和pageSize
page()方法中会把查询结果注入到分页构造器的records(本质是一个查询结果列表List<T>),pageinfo就是要的最终的结果。
public R<Page> page(int page, int pageSize, String name) {
Page<Employee> pageinfo = new Page<>(page, pageSize);
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
queryWrapper.orderByDesc(Employee::getUpdateTime);
//执行查询
employeeService.page(pageinfo, queryWrapper);
return R.success(pageinfo);
}
2
3
4
5
6
7
8
9
# 15.对象属性复制
BeanUtils中copyProperties的作用是将一个对象中的属性值赋值(拷贝)给另一个对象中对应的属性,其中属性类型和属性名都必须一致。
一般场景用于更新封装新的对象
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal, setmealDto);
2
# 16.使用@RequestParam转为特定数据类型接收
当url路径传过来的参数是一个数组时,有两种接收方式
路径:"/delete?ids=15161615,494848161"
直接用数组类型接收
public R<String> delete(Long[] ids)
或者使用@RequestParam注解,可以转为List集合类型接收
public R<String> delete(@RequestParam List<Long> ids)
# 17.集合构造stream
场景:用于把A类的List集合封装成具有更多业务属性的B类的List集合
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto SetmealDto = new SetmealDto();
BeanUtils.copyProperties(item, SetmealDto);
Long cId = item.getCategoryId();
Category category = categoryService.getById((cId));
SetmealDto.setCategoryName(category.getName());
return SetmealDto;
}).collect(Collectors.toList());
2
3
4
5
6
7
8
9
# 18.创建DTO对象接收前端复合的数据对象
前端想要菜品数据中,除了包含有菜品的基本信息Dish类,还要有这道菜的所有口味信息(菜品和口味两表联查),菜品的菜系类别。
可以选择重新封装一个DishDto类,包含前端要的菜品所有信息。核心思想就是前端要什么,就封装给什么。
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
2
3
4
5
6
7
# 19.全局处理统一返回类型R类
在日常开发中,如果根据业务定义各种各样的返回类型,不便于后期维护,也不便于前台进行数据处理。
因此可以封装一个统一返回类型(包含响应状态、返回数据、反馈信息msg等),并设置全局统一处理
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
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
# 20.短信发送
先生成校验码
public class ValidateCodeUtils {
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}
//随机生成指定长度字符串验证码
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
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
短信发送采用阿里云调用API进行发送,整个流程申请签名->申请模板(绑定好签名)->设置accesskeyid和密码->增加权限SMS
# 21.订单业务
IdWorker生成订单号
金额计算用BigDecimal/AtomicInteger原子并发类(实际上只要不是公共属性全局变量,那么局部变量不存在线程安全问题)
long orderId = IdWorker.getId();//单独设置订单号
AtomicInteger amount = new AtomicInteger(0);
//金额计算+拷贝购物车数据到订单详情中
List<OrderDetail> orderDetails=list.stream().map((item) -> {
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
2
3
4
5
6
7
8