全局异常处理
package com.xlm.reggie.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLIntegrityConstraintViolationException;
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.info(ex.getMessage());
if (ex.getMessage().contains("Duplicate entry")) {
String[] s = ex.getMessage().split(" ");
return R.error(s[2] + "已存在");
}
return R.error("未知错误");
}
}
静态资源映射
package com.xlm.reggie.comfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
3.用户登录(session存储登录信息)
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
/**
1.把前端提交的密码进行md5 加密处理
2.更具用户名查数据库
3.没查到就返回失败
4.密码比对(比对不成功返回错误信息)
5.查看员工状态,1可用 0禁用
6.登陆成功 再把id存入session
*/
//1.把前端提交的密码进行md5 加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//2.更具用户名查数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);
//3.没查到就返回失败
if (emp == null){
return R.error("用户名不存在");
}
//4.密码比对(比对不成功返回错误信息)
if (!emp.getPassword().equals(password)){
return R.error("密码错误!");
}
//5.查看员工状态,1可用 0禁用
if (emp.getStatus() == 0){
return R.error("账号已禁用!");
}
//6.登陆成功 再把id存入session
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}
4.拦截器拦截未登录
package com.xlm.reggie.filter;
/**
* 检查用户是否已经完成登录
*/
@Slf4j
@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;
//1.获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("拦截到请求 {}",requestURI);
String[] urls = new String[]{ //定义不需要被拦截的请求
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
//2.判断本次请求是否需要处理
boolean check = check(urls, requestURI);
//3.如果不需要处理,则直接放行
if (check){
log.info("本次请求{}不需要处理", requestURI);
filterChain.doFilter(request,response);
return;
}
//4.判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
//5.如果未登录则返回未登录结果,通过输出流方式像客户端相应数据
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
// log.info("拦截到请求: {} ", request.getRequestURI());
// filterChain.doFilter(request, response);
}
public boolean check(String[] urls, String requestURI) {
for (String url : urls) {
//把浏览器发过来的请求和我们定义的不拦截的url做比较,匹配则放行
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {
return true;
}
}
return false;
}
}
5.Reult格式
package com.xlm.reggie.common;
/**
* 通用返回结果,服务端返回数据将会封装此对象
* @param <T>
*/
@Data
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;
}
}
6.分页查询(需要先添加mp的分页插件,一个拦截器,返回Reult<Page>)
package com.xlm.reggie.comfig;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
7.类型转换器
步骤一:自定义消息转换类
package com.xlm.reggie.common;
/**
* 对象映射器:基于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);
}
}
步骤二:在前面的webMvcConfig 配置类中扩展spring mvc 的消息转换器,在此消息转换器中使用spring提供的对象转换器进行Java对象到json数据的转换;
相当于把我们创建的新转换器加入到2http那个类转换器中,在加入到converts转换器数据的第一位。
/**
* 扩展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);
}
8.公共字段自动填充
问题分析:
step1.在pojo对象中加入TableFiled注解
把相关的注解加在需要mybatis-plus自动帮我们填充的字段上面
@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;
step2.编写一个类实现,实现MetaObjectHandler接口
其中metaObject中会封装执行操作的pojo对象。
package com.xlm.reggie.common;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/***
* 自定义一个元数据对象处理器
*/
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser", BaseContext.get()); //这里的id是不能直接获取的,所以这里先写死,后面教你怎么动态获取员工id
metaObject.setValue("updateUser",BaseContext.get());
}
/**
* 更新操作,自动填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.get());
}
}
step3.编写BaseContxt类,内置有静态方法和变量,每个线程共用同一个。
package com.xlm.reggie.common;
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void set(Long id){
threadLocal.set(id);
}
public static Long get(){
return threadLocal.get();
}
}
step4.在之前的登录过滤器中保存当前用户id
//4.判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为:",request.getSession().getAttribute("employee"));
Long id = (Long) request.getSession().getAttribute("employee");
BaseContext.set(id);
filterChain.doFilter(request,response);
return;
}
9.自定义业务异常
step1.自定义异常类
package com.xlm.reggie.common;
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
step2 全局异常处理方法
package com.xlm.reggie.common;
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.info(ex.getMessage());
if (ex.getMessage().contains("Duplicate entry")) {
String[] s = ex.getMessage().split(" ");
return R.error(s[2] + "已存在");
}
return R.error("未知错误");
}
/**
* 异常处理方法
* @param ex
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.info(ex.getMessage());
return R.error(ex.getMessage());
}
}
step3.业务中抛出异常
@Override
public void remove(long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
//注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数
int count = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联了菜品,如果已经管理,直接抛出一个业务异常
if (count > 0){
//已经关联了菜品,抛出一个业务异常
throw new CustomException("当前分类项关联了菜品,不能删除");
}
//查询当前分类是否关联了套餐,如果已经管理,直接抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
//注意:这里使用count方法的时候一定要传入条件查询的对象,否则计数会出现问题,计算出来的是全部的数据的条数
int setmealCount = setmealService.count(setmealLambdaQueryWrapper);
if (setmealCount > 0){
//已经关联了套餐,抛出一个业务异常
throw new CustomException("当前分类项关联了套餐,不能删除");
}
//正常删除
super.removeById(id);
}
10.文件上传下载
step1.在配置文件添加路径参数
reggie:
path: D:\img\
step2 编写代码,文件上传 ,创建一个新的CommonController
package com.xlm.reggie.controller;
import com.xlm.reggie.common.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file) throws IOException {
//原始文件名称
String originalFilename = file.getOriginalFilename();
//使用uid重新生成文件名,防止文件名称重复造成文件覆盖
String subffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + subffix;
//若目录不存在,则创建
File dir = new File(basePath);
if (!dir.exists()){
dir.mkdirs();//目录不存在,则创建一个目录
}
file.transferTo(new File(basePath + fileName));
return R.success(fileName);
}
}
step3.文件下载
package com.xlm.reggie.controller;
import com.xlm.reggie.common.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.UUID;
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file) throws IOException {
//原始文件名称
String originalFilename = file.getOriginalFilename();
//使用uid重新生成文件名,防止文件名称重复造成文件覆盖
String subffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + subffix;
//若目录不存在,则创建
File dir = new File(basePath);
if (!dir.exists()){
dir.mkdirs();//目录不存在,则创建一个目录
}
file.transferTo(new File(basePath + fileName));
return R.success(fileName);
}
@GetMapping("/download")
public void dowload(String name, HttpServletResponse response) {
try {
FileInputStream in = new FileInputStream(new File(basePath + name));
ServletOutputStream out = response.getOutputStream();
//设置回写文件类型
response.setContentType("image/jpeg");
int len;
byte[] buffer = new byte[1024];
while ((len = in.read(buffer)) != -1){
out.write(buffer,0,len);
out.flush();
}
in.close();
out.close();
} catch (Exception e){
e.printStackTrace();
}
}
}
11.Dto类的理解(类似于学生表-课程表-成绩表)
step1.
setmeal(套餐类):setmeal_id
dish(餐品类):dish_id
setmeal_dish(关联表):setmeal_id、dish_id、id
其中一个套餐包含多个餐品,加入新增一个套餐,也要包含一个List<SetmealDish>、当将setmeal和list<SetmealDish>信息传入后端时,没有可以接受的类,因此创建一个setmealDto类,它继承setmeal而且还给他添加一个属性list<SetmealDish>,这样就可以传入后端了。
@Data
public class SetmealDto extends Setmeal {
private List<SetmealDish> setmealDishes;
private String categoryName;
}
step2.但是还需要创建setmeal、dish、setmeal_dish对应的service和mapper,但是controller层只需要setmeal和dish,在conroller层引入他本身的SetmealService和他们的关联类的SetmealDishService即可,如下图所示。
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private CategoryService categoryService;
@Autowired
private SetmealDishService setmealDishService;
}
step3.此外,一般还需要在setmealService新建方法来保存数据,因为系统自带的参数是Setmeal而不是SetmealDto
public interface SetmealService extends IService<Setmeal> {
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto);
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
public void removeWithDish(List<Long> ids);
}
step4.实现SetmealService中的新增方法,我们知道传过来的参数为setmealDto类型,他包含setmeal和List<SetmealDish>,因此只需要两步 1. 在setmeal中添加数据(教师类)setmeal 2.在setmealDto(教师-学生类)中添加数据List<SetmealDish> 。 由于List<SetmealDish>中没有自带id,因此要把setmealDto中封装的id取出来给SetmealDto用。
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {
@Autowired
private SetmealDishService setmealDishService;
@Override
public void saveWithDish(SetmealDto setmealDto) {
//1.保存套餐基本信息,操作setmeal,执行insert操作
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
//2.保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
}
step5.在函数头上添加@Transactional 和启动类添加@EnableTransactionManagement
12.套餐信息分页查询(续11.Dto的理解)
当搜索框输入13是,会传递以下分页请求.
在我们的setmealDto类中不仅有setmeal_dish的集合,而且还有继承setmeal的全属性。还有我们新增的categoryName属性,这个是菜品分类名,是category中才有的属性,category中的categoryid和setmeal中的categoryid将这两个表关联起来。
step1.未优化版本
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> select(Integer page, Integer pageSize, String name) {
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
Page<SetmealDto> pageInfoDto = new Page<>(page, pageSize);
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
//添加查询条件,根据name进行模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo);
return null;
}
可以看出此时没有套餐分类的数据。这个数据在category类中,通过categoryid相关联。因此要引入categoryService来通过id返回Category对象,在找到该对象的名称即可。
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
//对象拷贝A
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
//分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}
setmealService.page(pageInfo,queryWrapper);执行前
执行完分页查询之后,records中就有数据了。
13.批量删除
单个删除和批量删除时的请求
删除的时候不仅要删setmeal表还要删除setmeal_dish这个关联表。
因此需要自己创建service函数去删除。
/**
* 删除套餐,同时需要删除套餐和菜品的关联数据
* @param ids
*/
@Transactional
public void removeWithDish(List<Long> ids) {
//select count(*) from setmeal where id in (1,2,3) and status = 1
//查询套餐状态,确定是否可用删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);
int count = this.count(queryWrapper);
if(count > 0){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}
//如果可以删除,先删除套餐表中的数据---setmeal
this.removeByIds(ids);
//delete from setmeal_dish where setmeal_id in (1,2,3)
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
//删除关系表中的数据----setmeal_dish
setmealDishService.remove(lambdaQueryWrapper);
}
14.操作Redis
Redis是一个基于内存的key-value数据库
step1.导入坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
step2.设置Redis相关配置,在spring配置下
spring:
redis:
host: localhost
port: 6379
#password: 123456
database: 0 #默认操作0号数据库,配置文件中默认有16个数据库 0 - 15号
jedis:
pool:
max-active: 8 #最大连接池
max-wait: 1ms #连接池中最大阻塞时间
max-idle: 4 #连接池中最大空闲连接
min-idle: 0 #连接池中最小空闲连接
step3.对于key-value的操作全靠一个RedisTemplete类来实现,添加配置类。这样我们注入的RedisTemplete就不是默认的,默认的序列化器不适合我们,对于key的默认序列化器是JdkSerializationRedisSerializer,在图形界面为乱码,因此需要设置成新的new StringRedisSerializer()。这样自动装配的RedisTemplate对象就是我们自己的。
/**
* Redis配置类
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//默认的Key序列化器为:JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
}
step4.测试
package com.xlm.springdataredis;
@SpringBootTest
@RunWith(SpringRunner.class)
class SpringdataredisApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
/**
* 操作String类型数据
*/
@Test
public void testString(){
redisTemplate.opsForValue().set("city123","beijing");
String value = (String) redisTemplate.opsForValue().get("city123");
System.out.println(value);
redisTemplate.opsForValue().set("key1","value1",10l, TimeUnit.SECONDS);
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("city1234", "nanjing");
System.out.println(aBoolean);
}
/**
* 操作Hash类型数据
*/
@Test
public void testHash(){
HashOperations hashOperations = redisTemplate.opsForHash();
//存值
hashOperations.put("002","name","xiaoming");
hashOperations.put("002","age","20");
hashOperations.put("002","address","bj");
//取值
String age = (String) hashOperations.get("002", "age");
System.out.println(age);
//获得hash结构中的所有字段
Set keys = hashOperations.keys("002");
for (Object key : keys) {
System.out.println(key);
}
//获得hash结构中的所有值
List values = hashOperations.values("002");
for (Object value : values) {
System.out.println(value);
}
}
/**
* 操作List类型的数据
*/
@Test
public void testList(){
ListOperations listOperations = redisTemplate.opsForList();
//存值
listOperations.leftPush("mylist","a");
listOperations.leftPushAll("mylist","b","c","d");
//取值
List<String> mylist = listOperations.range("mylist", 0, -1);
for (String value : mylist) {
System.out.println(value);
}
//获得列表长度 llen
Long size = listOperations.size("mylist");
int lSize = size.intValue();
for (int i = 0; i < lSize; i++) {
//出队列
String element = (String) listOperations.rightPop("mylist");
System.out.println(element);
}
}
/**
* 操作Set类型的数据
*/
@Test
public void testSet(){
SetOperations setOperations = redisTemplate.opsForSet();
//存值
setOperations.add("myset","a","b","c","a");
//取值
Set<String> myset = setOperations.members("myset");
for (String o : myset) {
System.out.println(o);
}
//删除成员
setOperations.remove("myset","a","b");
//取值
myset = setOperations.members("myset");
for (String o : myset) {
System.out.println(o);
}
}
/**
* 操作ZSet类型的数据
*/
@Test
public void testZset(){
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
//存值
zSetOperations.add("myZset","a",10.0);
zSetOperations.add("myZset","b",11.0);
zSetOperations.add("myZset","c",12.0);
zSetOperations.add("myZset","a",13.0);
//取值
Set<String> myZset = zSetOperations.range("myZset", 0, -1);
for (String s : myZset) {
System.out.println(s);
}
//修改分数
zSetOperations.incrementScore("myZset","b",20.0);
//取值
myZset = zSetOperations.range("myZset", 0, -1);
for (String s : myZset) {
System.out.println(s);
}
//删除成员
zSetOperations.remove("myZset","a","b");
//取值
myZset = zSetOperations.range("myZset", 0, -1);
for (String s : myZset) {
System.out.println(s);
}
}
/**
* 通用操作,针对不同的数据类型都可以操作
*/
@Test
public void testCommon(){
//获取Redis中所有的key
Set<String> keys = redisTemplate.keys("*");
for (String key : keys) {
System.out.println(key);
}
//判断某个key是否存在
Boolean itcast = redisTemplate.hasKey("itcast");
System.out.println(itcast);
//删除指定key
redisTemplate.delete("myZset");
//获取指定key对应的value的数据类型
DataType dataType = redisTemplate.type("myset");
System.out.println(dataType.name());
}
}
15.redis缓存短信验证码
step1.在UserController的sendMsg中加入RedisTemplete
//Redis中保存验证码,时效5min
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
step2.在login
//从redis中获取验证码
Object codeInRedis = redisTemplate.opsForValue().get(phone);
step3.登录成功后删除
package com.itheima.reggie.controller;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 发送手机短信验证码
* @param user
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
//获取手机号
String phone = user.getPhone();
if(StringUtils.isNotEmpty(phone)){
//生成随机的4位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code={}",code);
//调用阿里云提供的短信服务API完成发送短信
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);
//需要将生成的验证码保存到Session
// session.setAttribute(phone,code);
//Redis中保存验证码,时效5min
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
return R.success("手机验证码短信发送成功");
}
return R.error("短信发送失败");
}
/**
* 移动端用户登录
* @param map
* @param session
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session){
log.info(map.toString());
//获取手机号
String phone = map.get("phone").toString();
//获取验证码
String code = map.get("code").toString();
//从Session中获取保存的验证码
//Object codeInSession = session.getAttribute(phone);
//从redis中获取验证码
Object codeInRedis = redisTemplate.opsForValue().get(phone);
//进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
//if(codeInSession != null && codeInSession.equals(code))
if(true){
//如果能够比对成功,说明登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if(user == null){
//判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user",user.getId());
redisTemplate.delete(phone);
return R.success(user);
}
return R.error("登录失败");
}
}
具体代码可以建仓库分支v1.0
16.redis缓存菜品分类
如下所示,点击每个category分类是不需要在查询数据库,保存到redis中即可
3- 11行和47-49行为新增代码,step1.新建key和value的形式 step2.从redis中获取 step3.如果存在。直接获取,如果不存在按原代码执行 step4.如果不存在,最后要保存到redis中。 step5.在save和update代码中删除redis,防止redis和刚更新的数据库数据不一致。
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//dish_1369456161516_1
List<DishDto> dishDtoList = null;
//从redis中获取缓存
dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
//如果存在,直接返回,不需要查询数据库
if(dishDtoList != null){
return R.success(dishDtoList);
}
//如果不存在,需要查询数据库,并存放在redis上
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);
//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(queryWrapper);
dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();//分类id
//根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品的id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
//SQL:select * from dish_flavor where dish_id = ?
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());
//如果不存在,需要查询数据库,并存放在redis上
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
return R.success(dishDtoList);
}
update 和save
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);
//清理所有缓存数据
Set dishes = redisTemplate.keys("dish_*");
redisTemplate.delete(dishes);
// //清理精确地redis缓存
// String key = "dish_" + dishDto.getCategoryId() + "_" + dishDto.getStatus();
// redisTemplate.delete(key);
return R.success("新增菜品成功");
}
17.Spring Cache
采用注解的方式来实现redis
17.1 Spring Cache介绍
Spring Cache是一个框架,实现了基本注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache提供了一层抽象,底层可以切换不同的cache实现,具体就是通过CacheManager接口来统一不同的缓存技术。
CacheManager是Spring提供的各种缓存技术抽象接口。
针对不同的缓存技术需要实现不同的CacheManager:
CacheManger | 描述 |
EhCacheCacheManager | 使用EhCache作为缓存技术 |
GuavaCacheManager | 使用Googke的GuavaCache作为缓存技术 |
RedisCacheManager | 使用Rdis作为缓存技术 |
17.2 Spring Cache常用注解
注解 | 说明 |
@EnableCaching | 开启缓存注解功能 |
@Cacheable | 在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或者多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需要在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存技术支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
17.3使用方法
step1. 导入坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
step 2.加入@EnableCaching注解
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
@EnableCaching
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功...");
}
}
step3.配置application.yml
server:
port: 8080
spring:
application:
#应用的名称,可选
name: reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 1234
redis:
host: localhost
port: 6379
#password: 123456
database: 0 #默认操作0号数据库,配置文件中默认有16个数据库 0 - 15号
jedis:
pool:
max-active: 8 #最大连接池
max-wait: 1ms #连接池中最大阻塞时间
max-idle: 4 #连接池中最大空闲连接
min-idle: 0 #连接池中最小空闲连接
cache:
redis:
time-to-live: 1800000 #单位ms 180w毫秒就是30min
mybatis-plus:
configuration:
#在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: ASSIGN_ID
reggie:
path: D:\img\
step4.修改代码
/**
* 新增套餐
* @param setmealDto
* @return
*/
@CacheEvict(value = "setmealCache",allEntries = true)
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
/**
* 删除套餐
* @param ids
* @return
*/
@CacheEvict(value = "setmealCache",allEntries = true)
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids:{}",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}
/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
}
18. MySQL读写分离
主从复制的介绍
Mysql主从复制是一个异步的复制过程,底层是基于Mysql数据库自带的二进制日志功能。就是一台或多台Mysql数据库(slave,即从库)从另一台Mysql数据库(master,即主库)进行日志的复制然后再解析日志并应用到自身,最终实现从库的数据和主库的数据保持一致,Mysql主从复制是Mysql数据库自带的功能,无需借助第三方工具。
MySQL复制过程分成三步:
master将改变记录到二进制日志(binary log)
slave将master的binary log拷贝到它的中继日志(relay log)
slave重做中继日志中的事件,将改变应用到自己的数据库中
其中从库可以有多个
19.Nginx
下载与安装
可以到Nginx官方网站下载Nginx的安装包,地址为:https://nginx.org/en/download.html
安装过程:
1、安装依赖包 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel
先yum install wget ,这样就能使用wget命令
2、下载Nginx安装包wget https://nginx.org/download/nginx-1.16.1.tar.gz
3、解压 tar -zxvf nginx-1.16.1.tar.gz
4、cd nginx-1.16.1
5、./configure --prefix=/usr/local/nginx
6、make && make install
安装好nginx
conf文件夹存放配置文件、html存放静态页面文件、logs存放日志文件、sbin存放脚本文件(比如启动停止Nginx等)。conf/nginx.cong 是nginx配置文件。sbin/nginx用于启动停止nginx服务
查看版本
检查配置文件正确性
启动和停止
在sbin目录下启动Nginx服务使用如下命令: ./nginx
如果启动的时候端口被占用了 我们可以:
sudo netstat -ntlp 查看运行中端口
sudo kill 进程id
然后再次启动服务就可以了
停止Nginx服务使用如下命令: ./nginx -s stop
启动完成后可以查看Nginx进程: ps -ef | grep nginx
要在电脑访问 我们先关闭防火墙: systemctl stop firewalld
这样我们直接 访问linux的静态ip就可以了
由于nginx命令这些都太枯燥,我也听不下去,就不写了。可以看其他人的博客,啊呜呜
20.Nginx的具体应用
部署静态资源
反向代理
负载均衡
1.部署静态资源
nginx服务器部署静态资源要比Tomcat更加高效。只需要将静态资源放进html文件夹中即可。
默认访问80端口,也可以在conf文件中修改端口号。
2.正向代理和反向代理
正向代理
Nginx 不仅可以做反向代理,实现负载均衡。还能用作正向代理来进行上网等功能。 正向代理:如果把局域网外的 Internet 想象成一个巨大的资源库,则局域网中的客户端要访 问 Internet,则需要通过代理服务器来访问,这种代理服务就称为正向代理。
简单一点:通过代理服务器来访问服务器的过程 就叫 正向代理。
需要在客户端配置代理服务器进行指定网站访问
反向代理
反向代理,其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问。
我们只 需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返 回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器 地址,隐藏了真实服务器 IP 地址。
客户端可以知道正向代理服务器的存在,而反向代理客户端并不知道真正目标服务器的存在。
客户端以为反向代理服务器就是自己的目标服务器。
配置反向代理
只需要添加一句转发的话
3.负载均衡
也相当于反向代理,但代理的不是一台服务器,而是多台服务器,负载均衡器内置算法,将客户端命令转发给web服务器
负载均衡配置
weight表示权重,表示分发到这个web服务器的概率权重。默认为1
负载均衡策略
21.前后端分离
1.YApi的介绍
主要是制作api文档,了解即可
2.Swagger介绍
使用Swagger你只需要安装它的规范去定义接口以及接口相关的信息,再通过Swagger衍生出来的一些列项目和工具就可以做到生成各种格式的接口文档,以及在线接口调试页面等
使用方式
step1.导入meven坐标
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
step2.导入knife4j相关配置(WebMvcConfig)
添加两个注解 + 两个静态资源映射 + 两个方法
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始进行静态资源映射...");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 扩展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);
}
@Bean
public Docket createRestApi() {
// 文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
step3.定义不需要拦截器处理
后四个为新添加的
//定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login",
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
};
step4.访问 http://localhost:8080/doc.html
注解介绍:
为了解决上述的问题,Swagger提供了很多的注解,通过这些注解,我们可以更好更清晰的描述我们的接口,包含接口的请求参数、响应数据、数据模型等。核心的注解,主要包含以下几个:
@RestController
@RequestMapping("/setmeal")
@Slf4j
@Api(tags = "套餐相关接口")
public class SetmealController {
@CacheEvict(value = "setmealCache",allEntries = true)
@PostMapping
@ApiOperation(value = "新增套餐接口")
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
@Data
@ApiModel("返回结果")
public class R<T> implements Serializable{
/**
* 套餐
*/
@Data
@ApiModel("套餐")
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键")
private Long id;
//分类id
@ApiModelProperty("分类id")
private Long categoryId;
//套餐名称
@ApiModelProperty("套餐名称")
private String name;
//套餐价格
@ApiModelProperty("套餐价格")
private BigDecimal price;
//状态 0:停用 1:启用
@ApiModelProperty("状态")
private Integer status;
//编码
@ApiModelProperty("套餐编号")
private String code;
//描述信息
@ApiModelProperty("描述信息")
private String description;
//图片
@ApiModelProperty("图片")
private String image;
@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;
}
@GetMapping("/page")
@ApiOperation(value = "套餐分页查询接口")
@ApiImplicitParams({
@ApiImplicitParam(name = "page",value = "页码",required = true),
@ApiImplicitParam(name = "pageSize",value = "每页记录数",required = true),
@ApiImplicitParam(name = "name",value = "套餐名称",required = false)
})
public R<Page> page(int page,int pageSize,String name){
//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>();
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
//对象拷贝
BeanUtils.copyProperties(item,setmealDto);
//分类id
Long categoryId = item.getCategoryId();
//根据分类id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null){
//分类名称
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);
return R.success(dtoPage);
}