1.全局异常处理器
spring mvc架构中各层会出现大量的try{...} catch{...} finally{...}代码块,不仅有大量的冗余代码,而且还影响代码的可读性。这样就需要定义个全局统一异常处理器,以便业务层再也不必处理异常。
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 捕获业务异常 * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(BaseException ex){ log.error("异常信息:{}", ex.getMessage()); return Result.error(ex.getMessage()); } }springboot优雅的做全局异常处理(完整代码+已运行使用)_yuqueglobalexceptionhandler extends commonexceptio-CSDN博客
@Responsebody将对象转换为json返回
2. 定义消息返回接收对象
<T> Result<T><T>放在方法名前,这表示这些方法可以处理任何类型的数据。
@Data public class Result<T> implements Serializable { private Integer Code;//编码,1成功 private String msg;//错误信息 private T data;//数据 public static <T> Result<T> success(){ Result<T> result = new Result<T>(); result.Code = 1 ; return result; } public static <T> Result<T> success(T object){ Result<T> result = new Result<>(); result.Code=1; result.data = object; return result; } public static <T> Result<T> error(String msg){ Result<T> result = new Result<>(); result.Code = 0; result.msg = msg; return result; } }3.登录校验
3.1 md5加密存储,md5是单向的,不可逆
password = DigestUtils.md5DigestAsHex(password.getBytes());
3.2 jwt
jwt 令牌用于登录校验,相当于一个身份,需要注意,令牌的生成和使用的秘钥需要一致
在登录操作中,需要给前端返回一个jwt
这一块使用jwt携带用户id,下发令牌,前端接收到令牌,保存在浏览器本地(移动端,电脑端均支持)每次前端发起请求,后端通过拦截器拦截,查看是否有id
3.2.1 jwt的使用
JWT 的使用也需要注意安全方面的问题。例如,不应该在 JWT 中存储敏感信息,因为其 payload 部分是可以被解码的
public class JwtUtil { /** * 生成jwt * 使用Hs256算法, 私匙使用固定秘钥 * * @param secretKey jwt秘钥 * @param ttlMillis jwt过期时间(毫秒) * @param claims 设置的信息 * @return */ public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) { // 指定签名的时候使用的签名算法,也就是header那部分 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成JWT的时间 long expMillis = System.currentTimeMillis() + ttlMillis; Date exp = new Date(expMillis); // 设置jwt的body JwtBuilder builder = Jwts.builder() // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) // 设置签名使用的签名算法和签名使用的秘钥 .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) // 设置过期时间 .setExpiration(exp); return builder.compact(); } /** * Token解密 * * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个 * @param token 加密后的token * @return */ public static Claims parseJWT(String secretKey, String token) { // 得到DefaultJwtParser Claims claims = Jwts.parser() // 设置签名的秘钥 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) // 设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } } //登录成功后,生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT( jwtProperties.getAdminSecretKey(), jwtProperties.getAdminTtl(), claims);注意此处的jwtProperties类,使用了
@ConfigurationProperties(prefix = "sky.jwt")将外部配置(如application.yml)里面的数据绑定到该类,employee是从service层返回的结果
3.2.2拦截器的使用
拦截器有两个步骤,一个是定义拦截器的设置,实现 HandlerInterceptor 接口,重写方法
/** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt * * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getAdminTokenName());//此处更改为你的token命名 //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:", empId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } }另一个则是注册拦截器
@Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; /** * 注册自定义拦截器 * * @param registry */ protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**")//添加拦截的路径 .excludePathPatterns("/admin/employee/login");//去掉无需拦截的路径 } }通过这两步就可以实现一个拦截器的基本使用
4.swagger的knife4j
开发阶段使用的框架,帮助后端开发人员进行接口测试
4.1 在配置类中加入Knife4j配置
/** * 通过knife4j生成接口文档 * @return */ @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; }4.2 设置静态资源映射
/** * 设置静态资源映射 * @param registry */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }4.3 常用注解
@Api(tags = "员工相关接口")//写在类上 @ApiOperation("员工登录")//写在方法上5.分页查询
5.1 基础使用
分页查询需要前端传递,查询的用户,页码,每页记录数,返回分页总数,还有每一页信息
这一块使用了mybatics提供的插件pagehelper,
注意此块返回类型必须使用Page<>类型,第一步使用的startPage使用底层的方法在 动态映射中自动添加limit字段
此处mapper的扫描路径应该在application.yml中定义
mybatis: #mapper配置文件 mapper-locations: classpath:mapper/*.xml5.2 日期序列化
这一块有两种方式,一种是在属性上加入注解@JsonFormat(pattern =
"yyy-MM-dd HH:mm")
第二种则是拓展springmvc 消息转化器,统一处理
//拓展springmvc 消息转化器 protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){ log.info("拓展消息转换器"); //创建一个消息转换器对象 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); //为消息转换器设置一个对象转换器,对象转换器可以将java对象序列化为json数据 converter.setObjectMapper(new JacksonObjectMapper()); //将自己的消息转化器加入容器 converters.add(0,converter); }package com.tlias.json; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; /** * 对象映射器:基于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_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm"; 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(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); } }6. redis
6.1 redis基础操作
6.2 redis在java里面的运用(spring data redis)
这里分三步
6.2.1 配置redis数据源
6.2.2 编写配置类,编写RedisTemplate对象
在application.yml中编写
6.2.3 通过RedisTemplate操作redis
需要先确保本地redis已经运行起来
创建配置对象,通过spring容器进行引入即可使用
@Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ log.info("开始创建redis模板对象。。。"); RedisTemplate redisTemplate = new RedisTemplate(); //设置redis连接工厂对象 redisTemplate.setConnectionFactory(redisConnectionFactory); //设置redis key序列化器 //防止保存到redis里面的键出现看不懂的情况 redisTemplate.setKeySerializer(new StringRedisSerializer()); return redisTemplate; } } @SpringBootTest public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate(){ ValueOperations valueOperations = redisTemplate.opsForValue(); valueOperations.set("color","white"); String color = (String) valueOperations.get("color"); System.out.println(color); } }6.3 redis缓存实践
每个菜品下的数据进行缓存
数据库菜品变更进行清除数据
@GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list(Long categoryId) { //构造redis中的key dish_分类id String key = "dish_" + categoryId; //查询redis中是否存在菜品数据 List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); //如果存在,直接返回,无须查询数据库 if (list != null && list.size()>0 ){ return Result.success(list); } Dish dish = new Dish(); dish.setCategoryId(categoryId); dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品 //如果不存在,查询数据库,将查询到的数据放入redis中 list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key,list); return Result.success(list); }//清理缓存数据 String key ="dish_" + dishDTO.getCategoryId(); cleanCache(key); private void cleanCache(String pattern){ Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); }6.3.1 Spring Cache
是一个框架,基于注解的缓存功能,spring Cache提供了一层抽象,底层可以切换不同的缓存实现
如(redis,caffeine,EHCache)
引入依赖
在启动类加入@EnableCaching 在相应的方法上加入相应注解
@PostMapping @CachePut(cacheNames = "userCache", key=#user.id) // key的生成 userCache::100 //一个冒号代表一层,即树形结构存储,一般用上面这一种方式 @CachePut(cacheNames = "userCache", key = #result.id) public User save(@RequestBody User user){ userMapper.insert(user); return user; } //删除全部数据 @CacheEvict(cacheNames = "userCache" , allEntries = true)7.httpClient
后端构造http请求,与okhttp类似(个人觉得okhttp比较好用)RestTemplate,相对而言这三个技术都是发送http请求
/** * Http工具类 */ public class HttpClientUtil { static final int TIMEOUT_MSEC = 5 * 1000; /** * 发送GET方式请求 * @param url * @param paramMap * @return */ public static String doGet(String url,Map<String,String> paramMap){ // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); String result = ""; CloseableHttpResponse response = null; try{ URIBuilder builder = new URIBuilder(url); if(paramMap != null){ for (String key : paramMap.keySet()) { builder.addParameter(key,paramMap.get(key)); } } URI uri = builder.build(); //创建GET请求 HttpGet httpGet = new HttpGet(uri); //发送请求 response = httpClient.execute(httpGet); //判断响应状态 if(response.getStatusLine().getStatusCode() == 200){ result = EntityUtils.toString(response.getEntity(),"UTF-8"); } }catch (Exception e){ e.printStackTrace(); }finally { try { response.close(); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } return result; } /** * 发送POST方式请求 * @param url * @param paramMap * @return * @throws IOException */ public static String doPost(String url, Map<String, String> paramMap) throws IOException { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); // 创建参数列表 if (paramMap != null) { List<NameValuePair> paramList = new ArrayList(); for (Map.Entry<String, String> param : paramMap.entrySet()) { paramList.add(new BasicNameValuePair(param.getKey(), param.getValue())); } // 模拟表单 UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList); httpPost.setEntity(entity); } httpPost.setConfig(builderRequestConfig()); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (Exception e) { throw e; } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } /** * 发送POST方式请求 * @param url * @param paramMap * @return * @throws IOException */ public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException { // 创建Httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null; String resultString = ""; try { // 创建Http Post请求 HttpPost httpPost = new HttpPost(url); if (paramMap != null) { //构造json格式数据 JSONObject jsonObject = new JSONObject(); for (Map.Entry<String, String> param : paramMap.entrySet()) { jsonObject.put(param.getKey(),param.getValue()); } StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8"); //设置请求编码 entity.setContentEncoding("utf-8"); //设置数据类型 entity.setContentType("application/json"); httpPost.setEntity(entity); } httpPost.setConfig(builderRequestConfig()); // 执行http请求 response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "UTF-8"); } catch (Exception e) { throw e; } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } private static RequestConfig builderRequestConfig() { return RequestConfig.custom() .setConnectTimeout(TIMEOUT_MSEC) .setConnectionRequestTimeout(TIMEOUT_MSEC) .setSocketTimeout(TIMEOUT_MSEC).build(); } }这一块是为了小程序请求后端之后,后端发送请求到微信接口服务,获取open_id,这里需要去查看微信官方文档
8.公共字段自动填充
8.1 获取当前用户登录id
客户端发起的每一次请求就是一个线程
ThreadLocal 不是Thread 而是Thread的局部变量
ThreadLocal为每一个线程提供单独存储空间,只有在线程内才能获取到对应的值
public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } }8.2具体实现
aop和拦截器类似
aop是面向切面编程(面向方法编程) ,比如统计每个业务方法的执行耗时
通知,切入点(对哪些类哪些方法进行拦截)
标识AutoFill注解
//用于标识某个方法需要进行公共字段自动填充,自定义注解 //指定注解加在哪里 @Target(ElementType.METHOD)//只能加在方法上 @Retention(RetentionPolicy.RUNTIME) public @interface AutoFIll { //数据库操作类型,update,insert,枚举类型 OperationType value(); }自定义切面
package com.sky.aspect; import com.sky.annotation.AutoFIll; import com.sky.constant.AutoFillConstant; import com.sky.context.BaseContext; import com.sky.enumeration.OperationType; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.LocalDateTime; //自定义切面类型,实现公共字段自动填充 @Aspect @Component @Slf4j public class AutoFillAspect { //定义切入点 //annotation 同时满足加入AutoFill @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFIll)") public void autoFillPointCut(){} //前置通知,为公共字段赋值 //指定切入点 @Before("autoFillPointCut()") //joinPoint连接点,什么方法被拦截到了,具体参数 public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { log.info("开始进行公共字段的自动填充"); //获取到当前被拦截的方法上的数据库操作类型 MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象 AutoFIll autoFIll = signature.getMethod().getAnnotation(AutoFIll.class);//获得方法上的注解对象 OperationType operationType = autoFIll.value(); //获取到当前被拦截的方法的参数--实体对象 Object[] args = joinPoint.getArgs(); if(args == null || args.length== 0){ return; } Object entity =args[0]; //准备赋值的数据 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); //根据当前不同的操作类型,为对应的属性通过反射进行赋值 if (operationType == OperationType.INSERT){ //为四个公共字段赋值 Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setCreateUser.invoke(entity,currentId); setCreateTime.invoke(entity,now); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } else if (operationType ==OperationType.UPDATE) { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } } }9.比较杂的点
这一块使用了第一点的全局异常处理器,处理sql语句异常,应关注里面,split方法的使用
@RestController = @Controller + @ResponseBody
@ResponseBody使用json返回对象 当前端传回来的数据是json时候,在接收对象前使用@RequestBody
路径参数需要多加@PathVariable
@ConfigurationProperties(prefix = )将application.yml绑定到该类中
@Component //该注解将外部配置绑定到当前类 @ConfigurationProperties(prefix = "sky.jwt") @Data public class JwtProperties { /** * 管理端员工生成jwt令牌相关配置 */ private String adminSecretKey; private long adminTtl; private String adminTokenName; /** * 用户端微信用户生成jwt令牌相关配置 */ private String userSecretKey; private long userTtl; private String userTokenName; }aop面向方法编程,面向特定方法编程,比如统计每个业务方法的执行耗时
//使其符合前端的yyyy-MM-dd
@DateTimeFormat(pattern = "yyyy-MM-dd")
//日期计算,计算指定日期的后一天对应日期
begin =begin.plusDays(1);
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);精确到分秒
//把datelist集合里面的元素取出来,加上,分隔,变成字符串
.dateList(StringUtils.join(dateList,","))
10.Spring Task
可以按照某个约定的时间自动执行某段程序 ,定时任务框架
cron表达式 直接使用cron表达式在线生成器
解决用户一直待支付状态,以及订单一直处于待派送的状态
@Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper; //处理超时订单方法 @Scheduled(cron = "0 * * * * ?")//每分钟触发一次 public void processTimeoutOrder(){ log.info("定时处理超时订单,{}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); //select * from orders where status = ? and order_time < (当前时间-15) List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time); if (ordersList != null && ordersList.size()>0){ for (Orders orders : ordersList){ orders.setStatus(Orders.CANCELLED); orders.setCancelReason("订单超时,自动取消"); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); } } } //处理一直处于派送中状态的订单 @Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点触发一次 public void processDeliveryOrder(){ LocalDateTime time = LocalDateTime.now().plusMinutes(-60); log.info("定时处理处于派送中的订单,{}",LocalDateTime.now()); List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time); if (ordersList != null && ordersList.size()>0){ for (Orders orders : ordersList){ orders.setStatus(Orders.COMPLETED); orderMapper.update(orders); } } } }11.WebSocket
http为请求响应模式,单向,短连接
websocket为长连接,双向
http和websocket都是基于tcp
使用场景:视频弹幕,网页聊天,实时
在外卖系统中,ws:localhost并没有指定端口号,是因为在naginx里面已经进行了配置
步骤为三步
1.导入maven
2.导入服务端组件webSocketServer
3.配置类WebSocketConfiguration,注册组件
package com.sky.websocket; import org.springframework.stereotype.Component; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * WebSocket服务 */ @Component @ServerEndpoint("/ws/{sid}") public class WebSocketServer { //存放会话对象 //多个连接 private static Map<String, Session> sessionMap = new HashMap(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { System.out.println("客户端:" + sid + "建立连接"); sessionMap.put(sid, session); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } /** * 连接关闭调用的方法 * * @param sid */ @OnClose public void onClose(@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } /** * 群发 * * @param message */ public void sendToAllClient(String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { //服务器向客户端发送消息 session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }package com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置类,用于注册WebSocket的Bean */ //ServerEndPointExporter用于自动注册使用@ServerEndpoint注解的WebSocket端点。使得客户端可以通过WebSocket连接到这个端点。 @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }12.Apache ECharts
基于JavaScript的可视化图表库(用直观的图展示数据)
后端提供符合格式要求的动态数据,响应给前端展示图表
@Autowired private ReportService reportService; //@GetMapping("/turnoverStatistics") @ApiOperation("营业额统计") public Result<TurnoverReportVO> turnoverStatistics( //使其符合前端的yyyy-MM-dd @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin , @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end ){ log.info("营业额数据统计,{},{}",begin,end); return Result.success(reportService.getTurnoverStatics(begin,end)); } public TurnoverReportVO getTurnoverStatics(LocalDate begin, LocalDate end) { //当前集合用于存放从begin到end范围内的每天日期 List<LocalDate> dateList = new ArrayList<>(); dateList.add(begin); while (!begin.equals(end)){ //日期计算,计算指定日期的后一天对应日期 begin =begin.plusDays(1); dateList.add(begin); } List<Double> turnoverList = new ArrayList<>(); for (LocalDate date : dateList) { //查询date日期对应的营业额数据,状态为已完成的订单金额统计 LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); //select sum(amount) from orders where order_time > ? and order_time <? and status = 5 Map map = new HashMap(); map.put("begin",beginTime); map.put("end",endTime); map.put("status", Orders.COMPLETED); Double turnover = orderMapper.sumByMap(map); //防止返回的数据是null turnover = turnover == null ? 0.0 : turnover; turnoverList.add(turnover); } return TurnoverReportVO .builder() //把datelist集合里面的元素取出来,加上,分隔,变成字符串 .dateList(StringUtils.join(dateList,",")) .turnoverList(StringUtils.join(turnoverList,",")) .build(); } <select id="sumByMap" resultType="java.lang.Double"> select sum(amount) from orders <where> <if test="begin != null"> and order_time > #{begin} </if> <if test="end != null"> and order_time < #{end} </if> <if test="status != null"> and status = #{status} </if> </where> </select>13.Apache POI
步骤有 导入maven坐标,创建sheet页,sheet页创建行,创建单元格,setcellvalue写入数据,写入(输出流) ,关闭资源
13.1 导出excel报表
无请求参数,无返回数据,服务端通过输出流进行下载
1 设计模版,存放在resource目录下新建Template
2 查询近30天数据
3 POI写入
4 输出流下载到浏览器
@GetMapping("/export") @ApiOperation("导出运营数据报表") //这一块使用的httpServletResponse是为了获取输出流 public void export(HttpServletResponse response){ reportService.exportBusinessData(response); } public void exportBusinessData(HttpServletResponse response) { //查询数据库,获取营业数据,查询最近30天运营数据 LocalDate dateBegin = LocalDate.now().minusDays(30); LocalDate dateEnd = LocalDate.now().minusDays(1); //查询概览数据 BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX)); //通过POI将数据写入到excel //类路径中获取资源文件作为输入流 InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx"); try{ XSSFWorkbook excel = new XSSFWorkbook(in) ; //获取表格文件的sheet页 XSSFSheet sheet = excel.getSheet("sheet1"); //填充数据--时间 sheet.getRow(1).getCell(1).setCellValue("时间"+dateBegin+"至"+dateEnd); //获得第四行 XSSFRow row = sheet.getRow(3); row.getCell(2).setCellValue(businessDataVO.getTurnover()); row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate()); row.getCell(6).setCellValue(businessDataVO.getNewUsers()); //获得第五行 row = sheet.getRow(4); row.getCell(2).setCellValue(businessDataVO.getValidOrderCount()); row.getCell(4).setCellValue(businessDataVO.getUnitPrice()); //填充明细数据 for (int i=0;i<30;i++){ LocalDate date = dateBegin.plusDays(1); //查询某一天的营业数据 BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); row = sheet.getRow(7 + i); row.getCell(1).setCellValue(date.toString()); row.getCell(2).setCellValue(businessData.getTurnover()); row.getCell(3).setCellValue(businessData.getValidOrderCount()); row.getCell(4).setCellValue(businessData.getOrderCompletionRate()); row.getCell(5).setCellValue(businessData.getUnitPrice()); row.getCell(6).setCellValue(businessData.getNewUsers()); } //通过输出流将excel下载到客户端浏览器 ServletOutputStream out = response.getOutputStream(); excel.write(out); //关闭资源 out.close(); excel.close(); }catch (IOException e){ e.printStackTrace(); } }