# 代码片段笔记 **Repository Path**: BoomMiHua/code-snippet-notes ## Basic Information - **Project Name**: 代码片段笔记 - **Description**: 代码片段笔记 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 7 - **Forks**: 2 - **Created**: 2021-01-15 - **Last Updated**: 2025-05-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 代码片段笔记 ## 正则表达式 ### `js`正则替换 `js正则的替换规则: 第一个参数没有引号 以/开头 以/结尾,第二个参数有引号` ```javascript new Date().toJSON().substr(0,8)+'01'; 当月的第一天"2019-03-01" new Date().toJSON().substr(0,10); 今天 "2019-03-11" new Date().toLocaleDateString(); 今天 "2019/3/11" new Date().toISOString().slice(0,10); 今天 "2019-03-11" new Date().toTimeString().slice(0,8) 今天的时间 "16:22:30" ``` ```javascript '-1.9.8.00....235...' .replace(/(-?\d+\.)((\.*\d*)+)/,'$1*$2') .replace(/\./g,'') .replace(/\*/g,'.'); "-1.9800235" ``` **正则匹配密码校验** 包含 **数字**,**字母**(不区分大小写),**特殊字符** 。(长度8-20位) `/^(?=.*\d)(?=.*[a-zA-Z])(?=.*[^\da-zA-Z])[\S]{8,20}$/` ### `Java`正则 ```java String pattern = "^\\d{6}$"; //第一步写正则 Pattern r = Pattern.compile(pattern); //第二步编译正则 Matcher m = r.matcher("12345"); //第三步匹配 System.out.println(m.matches()); m.matches(); //第四步取匹配结果 boolean型 ``` ```java if (!ReUtil.isMatch("^[a-z]+[a-z0-9_]*[a-z0-9]$",fieldName)) { throw new RuntimeException("存在字段名称不规范,请遵循数据库字段命名规则。"); } ``` -------------- ## `Java`相关代码 ### inputstream转base64 ```java try (InputStream inputStream = file.getInputStream()) { return ResponseEntity.ok(Base64.encodeBase64String(IOUtils.toByteArray(inputStream))); } catch (Exception e) { log.error("转换base64异常{}", e.getMessage()); return null; } ``` ### list中文排序 ```java List list = Arrays.asList("王五", "阿猫", "阿狗", "李四"); Collections.sort(list); System.out.println(list); list.sort((o1, o2) -> Collator.getInstance(Locale.CHINESE).compare(o1, o2)); System.out.println(list); ``` ```java List collect = respList.stream() .sorted( // 先根据 getTempSort 字段 倒叙 排 Comparator.comparing(KpiApproved::getTempSort,Comparator.reverseOrder()) // 再根据 getCustomId 字段 升序 排 .thenComparing(KpiApproved::getCustomId) // 再根据 getCheckedName 字段 倒叙(使用中文首字母排序方式) 排 .thenComparing(KpiApproved::getCheckedName,Collator.getInstance(Locale.CHINESE).reversed()) ).collect(Collectors.toList()); ``` ### java8日期类 ```java // Instant ---> Date long timestamp = o.getLong("timestamp"); //生成时间戳 秒级 Instant instant = Instant.ofEpochSecond(timestamp); //生成时间戳 毫秒级 Instant instant = Instant.ofEpochMilli(timestamp); Date createTime = Date.from(instant); --------------------------------------------------------------- // DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") // LocalDateTime.from(DateTimeFormatter) LocalDateTime localDateTime = DateUtil.parseLocalDateTimeFormatyMdHms(datetime); // LocalDateTime ---> Instant Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant(); Date dateTime = Date.from(instant); ``` ### 公共前缀 ```java /** * 获取部门集合的最长公共 CustomId * * @param list * @return */ public static String maxCommonCustomId(List list) { if (null == list || list.size() == 0) { return ""; } String ans = list.get(0).getCustomId(); for (int i = 1; i < list.size(); i++) { int j = 0; for (; j < ans.length() && j < list.get(i).getCustomId().length(); j++) { if (ans.charAt(j) != list.get(i).getCustomId().charAt(j)) { break; } } ans = ans.substring(0, j); if (ans.equals("")) { return ans; } } if (ans.length() % 2 == 0) { return ans.substring(0, ans.length() - 1); } return ans; } ``` ### 部门树重构 ```java /** * 部门树重构 * 根据 root部门 和 部门集合 构建部门树 * @param initList 部门集合 * @param parent root部门 * @return */ public static EaDept treeBuilder(List initList, EaDept parent){ for (EaDept it : initList) { if (parent.getId().equals(it.getId())) { continue; } if (parent.getId().equals(it.getParentid())) { if (parent.getChildren() == null) { ArrayList objects = new ArrayList<>(); objects.add(treeBuilder(initList,it)); parent.setChildren(objects); }else { parent.getChildren().add(treeBuilder(initList,it)); } } } return parent; } ``` ### `@Transactional`细节 ```java //方法内不做try catch捕捉异常,可以正常回滚 @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public boolean decrementProductStoreLock(int goodsId, int buyNum) { return decrestock(goodsId, buyNum); } //异常时不处理,会导致不回滚 @Override @Transactional(rollbackFor={Exception.class}) public boolean decrementProductStoreLockWithCatch(int goodsId, int buyNum) { try { return decrestock(goodsId, buyNum); } catch (Exception e) { System.out.println("捕捉到了异常in service method"); e.printStackTrace(); String errMsg = e.getMessage(); //throw e; return false; } } //异常时手动抛出异常,可以正常回滚 @Override @Transactional(rollbackFor={Exception.class}) public boolean decrementProductStoreLockWithCatch1(int goodsId, int buyNum) { try { return decrestock(goodsId, buyNum); } catch (Exception e) { System.out.println("捕捉到了异常in service method"); e.printStackTrace(); String errMsg = e.getMessage(); throw e; } } //异常时手动rollback,可以正常回滚 @Override @Transactional(isolation = Isolation.REPEATABLE_READ) public boolean decrementProductStoreLockWithCatch2(int goodsId, int buyNum) { try { return decrestock(goodsId, buyNum); } catch (Exception e) { System.out.println("捕捉到了异常in service method"); e.printStackTrace(); String errMsg = e.getMessage(); //手动rollback TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); return false; } } ``` ### 数字前面自动补零 ```java /** * 数字前面自动补零 * @param number 数字 * @param length 补零后的长度 * @return */ public static String formatNumber(int number,int length){ NumberFormat formatter = NumberFormat.getNumberInstance(); formatter.setMinimumIntegerDigits(length); formatter.setGroupingUsed(false); return formatter.format(number); } ``` ### 全局排除`logback`依赖 ```xml org.springframework.boot spring-boot-starter-logging * * org.springframework.boot spring-boot-starter-log4j2 com.fasterxml.jackson.dataformat jackson-dataformat-yaml ``` ### 实体驼峰转成下划线 ```java public class FastJsonConfig { public static SerializeConfig fastJsonConfig; static { fastJsonConfig = new SerializeConfig(); fastJsonConfig.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase; } /** * 将实体类 的驼峰转成 下划线的方式 * * @param data 数据 * @return {@link String} */ public static String getJsonStr(Object data){ return JSONObject.toJSONString(data, fastJsonConfig); } } ``` ### 集合分页流 ```java /** * 数据集合分页 * 依赖 hutool-core * * @param req 数据集合 * @param pageNo 当前页 * @param pageSize 每页大小 * @return {@link Stream} */ public static Stream pageStream(Collection req, int pageNo, int pageSize){ if (CollUtil.isEmpty(req)) { return Stream.empty(); } /* todo 关闭注释 就是默认返回有数据的最后一页 int totalPage = PageUtil.totalPage(req.size(), pageSize); if (pageNo > totalPage) { pageNo = totalPage; }*/ int start = PageUtil.getStart(pageNo - 1, pageSize); return req.stream().skip(start).limit(pageSize); } /** * 数据集合分页 直接返回JsonPage * @jsonKey pageSize 每页大小 * @jsonKey pageTotal 总页数 * @jsonKey totalSize 总数据集合大小 * @jsonKey currentPage 当前页 * @jsonKey records 数据集合List * 依赖 hutool-core * * @param req 数据集合 * @param pageNo 当前页 * @param pageSize 每页大小 * @return {@link JSONObject} */ public static JSONObject pageJson(Collection req, int pageNo, int pageSize){ JSONObject jsonObject = new JSONObject(); int pageTotal = 0, currentPage = 0 ,totalSize = 0; Collection records; if (CollUtil.isEmpty(req)) { records = Collections.emptyList(); }else { totalSize = req.size(); pageTotal = PageUtil.totalPage(totalSize, pageSize); currentPage = Math.min(pageNo, pageTotal); int start = PageUtil.getStart(currentPage - 1, pageSize); records = req.stream().skip(start).limit(pageSize).collect(Collectors.toList()); } jsonObject.put("pageSize",pageSize); jsonObject.put("totalSize",totalSize); jsonObject.put("pageTotal",pageTotal); jsonObject.put("currentPage",currentPage); jsonObject.put("records",records); return jsonObject; } ``` - 集合分页流使用Demo `List collect = MyUtil.pageStream(req, pageNo, 20).collect(Collectors.toList());` - 集合分页`pageJson`方法是使用 `JSONObject jsonObject = MyUtil.pageJson(req, 16, 20);` ## `springboot` ### 多线程配置 ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ThreadPoolExecutor; @Configuration public class TranslationThreadPoolConfig { // 核心线程池大小 private int corePoolSize = 20; // 最大可创建的线程数 private int maxPoolSize = 50; // 队列最大长度 private int queueCapacity = 1000; // 线程池维护线程所允许的空闲时间 private int keepAliveSeconds = 300; //线程名称前缀 private String threadNamePrefix = "线程名称前缀"; @Primary @Bean(name = "translationThreadPoolTaskExecutor") public ThreadPoolTaskExecutor translationThreadPoolTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setMaxPoolSize(maxPoolSize); executor.setCorePoolSize(corePoolSize); executor.setQueueCapacity(queueCapacity); executor.setKeepAliveSeconds(keepAliveSeconds); executor.setThreadNamePrefix(threadNamePrefix); // 线程池对拒绝任务(无线程可用)的处理策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return executor; } } ``` `controller`内使用 ```java @Autowired @Qualifier("translationThreadPoolTaskExecutor") private Executor executor; @PutMapping(value = "/save") public ResponseEntity save(@RequestBody List kList) { try { //1.需要多线程遍历的list kList //2.多线程执行行转译 List> collect = kList.stream().map(it -> CompletableFuture.supplyAsync(() -> //调用别的方法 kpiApprovedService.saveHandleKpi(Collections.singletonList(it),user), executor) ).collect(Collectors.toList()); //3.获取返回值 //List list = collect.stream().map(CompletableFuture::join).collect(Collectors.toList()); return ResponseEntity.ok(new ResponseResult<>(new ResponseResult(HttpStatus.OK))); } catch (Exception e) { logger.error("出现异常:{}");); } } ``` ### `springboot`优雅统计耗时 ```java public static void main(String[] args) throws Exception { // 强烈每一个秒表都给一个id,这样查看日志起来能够更加的精确 StopWatch sw = new StopWatch("列表请求接口"); sw.start("获取当前用户"); Thread.sleep(1000); sw.stop(); sw.start("根据当前用户刷选权限sql"); Thread.sleep(2000); sw.stop(); sw.start("根据权限获取数据列表sql"); Thread.sleep(500); sw.stop(); logger.info(sw.prettyPrint()); // 这个方法打印在我们记录日志时是非常友好的 还有百分比的分析哦 } ``` jackjson序列化问题 ```java @JsonProperty(access = JsonProperty.Access.READ_ONLY) private Date updateTime; ``` - **READ_ONLY**修饰的字段仅支持**get方法**; - **WRITE_ONLY**修饰的字段仅支持**set方法**; 即: **WRITE_ONLY: 仅做反序列化操作。 READ_ONLY:仅做序列化操作。** ### Jackson注解Date转换 Date转换时间戳(字符串类型)自定义的类`DateTimestampOfString` ```java /** * 日期时间戳字符串 */ public class DateTimestampOfString extends StdSerializer { /** * 序列化 */ @Override public void serialize(Date value, JsonGenerator gen, SerializerProvider provider) throws IOException { gen.writeString(this.valueToString(value)); } public DateTimestampOfString() { super(Date.class); } public final String valueToString(Date value) { long time = value.getTime(); return String.valueOf(time); } } ``` **注解使用** ```java @JsonFormat(pattern = "YYYY") private Date timeOne; @JsonSerialize(using = DateTimestampOfString.class) private Date planLeaveTime; ``` **结果展示** ```json { "timeOne": "2021", // 这个使用的JsonFormat "planLeaveTime": "1621933399521" } ``` `@JsonProperty(access = JsonProperty.Access.READ_ONLY)` 和**@RequestBody** 注解的实体搭配使用--> updateTime前台传的字段有值,也能不接收 ```java @JsonProperty(access = JsonProperty.Access.READ_ONLY) private Date updateTime; // 即 不论前端是否传这个值 都是null ``` ```java @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private Date updateTime; // 这个字段始终不会传给前端 ``` ### `ResponseEntity`Demo > `ResponseEntity.ok`已经包含了返回`200`**Http**响应码,我们还可以通过`ResponseEntity.status(HttpStatus|int)`来自定义返回的响应码 **自定义响应头** ```java ResponseEntity.status(HttpStatus.OK) .body(Object) ``` **响应头** 通常我们指定**Spring MVC**接口的响应头是通过`@RequestMapping`和其**Restful**系列注解中的`header()`、`consumes`、`produces()`这几个属性设置。如果你使用了`ResponseEntity`,可以通过链式调用来设置: ```java ResponseEntity.status(HttpStatus.OK) .allow(HttpMethod.GET) .contentType(MediaType.APPLICATION_JSON) .contentLength(1048576) .header("My-Header","mark.cn") .build(); ``` 所有的标准请求头都有对应的设置方法,你也可以通过`header(String headerName, String... headerValues)`设置自定义请求头 > 通常让你写个下载文件接口都是拿到`HttpServletResponse`对象,然后配置好`Content-Type`往里面写流。如果用`ResponseEntity`会更加简单优雅。 ```java @AnonymousGetMapping("/download") public ResponseEntity load() { // ClassPathResource classPathResource = new ClassPathResource("logback.xml"); String level = "\"\",\"Niveles\",\"Paisaje\"\n\"1\",\"5:20\",\"Colinas,Llano,Veg media,Veg escasa\""; ByteArrayResource source =new ByteArrayResource(level.getBytes(StandardCharsets.UTF_8)); // String filename = classPathResource.getFilename(); String filename = "demo.txt"; HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentDisposition(ContentDisposition .builder("attachment") // if 5.2.12 // .builder("inline") // if 5.2.12 // .inline() // else 5.3.7 .filename(filename, StandardCharsets.UTF_8).build()); return ResponseEntity.ok() .headers(httpHeaders) // .body(classPathResource); .body(source); } ``` > 上面是一个把**Spring Boot**配置文件 `application.yml`下载下来的例子。主要分为三步: - 将要下载的文件封装成`org.springframework.core.io.Resource`对象,它有很多实现。这里用了`ClassPathResource`,其它`InputStreamResource`、`PathResource`都是常用的实现。 - 然后配置下载文件请求头`Content-Disposition`。针对下载它有两种模式: `inline`表示在浏览器直接展示文件内容;`attachment`表示下载为文件。另外下载后的文件名也在这里指定,请不要忘记**文件扩展名**,例如这里`application.yml`。如果不指定`Content-Disposition`,你需要根据文件**扩展名**设置对应的`Content-Type`,会麻烦一些。 - 最后是组装`ResponseEntity`返回。 ```java /** * 下载cer文件 * String --to--> ByteArrayResource * * @param id * @return * @throws Exception */ @GetMapping("cer/download/{id}") public ResponseEntity loadCer(@PathVariable("id") String id) throws Exception { Eacompany eacompany = eacompanyService.selectById(id); String certificate = eacompany.getCertificate(); InputStream inputStream = IOUtils.toInputStream(certificate); ByteArrayResource resource = new ByteArrayResource(IOUtils.toByteArray(inputStream)); HttpHeaders headers = new HttpHeaders(); headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); headers.add("Content-Disposition", "attachment; filename=" + id+".cer"); headers.add("Pragma", "no-cache"); headers.add("Expires", "0"); headers.add("Last-Modified", new Date().toString()); headers.add("ETag", String.valueOf(System.currentTimeMillis())); return ResponseEntity.ok().headers(headers).contentType(MediaType.parseMediaType("application/octet-stream")) .body(resource); } ``` ### `easyexcel`配合`ostermiller`下载zip 1. #### 使用第三方依赖 ```xml org.ostermiller utils 1.07.00 ``` 3. #### 在其他文件中使用**`CircularByteBuffer cbb = new CircularByteBuffer(CircularByteBuffer.INFINITE_SIZE);`** ```java @GetMapping("downloadZip") public void downloadZip(HttpServletResponse response) throws IOException { // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman try { // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系 String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".zip"); response.setContentType("application/octet-stream; charset=utf-8"); ServletOutputStream out = response.getOutputStream(); //压缩输出流 ZipOutputStream zipOutputStream = new ZipOutputStream(out); CircularByteBuffer cbb = new CircularByteBuffer(CircularByteBuffer.INFINITE_SIZE); for (int i = 0; i < 2; i++) { ZipEntry z = new ZipEntry("测试"+i+".xlsx"); zipOutputStream.putNextEntry(z); // 这里需要设置不关闭流 EasyExcel.write(cbb.getOutputStream(), DownloadData.class).sheet("模板") .doWrite(data()); int bytesRead; byte[] buffer = new byte[1024]; while ((bytesRead = cbb.getInputStream().read(buffer, 0, 1024)) != -1) { zipOutputStream.write(buffer, 0, bytesRead); } cbb.clear(); } zipOutputStream.flush(); zipOutputStream.close(); } catch (Exception e) { // 重置response response.reset(); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); Map map = new HashMap<>(); map.put("status", "failure"); map.put("message", "下载文件失败" + e.getMessage()); response.getWriter().println(JSON.toJSONString(map)); } } ``` 4. 使用ResponseEntity的下载 ```java @GetMapping("downloadZip") public ResponseEntity downloadZip() throws IOException { try { String fileName = "测试.zip"; HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentDisposition(ContentDisposition .builder("attachment") // if 5.2.12 // .builder("inline") // if 5.2.12 // .inline() // else 5.3.7 .filename(fileName, StandardCharsets.UTF_8).build()); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream tempOut = new ByteArrayOutputStream(); ByteArrayInputStream bais; // 压缩输出流 ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); for (int i = 0; i < 2; i++) { ZipEntry z = new ZipEntry("测试"+i+".xlsx"); zipOutputStream.putNextEntry(z); // 这里需要设置不关闭流 EasyExcel.write(tempOut, DemoData.class).sheet("模板") .doWrite(data()); bais = new ByteArrayInputStream(tempOut.toByteArray()); int bytesRead; byte[] buffer = new byte[1024]; while ((bytesRead = bais.read(buffer, 0, 1024)) != -1) { zipOutputStream.write(buffer, 0, bytesRead); } bais.reset(); tempOut.reset(); } zipOutputStream.flush(); zipOutputStream.close(); byte[] bytes = outputStream.toByteArray(); ByteArrayResource source =new ByteArrayResource(bytes); return ResponseEntity.ok() .headers(httpHeaders) .body(source); } catch (Exception e) { return ResponseEntity.badRequest().build(); } } ``` ### `aop`Demo 1. #### 相关依赖 ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop ``` 2. #### 编写注解 ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogFilter { String value() default "" ; } ``` 3. #### 编写对应`AOP`类 ```java @Aspect @Component public class LogAspect { private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class) ; @Pointcut("@annotation(com.boot.aop.config.LogFilter)") public void logPointCut (){ } @Around("logPointCut()") public Object around (ProceedingJoinPoint point) throws Throwable { Object result = null ; beforeRequestLog(point); try{ // 执行方法 result = point.proceed(); // 保存请求日志 new Thread(()->saveRequestLog(point)).start(); } catch (Exception e){ // 保存异常日志 saveExceptionLog(point,e.getMessage()); } return result; } private void saveExceptionLog (ProceedingJoinPoint point,String exeMsg){ LOGGER.info("捕获异常:"+exeMsg); } private void saveRequestLog (ProceedingJoinPoint point){ MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); LOGGER.info("请求方法:"+method.getName()); // 获取方法上LogFilter注解 LogFilter logFilter = method.getAnnotation(LogFilter.class); String value = logFilter.value() ; LOGGER.info("模块描述:"+value); } private void beforeRequestLog (ProceedingJoinPoint point){ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); LOGGER.info("before:{}","aop前置执行"); LOGGER.info("before请求路径:"+request.getRequestURL()); } } ``` 4. #### `controller`中使用刚刚编写的注解 ```java @LogFilter @PostMapping("/saveRequestLog") public String saveRequestLog (@RequestParam("name") String name){ LOGGER.info("AOP对象name:{}",name); return "success:"+name ; } ``` ### 自定义异常处理 1. #### 定义一个描述对象和异常对象 ```java /** * 定义一个描述对象 */ public class ReturnException { // 响应码 private Integer code; // 异常描述 private String msg; // 请求的Url private String url; //省略get and set } /** * 自定义业务异常 */ public class ServiceException extends Exception { public ServiceException (String msg){ super(msg); } } ``` 2. #### 自定义业务异常映射,返回`JSON`格式提示 ```java @ControllerAdvice // 异常以Json格式返回 等同 ExceptionHandler + ResponseBody 注解 // @RestControllerAdvice public class HandlerException { /** * 自定义业务异常映射,返回JSON格式提示 */ @ExceptionHandler(value = ServiceException.class) @ResponseBody public ReturnException handler01 (HttpServletRequest request,ServiceException e){ ReturnException returnException = new ReturnException() ; returnException.setCode(600); returnException.setMsg(e.getMessage()); returnException.setUrl(String.valueOf(request.getRequestURL())); return returnException ; } } ``` 3. #### `controller`中使用 ```java @RequestMapping("/exception01") public String exception01 () throws ServiceException { throw new ServiceException("业务异常:ID 不能为空"); } ``` ### @ExceptionHandler 1. 必须要求 该方法必须要和出现问题的控制器在一个类中,才能生效。 ```java @Controller public class UserController { @RequestMapping("/pagejump") public String pageJump(){ String str=null; str.length(); return "ok"; } //该注解的作用是根据不同的异常跳转到不同的试图 @ExceptionHandler(value = {java.lang.NullPointerException.class}) public ModelAndView nullpointException(Exception e){ ModelAndView mv=new ModelAndView(); //将异常信息添加到ModelAndView中 mv.addObject("error1",e.toString()); //出现异常跳转到名为error1的视图 mv.setViewName("error1"); return mv; } } ``` > 如果别的类报空指针异常,则不会进该异常处理方法 2. 或者 结合@ControllerAdvice和@ExceptionHandler一起使用。 ```java @RestControllerAdvice public class SaTokenExceptionHandler { // 全局异常拦截(拦截项目中的NotLoginException异常) @ExceptionHandler(NotLoginException.class) public R handlerNotLoginException(NotLoginException nle, HttpServletRequest request, HttpServletResponse response) throws Exception { // 打印堆栈,以供调试 nle.printStackTrace(); // 判断场景值,定制化异常信息 String message = ""; switch (nle.getType()) { case NotLoginException.NOT_TOKEN: message = "未提供token"; break; case NotLoginException.INVALID_TOKEN: message = "token无效"; break; case NotLoginException.TOKEN_TIMEOUT: message = "token已过期"; break; case NotLoginException.BE_REPLACED: message = "token已被顶下线"; break; case NotLoginException.KICK_OUT: message = "token已被踢下线"; break; default: message = "当前会话未登录"; break; } // 返回给前端 return R.err(message); } } ``` ---------- ## `JVM` ```java -Xms1000M //设置JVM初始化内存为1000m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -Xmx1800M //设置JVM最大可用内存为1800M。 -Xmn350M //设置年轻代大小为350M。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。 -Xss300K //设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 -XX:+DisableExplicitGC -XX:SurvivorRatio=4 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC ``` 1. **第一次优化** > 一看参数,马上觉得新生代为什么这么小,这么小的话怎么提高吞吐量,而且会导致`YoungGC`的频繁触发,如上如的新生代收集就耗时830s。初始化堆内存没有和最大堆内存一致,查阅了各种资料都是推荐这两个值设置一样的,可以防止在每次`GC`后进行内存重新分配。基于前面的知识,于是进行了第一次的线上调优:提升新生代大小,将初始化堆内存设置为最大内存 > > ```wiki > -Xmn350M -> -Xmn800M > -XX:SurvivorRatio=4 -> -XX:SurvivorRatio=8 > -Xms1000m ->-Xms1800m > ``` > > 将`SurvivorRatio`修改为`8`的本意是想**让垃圾在新生代时尽可能的多被回收掉**。就这样将配置部署到线上两台服务器(`prod`,`prod2`另外两台不变方便对比)上后,运行了5天后,观察`GC`结果,`YoungGC`减少了一半以上的次数,时间减少了400s,但是`FullGC`的平均次数增加了41次。`YoungGC`**基本符合预期设想**,但是这个`FullGC`**就完全不行了**。就这样第一次优化宣告失败。 2. **第二次优化** > 在优化的过程中,我们的主管发现了有个对象T在内存中有一万多个实例,而且这些实例占据了将近`20M`的内存。于是根据这个`bean`对象的使用,在项目中找到了原因:匿名内部类引用导致的,伪代码如下: > > ```java > public void doSmthing(T t){ > redis.addListener(new Listener(){ > public void onTimeout(){ > if(t.success()){ > //执行操作 > } > } > }); > } > ``` > > 由于`listener`在**回调后不会进行释放**,而且回调是个超时的操作,当某个事件超过了设定的时间(1分钟)后才会进行回调,这样就导致了T这个对象始终无法回收,所以内存中会存在这么多对象实例。 通过上述的例子发现了存在内存泄漏后,首先对程序中的`error log`文件进行排查,首先先解决掉所有的`error`事件。然后再次发布后,`GC`操作还是基本不变,虽然解决了一点内存泄漏问题,但是可以说明没有解决根本原因,服务器还是继续莫名的重启。 3. **内存泄漏调查** 经过了第一次的调优后发现内存泄漏的问题,于是大家都开始将进行内存泄漏的调查,首先排查代码,不过这种效率是蛮低的,基本没发现问题。于是在线上不是很繁忙的时候继续进行dump内存,终于抓到了一个大对象。 4. 第二次调优 内存泄漏的问题已经解决了,剩下的就可以继续调优了,经过查看 `GC log`,发现前三次 `GullGC` 时,老年代占据的内存还不足30%,却发生了`FullGC`。于是进行各种资料的调查,发现 `metaspace` 会导致 `FullGC` 的情况,服务器默认的 `metaspace` 是 `21M`,在 `GC log` 中看到了最大的时候 `metaspace` 占据了 `200M`左右,于是进行如下调优,以下分别为 `prod1` 和 `prod2` 的修改参数,`prod3,prod4`保持不变 >-Xmn350M -> -Xmn800M -Xms1000M ->1800M -XX:MetaspaceSize=200M -XX:CMSInitiatingOccupancyFraction=75 **和** >-Xmn350M -> -Xmn600M -Xms1000M ->1800M -XX:MetaspaceSize=200M -XX:CMSInitiatingOccupancyFraction=75 5. 总结 - `FullGC`一天超过一次肯定就不正常了 - 发现`FullGC`频繁的时候**优先调查内存泄漏问题** - **内存泄漏解决后**,`jvm`可以调优的空间就比较少了,作为学习还可以,否则不要投入太多的时间 - 如果发现`CPU`持续偏高,排除代码问题后可以找运维咨询下阿里云客服,这次调查过程中就发现CPU 100%是由于服务器问题导致的,进行服务器迁移后就正常了。 - `数据查询`的时候也是**算作服务器的入口流量的**,如果访问业务没有这么大量,而且没有攻击的问题的话可以往**数据库方面调查** - 有必要时常关注服务器的`GC`,可以及早发现问题 ## `js`相关代码 ### 浏览器导出方法 ```js //导出下载判断浏览器 exportSearchList (dowLoadFileName, result) { const blob = new Blob([result]) const fileName = dowLoadFileName // 判断浏览器 var brower = '' if (navigator.userAgent.indexOf('Edge') > -1) { brower = 'Edge' } if ('download' in document.createElement('a')) { // 非IE下载 if (brower === 'Edge') { navigator.msSaveBlob(blob, fileName) return } const elink = document.createElement('a') elink.download = fileName elink.style.display = 'none' elink.href = URL.createObjectURL(blob) document.body.appendChild(elink) elink.click() // 释放URL 对象 URL.revokeObjectURL(elink.href) document.body.removeChild(elink) } else { // IE10+下载 navigator.msSaveBlob(blob, fileName) } } ``` ### 两个数组的交集 ```js /** 获取两个 Arr数组的 交集intersection */ getIntersection (arr1, arr2) { let _arr2Set = new Set(arr2) // 交集 let intersection = arr1.filter(item => _arr2Set.has(item)) // 并集 //let union = Array.from(new Set([...arr1, ...arr2])) return intersection }, ``` # vue ## 自定义指令 过滤表情指令 `v-emoji` **方法一:** 将触发的方法绑定到input的原生事件上 ```js Vue.directive('emoji', { bind: function(el) { const input = el.querySelectorAll('.van-field__control,.el-input__inner,.el-textarea__inner')[0] const regexArr = ['(\ud83c[\udc00-\udfff])', '(\ud83d[\udc00-\udfff])', '(\ud83e[\udc00-\udfff])', '[\u2100-\u32ff]', '[\u0030-\u007f][\u20d0-\u20ff]', '[\u0080-\u00ff]'] const regExp = new RegExp(regexArr.join('|'), 'gm') function cleanupEmoji() { if (input.value != null) { input.value = input.value.replace(regExp, '') } } input.oninput = function() { cleanupEmoji() } input.onblur = function() { cleanupEmoji() } } }) ``` **方法二:** 不把触发的方法绑定到input的原生事件上,而是通过 `udate` 去触发 > 虚拟节点`vnode`参数中有一个上下文对象`context`,它用来表示宿主对象所在的组件对象。那么借助`$nextTick`就可以实现数据更新后,`dom`跟着渲染。 ```js Vue.directive('emoji', { bind: function(el) {}, update: function(el, binding, v) { const input = el.querySelectorAll('.van-field__control,.el-input__inner,.el-textarea__inner')[0] const regexArr = ['(\ud83c[\udc00-\udfff])', '(\ud83d[\udc00-\udfff])', '(\ud83e[\udc00-\udfff])', '[\u2100-\u32ff]', '[\u0030-\u007f][\u20d0-\u20ff]', '[\u0080-\u00ff]'] const regExp = new RegExp(regexArr.join('|'), 'gm') function cleanupEmoji() { if (input.value != null) { input.value = input.value.replace(regExp, '') } } // 通过VNode.context上下文 对象 触发dom渲染 v.context.$nextTick(() => { cleanupEmoji() }) } }) ``` **注意:**方法二`通过VNode.context上下文 对象 触发dom渲染` 时,要是出现频率过高的情况会出现短暂失效的特殊情况,所以**推荐方法一** > 上述方法在h5移动端的开发中可能会出现一些问题 -> 输入框的内容看似清除了emoji,但是在提交请求的时候却带上了。 > > 这种情况可以试试**全局公共方法** ## 自定义全局公共方法 1. 在`main.js`里写下如下代码 --过滤表情符号 ```js const emojiRegexArr = ['(\ud83c[\udc00-\udfff])', '(\ud83d[\udc00-\udfff])', '(\ud83e[\udc00-\udfff])', '[\u2100-\u32ff]', '[\u0030-\u007f][\u20d0-\u20ff]', '[\u0080-\u00ff]'] const emojiRegex = new RegExp(emojiRegexArr.join('|'), 'gm') Vue.prototype.$emojiCommon = { clearEmoji: function(value) { return value.replace(emojiRegex, '') }, emojiRegex:emojiRegex } ``` 2. 在其他的`vue`文件的输入框中调用 `:formatter="$emojiCommon.clearEmoji"` ```js ``` ## pdf预览 ``` base64toFile (base64, filename) { var bstr = window.atob(base64) var n = bstr.length var u8arr = new Uint8Array(n) const name = filename while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new File([u8arr], name, { type: 'application/pdf' }) } ``` ## js-file-download ```txt 安装: npm install js-file-download --save 使用:(在需要使用的页面引入) import fileDownload from 'js-file-download' ``` 在请求是需要设置下:**responseType: 'blob'** ```js // 下载模板3 export function downloadTemplate3(params) { return request({ url: pre + "/test/downloadTemplate", method: "get", params, responseType: "blob" }); } ``` > 如果不设置`responseType: 'blob'`, 可能会出现打开文件损坏问题; > 对应的java下载模板: > > ```java > /** > * 获取资源响应实体 > * > * @param resource 资源 > * @param fileName 文件名称 > * @return {@link ResponseEntity} > */ > private ResponseEntity getResourceResponseEntity(Resource resource, String fileName) { > HttpHeaders headers = new HttpHeaders(); > headers.add("Cache-Control", "no-cache, no-store, must-revalidate"); > headers.add("Content-Disposition", "attachment; filename=" > + new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); > headers.add("Pragma", "no-cache"); > headers.add("Expires", "0"); > headers.add("Last-Modified", new Date().toString()); > headers.add("ETag", String.valueOf(System.currentTimeMillis())); > return ResponseEntity.ok().headers(headers).contentType(MediaType.APPLICATION_OCTET_STREAM) > .body(resource); > } > ``` > > > application/octet-stream 指定下就好了 ## iconv-lite解决中文乱码问题 首先需要下载模块包`npm install iconv-lite` 在项目中引入**iconv-lite** `import iconv from 'iconv-lite'` ```js const data = "Х©╫И─░2022"; const buf = iconv.encode(data, 'KOI8-R'); ``` > 其中data即为需要转码字符 ## 下载文件损坏问题 ```txt 现象: 1.postman下载文件正常;2.js代码在其他项目正常;3.js在本项目下载的文件损坏; 项目前期使用了mockjs进行了模拟请求。导致请求结果被mock拦截,修改了返回类型。 ``` > 因为main.js中引入了mock.js,但其实并没有使用,就算没有使用也会拦截到我的接口请求并改变返回的内容. > 结论就是,在main.js中注释掉mock.js就好啦!~~ # mySql ## `mysql批量更新` ```mysql #表中已经有此行数据(根据主键或者唯一索引判断)则先删除此行数据,然后插入新的数据。否则,直接插入新数据。 replace into `user`(id,name,age) VALUES(9,'aaa',9),(5,'ab5',29); #MYSQL中的ON DUPLICATE KEY UPDATE,是基于主键(PRIMARY KEY)或唯一索引(UNIQUE INDEX)使用的。 #如果已存在该唯一标示或主键就更新,如果不存在该唯一标示或主键则作为新行插入。 insert into `user`(id,name,age) VALUES(9,'w',19),(5,'w2',39) ON DUPLICATE KEY UPDATE name = VALUES(name), age = VALUES(age); ``` ```xml insert into user_relation(id,relation_type, standard_from_uuid, standard_to_uuid, relation_score, stat, last_process_id, is_deleted, gmt_created, gmt_modified,relation_desc)VALUES (#{item.id,jdbcType=BIGINT},#{item.relationType,jdbcType=VARCHAR}, #{item.standardFromUuid,jdbcType=VARCHAR}, #{item.standardToUuid,jdbcType=VARCHAR}, #{item.relationScore,jdbcType=DECIMAL}, #{item.stat,jdbcType=TINYINT}, #{item.lastProcessId,jdbcType=BIGINT}, #{item.isDeleted,jdbcType=TINYINT}, #{item.gmtCreated,jdbcType=TIMESTAMP}, #{item.gmtModified,jdbcType=TIMESTAMP},#{item.relationDesc,jdbcType=VARCHAR}) ON DUPLICATE KEY UPDATE id=VALUES(id),relation_type = VALUES(relation_type),standard_from_uuid = VALUES(standard_from_uuid),standard_to_uuid = VALUES(standard_to_uuid), relation_score = VALUES(relation_score),stat = VALUES(stat),last_process_id = VALUES(last_process_id), is_deleted = VALUES(is_deleted),gmt_created = VALUES(gmt_created), gmt_modified = VALUES(gmt_modified),relation_desc = VALUES(relation_desc) ``` # mvn ```bash mvn install:install-file -DgroupId=gid -DartifactId=aid -Dversion=version -Dfile=xxx.jar -Dpackaging=jar -DpomFile=本地的pom.xml路径 ``` demo: `mvn install:install-file -DgroupId=com.aisino.cer -DartifactId=javasdk -Dversion=1.0.2 -Dfile=E:\GHOST\sdk\javasdk-0.0.1-bec.jar -Dpackaging=jar -DpomFile=E:\GHOST\sdk\pom.xml` # Java自定义日期比较 ```java class MyDateComparator implements Comparator { private final Boolean isNullLast; private final Boolean isASC; protected int nullOrder = 1; protected int ascOrder = 1; public MyDateComparator(Boolean isNullLast, Boolean isASC) { this.isNullLast = isNullLast; this.isASC = isASC; } public MyDateComparator(Boolean isNullLast) { this.isNullLast = isNullLast; this.isASC = true; } public MyDateComparator() { this.isNullLast = true; this.isASC = true; } public int compare(Date o1, Date o2) { nullOrder = isNullLast ? 1 : -1; ascOrder = isASC ? 1 : -1; if (Objects.isNull(o1)) { return nullOrder; } if (Objects.isNull(o2)) { return -nullOrder; } if (o1.equals(o2)) { return 0; } return o1.getTime() - o2.getTime() > 0 ? ascOrder : -ascOrder; } } ``` 上面比较器的用法 ```java aas = aas.stream().sorted(Comparator.comparing(AA::getAge).reversed() // 这个使用自定义的 比较器 MyDateComparator // .thenComparing(AA::getTime,new MyDateComparator(false))).collect(Collectors.toList()); // 这个使用 jdk8 自带的 Comparator.nullsFirst(Date::compareTo) .thenComparing(AA::getTime,Comparator.nullsFirst(Date::compareTo)).reversed()).collect(Collectors.toList()); ```