• [技术干货] spring boot使用自定义注解校验json-schema【转载】
    一、首先有两个开源工具https://github.com/everit-org/json-schema和https://github.com/java-json-tools/json-schema-validator    区别:从性能上来说everit完全是碾压fge,官方说的至少两倍,实际测试过程中,差不多有20倍的差距。虽然fge使用的是jackson json,相对来说学习成本可能较低,但是使用下来发现everit的使用也并不复杂,需要注意的是包需要导入正确(org.json)。fge唯一的优势在于错误信息比较详细。还有一点区别在于,everit验证失败是抛出异常,而fge是判断返回一个boolean类型的值。二、Spring boot 自定义注解集成jsonSchema    1.首先说一下ConstraintValidator这个类主要是用于验证bean里面的属性与@Valid或者@Validated注解配套使用,如果不加这个注解,那么你自定义的注解也不会生效,这一点很重要。    2.一般项目中controller方法中的参数都bean来接收,那么你就可以用ConstraintValidator来验证字段属性是否合法。那么如果controller传入的参数是Map对象就不能使用这个方法校验字段是否合法,这个时候就需要我们在代码里面写入一堆的if else判断,这样增加了代码的冗余。三、话不多说,直接上我写的校验代码案例:  第一步:创建注解类   package com.example.demo.config;    import java.lang.annotation.*;    @Retention(RetentionPolicy.RUNTIME)    @Documented    @Target({ElementType.PARAMETER,ElementType.METHOD})    public @interface JsonValid {        String schemaName();        String message() default "Json数据校验失败";    }第二步:校验规则切面类   package com.example.demo.service;    import com.alibaba.fastjson.JSONObject;    import com.example.demo.config.JsonValid;    import org.aspectj.lang.JoinPoint;    import org.aspectj.lang.annotation.Aspect;    import org.aspectj.lang.annotation.Before;    import org.aspectj.lang.annotation.Pointcut;    import org.everit.json.schema.Schema;    import org.everit.json.schema.ValidationException;    import org.everit.json.schema.loader.SchemaLoader;    import org.json.JSONTokener;    import org.slf4j.Logger;    import org.slf4j.LoggerFactory;    import org.springframework.stereotype.Component;    import java.io.InputStream;    /**    * @author zhouxiang    * @describe    * @create 2021/10/20 14:47    * @since 1.0.0    */    @Component    @Aspect    public class JsonValidatedAop {        private final Logger logger = LoggerFactory.getLogger(JsonValidatedAop.class);        @Pointcut("@annotation(com.example.demo.config.JsonValid)")        private void pointcut() {        }        @Before("pointcut() && @annotation(jsonValid)")        public void before(JoinPoint joinPoint, JsonValid jsonValid) {            Object[] args = joinPoint.getArgs();            if(args != null) {                String jsonString = JSONObject.toJSONString(args[0]);                /** 验证的结果,可以返回,也可以写个全局捕捉的异常 */                String validMsg = validJson(jsonString, jsonValid.schemaName());            }            logger.info("数据:"+ JSONObject.toJSONString(args) + jsonValid.schemaName());        }        /**        * 使用开源工具https://github.com/java-json-tools/json-schema-validator校验json        *        * @param jsonString 被校验数据        * @param schemaName 校验schema名称        * @return 返回失败消息,为空则校验成功        */        public String validJson(String jsonString, String schemaName) {            StringBuilder sBuilder = new StringBuilder();            try {                //InputStream inputStream = getClass().getResourceAsStream("/schema/hello.json");                //org.json.JSONObject rawSchema = new org.json.JSONObject(new JSONTokener(inputStream));                org.json.JSONObject rawSchema = new org.json.JSONObject(jsonString);                InputStream in1 = getClass().getResourceAsStream("/schema/"+schemaName);                org.json.JSONObject sSchema = new org.json.JSONObject(new JSONTokener(in1));                Schema schema = SchemaLoader.load(sSchema);                schema.validate(rawSchema);            } catch (ValidationException e) {                logger.error(e.getMessage());                sBuilder.append(e.getMessage());            }            return sBuilder.toString();        }    }第三步:controller类   package com.example.demo.controller;    import com.example.demo.config.JsonValid;    import org.slf4j.Logger;    import org.slf4j.LoggerFactory;    import org.springframework.web.bind.annotation.PostMapping;    import org.springframework.web.bind.annotation.RequestBody;    import org.springframework.web.bind.annotation.RestController;    import java.util.HashMap;    import java.util.Map;    /**    * @author zhouxiang    * @describe    * @create 2021/8/23 11:08    * @since 1.0.0    */    @RestController    public class ValidatorController {        private final Logger logger = LoggerFactory.getLogger(ValidatorController.class);        @PostMapping("/test1")        @JsonValid(schemaName = "s.json")        public Map test(@RequestBody Map map)        {            logger.info("接收的数据:"+map.toString());            Map responseMap = new HashMap<>();            responseMap.put("ResponseStatusListObject", "success");            return responseMap;        }    }第四步:测试数据    url:http://ip:8083/demo/test1 POST    json数据:    {      "rectangle" : {        "a" : 6,        "b" : "21"      }    }    Schema测试文件放在resources/schema/s.json:    {      "type" : "object",      "properties" : {        "rectangle" : {"$ref" : "#/definitions/Rectangle" }      },      "definitions" : {        "size" : {          "type" : "number",          "minimum" : 0        },        "Rectangle" : {          "type" : "object",          "properties" : {            "a" : {"$ref" : "#/definitions/size"},            "b" : {"$ref" : "#/definitions/size"}          }        }      }    }作者:祥子_b5dd链接:https://www.jianshu.com/p/7044dad582b6 
  • [技术干货] Spring Boot 如何热加载jar实现动态插件?【转载】
    一、背景动态插件化编程是一件很酷的事情,能实现业务功能的 解耦 便于维护,另外也可以提升 可扩展性 随时可以在不停服务器的情况下扩展功能,也具有非常好的 开放性 除了自己的研发人员可以开发功能之外,也能接纳第三方开发商按照规范开发的插件。常见的动态插件的实现方式有 SPI、OSGI 等方案,由于脱离了 Spring IOC 的管理在插件中无法注入主程序的 Bean 对象,例如主程序中已经集成了 Redis 但是在插件中无法使用。本文主要介绍在 Spring Boot 工程中热加载 jar 包并注册成为 Bean 对象的一种实现思路,在动态扩展功能的同时支持在插件中注入主程序的 Bean 实现功能更强大的插件。二、热加载 jar 包通过指定的链接或者路径动态加载 jar 包,可以使用 URLClassLoader 的 addURL 方法来实现,样例代码如下:ClassLoaderUtil 类public class ClassLoaderUtil { public static ClassLoader getClassLoader(String url) { try { Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); if (!method.isAccessible()) { method.setAccessible(true); } URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader()); method.invoke(classLoader, new URL(url)); return classLoader; } catch (Exception e) { log.error("getClassLoader-error", e); return null; } }}其中在创建 URLClassLoader 时,指定当前系统的 ClassLoader 为父类加载器 ClassLoader.getSystemClassLoader() 这步比较关键,用于打通主程序与插件之间的 ClassLoader ,解决把插件注册进 IOC 时的各种 ClassNotFoundException 问题。三、动态注册 Bean将插件 jar 中加载的实现类注册到 Spring 的 IOC 中,同时也会将 IOC 中已有的 Bean 注入进插件中;分别在程序启动时和运行时两种场景下的实现方式。3.1. 启动时注册 Bean使用 ImportBeanDefinitionRegistrar 实现在 Spring Boot 启动时动态注册插件的 Bean,样例代码如下:PluginImportBeanDefinitionRegistrar 类public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { private final String targetUrl = "file:/D:/SpringBootPluginTest/plugins/plugin-impl-0.0.1-SNAPSHOT.jar"; private final String pluginClass = "com.plugin.impl.PluginImpl"; @SneakyThrows @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl); Class clazz = classLoader.loadClass(pluginClass); BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz); BeanDefinition beanDefinition = builder.getBeanDefinition(); registry.registerBeanDefinition(clazz.getName(), beanDefinition); }}3.2. 运行时注册 Bean程序运行时动态注册插件的 Bean 通过使用 ApplicationContext 对象来实现,样例代码如下:@GetMapping("/reload")public Object reload() throws ClassNotFoundException { ClassLoader classLoader = ClassLoaderUtil.getClassLoader(targetUrl); Class clazz = classLoader.loadClass(pluginClass); springUtil.registerBean(clazz.getName(), clazz); PluginInterface plugin = (PluginInterface)springUtil.getBean(clazz.getName()); return plugin.sayHello("test reload");}SpringUtil 类@Componentpublic class SpringUtil implements ApplicationContextAware { private DefaultListableBeanFactory defaultListableBeanFactory; private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext; this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); } public void registerBean(String beanName, Class clazz) { BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz); defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition()); } public Object getBean(String name) { return applicationContext.getBean(name); }}四、总结本文介绍的插件化实现思路通过 共用 ClassLoader 和 动态注册 Bean 的方式,打通了插件与主程序之间的类加载器和 Spring 容器,使得可以非常方便的实现插件与插件之间和插件与主程序之间的 类交互,例如在插件中注入主程序的 Redis、DataSource、调用远程 Dubbo 接口等等。但是由于没有对插件之间的 ClassLoader 进行 隔离 也可能会存在如类冲突、版本冲突等问题;并且由于 ClassLoader 中的 Class 对象无法销毁,所以除非修改类名或者类路径,不然插件中已加载到 ClassLoader 的类是没办法动态修改的。所以本方案比较适合插件数据量不会太多、具有较好的开发规范、插件经过测试后才能上线或发布的场景。作者:zlt2000链接:https://www.jianshu.com/p/5f37f4986fa9
  • [技术干货] Java获取当前是否节假日【转载】
    当我们有业务需求的时候,需要判断当前时间是否为节假日有3种方式Jsoup 爬虫爬取 (万年日历查询 或者 360日历 百度日历)自己维护一套日历数据表(成本较高)调用开发 api 获取别人维护好的数据本文采用的是3api接口地址: http://timor.tech/api/holiday/ 较稳定可靠@Component public class HolidayUtil { @Autowired private RestTemplateUtil restTemplateUtil; @Autowired private RedisUtil redisUtil; /** * 获取是否节假日 * @return */ public Sting getNowDayHoliday(){ LocalDate now = LocalDate.now(); //我们利用redis 将数据存起来 String key = RedisConstant.HOLIDAY + now; if(redisUtil.hasKey(key)){ return redisUtil.get(key); }else{ String url = "http://timor.tech/api/holiday/info/" + now; Map headers = new HashMap<>(); headers.put("Content-Type", "application/json;charset=UTF-8"); //模拟真实请求 headers.put("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36"); ResponseEntity responseEntity = restTemplateUtil.get(url,headers, String.class); //json转换 Map map = JsonUtil.stringToCollect(responseEntity.getBody()); if(map.get("code").equals(0)){ redisUtil.set(key,map.get("type"),86400); return map.get("type"); }else{ return null; } } } }RestTemplateUtil: /** * 带请求头的GET请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public ResponseEntity get(String url, Map headers, Class responseType, Object... uriVariables) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setAll(headers); return get(url, httpHeaders, responseType, uriVariables); } /** * 带请求头的GET请求调用方式 * * @param url 请求URL * @param headers 请求头参数 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public ResponseEntity get(String url, HttpHeaders headers, Class responseType, Object... uriVariables) { HttpEntity requestEntity = new HttpEntity<>(headers); return exchange(url, HttpMethod.GET, requestEntity, responseType, uriVariables); /** * 通用调用方式 * * @param url 请求URL * @param method 请求方法类型 * @param requestEntity 请求头和请求体封装对象 * @param responseType 返回对象类型 * @param uriVariables URL中的变量,按顺序依次对应 * @return ResponseEntity 响应对象封装类 */ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, Class responseType, Object... uriVariables) { return restTemplate.exchange(url, method, requestEntity, responseType, uriVariables); } }输出{"code":0,"type":{"type":0,"name":"周二","week":2},"holiday":null}链接:https://www.jianshu.com/p/c0c1cdd8bd00
  • LinkedHashMap-转载
     ##### LinkedHashMap    LinkedHashMap此实现与 HashMap 的不同之处在于,后者维护着一个运行于所有条目的双重链接    列表。存储的数据是有序的【添加顺序】        public class LinkedHashMap extends HashMap implements Map        内部类     static class Entry extends HashMap.Node {         Entry before, after;          Entry(int hash, K key, V value, Node next) {             super(hash, key, value, next);          }      }        HashMap是无序的,当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap了        Map map=new LinkedHashMap();    Random r=new Random();    for(int i=0;i<10;i++)     map.put(r.nextInt(100), i+"-value");    System.out.println(map);     实际上是调用map.entrySet对数据进行遍历,生成String        特殊新增的构造器        new LinkedHashMap<>(16, 0.75f, true)    LinkedHashMap存储数据是有序的(默认false,其中使用before/after记录了插入顺序)  public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {    super(initialCapacity, loadFactor);     this.accessOrder = accessOrder;  }  accessOrder涉及到LinkedHashMap存储的顺序,具体的存储顺序分为两种:插入顺序(false)和  访问顺序(true调用get方法会导致位置的移动)。accessOrder设为false表示插入顺序存储的,  这也是默认值,表示LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的。  设置采用访问顺序  如果一个key被访问过了的元素则自动添加到默认位置的末尾。如果不进行访问操作,则为插入顺序一致  Map map=new LinkedHashMap<>(10,0.75f,true);  Random r=new Random();  map.put(111,"value");  for(int i=0;i<10;i++) map.put(r.nextInt(100), i+"value");  map.get(111);  //约瑟夫环的最佳写法  如果true采用访问顺序,而111对应的数据被访问过后自动自动到末尾;如果false则位置永远不变  ###### 总结  - LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。  - HashMap无序;LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序那put和get  操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)  - LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序  - LinkedHashMap是线程不安全的   /  public class TestLinkedHashMap { public static void main(String[] args) { Map map = new LinkedHashMap<>(10, 0.75f, true);  Random r = new Random();  map.put(111, "value");  int kk = 0;  for (int i = 0; i < 10; i++) { kk=r.nextInt(100);  map.put(kk, i + "value");  }  map.get(kk);  map.get(111);  System.out.println(map);  }  } ———————————————— 版权声明:本文为CSDN博主「zjh0101」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/zjh0101/article/details/126670694 
  • 使用打表法找规律-转载
     使用打表法找规律 作者:Grey  原文地址:  博客园:使用打表法找规律  CSDN:使用打表法找规律  打表法的使用条件 打表法适合:输入简单,输出也简单(只有一个数),可以暴力把一部分结果打印出来找规律,看下能否找到一个公式来优化代码。  买苹果问题 题目描述见:牛客:买苹果  暴力解法思路  如果是奇数,直接返回 -1,因为 6 和 8 不可能组成奇数  假设有 n 个苹果,最多需要n/8个 8 号袋,假设n/8=m,如果m==0,则最少需要m个 8 号袋子即可,如果m!=0,则看剩下的苹果能否被 被 6 号袋子消化,如果不能消化,则则减少一个 8 号袋子(m--),继续看剩下能否被 6 号袋搞定。  暴力解法的代码如下      public static int minBags(int n) {         if ((n & 1) == 1) {             return -1;         }          if (n % 8 == 0) {             // 全部可以被 8 号袋子分解             return n / 8;         }         int use8 = n / 8;         int rest = n % 8;         while (rest != 0) {             if (rest % 6 == 0) {                 // 分配了 8 号袋子,剩下的分配 6 号袋子,正好分配完。                 return use8 + (rest / 6);             } else {                 // 分配了 8 号袋子,剩下的分配 6 号袋子,无法分配完,则减少一个 8 号袋子                 if (use8 > 0) {                     use8--;                     rest += 8;                 } else {                     return -1;                 }             }         }         return -1;     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 打印出前几项的结果观察一下  当有0个苹果的时候,最少需要:0个袋子 当有1个苹果的时候,最少需要:-1个袋子 当有2个苹果的时候,最少需要:-1个袋子 当有3个苹果的时候,最少需要:-1个袋子 当有4个苹果的时候,最少需要:-1个袋子 当有5个苹果的时候,最少需要:-1个袋子 当有6个苹果的时候,最少需要:1个袋子 当有7个苹果的时候,最少需要:-1个袋子 当有8个苹果的时候,最少需要:1个袋子 当有9个苹果的时候,最少需要:-1个袋子 当有10个苹果的时候,最少需要:-1个袋子 当有11个苹果的时候,最少需要:-1个袋子 当有12个苹果的时候,最少需要:2个袋子 当有13个苹果的时候,最少需要:-1个袋子 当有14个苹果的时候,最少需要:2个袋子 当有15个苹果的时候,最少需要:-1个袋子 当有16个苹果的时候,最少需要:2个袋子 当有17个苹果的时候,最少需要:-1个袋子 当有18个苹果的时候,最少需要:3个袋子 当有19个苹果的时候,最少需要:-1个袋子 当有20个苹果的时候,最少需要:3个袋子 当有21个苹果的时候,最少需要:-1个袋子 当有22个苹果的时候,最少需要:3个袋子 当有23个苹果的时候,最少需要:-1个袋子 当有24个苹果的时候,最少需要:3个袋子 当有25个苹果的时候,最少需要:-1个袋子 当有26个苹果的时候,最少需要:4个袋子 当有27个苹果的时候,最少需要:-1个袋子 当有28个苹果的时候,最少需要:4个袋子 当有29个苹果的时候,最少需要:-1个袋子 当有30个苹果的时候,最少需要:4个袋子 当有31个苹果的时候,最少需要:-1个袋子 当有32个苹果的时候,最少需要:4个袋子 当有33个苹果的时候,最少需要:-1个袋子 当有34个苹果的时候,最少需要:5个袋子 当有35个苹果的时候,最少需要:-1个袋子 当有36个苹果的时候,最少需要:5个袋子 当有37个苹果的时候,最少需要:-1个袋子 当有38个苹果的时候,最少需要:5个袋子 当有39个苹果的时候,最少需要:-1个袋子 ...  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 通过观察,可以得到如下结论  奇数个苹果,返回 -1。  n <= 5或者n==10的时候,返回 -1。  n == 6或者n == 8的时候,返回 1。  n % 8 == 0时候,全部用 8 号袋子,即n / 8; 当n%8!=0时,需要n/8+1个袋子。  代码可以优化为      // 打表方式优化     public static int minBags2(int n) {         if (n <= 5 || n == 10 || (n & 1) == 1) {             return -1;         }         if (n == 6 || n == 8) {             return 1;         }          return n % 8 == 0 ? n / 8 : n / 8 + 1;     } 1 2 3 4 5 6 7 8 9 10 11 牛羊吃草问题 牛客:青草游戏  暴力解法思路  在 n <= 4的情况下,我们可以通过观察得到牛羊的胜败情况          // 0 羊胜         // 1 牛胜         // 2 羊胜         // 3 牛胜         // 4 牛胜         if (n < 5) { // base case             return (n == 0 || n == 2) ? "yang" : "niu";         } 1 2 3 4 5 6 7 8 当n >= 5的情况下,我们可以暴力枚举所有情况,  当牛选择吃 1 份的时候,接下来的子过程,是谁赢,如果子过程是牛赢,说明主过程是羊获胜了。 当牛选择吃 4 份的时候,接下来的子过程,是谁赢,如果子过程是牛赢,说明主过程是羊获胜了。 … 当牛选择吃 4^x 份的时候,接下来的子过程,是谁赢,如果子过程是牛赢,说明主过程是羊获胜了。 (注:4^x <= n)。  暴力解法的代码如下:      public static String winner(int n) {         // 0 羊         // 1 牛         // 2 羊         // 3 牛         // 4 牛         if (n < 5) { // base case             return (n == 0 || n == 2) ? "yang" : "niu";         }         // n >= 5 时         int base = 1; // 当前先手(牛)决定吃的草数         // 当前是先手(牛)在选         while (base <= n) {             // 当前一共n份草,先手(牛)吃掉的是base份,n - base 是留给后手(羊)的草             if (winner(n - base).equals("yang")) {                 return "niu";             }             if (base > (n >> 2)) { // 防止base*4之后溢出                 break;             }             base <<= 2;         }         return "yang";     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 打印出前面的一些输出  0 堆草,获胜者是:yang 1 堆草,获胜者是:niu 2 堆草,获胜者是:yang 3 堆草,获胜者是:niu 4 堆草,获胜者是:niu 5 堆草,获胜者是:yang 6 堆草,获胜者是:niu 7 堆草,获胜者是:yang 8 堆草,获胜者是:niu 9 堆草,获胜者是:niu 10 堆草,获胜者是:yang 11 堆草,获胜者是:niu 12 堆草,获胜者是:yang 13 堆草,获胜者是:niu 14 堆草,获胜者是:niu 15 堆草,获胜者是:yang 16 堆草,获胜者是:niu 17 堆草,获胜者是:yang 18 堆草,获胜者是:niu 19 堆草,获胜者是:niu 20 堆草,获胜者是:yang 21 堆草,获胜者是:niu 22 堆草,获胜者是:yang 23 堆草,获胜者是:niu 24 堆草,获胜者是:niu  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 通过观察可知,可以得到结论  n % 5 == 0 || n % 5 == 2情况下,获胜者都是羊,否则获胜者是牛。  优化后的代码如下:      public static String winner2(int n) {         if (n % 5 == 0 || n % 5 == 2) {             return "yang";         } else {             return "niu";         }     } 1 2 3 4 5 6 7 判断一个数是否可以表示成若干(数量>1)连续正数和的数 题目描述如下  定义一种数:可以表示成若干(数量 > 1)连续正数和的数 比如: 5 = 2 + 3,5 就是这样的数 12 = 3 + 4 + 5,12 就是这样的数 1不是这样的数,因为要求数量大于1个、连续正数和 2 = 1 + 1,2 也不是,因为等号右边不是连续正数 给定一个参数 N,返回是不是可以表示成若干连续正数和的数  按照题目的描述,我们可以把暴力解法写出来,就是枚举 num 是否 可以被 1 + 2 + ... + n 2 + 3 + ... + n 3 + 4 + ... + n … 上述任何一个式子分解。  暴力解法的代码如下      public static boolean isMSum1(int num) {         if (num <= 2) {             return false;         }         int sum = 1;         for (; sum < num; sum++) {             int o = sum;             int i = sum + 1;             sum += i;             if (sum > num) {                 return false;             }             while (sum < num) {                 i++;                 sum += i;             }             if (sum == num) {                 return true;             }             sum = o;         }         return false;     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 打印出前面若干项的结果  1 : false 2 : false 3 : true 4 : false 5 : true 6 : true 7 : true 8 : false 9 : true 10 : true 11 : true 12 : true 13 : true 14 : true 15 : true 16 : false 17 : true 18 : true 19 : true 20 : true 21 : true 22 : true 23 : true 24 : true 25 : true 26 : true 27 : true 28 : true 29 : true 30 : true 31 : true 32 : false  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 观察后发现  num == 1 || num == 2时,为 false。  如果一个数是 2 的某次方,则返回 false,否则则返回 true。  如何判断一个数是不是 2 的某次方呢?  有如下三种方法  num == (num & (-num)) num == (num & (~num + 1)) (n & (num - 1)) == 0 1 2 3 所以,优化后的代码如下      public static boolean isMSum2(int num) {         if (num == 1 || num == 2) {             return false;         }         return ((num - 1) & num) != 0;     } ———————————————— 版权声明:本文为CSDN博主「GreyZeng」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/hotonyhui/article/details/126671270 
  • Java哈希表和哈希冲突-转载
     哈希表 Hash 一般翻译为散列,也有直接音为哈希的,这就是把任意长度的输入通过散列算法,变换  成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值  所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。  哈希冲突  当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,就把它叫做碰撞(哈希碰撞)。哈希碰撞的解决方案为开放地址法和链地址法。  以key/value的方式存储数据,采用拉链法综合了数组和链表结构。如果key已知则存取效率较高,但是删除慢,如果不知道key存取则慢,对存储空间使用不充分。最典型的实现是HashMap  JDK8+对桶内数据处理从链表转换为红黑树,则查询效率从O(N)提高到O(logN)。当链表中的个数超过8个时  会转换为红黑树  Map实现类  HashMap、TreeMap、LinkedHashMap、Hashtable等  HashMap  类定义  public class HashMap extends AbstractMap implements Map,Cloneable,Serializable;  注意:Map接口的定义  public interface Map没有父接口  具体的内部数据存储方式  transient Node[] table;//哈希表的本质是一个数组,数组中每一个元素称为一个桶,桶中存放的是键值对  transient Set> entrySet; 并不是用于实际存储数据,主要用于针对entrySet和keySet两个视图提供支持。  transient int size;当前集合中的元素个数。  final float loadFactor ;当前集合的负载因子,当前Map集合中扩容的阈值【负载因子*最大容积】  transient int modCount;修改次数,主要用于实现多线程操作时快死异常  重要的阈值  static final int DEFAULT_INITIAL_CAPACITY=1<<4;//aka16 默认的初始化容积  static final int MAXIMUM_CAPACITY=1<<30;最大容积值,实际上就是2的30次方  static final int TREEIFY_THRESHOLD = 8;//树化阈值:即链表转成红黑树的阈值,在存储数据时,当链表长  度 > 该值时,则将链表转换成红黑树  static final int MIN_TREEIFY_CAPACITY = 64;//最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才  允许树形化链表 (即将链表转换成红黑树)否则,若桶内元素太多时,则直接扩容,而不是树形化  static final int UNTREEIFY_THRESHOLD = 6;//桶的链表还原阈值:即红黑树转为链表的阈值,当在扩容resize  )时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将 红  黑树转换成链表  内部存储的实现  静态内部类用于实现Entry,HahMap中存放的key/value对就被封为Node对象。其中Key就是存放的键值,决定具体存放位置;  Value是具体存放数据,hash就是当前Node对象的hash值,next用于指向下一个Node  节点(单向链表)  HashMap()采用所有默认配置值,其中的参数值有initial Capacity:int初始化容积,默认值16,第二参数为加载因子,默认值为0.75,表示存储16*0.75个元素后,如果大于这个值则需要进行扩容。  Map主要用于存储键key值value对,根据键得到值,因此键不允许重复,但允许值重复。  HashMap是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,  具有很快的访问速度。  HashMap最多只允许一条记录的键为null;允许多条记录的值为null  HashMap不支持线程的同步,即任一时刻可以有多个线程同时写Hash Map;可能会导致数据不一致,在JDK1.7中会出现环形链,虽然JDK1.8不会出现环形链,但是还会有rehash操作出现死循环、脏读问题、size值不准确等问题。如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力。  如何判断环形链?  创建一个Set,然后遍历整个集合,将每个元素的key值存入set,如果要存的key值已经存储在set中,则出现环型链.  Java7中使用Entry来代表每个HashMap中的数据节点,Java8中使用Node,基本没有区别,都是key,value,hash和next这四个属性,不过,Node只能用于链表的情况,红黑树的情况需要使用TreeNode。  TREEIFY_THRESHOLD为 8如果新插入的值是链表中的第 9 个会触发下面的 treeifyBin(树化操作,就是将单向链转换为红黑树),也就是将链表转换为红黑树。  JDK8+插入数据到链表的最后面,Java7是插入到链表的最前面  HashMap的put方法的具体流程  public V put(K key, V value) { 以key存储value值,返回原始位置上的value值 先执行hash(key)根据key获取一个hash值, 参数2是要存储的key值,参数3是要存储的value,参数4表示如果当前位置已存在一个值 ,是否替换,false是替换,true是不替换。参数5是否在创建模式,如果为false,则表是在创建模式 return putVal(hash(key), key, value, false, true); } ———————————————— 版权声明:本文为CSDN博主「Lxcjl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Lxcjl/article/details/126651922 
  • [技术干货] java中的集合框架基础-5-转载
     1.HashMap总结  - 结构特点:Java8-中的HashMap是基于“数组+链表”的方式(链表法解决冲突),到了Java8,应该是“数组+链表/红黑树”的方式,默认阈值为树化8退化6   - 线程安全:HashMap是不安全,JDK1.8-并发由于插入采用的是头插法可能会导致环形链,JDK1.8+插入数据采用的是尾插法可能会导致数据丢失。集合框架中有两种线程安全的实现Collections.synchronizedMap(new java.util.HashMap<>())和juc包中的ConcurrentHashMap,前者是锁整个表,后者采用乐观锁的方式     - 性能特点:HashMap可以在常数时间内增加,删除,查找元素,但这也是一种平均情况,使用load factor装载因子计算阈值就是为了减少冲突带来的性能退化     - 扩容方法:HashMap的桶数组一次扩展为原数组的2倍,控制扩展和移动的次数,这里需要执行rehash计算。如果容量小于64只会进行简单扩容,如果容量大于64则会进行树化改造。树化处理可以避免哈希碰撞攻击   2.Hashtable 线程安全的,不允许null的键或值;是线程安全的但是Hashtable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁。多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差  HashMap允许键和值为null,只是key只能有一个null,值允许null多个由于线程安全,则在非多线程环境下不建议使用   Hashtable是一个散列表,它存储的内容是键值对(key-value)映射。通过"拉链法"实现的哈希表    Hashtable继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。  Hashtable的构造器  public Hashtable(int initialCapacity, float loadFactor) {         //针对初始化容积和负载因子进行合法性验证             if (initialCapacity < 0)             throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);         if (loadFactor <= 0 || Float.isNaN(loadFactor))             throw new IllegalArgumentException("Illegal Load: "+loadFactor);         if (initialCapacity==0)             initialCapacity = 1;                      this.loadFactor = loadFactor;          table = new Entry[initialCapacity];  //初始化数组,HashMap采用的是延迟初始化数组的策略         threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); //获取扩容的阈值     }                 MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8                         public Hashtable(int initialCapacity) {         this(initialCapacity, 0.75f);     }       public Hashtable() {  //无参构造器的默认值  容积为11,负载因子0.75           this(11, 0.75f);     }         public HashMap() {             this.loadFactor = DEFAULT_LOAD_FACTOR; //在第一次添加put数据时,才进行数组的初始化操作,初始值为16,负载因子0.75         }  成员方法put  public synchronized V put(K key, V value) {  //线程安全,以当前hashtable对象充当锁         if (value == null) {  //value值不允许为空             throw new NullPointerException();         }         Entry tab[] = table;         int hash = key.hashCode(); //直接获取key的hash值,但是HashMap中专门定义了hash方法,用于对key的hashCode值进行扰动处理         int index = (hash & 0x7FFFFFFF) % tab.length;   //去除hash值中的符号位求余计算获取数组的对应下标索引         Entry entry = (Entry)tab[index];  //获取对应位置上的链头遍历整个链表,查找key值相同的Entry对象【首先进行hash值比较,如果相等则调用equals方法】,如果相等则后盖前         for(; entry != null ; entry = entry.next) {             if ((entry.hash == hash) && entry.key.equals(key)) {                 V old = entry.value;                 entry.value = value;                 return old;             }         }         //如果链表上没有相等的key,则添加entry对象到链表上         addEntry(hash, key, value, index);   //参数1是key的hash值,并没有进行扰动处理,参数2key,参数3value,参数4是数组索引序号         return null;     }  Hashtable的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的;在hashmap中允许key和value为null,只是key只能有一个null,如果出现冲突后盖前;如果使用null的key和value 则会出现一个运行时异常NullPonterException   3.HashMap 与 HashTable区别  1. 线程安全:HashMap是非线程安全的,HashTable是线程安全的;HashTable内部的方法基本都经过 synchronized修饰。(如果要保证线程安全的话就使用ConcurrentHashMap)   2. 效率:因为线程安全的问题,HashMap要比HashTable效率高一点。另外HashTable基本被淘汰,不 要在代码中使用它   3. 对Null key和Null value的支持:HashMap中null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null。但是在HashTable中put进的键值只要有一个null,直接抛NullPointerException   4. 初始容量大小和每次扩充容量大小的不同 :     创建时如果不指定容量初始值,Hashtable默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。      创建时如果给定了容量初始值,那么Hashtable会直接使用给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说HashMap总是使用2的幂作为哈希表的大小。   5. 底层数据结构:JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认8)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制。    6. 推荐使用:在Hashtable的类注释可以看到,Hashtable是保留类不建议使用,推荐在单线程环境下使用HashMap替代,如果需要多线程使用则用ConcurrentHashMap替代。 ———————————————— 版权声明:本文为CSDN博主「L to the B」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_56627229/article/details/126650598 
  • [技术干货] List&Map集合-转载
     List集合 List接口 继承于Collection接口 有序可重复原则,集合中每个元素具有对应的索引,可以通过索引直接访问该元素 List集合默认按照添加元素的顺序设置索引 List集合关心是否有序但并不关心是否重复 List接口的实现类由ArrayList动态数组、Vector动态数组、LinkedList双向链表、Stack栈 List集合针对索引的方法 1、添加元素  void add(int index, Object element):将元素element插入到List集合的index处。 boolean addAll(int index, Collection c):将集合c所包含的所有元素都插入到List集合的index处。 2、获取元素  Object get(int index): 返回集合index索引处的元素。 List subList(int fromIndex, int toIndex):返回从索引 fromlndex (包含)到索引toIndex(不包含)处所有集合元素组成的子集合。 3、获取元素索引  int indexOf(Object obj):返回对象。在List集合中第一次出现的位置索引。 int lastIndexOf(Object obj):返回对象。在List集合中最后一次出现的位置索引。 public static void main(String[] args) {         List list = new ArrayList<>();         list.add("詹姆斯");         list.add("韦德");         list.add("欧文");         list.add("杜兰特");         list.add("西蒙斯");         System.out.println(list);         list.add(3,"哈登");//添加到欧文之后         System.out.println(list);         System.out.println(list.get(4));获取指定索引位置处的元素---->杜兰特         System.out.println(list.subList(1,3));//返回两个索引之间的元素作为子集合输出,不包含后一个索引位置处的元素         list.add("詹姆斯");         list.add("詹姆斯");         list.add("詹姆斯");         System.out.println(list);         System.out.println(list.indexOf("詹姆斯"));//获取“詹姆斯”在集合中第一次出现的索引         System.out.println(list.lastIndexOf("詹姆斯"));//获取“詹姆斯”在集合中最后一次出现的索引         list.remove(1);//删除1处索引元素“韦德”         System.out.println(list);         list.set(4,"伦纳德");//将索引4处的西蒙斯换成伦纳德         System.out.println(list);         for (String s : list) {             System.out.print(s+" ");         }     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 重写equals方法判断     @Override     public boolean equals(Object o) {         //首先判断传入该方法的对象是否是E类对象         if (o instanceof E){             E obj = (E) o;//强制转换             if(this.getCount() == obj.getCount())                 return true;//相等返回true  否则返回false         }         return false;     } 1 2 3 4 5 6 7 8 9 10 ListIterator import java.util.ListIterator;//导包 1 List集合提供了一个额外的方法listiterator()方法用于返回一个iterator对象,ListIterator接口继承了Iterator接口,提供了操作List集合的方法,与Iterator不同的是,它可以双向移动而Iterator只能单向移动  常用方法:  void add():通过迭代器添加元素到对应集合 void set(Object obj):通过迭代器替换正迭代的元素 void remove():通过迭代器删除刚迭代的元素 boolean hasPrevious():如果以逆向遍历列表,往前是否还有元素。 Object previous():返回列表中的前一个元素。 int previousIndex():返回列表中的前一个元素的索引 boolean hasNext() Object next() int nextIndex() ArrayList list = new ArrayList<>();         list.add("1");         list.add("2");         list.add("3"); //首先进行正向遍历,可以把迭代器的遍历理解为具有一个指针,当正向遍历到最后一个元素时,在进行逆向遍历 ListIterator iterator = list.listIterator();         while(iterator.hasNext()){             System.out.print(iterator.next()+" ");         }         System.out.println(); //逆向遍历             while(iterator.hasPrevious()){                 System.out.print(iterator.previous()+" ");             } //set方法使用 public static void main(String[] args) {         List list = new ArrayList<>();         list.add(1);         list.add(2);         list.add(3);         list.add(4);         list.add(5);         list.add(6);         list.add(7);         ListIterator it = list.listIterator();         while (it.hasNext()){             Integer intt = it.next();             if (intt.equals(3)){                 it.set(999);             }         }         System.out.println(list);     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 使用Listerator时注意处理并发异常现象  public static void main(String[] args) {         ArrayList list = new ArrayList<>();         list.add("1");         list.add("2");         list.add("3");         //下面代码演示Listiterator并发异常现象         ListIterator it = list.listIterator();             while (it.hasNext()) {                 list.add(1, "牛");                 System.out.println(it.next());             }         //并发异常,使用迭代器遍历时不能使用集合对象修改集合数据         //java.util.ConcurrentModificationException     }         //逆向遍历         while(iterator.hasPrevious()){                 System.out.print(iterator.previous()+" ");          }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 LinkedList实现类 LinkedList集合支持双链表,定位首尾元素速度较快,增删首尾元素速度快。 故而提供了多种API来操作LinkedList集合的首尾元素 LinkedList与ArrayList不同的是,ArrayList的底层是数组实现,但LinkedList的底层是利用链表实现 LinkedList做队列     public static void main(String[] args) {         //LinkedList做队列使用 ---->先进先出,后进后出         LinkedList queue = new LinkedList<>();         //入队         queue.addLast("一号");         queue.addLast("二号");         queue.addLast("三号");         queue.addLast("四号");         queue.addLast("五号");         System.out.println(queue);         //出队         System.out.println(queue.removeFirst());         System.out.println(queue.removeFirst());         System.out.println(queue);     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 LinkedList做栈     public static void main(String[] args) {         //LinkedList做栈使用---->先进后出,后进先出         //可以理解为装填子弹,第一颗装入之后装第二课,第二课将第一颗往下挤,第一颗位于首位,以此类推         LinkedList stack = new LinkedList<>();         //压栈         stack.push("第一颗子弹");         stack.push("第二颗子弹");         stack.push("第三颗子弹");         stack.push("第四颗子弹");         //出栈         System.out.println(stack.pop());         System.out.println(stack.pop());         System.out.println(stack);     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 做栈使用时发现: 入栈方法push()与addFirst()是相同的效果,并且push()的底层实现也是调用了addFirst()  //push方法的底层实现 public void push(E e) {        addFirst(e);    } 1 2 3 4 出栈方法pop()与removeFirst()也是相同效果  //pop()方法底层实现 public E pop() {        return removeFirst();    } 1 2 3 4 Map集合 概述 Map集合是一种与Collection不同的集合体系,Collection是单列集合 与Collection集合相同的是,Map是所有双列集合的父接口 其实现类有HashMap、TreeMap、LinkedHashMap Map集合是双列集合,它每个元素包含两个数据,即“键key”和“值value” Map集合也成为键值对集合,每个元素的格式:key=value Map集合的完整格式:Map {key1=value1,key2=value2,key3=value3,...} 1 与Set集合相同:无序、不重复、无索引、值可以为空 不重复:后面添加的元素在前面若有重复则会覆盖掉之前的元素 Map集合的特点由键key决定 Set集合的底层实现就是利用了Map集合 Set sets = new HashSet<>();         /*HashSet底层就是HashMap集合         public HashSet() {         map = new HashMap<>();         }*/         Set integerSet = new TreeSet<>();         /*TreeSet底层实现就是TreeMap集合         public TreeSet() {         this(new TreeMap());         }*/ 1 2 3 4 5 6 7 8 9 10 常用API* public static void main(String[] args) {         Map maps = new HashMap<>();         //1. 添加元素         maps.put("Benz",4);         maps.put("Ferrari",5);         maps.put("Lamborghini",13);         maps.put("Maserati",7);         System.out.println(maps);         //2. 清空Map集合         //maps.clear();         //System.out.println(maps);         //3. 判断集合是否为空,是返回true,反之false         System.out.println(maps.isEmpty());//true         //4. 根据键key获取对应值value         Integer temp = maps.get("Maserati");         System.out.println(temp);         //5. 根据键删除整个元素---->删除键key返回值         maps.remove("Benz");         System.out.println(maps);         //6. 判断是否包含某个键key         System.out.println(maps.containsKey("Benz"));         //7. 判断是否包含某个值         System.out.println(maps.containsValue(7));         //8. 获取全部键key的集合----->  public set keySet()         //map集合是无需不重复,返回的是set集合         //写法一:         System.out.println(maps.keySet());         //写法二:         Set keys = maps.keySet();         for (String key : keys) {             System.out.print(key+" ");         }         System.out.println();         //9. 获取全部值value的集合----->public Collection values()         //Map集合值不做要求,值value可以重复,所以要用Collection集合接收并返回         //写法一:         System.out.println(maps.values());         //写法二:         Collection values = maps.values();         for (Integer value : values) {             System.out.print(value+" ");         }         System.out.println();         //10. 获取集合的大小         System.out.println(maps.size());         //11. 合并其他Map集合         Map mapss = new HashMap<>();         mapss.put("Mi",2);         mapss.put("iPhone",2);         mapss.put("华为",2);         mapss.put("MEIZU",2);         //将mapss集合全部注入maps集合中即大小size变成7         maps.putAll(mapss);         System.out.println(maps.size());//3--->7     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 Map集合遍历 通过键key来获取每个值     public static void main(String[] args) {         //通过键找值         Map maps = new HashMap<>();         maps.put("Benz",4);         maps.put("Ferrari",5);         maps.put("Lamborghini",13);         maps.put("Maserati",7);         //调用KeySet方法拿到键的Set集合         Set keyset = maps.keySet();         //遍历键集合,通过调用get方法获取每个键的value值         for (String key : keyset) {             Integer value = maps.get(key);             System.out.println(key+" = "+value);         }     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 通过Set>和entrySet方法将Map集合打成Set集合再进行遍历 public static void main(String[] args) {          Map maps = new HashMap<>();         maps.put("Benz",4);         maps.put("Ferrari",5);         maps.put("Lamborghini",13);         maps.put("Maserati",7);         /*         正常情况下,可以利用foreach遍历set、list等单列集合,因为单列集合中的元素有自己特定的数据类型         但是map集合中的元素是一个键值对,没有自己的数据类型也就不能直接通过foreach遍历         故而把Map集合通过Set>  entrySet转换城Set集合进行遍历         Map.Entry是Map集合的一个实体类型,换而言之         Map.Entry就是的数据类型         此时将key和value键值对看作一个整体         查看Entry底层源码发现,Entry具有能够直接获取Map集合Key与Value的方法         interface Entry {                     K getKey();                     V getValue();          */         Set> entrySet = maps.entrySet();         for (Map.Entry stringIntegerEntry : entrySet) {             String strEntry = stringIntegerEntry.getKey();             Integer intEntry = stringIntegerEntry.getValue();             System.out.println(strEntry+"=>"+intEntry);             //System.out.print(stringIntegerEntry+"  ");         }     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Map集合存储自定义类型元素 class Student{     private String name;     private int age;     public Student() {}     public Student(String name, int age) {this.name = name;this.age = age;}     public String getName() {return name;}     public void setName(String name) {this.name = name;}     public int getAge() {return age;}     public void setAge(int age) {this.age = age;}     @Override     public String toString() {return "Student {"+name+age+"岁"+"}";} } public class TestMain {     //将学生与班级进行对应  学生作为键,班级作为值     public static void main(String[] args) {         Map studentMessage = new HashMap<>();         studentMessage.put(new Student("张三",19),2250);         studentMessage.put(new Student("李四",21),2266);         studentMessage.put(new Student("王五",20),2266);         studentMessage.put(new Student("赵六",17),2266);         studentMessage.put(new Student("冯七",22),2250);         Set> entrySet = studentMessage.entrySet();         for (Map.Entry studentIntegerEntry : entrySet) {             System.out.println(studentIntegerEntry);         }     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 TreeMap TreeMap作为TreeSet的底层实现,仍然是两种排序方式即自然排与定制排 自然排序 public static void main(String[] args) {         Map studentMessage = new TreeMap<>();             studentMessage.put(47,"1");             studentMessage.put(22,"2");             studentMessage.put(39,"3");             studentMessage.put(10,"4");             studentMessage.put(1,"4");             //默认升序排序             /*             key=1value4             key=10value4             key=22value2             key=39value3             key=47value1              */             studentMessage.forEach((k,v)-> System.out.println("key="+k+"value"+v));     }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 定制排序一: 此类实现Comparable接口,并且重写CompareTo比较方法 class R implements Comparable{     private String name;     private int age;     public R(){}     public R(String name,int age){this.name=name;this.age=age;}     public void setName(String name){this.name=name;}     public void setAge(int age){this.age=age;}     public String getName(){return name;}     public int getAge(){return age;}     public String toString(){return "姓名:"+name+"  年龄"+age;}     public int compareTo(R o){         //按照年龄排序--->降序         return Integer.compare(o.age,this.age);     } } public class TestMain {     public static void main(String[] args) {         Map maps = new TreeMap<>();         maps.put(new R("张三",25),"GZ2266期");         maps.put(new R("李四",36),"GZ2250期");         maps.put(new R("王五",67),"GZ2266期");         maps.put(new R("汪苏泷",33),"GZ2250期");         maps.put(new R("刘德华",51),"GZ2250期");         maps.put(new R("吴京",42),"GZ2250期");         maps.forEach((k,v)-> System.out.println("key:"+k+"value:"+v));     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 定制排序二: 利用匿名内部类写法,向TreeMap集合中注入Comparator比较器,并且重写compare比较方法 class R{     private int count;     public R(){}     public R(int count){this.count=count;}     public void setCount(int count){this.count=count;}     public int getCount(){return count;}     @Override     public String toString() {return "R{" + "count=" + count + '}';} } public class TestMain {     public static void main(String[] args) {         Map maps = new TreeMap<>(new Comparator() {             @Override             public int compare(R o1, R o2) {                 //按照降序                 return o2.getCount() - o1.getCount();             }         });         maps.put(new R(1),"1");         maps.put(new R(2),"2");         maps.put(new R(3),"3");         maps.put(new R(4),"4");         maps.put(new R(5),"5");         System.out.println(maps);         /*{R{count=5}=5, R{count=4}=4, R{count=3}=3, R{count=2}=2, R{count=1}=1}*/     } }  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 LinkedHashMap HashSet有一个子类是LinkedHashSet,HashMap也有一个LinkedHashMap子类; LinkedHashMap也使用双向链表维护Map集合的键值对次序 public static void main(String[] args) {         Map subjectScore  = new LinkedHashMap<>();         subjectScore.put("语文",127);         subjectScore.put("数学",147);         subjectScore.put("英语",148);         subjectScore.put("理科综合",292);         subjectScore.put("口语",93);         //按照添加顺序输出         subjectScore.forEach((k,v)-> System.out.println("key="+k+"value="+v));     } ———————————————— 版权声明:本文为CSDN博主「绿仔牛奶_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/yuqu1028/article/details/126650400 
  • [技术干货] html2canvas 使用总结
    使用先来看下如何在 vue 项目中应用的import html2canvas from "html2canvas";// 生成快照const convertToImage = (container, options = {}) => { // 设置放大倍数 const scale = window.devicePixelRatio; // 传入节点原始宽高 const _width = container.offsetWidth; const _height = container.offsetHeight; let { width, height } = options; width = width || _width; height = height || _height; // html2canvas配置项 const ops = { scale, // width, // height, useCORS: true, allowTaint: false, ...options }; return html2canvas(container, ops).then(canvas => { // 返回图片的二进制数据 return canvas.toDataURL("image/png"); });}// 调用函数,取到截图的二进制数据,对图片进行处理(保存本地、展示等)const imgBlobData = await convertToImage(element);仅此而已~~~遇到的问题如果只是这样就结束了,那这也太简单了吧,但是人无完人,再美的东西也会有瑕疵,下面列举一些 html2canvas 的问题及解决办法1、图片跨域解决方案:设置配置项 allowTaint: falsecanvas 的 CanvasRenderingContext2D 属于浏览器的对象,如果渲染过跨域资源,浏览器就认定 canvas 已经被污染了 Taint:污点设置配置项 useCORS: false表示允许跨域资源共享,注意不能与 allowTaint 同时配置为 trueimg 标签中添加 crossOrigin = "anonymous"anonymous:如果使用这个值的话就会在请求 header 中带上 Origin 属性,但请求不会带上 cookie 和客户端 ssl 证书等其他的一些认证信息图片服务器配置 Access-Control-Allow-Origin: *重要的配置项,是跨域问题的根本源泉,需要后端配合2、截图锯齿解决方案:根据设备像素比进行缩放// 设置放大倍数const scale = window.devicePixelRatio;3、截图不全解决方案:截图之前将页面滚动到顶部document.body.scrollTop = document.documentElement.scrollTop = 0;const imgBlobData = await convertToImage(element);4、对 css3 支持不好html2canvas 暂不支持的 CSS 样式属性:background-blend-mode、background-clip: text、box-decoration-break、repeating-linear-gradient()、font-variant-ligatures、mix-blend-mode、writing-mode、writing-mode、border-image、box-shadow、filter、zoom、transform解决方案:对于一些必要的样式,可以选择使用图片做兜底实现box-shadow 可以参考 这个pr,修改源码解决,但是,实际效果也不是太理想……5、svg 标签问题原因:vue-lottie 动画库渲染的标签是 svg(也可能是你自己写的 svg 标签)html2canvas 对于 svg 标签的支持也不尽人意,解决办法同样是用图片做兜底在项目中,我们是用 svg 做动画,截图的时候把动画换成一张静态图,这样只要设置要静态图的样式,截图效果还是可以接受的6、其他建议:在页面开发前尽量跟产品确认好这个页面到底要不要截图,如果需要截图,那么搬砖的时候就要注意不要使用以上 css3 特性了,否则,就后期就只能含着眼泪、咬着牙修 bug 了原文链接:https://www.jianshu.com/p/e74dab30ea2c
  • [技术干货] Spring缓存注解@Cacheable,@CachePut , @CacheEvict使用
     注释介绍 redis与Spring Cache的整合请参看上章 @Cacheable @Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 @Cacheable 作用和配置方法 参数    解释    example value    缓存的名称,在 spring 配置文件中定义,必须指定至少一个    例如: @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} key    缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合    @Cacheable(value=”testcache”,key=”#userName”) condition    缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存    @Cacheable(value=”testcache”,condition=”#userName.length()>2”) 实例 @Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。“accountCache”缓存是在 spring*.xml 中定义的名称。 @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache  public Account getAccountByName(String userName) {      // 方法内部实现不考虑缓存逻辑,直接实现业务      System.out.println("real query account."+userName);       return getFromDB(userName);  }  @CachePut @CachePut 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 @CachePut 作用和配置方法 参数    解释    example value    缓存的名称,在 spring 配置文件中定义,必须指定至少一个    @CachePut(value=”my cache”) key    缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合    @CachePut(value=”testcache”,key=”#userName”) condition    缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存    @CachePut(value=”testcache”,condition=”#userName.length()>2”) 实例 @CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。 @CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存 public Account updateAccount(Account account) {     return updateDB(account);  }  @CacheEvict @CachEvict 的作用 主要针对方法配置,能够根据一定的条件对缓存进行清空 @CacheEvict 作用和配置方法 参数    解释    example value    缓存的名称,在 spring 配置文件中定义,必须指定至少一个    @CacheEvict(value=”my cache”) key    缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合    @CacheEvict(value=”testcache”,key=”#userName”) condition    缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存    @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) allEntries    是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存    @CachEvict(value=”testcache”,allEntries=true) beforeInvocation    是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存    @CachEvict(value=”testcache”,beforeInvocation=true) 实例 @CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存   public void updateAccount(Account account) {      updateDB(account);  }  @CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存 public void reload() {      reloadAll() } @Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache  public Account getAccountByName(String userName) {   // 方法内部实现不考虑缓存逻辑,直接实现业务  return getFromDB(userName);  } @CacheConfig 所有的@Cacheable()里面都有一个value=“xxx”的属性,这显然如果方法多了,写起来也是挺累的,如果可以一次性声明完 那就省事了, 所以,有了@CacheConfig这个配置,@CacheConfig is a class-level annotation that allows to share the cache names,如果你在你的方法写别的名字,那么依然以方法的名字为准。 @CacheConfig("books") public class BookRepositoryImpl implements BookRepository {      @Cacheable     public Book findBook(ISBN isbn) {...} } 条件缓存 下面提供一些常用的条件缓存 //@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存;  @Cacheable(value = "user", key = "#id", condition = "#id lt 10") public User conditionFindById(final Long id)   //@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存;  @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")   public User conditionSave(final User user)    //@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反) @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'") public User conditionSave2(final User user)    //@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存; @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")   public User conditionDelete(final User user)    @Caching 有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id–>user;username—>user;email—>user的缓存;此时就需要@Caching组合多个注解标签了。 @Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) public User save(User user) { 自定义缓存注解 比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如: @Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface UserSaveCache { } 这样我们在方法上使用如下代码即可,整个代码显得比较干净。 @UserSaveCache public User save(User user) 扩展 比如findByUsername时,不应该只放username–>user,应该连同id—>user和email—>user一起放入;这样下次如果按照id查找直接从缓存中就命中了 @Caching(     cacheable = {        @Cacheable(value = "user", key = "#username")     },     put = {        @CachePut(value = "user", key = "#result.id", condition = "#result != null"),        @CachePut(value = "user", key = "#result.email", condition = "#result != null")     } ) public User findByUsername(final String username) {     System.out.println("cache miss, invoke find by username, username:" + username);     for (User user : users) {         if (user.getUsername().equals(username)) {             return user;         }     }     return null; } 其实对于:id—>user;username—->user;email—>user;更好的方式可能是:id—>user;username—>id;email—>id;保证user只存一份;如: @CachePut(value="cacheName", key="#user.username", cacheValue="#user.username")   public void save(User user)    @Cacheable(value="cacheName", key="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())")   public User findByUsername(String username)   SpEL上下文数据 Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档: 名称    位置    描述    示例 methodName    root对象    当前被调用的方法名    root.methodName method    root对象    当前被调用的方法    root.method.name target    root对象    当前被调用的目标对象    root.target targetClass    root对象    当前被调用的目标对象类    root.targetClass args    root对象    当前被调用的方法的参数列表    root.args[0] caches    root对象    当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache    root.caches[0].name argument name    执行上下文    当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数    user.id result    执行上下文    方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache evict’的beforeInvocation=false)    result @CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true)   public void conditionUpdate(User user)  ———————————————— 原文链接:https://blog.csdn.net/whatlookingfor/article/details/51833378/ 
  • [技术干货] cacheable怎么获取缓存值_SpringBoot2.x操作缓存的新姿势
     一、介绍 spring cache 是spring3版本之后引入的一项技术,可以简化对于缓存层的操作,spring cache与springcloud stream类似,都是基于抽象层,可以任意切换其实现。 其核心是CacheManager、Cache这两个接口,所有由spring整合的cache都要实现这两个接口、Redis的实现类则是 RedisCache 和 RedisManager。 二、使用 1、查询 需要导入的依赖    org.springframework.bootgroupId>    spring-boot-starter-cacheartifactId>dependency>    org.springframework.bootgroupId>    spring-boot-starter-data-redisartifactId>dependency> 编写对于cache的配置 @EnableCaching@SpringBootConfigurationpublic class CacheConfig {    @Autowired    private RedisConnectionFactory connectionFactory;    @Bean // 如果有多个CacheManager的话需要使用@Primary直接指定那个是默认的    public RedisCacheManager cacheManager() {        RedisSerializer redisSerializer = new StringRedisSerializer();        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);        ObjectMapper om = new ObjectMapper();        // 防止在序列化的过程中丢失对象的属性        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);        // 开启实体类和json的类型转换        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);        jackson2JsonRedisSerializer.setObjectMapper(om);        // 配置序列化(解决乱码的问题)        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()   .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))             .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))                // 不缓存空值                .disableCachingNullValues()                // 1分钟过期                .entryTtl(Duration.ofMinutes(1))                ;        RedisCacheManager cacheManager = RedisCacheManager.builder(connectionFactory)                .cacheDefaults(config)                .build();        return cacheManager;    }} 进行以上配置即可使用springboot cache了,还有一个key的生成策略的配置(可选) @Beanpublic KeyGenerator keyGenerator() {    return (target, method, params) -> {        StringBuffer key = new StringBuffer();        key.append(target.getClass().getSimpleName() + "#" + method.getName() + "(");        for (Object args : params) {            key.append(args + ",");        }        key.deleteCharAt(key.length() - 1);        key.append(")");        return key.toString();    };} 注意:如果配置了KeyGenerator ,在进行缓存的时候如果不指定key的话,最后会把生成的key缓存起来,如果同时配置了KeyGenerator 和key则优先使用key。推荐:深入SpringBoot核心注解原理 在controller或者service的类上面添加 @CacheConfig ,注解里面的参数详情见下表: 在标有@CacheConfig的类里面编写一个查询单个对象的方法并添加 @Cacheable注解 @Cacheable(key = "#id", unless = "#result == null") @PatchMapping("/course/{id}")public Course courseInfo(@PathVariable Integer id) {    log.info("进来了 .. ");    return courseService.getCourseInfo(id);} 执行完该方法后,执行结果将会被缓存到Redis:  @Cacheable注解中参数详情见下表:  2、修改 编写一个修改的方法,参数传对象,返回值也改成这个对象 @PutMapping("/course")public Course modifyCoruse(@RequestBody Course course) {    courseService.updateCourse(course);    return course;} 在方法上面添加 @CachePut(key = "#course.id") 注解,这个注解表示将方法的返回值更新到缓存中,注解中的参数和 @Cacheable 中的一样,这里就略过了。扩展:SpringBoot内容聚合 3、删除 编写删除方法,在方法上添加@CacheEvict 注解 @CacheEvict(key = "#id")@DeleteMapping("/course/{id}")public void removeCourse(@PathVariable Integer id) {   courseService.remove(id);} @CacheEvict 的参数信息见下表: 三、 基于代码的Cache的使用 因为我们有配置的CacheManager,所以可以利用RedisCacheManager对象去手动操作cache,首先将CacheManager注入进来: @Resource private CacheManager cacheManager;@PatchMapping("/course2/{id}")public Course course2(@PathVariable Integer id) {    // 获取指定命名空间的cache    Cache cache = cacheManager.getCache("course");    // 通过key获取对应的value    Cache.ValueWrapper wrapper = cache.get(2);    if (wrapper == null) {        // 查询数据库        Course course = courseService.getCourseInfo(id);        // 加入缓存        cache.put(course.getId(), course);        return course;    } else {        // 将缓存的结果返回        // 因为配置了enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);        // 所以在进行强转的时候不会报错        return (Course) wrapper.get();  ————————————————原文链接:https://blog.csdn.net/weixin_34476847/article/details/113050518 
  • [技术干货] 滑动图片验证
    1.免费接口https://007.qq.com/product.html?ADTAG=index.block(这个是腾讯的验证码,目前是免费的,2018-09-13)2.登录注册(自己按照提示注册一个就好了,他上面也有比较完整的接入文档)3.AppID和App Secret Key(注册完成后,你会有一个AppID和App Secret Key,这个一定要记住,后面要用到,忘记了也可以登录自己的账户可以查询到)4.前端部分  --> 引入js文件      --> 激活验证码    (点击按钮,会自动激活验证码,data-cbfn:回调函数)  --> 回调函数       (这里,发起的Ajax请求,主要是要在后端再做一次验证)5.后端部分 (基于.net,参考文档上有其他方式的)  --> 服务器接入的url     https://ssl.captcha.qq.com/ticket/verify  --> 必填参数       --> 后端发起请求       --> 返回数据格式    (再在后台做二次验证,我的理解是主要是要验证当前的用户是否是请求的用户,以及前端的信息是否有做过其他更改,这样会更可靠)原文链接:https://www.cnblogs.com/LiCoco/p/9642176.html
  • [技术干货] springboot整合shiro-ehcache缓存
     shiro使用ehcache缓存 pom添加依赖       org.apache.shiro     shiro-ehcache     1.4.0  在ShiroConfig中添加缓存管理器 /**  * shiro缓存管理器;  * 需要添加到securityManager中  * @return  */ @Bean public EhCacheManager ehCacheManager(){     EhCacheManager cacheManager = new EhCacheManager();     cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");     return cacheManager; } 将缓存管理器添加到SecurityManager中 /**  * 配置核心安全事务管理器  * @param shiroRealm  * @return  */ @Bean(name="securityManager") public SecurityManager securityManager() {     DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();     //设置自定义realm.     securityManager.setRealm(shiroRealm());     //配置记住我 参考博客:     securityManager.setRememberMeManager(rememberMeManager());      //配置 ehcache缓存管理器 参考博客:     securityManager.setCacheManager(ehCacheManager());      //配置自定义session管理,使用redis 参考博客:     //securityManager.setSessionManager(sessionManager());      return securityManager; } 修改shiroRealm开启缓存 /**  *  身份认证realm; (这个需要自己写,账号密码校验;权限等)  * @return  */ @Bean public ShiroRealm shiroRealm(){     ShiroRealm shiroRealm = new ShiroRealm();     shiroRealm.setCachingEnabled(true);     //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false     shiroRealm.setAuthenticationCachingEnabled(true);     //缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置     shiroRealm.setAuthenticationCacheName("authenticationCache");     //启用授权缓存,即缓存AuthorizationInfo信息,默认false     shiroRealm.setAuthorizationCachingEnabled(true);     //缓存AuthorizationInfo信息的缓存名称  在ehcache-shiro.xml中有对应缓存的配置     shiroRealm.setAuthorizationCacheName("authorizationCache");     return shiroRealm; } 在config下添加ehcache-shiro.xml                    name:缓存名称。        maxElementsOnDisk:硬盘最大缓存个数。0表示不限制        maxEntriesLocalHeap:指定允许在内存中存放元素的最大数量,0表示不限制。        maxBytesLocalDisk:指定当前缓存能够使用的硬盘的最大字节数,其值可以是数字加单位,单位可以是K、M或者G,不区分大小写,                           如:30G。当在CacheManager级别指定了该属性后,Cache级别也可以用百分比来表示,                           如:60%,表示最多使用CacheManager级别指定硬盘容量的60%。该属性也可以在运行期指定。当指定了该属性后会隐式的使当前Cache的overflowToDisk为true。        maxEntriesInCache:指定缓存中允许存放元素的最大数量。这个属性也可以在运行期动态修改。但是这个属性只对Terracotta分布式缓存有用。        maxBytesLocalHeap:指定当前缓存能够使用的堆内存的最大字节数,其值的设置规则跟maxBytesLocalDisk是一样的。        maxBytesLocalOffHeap:指定当前Cache允许使用的非堆内存的最大字节数。当指定了该属性后,会使当前Cache的overflowToOffHeap的值变为true,                              如果我们需要关闭overflowToOffHeap,那么我们需要显示的指定overflowToOffHeap的值为false。        overflowToDisk:boolean类型,默认为false。当内存里面的缓存已经达到预设的上限时是否允许将按驱除策略驱除的元素保存在硬盘上,默认是LRU(最近最少使用)。                       当指定为false的时候表示缓存信息不会保存到磁盘上,只会保存在内存中。                       该属性现在已经废弃,推荐使用cache元素的子元素persistence来代替,如:。        diskSpoolBufferSizeMB:当往磁盘上写入缓存信息时缓冲区的大小,单位是MB,默认是30。        overflowToOffHeap:boolean类型,默认为false。表示是否允许Cache使用非堆内存进行存储,非堆内存是不受Java GC影响的。该属性只对企业版Ehcache有用。        copyOnRead:当指定该属性为true时,我们在从Cache中读数据时取到的是Cache中对应元素的一个copy副本,而不是对应的一个引用。默认为false。        copyOnWrite:当指定该属性为true时,我们在往Cache中写入数据时用的是原对象的一个copy副本,而不是对应的一个引用。默认为false。        timeToIdleSeconds:单位是秒,表示一个元素所允许闲置的最大时间,也就是说一个元素在不被请求的情况下允许在缓存中待的最大时间。默认是0,表示不限制。        timeToLiveSeconds:单位是秒,表示无论一个元素闲置与否,其允许在Cache中存在的最大时间。默认是0,表示不限制。        eternal:boolean类型,表示是否永恒,默认为false。如果设为true,将忽略timeToIdleSeconds和timeToLiveSeconds,Cache内的元素永远都不会过期,也就不会因为元素的过期而被清除了。        diskExpiryThreadIntervalSeconds :单位是秒,表示多久检查元素是否过期的线程多久运行一次,默认是120秒。        clearOnFlush:boolean类型。表示在调用Cache的flush方法时是否要清空MemoryStore。默认为true。        diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.        maxElementsInMemory:缓存最大数目        memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。             memoryStoreEvictionPolicy:                Ehcache的三种清空策略;                FIFO,first in first out,这个是大家最熟的,先进先出。                LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。                LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。              maxElementsInMemory="10000"             eternal="false"             timeToIdleSeconds="0"             timeToLiveSeconds="0"             overflowToDisk="false"             diskPersistent="false"             diskExpiryThreadIntervalSeconds="120"     />                      maxEntriesLocalHeap="2000"            eternal="false"            timeToIdleSeconds="0"            timeToLiveSeconds="0"            overflowToDisk="false"            statistics="true">                           maxEntriesLocalHeap="2000"            eternal="false"            timeToIdleSeconds="0"            timeToLiveSeconds="0"            overflowToDisk="false"            statistics="true">        在ShiroRealm的doGetAuthorizationInfo方法中添加日志,或者直接看控制台sql打印也行。 启动项目,使用admin登录,访问http://localhost:9090/userInfo/view 第一次访问,查看控制台,有sql查询,再次访问将不再打印查询数据库的日志,证明缓存生效了。上面的缓存配置时间配置为永久,请根据需求自己更改值来进行测试。 关于在ShiroRealm中对 用户信息 角色信息 权限信息的查询,如果需要添加缓存 请自行处理。 如果用户的权限发生改变怎么办? 上面已经启用了缓存,第一次请求走数据库查询,后续请求将直接查询ehcache缓存,假如这个时候在权限控制台分配了某个权限给某个角色,那么拥有这个角色的所有用户在下次请求之前都需要从数据库查询最新的权限信息。下面开始进行在权限发生改变时,该如何做:  在ShiroRealm中添加以下方法(清理缓存) /**  * 重写方法,清除当前用户的的 授权缓存  * @param principals  */ @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) {     super.clearCachedAuthorizationInfo(principals); }  /**  * 重写方法,清除当前用户的 认证缓存  * @param principals  */ @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) {     super.clearCachedAuthenticationInfo(principals); }  @Override public void clearCache(PrincipalCollection principals) {     super.clearCache(principals); }  /**  * 自定义方法:清除所有 授权缓存  */ public void clearAllCachedAuthorizationInfo() {     getAuthorizationCache().clear(); }  /**  * 自定义方法:清除所有 认证缓存  */ public void clearAllCachedAuthenticationInfo() {     getAuthenticationCache().clear(); }  /**  * 自定义方法:清除所有的  认证缓存  和 授权缓存  */ public void clearAllCache() {     clearAllCachedAuthenticationInfo();     clearAllCachedAuthorizationInfo(); } 在shiroConfig中添加以下bean /**  * 让某个实例的某个方法的返回值注入为Bean的实例  * Spring静态注入  * @return  */ @Bean public MethodInvokingFactoryBean getMethodInvokingFactoryBean(){     MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean();     factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");     factoryBean.setArguments(new Object[]{securityManager()});     return factoryBean; } 在UserController中添加下面两个方法     /**      * 给admin用户添加 userInfo:del 权限      * @param model      * @return      */     @RequestMapping(value = "/addPermission",method = RequestMethod.GET)     @ResponseBody     public String addPermission(Model model) {          //在sys_role_permission 表中  将 删除的权限 关联到admin用户所在的角色         roleMapper.addPermission(1,3);          //添加成功之后 清除缓存         DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager();         ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next();         //清除权限 相关的缓存         shiroRealm.clearAllCache();         return "给admin用户添加 userInfo:del 权限成功";      }      /**      * 删除admin用户 userInfo:del 权限      * @param model      * @return      */     @RequestMapping(value = "/delPermission",method = RequestMethod.GET)     @ResponseBody     public String delPermission(Model model) {          //在sys_role_permission 表中  将 删除的权限 关联到admin用户所在的角色         roleMapper.delPermission(1,3);         //添加成功之后 清除缓存         DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager)SecurityUtils.getSecurityManager();         ShiroRealm shiroRealm = (ShiroRealm) securityManager.getRealms().iterator().next();         //清除权限 相关的缓存         shiroRealm.clearAllCache();          return "删除admin用户userInfo:del 权限成功";      } 注意:在添加权限 或者 删除权限之后 都有调用shiroRealm.clearAllCache();来清除所有的缓存。 ———————————————— 原文链接:https://blog.csdn.net/qq_34021712/article/details/80309246 
  • SpringBoot文件上传异常的处理
    SpringBoot搭建的应用,一直工作得好好的,突然发现上传文件失败,提示org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid目录非法,实际查看目录,结果还真没有,下面就这个问题的表现,分析下SpringBoot针对文件上传的处理过程I. 问题分析0. 堆栈分析问题定位,最佳的辅助手段就是堆栈分析,首先捞出核心的堆栈信息org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:122) at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:113) at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.(StandardMultipartHttpServletRequest.java:86) at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:93) at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1128) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:960) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877) at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)从堆栈内容来看,问题比较清晰,目录非法,根据path路径,进入目录,结果发现,没有这个目录,那么问题的关键就是没有目录为什么会导致异常了,这个目录到底有啥用先简单描述下上面的原因,上传的文件会缓存到本地磁盘,而缓存的路径就是上面的/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT,接着引入的疑问就是:为什么上传的文件要缓存到本地为什么临时目录会不存在什么地方实现文件缓存1. 场景模拟要确认上面的问题,最直观的方法就是撸源码,直接看代码就有点蛋疼了,接下来采用debug方式来层层剥离,看下根源再哪里。首先是搭建一个简单的测试项目,进行场景复现, 首先创建一个接收文件上传的Controller,如下@RestController @RequestMapping(path = "/file") public class FileUploadRest { /** * 保存上传的文件 * * @param file * @return */ private String saveFileToLocal(MultipartFile file) { try { String name = "/tmp/out_" + System.currentTimeMillis() + file.getName(); FileOutputStream writer = new FileOutputStream(new File(name)); writer.write(file.getBytes()); writer.flush(); writer.close(); return name; } catch (Exception e) { e.printStackTrace(); return e.getMessage(); } } @PostMapping(path = "upload") public String upload(@RequestParam("file") MultipartFile file) { String ans = saveFileToLocal(file); return ans; } }其次就是使用curl来上传文件curl http://127.0.0.1:8080/file/upload -F "file=@/Users/user/Desktop/demo.jpg" -v然后在接收文件上传的方法中开启断点,注意下面红框中的 location, 就是文件上传的临时目录2. 源码定位上面的截图可以确认确实将上传的文件保存到了临时目录,验证方式就是进入那个目录进行查看,会看到一个tmp文件,接下来我们需要确定的是在什么地方,实现将数据缓存到本地的。左边红框是这次请求的完整链路,我们可以通过逆推链路,去定位可能实现文件缓存的地方如果对spring和tomcat的源码不熟的话,也没什么特别的好办法,从上面的链路中,多打一些断点,采用传说中的二分定位方法来缩小范围。通过最开始的request对象和后面的request对象分析,发现一个可以作为参考标准的就是上图中右边红框的request#parts属性;开始是null,文件保存之后则会有数据所以关键就是org.springframework.web.filter.HiddenHttpMethodFilter#doFilterInternal 中的 String paramValue = request.getParameter(this.methodParam); 这一行代码到这里在单步进去,主要的焦点将集中在 org.apache.catalina.connector.Request#parseParts进入上面方法的逻辑,很容易找到具体的实现位置 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest,这个方法的实现比较有意思,有必要贴出来看一下public List parseRequest(RequestContext ctx) throws FileUploadException { List items = new ArrayList<>(); boolean successful = false; try { FileItemIterator iter = getItemIterator(ctx); // 注意这里,文件工厂类,里面保存了临时目录的地址 // 这个对象首次是在 org.apache.catalina.connector.Request#parseParts 方法的 FileItemFactory fac = getFileItemFactory(); if (fac == null) { throw new NullPointerException("No FileItemFactory has been set."); } while (iter.hasNext()) { final FileItemStream item = iter.next(); // Don't use getName() here to prevent an InvalidFileNameException. final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name; // 创建一个临时文件对象 FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName); items.add(fileItem); try { // 流的拷贝,这块代码也挺有意思,将输入流数据写入输出流 // 后面会贴出源码,看下开源大佬们的玩法,和我们自己写的有啥区别 Streams.copy(item.openStream(), fileItem.getOutputStream(), true); } catch (FileUploadIOException e) { throw (FileUploadException) e.getCause(); } catch (IOException e) { throw new IOFileUploadException(String.format("Processing of %s request failed. %s", MULTIPART_FORM_DATA, e.getMessage()), e); } final FileItemHeaders fih = item.getHeaders(); fileItem.setHeaders(fih); } successful = true; return items; } catch (FileUploadIOException e) { throw (FileUploadException) e.getCause(); } catch (IOException e) { throw new FileUploadException(e.getMessage(), e); } finally { if (!successful) { for (FileItem fileItem : items) { try { fileItem.delete(); } catch (Exception ignored) { // ignored TODO perhaps add to tracker delete failure list somehow? } } } } }核心代码就两点,一个是文件工厂类,一个是流的拷贝;前者定义了我们的临时文件目录,也是我们解决前面问题的关键,换一个我自定义的目录永不删除,不就可以避免上面的问题了么;后面一个则是数据复用方面的首先看下FileItemFactory的实例化位置,在org.apache.catalina.connector.Request#parseParts中具体的location实例化代码为// TEMPDIR = "javax.servlet.context.tempdir"; location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR));3. 问题reviewa. 解决问题到上面,基本上就捞到了最终的问题,先看如何解决这个问题方法1应用重启方法2增加服务配置,自定义baseDirserver.tomcat.basedir=/tmp/tomcat方法3注入bean,手动配置临时目录@Bean MultipartConfigElement multipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); factory.setLocation("/tmp/tomcat"); return factory.createMultipartConfig(); }方法4配置不删除tmp目录下的tomcatvim /usr/lib/tmpfiles.d/tmp.conf # 添加一行 x /tmp/tomcat.*b. 流拷贝tomcat中实现流的拷贝代码如下,org.apache.tomcat.util.http.fileupload.util.Streams#copy(java.io.InputStream, java.io.OutputStream, boolean, byte[]) , 看下面的实现,直观影响就是写得真特么严谨public static long copy(InputStream inputStream, OutputStream outputStream, boolean closeOutputStream, byte[] buffer) throws IOException { OutputStream out = outputStream; InputStream in = inputStream; try { long total = 0; for (;;) { int res = in.read(buffer); if (res == -1) { break; } if (res > 0) { total += res; if (out != null) { out.write(buffer, 0, res); } } } if (out != null) { if (closeOutputStream) { out.close(); } else { out.flush(); } out = null; } in.close(); in = null; return total; } finally { IOUtils.closeQuietly(in); if (closeOutputStream) { IOUtils.closeQuietly(out); } } }c. 自问自答前面提出了几个问题,现在给一个简单的回答,因为篇幅问题,后面会单开一文,进行详细说明什么地方缓存文件上面的定位过程给出答案,具体实现逻辑在 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest为什么目录会不存在springboot启动时会创建一个/tmp/tomcat.*/work/Tomcat/localhost/ROOT的临时目录作为文件上传的临时目录,但是该目录会在n天之后被系统自动清理掉,这个清理是由linux操作系统完成的,具体的配置如下 vim /usr/lib/tmpfiles.d/tmp.conf# This file is part of systemd. # # systemd is free software; you can redistribute it and/or modify it # under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation; either version 2.1 of the License, or # (at your option) any later version. # See tmpfiles.d(5) for details # Clear tmp directories separately, to make them easier to override v /tmp 1777 root root 10d v /var/tmp 1777 root root 30d # Exclude namespace mountpoints created with PrivateTmp=yes x /tmp/systemd-private-%b-* X /tmp/systemd-private-%b-*/tmp x /var/tmp/systemd-private-%b-* X /var/tmp/systemd-private-%b-*/tmp为什么要缓存文件因为流取一次消费之后,后面无法再从流中获取数据,所以缓存方便后续复。原文链接:https://www.cnblogs.com/yihuihui/p/10372887.html
  • [技术干货] 基于RSA的前后端登陆密码加密JAVA实现
    RSA加密算法简介SA加密算法是一种非对称加密算法。在公开密钥加密和电子商业中RSA被广泛使用。对极大整数做因数分解的难度决定了RSA算法的可靠性。换言之,对一极大整数做因数分解愈困难,RSA算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用RSA加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的RSA钥匙才可能被强力方式解破。到目前为止,世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长,用RSA加密的信息实际上是不能被解破的。RSA加密的java实现实现的思路,由RSA随机生成一对公钥和私钥,公钥方到客户端,私钥放到服务端,发送数据的时候由公钥对传输数据进行加密,然后发送给服务端,服务端用私钥才能对数据进行解密.下面是代码实现的例子package com.yihur.demoimport org.apache.commons.codec.binary.Base64;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import javax.crypto.BadPaddingException;import javax.crypto.Cipher;import javax.crypto.IllegalBlockSizeException;import javax.crypto.NoSuchPaddingException;import java.nio.charset.StandardCharsets;import java.security.*;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.security.spec.InvalidKeySpecException;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.HashMap;import java.util.Map;/** * @author yihur * @description RSA加密 * @date 2019/4/3 */public class MyRSAencryptionMethod { private static Logger logger = LoggerFactory.getLogger(MyRSAencryptionMethod .class); /** * 用于封装随机产生的公钥与私钥 * * @author yihur * @date 2019/4/4 * @param * @return */ private static Map keyMap = new HashMap<>(); /** * 测试方法 * * @param args * @return void * * * 前端用crypto-js进行加密, * npm i jsencrypt, * 然后页面头引入import JSEncrypt from 'jsencrypt'; * const encrypt = new JSEncrypt(); * encrypt.setPublicKey('你的公钥'); * password = encrypt.encrypt(‘你的密码’);// 加密后的字符串 * @author yihur * @date 2019/4/4 */ public static void main(String[] args) { //生成公钥和私钥 genKeyPair(); //加密字符串 String message = "df723820"; System.out.println("随机生成的公钥为:" + keyMap.get(0)); System.out.println("随机生成的私钥为:" + keyMap.get(1)); String messageEn = encrypt(message, keyMap.get(0)); System.out.println("加密后的字符串为:" + messageEn); String messageDe = decrypt(messageEn, keyMap.get(1)); System.out.println("还原后的字符串为:" + messageDe); } /** * 随机生成密钥对 * * @param * @return void * @author yihur * @date 2019/4/4 */ public static void genKeyPair() { // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 KeyPairGenerator keyPairGen = null; try { keyPairGen = KeyPairGenerator.getInstance("RSA"); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); logger.info(e.getMessage()); } // 初始化密钥对生成器,密钥大小为96-1024位 assert keyPairGen != null; keyPairGen.initialize(1024, new SecureRandom()); // 生成一个密钥对,保存在keyPair中 KeyPair keyPair = keyPairGen.generateKeyPair(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥 String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded())); // 得到私钥字符串 String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded()))); // 将公钥和私钥保存到Map keyMap.put(0, publicKeyString); //0表示公钥 keyMap.put(1, privateKeyString); //1表示私钥 } /** * RSA公钥加密 * * @param str 加密字符串 * @param publicKey 公钥 * @return 密文 */ public static String encrypt(String str, String publicKey) { //base64编码的公钥 byte[] decoded = Base64.decodeBase64(publicKey); RSAPublicKey pubKey = null; String outStr = null; try { pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, pubKey); outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8))); } catch (InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException e) { e.printStackTrace(); logger.info(e.getMessage()); } //RSA加密 return outStr; } /** * RSA私钥解密 * * @param str 加密字符串 * @param privateKey 私钥 * @return 铭文 */ public static String decrypt(String str, String privateKey) { //64位解码加密后的字符串 byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8)); //base64编码的私钥 byte[] decoded = Base64.decodeBase64(privateKey); RSAPrivateKey priKey = null; //RSA解密 Cipher cipher = null; String outStr = null; try { priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded)); cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, priKey); outStr = new String(cipher.doFinal(inputByte)); } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException e) { e.printStackTrace(); logger.info(e.getMessage()); } return outStr; }}RSA加密的前端用法前端用crypto-js进行加密,npm i jsencrypt,然后页面头引入import JSEncrypt from 'jsencrypt';const encrypt = new JSEncrypt();encrypt.setPublicKey('你的公钥');password = encrypt.encrypt(‘你的密码’);// 加密后的字符串后续在实际应用中RSA加密也还是远远不够,一般还会加入MD5加密的方式,以及加密验证,token等等方式作为请求连接的校验,比如后端加密一个MD5字符串,给前端之后,前端用特定组合加上传输数据返回一个RSA加密的字符串,后端接收后解密,然后和自身的字符串进行对比,以确认数据来源的准确性.原文链接:https://www.jianshu.com/p/d1e303b976aa
总条数:2301 到第 页
上滑加载中