天天都在使用的 Java 注解,你真的了解它吗?

开发 后端
注解(Annotation)是一种可以放在 Java 类上,方法上,属性上,参数前面的一种特殊的注释,用来注释注解的注解叫做元注解。元注解我们平常不会编写,只需要添加到我们自己编写的注解上即可,。

[[353180]]

本文转载自微信公众号「Java极客技术」,作者鸭血粉丝 。转载本文请联系Java极客技术公众号。 

Hello,大家好,我是阿粉,Java 的注解相信大家天天都在用,但是关于注解的原理,大家都了解吗?这篇文章通过意见简单的示例给大家演示一下注解的使用和原理。

Java 元注解

注解(Annotation)是一种可以放在 Java 类上,方法上,属性上,参数前面的一种特殊的注释,用来注释注解的注解叫做元注解。元注解我们平常不会编写,只需要添加到我们自己编写的注解上即可,。

Java 自带的常用的元注解有@Target,@Retention,@Documented,@Inherited 分别有如下含义

  1. @Target:标记这个注解使用的地方,取值范围在枚举 java.lang.annotation.ElementType:TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE,ANNOTATION_TYPE,PACKAGE,TYPE_PARAMETER,TYPE_USE。
  2. @Retention :标识这个注解的生命周期,取值范围在枚举 java.lang.annotation.RetentionPolicy,SOURCE,CLASS,RUNTIME,一般定义的注解都是在运行时使用,所有要用 @Retention(RetentionPolicy.RUNTIME);
  3. @Documented:表示注解是否包含到文档中。
  4. @Inherited :使用@Inherited定义子类是否可继承父类定义的Annotation。@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效。

定义注解

上面介绍了几个元注解,下面我们定义一个日志注解来演示一下,我们通过定义一个名为OperationLog 的注解来记录一些通用的操作日志,比如记录什么时候什么人查询的哪个表的数据或者新增了什么数据。编写注解我们用的是 @interface 关键字,相关代码如下:

  1. package com.api.annotation; 
  2.  
  3. import java.lang.annotation.*; 
  4.  
  5. /** 
  6.  * <br> 
  7.  * <b>Function:</b><br> 
  8.  * <b>Author:</b>@author 子悠<br> 
  9.  * <b>Date:</b>2020-11-17 22:10<br> 
  10.  * <b>Desc:</b>用于记录操作日志<br> 
  11.  */ 
  12. @Target({ElementType.METHOD}) 
  13. @Retention(RetentionPolicy.RUNTIME) 
  14. @Documented 
  15. public @interface OperationLog { 
  16.  
  17.     /** 
  18.      * 操作类型 
  19.      * 
  20.      * @return 
  21.      */ 
  22.     String type() default OperationType.SELECT
  23.  
  24.     /** 
  25.      * 操作说明 
  26.      * 
  27.      * @return 
  28.      */ 
  29.     String desc() default ""
  30.  
  31.     /** 
  32.      * 请求路径 
  33.      * 
  34.      * @return 
  35.      */ 
  36.     String path() default ""
  37.  
  38.     /** 
  39.      * 是否记录日志,默认是 
  40.      * 
  41.      * @return 
  42.      */ 
  43.     boolean write() default true
  44.  
  45.     /** 
  46.      * 是否需要登录信息 
  47.      * 
  48.      * @return 
  49.      */ 
  50.     boolean auth() default true
  51.    /** 
  52.      * 当 type 为 save 时必须 
  53.      * 
  54.      * @return 
  55.      */ 
  56.     String primaryKey() default ""
  57.  
  58.     /** 
  59.      * 对应 service 的 Class 
  60.      * 
  61.      * @return 
  62.      */ 
  63.     Class<?> defaultServiceClass() default Object.class; 

说明

上面的注解,我们增加了@Target({ElementType.METHOD}) , @Retention(RetentionPolicy.RUNTIME), @Documented 三个元注解,表示我们这个注解是使用在方法上的,并且生命周期是运行时,而且可以记录到文档中。然后我们可以看到定义注解采用的u是@interface 关键字,并且我们给这个注解定义了几个属性,同时设置了默认值。主要注意的是平时我们编写的注解一般必须设置@Target和@Retention,而且 @Retention一般设置为RUNTIME,这是因为我们自定义的注解通常要求在运行期读取,另外一般情况下,不必写@Inherited。

使用

上面的动作只是把注解定义出来了,但是光光定义出来是没有用的,必须有一个地方读取解析,才能提现出注解的价值,我们就采用 Spring 的 AOP 拦截这个注解,将所有携带这个注解的方法所进行的操作都记录下来。

  1. package com.api.config; 
  2.  
  3. import lombok.extern.slf4j.Slf4j; 
  4. import org.aspectj.lang.ProceedingJoinPoint; 
  5. import org.aspectj.lang.annotation.Around; 
  6. import org.aspectj.lang.annotation.Aspect; 
  7. import org.aspectj.lang.annotation.Pointcut; 
  8. import org.aspectj.lang.reflect.MethodSignature; 
  9. import org.springframework.beans.factory.annotation.Autowired; 
  10. import org.springframework.core.annotation.Order
  11. import org.springframework.stereotype.Component; 
  12. import org.springframework.web.bind.annotation.GetMapping; 
  13. import org.springframework.web.bind.annotation.PostMapping; 
  14. import org.springframework.web.bind.annotation.RequestMapping; 
  15.  
  16. import javax.servlet.http.HttpServletRequest; 
  17. import java.lang.reflect.Field; 
  18. import java.lang.reflect.Method; 
  19. import java.util.*; 
  20.  
  21. /** 
  22.  * <br> 
  23.  * <b>Function:</b><br> 
  24.  * <b>Author:</b>@author 子悠<br> 
  25.  * <b>Date:</b>2020-11-17 14:40<br> 
  26.  * <b>Desc:</b>aspect for operation log<br> 
  27.  */ 
  28. @Aspect 
  29. @Component 
  30. @Order(-5) 
  31. @Slf4j 
  32. public class LogAspect { 
  33.     /** 
  34.      * Pointcut for methods which need to record operate log 
  35.      */ 
  36.     @Pointcut("within(com.xx.yy.controller..*) && @annotation(com.api.annotation.OperationLog)"
  37.     public void logAspect() { 
  38.     } 
  39.  
  40.     /** 
  41.      * record log for Admin and DSP 
  42.      * 
  43.      * @param joinPoint parameter 
  44.      * @return result 
  45.      * @throws Throwable 
  46.      */ 
  47.     @Around("logAspect()"
  48.     public Object around(ProceedingJoinPoint joinPoint) throws Throwable { 
  49.         Object proceed = null
  50.         String classType = joinPoint.getTarget().getClass().getName(); 
  51.         Class<?> targetCls = Class.forName(classType); 
  52.         MethodSignature ms = (MethodSignature) joinPoint.getSignature(); 
  53.         Method targetMethod = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes()); 
  54.         OperationLog operation = targetMethod.getAnnotation(OperationLog.class); 
  55.         if (null != operation && operation.write()) { 
  56.             SysMenuOpLogEntity opLogEntity = new SysMenuOpLogEntity(); 
  57.             StringBuilder change = new StringBuilder(); 
  58.             if (StrUtil.isNotBlank(operation.type())) { 
  59.                 switch (operation.type()) { 
  60.                     case OperationType.ADD
  61.                         proceed = joinPoint.proceed(); 
  62.                         String addString = genAddData(targetCls, operation.defaultServiceClass(), joinPoint.getArgs()); 
  63.                         opLogEntity.setAfterJson(addString); 
  64.                         change.append(OperationType.ADD); 
  65.                         break; 
  66.                     case OperationType.DELETE
  67.                         String deleteString = autoQueryDeletedData(targetCls, operation.primaryKey(), operation.defaultServiceClass(), joinPoint.getArgs()); 
  68.                         opLogEntity.setBeforeJson(deleteString); 
  69.                         change.append(OperationType.DELETE); 
  70.                         proceed = joinPoint.proceed(); 
  71.                         break; 
  72.                     case OperationType.EDIT: 
  73.                         change.append(OperationType.EDIT); 
  74.                         setOpLogEntity(opLogEntity, targetCls, operation.primaryKey(), operation.defaultServiceClass(), joinPoint.getArgs()); 
  75.                         proceed = joinPoint.proceed(); 
  76.                         break; 
  77.                     case OperationType.SELECT
  78.                         opLogEntity.setBeforeJson(getQueryString(targetCls, operation.defaultServiceClass(), joinPoint.getArgs())); 
  79.                         change.append(operation.type()); 
  80.                         proceed = joinPoint.proceed(); 
  81.                         break; 
  82.                     case OperationType.SAVE: 
  83.                         savedDataOpLog(opLogEntity, targetCls, operation.primaryKey(), operation.defaultServiceClass(), joinPoint.getArgs()); 
  84.                         change.append(operation.type()); 
  85.                         proceed = joinPoint.proceed(); 
  86.                         break; 
  87.                     case OperationType.EXPORT: 
  88.                     case OperationType.DOWNLOAD: 
  89.                         change.append(operation.type()); 
  90.                         proceed = joinPoint.proceed(); 
  91.                         break; 
  92.                     default
  93.                 } 
  94.                 opLogEntity.setExecType(operation.type()); 
  95.             } 
  96.             StringBuilder changing = new StringBuilder(); 
  97.             if (StrUtil.isNotBlank(opLogEntity.getExecType())) { 
  98.                 if (operation.auth()) { 
  99.                     LoginUserVO loginUser = getLoginUser(); 
  100.                     if (null != loginUser) { 
  101.                         opLogEntity.setUserId(loginUser.getUserId()); 
  102.                         opLogEntity.setUserName(loginUser.getUserName()); 
  103.                         changing.append(loginUser.getUserName()).append("-"); 
  104.                     } else { 
  105.                         log.error("用户未登录"); 
  106.                     } 
  107.                 } 
  108.                 opLogEntity.setCreateTime(DateUtils.getCurDate()); 
  109.                 opLogEntity.setRemark(getOperateMenuName(targetMethod, operation.desc())); 
  110.                 opLogEntity.setPath(getPath(targetMethod, targetMethod.getName())); 
  111.                 opLogEntity.setChanging(changing.append(change).toString()); 
  112.                 menuOpLogService.save(opLogEntity); 
  113.             } 
  114.         } 
  115.         return proceed; 
  116.     } 
  117.  
  118.     /** 
  119.      * query data by userId 
  120.      * 
  121.      * @param targetCls           class 
  122.      * @param defaultServiceClass default service class 
  123.      * @return 
  124.      * @throws Exception 
  125.      */ 
  126.     private String queryByCurrentUserId(Class<?> targetCls, Class<?> defaultServiceClass) throws Exception { 
  127.         BaseService baseService = getBaseService(targetCls, defaultServiceClass); 
  128.         LoginUserVO loginUser = dspBaseService.getLoginUser(); 
  129.         if (null != loginUser) { 
  130.             Object o = baseService.queryId(loginUser.getUserId()); 
  131.             return JsonUtils.obj2Json(o); 
  132.         } 
  133.         return null
  134.     } 
  135.  
  136.     /** 
  137.      * return query parameter 
  138.      * 
  139.      * @param targetCls           class 
  140.      * @param args                parameter 
  141.      * @param defaultServiceClass default service class 
  142.      * @return 
  143.      * @throws Exception 
  144.      */ 
  145.     private String getQueryString(Class<?> targetCls, Class<?> defaultServiceClass, Object[] args) { 
  146.         if (args.length > 0) { 
  147.             Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass); 
  148.             for (Object arg : args) { 
  149.                 if (arg.getClass().equals(entityClz) || arg instanceof BaseModel) { 
  150.                     return JsonUtils.obj2Json(arg); 
  151.                 } 
  152.             } 
  153.         } 
  154.         return null
  155.     } 
  156.  
  157.     /** 
  158.      * save record log while OperatorType is SAVE 
  159.      * 
  160.      * @param opLogEntity         entity 
  161.      * @param targetCls           class 
  162.      * @param primaryKey          primaryKey 
  163.      * @param defaultServiceClass default service class 
  164.      * @param args                parameter 
  165.      * @throws Exception 
  166.      */ 
  167.     private void savedDataOpLog(SysMenuOpLogEntity opLogEntity, Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] args) throws Exception { 
  168.         Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass); 
  169.         BaseService baseService = getBaseService(targetCls, defaultServiceClass); 
  170.         for (Object arg : args) { 
  171.             if (arg.getClass().equals(entityClz)) { 
  172.                 if (StrUtil.isNotBlank(primaryKey)) { 
  173.                     Field declaredField = entityClz.getDeclaredField(primaryKey); 
  174.                     declaredField.setAccessible(true); 
  175.                     Object primaryKeyValue = declaredField.get(arg); 
  176.                     //if primary key is not null that means edit, otherwise is add 
  177.                     if (null != primaryKeyValue) { 
  178.                         //query data by primary key 
  179.                         Object o = baseService.queryId(primaryKeyValue); 
  180.                         opLogEntity.setBeforeJson(JsonUtils.obj2Json(o)); 
  181.                     } 
  182.                 } 
  183.                 opLogEntity.setAfterJson(JsonUtils.obj2Json(arg)); 
  184.             } 
  185.         } 
  186.     } 
  187.  
  188.     /** 
  189.      * set parameter which edit data 
  190.      * 
  191.      * @param opLogEntity         entity 
  192.      * @param targetCls           class 
  193.      * @param primaryKey          primaryKey 
  194.      * @param defaultServiceClass default service class 
  195.      * @param args                parameter 
  196.      * @throws Exception 
  197.      */ 
  198.     private void setOpLogEntity(SysMenuOpLogEntity opLogEntity, Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] args) throws Exception { 
  199.         Map<String, String> saveMap = autoQueryEditedData(targetCls, primaryKey, defaultServiceClass, args); 
  200.         if (null != saveMap) { 
  201.             if (saveMap.containsKey(ASPECT_LOG_OLD_DATA)) { 
  202.                 opLogEntity.setBeforeJson(saveMap.get(ASPECT_LOG_OLD_DATA)); 
  203.             } 
  204.             if (saveMap.containsKey(ASPECT_LOG_NEW_DATA)) { 
  205.                 opLogEntity.setBeforeJson(saveMap.get(ASPECT_LOG_NEW_DATA)); 
  206.             } 
  207.         } 
  208.     } 
  209.  
  210.     /** 
  211.      * query data for edit and after edit operate 
  212.      * 
  213.      * @param targetCls           class 
  214.      * @param primaryKey          primaryKey 
  215.      * @param defaultServiceClass default service class 
  216.      * @param args                parameter 
  217.      * @return map which data 
  218.      * @throws Exception 
  219.      */ 
  220.     private Map<String, String> autoQueryEditedData(Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] args) throws Exception { 
  221.         if (StrUtil.isBlank(primaryKey)) { 
  222.             throw new Exception(); 
  223.         } 
  224.         Map<String, String> map = new HashMap<>(16); 
  225.         Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass); 
  226.         BaseService baseService = getBaseService(targetCls, defaultServiceClass); 
  227.         for (Object arg : args) { 
  228.             if (arg.getClass().equals(entityClz)) { 
  229.                 Field declaredField = entityClz.getDeclaredField(primaryKey); 
  230.                 declaredField.setAccessible(true); 
  231.                 Object primaryKeyValue = declaredField.get(arg); 
  232.                 //query the data before edit 
  233.                 if (null != primaryKeyValue) { 
  234.                     //query data by primary key 
  235.                     Object o = baseService.queryId(primaryKeyValue); 
  236.                     map.put(ASPECT_LOG_OLD_DATA, JsonUtils.obj2Json(o)); 
  237.                     map.put(ASPECT_LOG_NEW_DATA, JsonUtils.obj2Json(arg)); 
  238.                     return map; 
  239.                 } 
  240.             } 
  241.         } 
  242.         return null
  243.     } 
  244.  
  245.     /** 
  246.      * return JSON data which add operate 
  247.      * 
  248.      * @param targetCls           class 
  249.      * @param args                parameter 
  250.      * @param defaultServiceClass default service class 
  251.      * @return add data which will be added 
  252.      * @throws Exception 
  253.      */ 
  254.     private String genAddData(Class<?> targetCls, Class<?> defaultServiceClass, Object[] args) throws Exception { 
  255.         List<Object> parameter = new ArrayList<>(); 
  256.         for (Object arg : args) { 
  257.             if (arg instanceof HttpServletRequest) { 
  258.             } else { 
  259.                 parameter.add(arg); 
  260.             } 
  261.         } 
  262.         return JsonUtils.obj2Json(parameter); 
  263.     } 
  264.  
  265.     /** 
  266.      * query delete data before delete operate 
  267.      * 
  268.      * @param targetCls           class 
  269.      * @param primaryKey          primaryKey 
  270.      * @param defaultServiceClass default service class 
  271.      * @param ids                 ids 
  272.      * @return delete data which will be deleted 
  273.      * @throws Throwable 
  274.      */ 
  275.     private String autoQueryDeletedData(Class<?> targetCls, String primaryKey, Class<?> defaultServiceClass, Object[] ids) throws Throwable { 
  276.         if (StrUtil.isBlank(primaryKey)) { 
  277.             throw new OriginException(TipEnum.LOG_ASPECT_PRIMARY_KEY_NOT_EXIST); 
  278.         } 
  279.         //get service 
  280.         BaseService baseService = getBaseService(targetCls, defaultServiceClass); 
  281.         //get entity 
  282.         Class<?> entityClz = getEntityClz(targetCls, defaultServiceClass); 
  283.         //query deleted data by primary key 
  284.         Query query = new Query(); 
  285.         WhereOperator whereOperator = new WhereOperator(entityClz); 
  286.         Set<Object> set = new HashSet<>(Arrays.asList((Object[]) ids[0])); 
  287.         whereOperator.and(primaryKey).in(set.toArray()); 
  288.         query.addWhereOperator(whereOperator); 
  289.         List list = baseService.queryList(query); 
  290.         return JsonUtils.obj2Json(list); 
  291.     } 
  292.  
  293.  
  294.     /** 
  295.      * return service by targetCls 
  296.      * 
  297.      * @param targetCls           current controller class 
  298.      * @param defaultServiceClass default service class 
  299.      * @return service instance 
  300.      * @throws Exception 
  301.      */ 
  302.     private BaseService getBaseService(Class<?> targetCls, Class<?> defaultServiceClass) throws Exception { 
  303.         //根据类名拿到对应的 service 名称 
  304.         String serviceName = getServiceName(targetCls, defaultServiceClass); 
  305.         BaseService baseService; 
  306.         if (null != defaultServiceClass) { 
  307.             baseService = (BaseService) ApplicationContextProvider.getBean(serviceName, defaultServiceClass); 
  308.         } else { 
  309.             Class<?> type = targetCls.getDeclaredField(serviceName).getType(); 
  310.             baseService = (BaseService) ApplicationContextProvider.getBean(serviceName, type); 
  311.         } 
  312.         return baseService; 
  313.     } 
  314.  
  315.     /** 
  316.      * return service name 
  317.      * 
  318.      * @param targetCls           current controller class 
  319.      * @param defaultServiceClass default service class 
  320.      * @return service name 
  321.      */ 
  322.     private String getServiceName(Class<?> targetCls, Class<?> defaultServiceClass) { 
  323.         if (null != defaultServiceClass && Object.class != defaultServiceClass) { 
  324.             return StrUtil.left(defaultServiceClass.getSimpleName(), 1).toLowerCase() + defaultServiceClass.getSimpleName().substring(1); 
  325.         } 
  326.         return StrUtil.left(targetCls.getSimpleName(), 1).toLowerCase() + targetCls.getSimpleName().substring(1).replace("Controller""Service"); 
  327.     } 
  328.  
  329.  
  330.     /** 
  331.      * return entity class 
  332.      * 
  333.      * @param targetCls           current controller class 
  334.      * @param defaultServiceClass default service class 
  335.      * @return entity class 
  336.      * @throws Exception 
  337.      */ 
  338.     private Class<?> getEntityClz(Class<?> targetCls, Class<?> defaultServiceClass) { 
  339.         try { 
  340.             Class<?> type; 
  341.             if (null != defaultServiceClass && Object.class != defaultServiceClass) { 
  342.                 type = defaultServiceClass; 
  343.             } else { 
  344.                 type = targetCls.getDeclaredField(getServiceName(targetCls, null)).getType(); 
  345.             } 
  346.             String entityName = type.getName().replace("service""entity").replace("Service""Entity"); 
  347.             Class<?> entityClz = Class.forName(entityName); 
  348.             return entityClz; 
  349.         } catch (Exception e) { 
  350.             log.error("获取 class 失败"); 
  351.         } 
  352.         return null
  353.     } 
  354.  
  355.  
  356.     /** 
  357.      * require path 
  358.      * 
  359.      * @param targetMethod target method 
  360.      * @param defaultPath  default require path 
  361.      * @return require path 
  362.      */ 
  363.     private String getPath(Method targetMethod, String defaultPath) { 
  364.         String path = defaultPath; 
  365.         PostMapping postMapping = targetMethod.getAnnotation(PostMapping.class); 
  366.         GetMapping getMapping = targetMethod.getAnnotation(GetMapping.class); 
  367.         RequestMapping requestMapping = targetMethod.getAnnotation(RequestMapping.class); 
  368.         if (null != postMapping) { 
  369.             path = postMapping.value()[0]; 
  370.         } else if (null != getMapping) { 
  371.             path = getMapping.value()[0]; 
  372.         } else if (null != requestMapping) { 
  373.             path = requestMapping.value()[0]; 
  374.         } 
  375.         return path; 
  376.     } 
  377.  

上面的代码中我们定义了一个切面指定需要拦截的包名和注解,因为涉及到很多业务相关的代码,所以不能完整的提供出来,但是整个思路就是这样的,在每种操作类型前后将需要记录的数据查询出来进行记录。代码很长主要是用来获取相应的参数值的,大家使用的时候可以根据自己的需要进行取舍。比如在新增操作的时候,我们将新增的数据进行记录下来;编辑的时候将编辑前的数据查询出来和编辑后的数据一起保存起来,删除也是一样的,在删除前将数据查询出来保存到日志表中。

同样导出和下载都会记录相应信息,整个操作类型的代码如下:

  1. package com.api.annotation; 
  2.  
  3. /** 
  4.  * <br> 
  5.  * <b>Function:</b><br> 
  6.  * <b>Author:</b>@author 子悠<br> 
  7.  * <b>Date:</b>2020-11-17 22:11<br> 
  8.  * <b>Desc:</b>无<br> 
  9.  */ 
  10. public interface OperationType { 
  11.     /** 
  12.      * 新增 
  13.      **/ 
  14.     String ADD = "add"
  15.     /** 
  16.      * 删除 
  17.      **/ 
  18.     String DELETE = "delete"
  19.     /** 
  20.      * 使用实体参数修改 
  21.      **/ 
  22.     String EDIT = "edit"
  23.     /** 
  24.      * 查询 
  25.      **/ 
  26.     String SELECT = "select"
  27.  
  28.     /** 
  29.      * 新增和修改的保存方法,使用此类型时必须配置主键字段名称 
  30.      **/ 
  31.     String SAVE = "save"
  32.  
  33.     /** 
  34.      * 导出 
  35.      **/ 
  36.     String EXPORT = "export"
  37.  
  38.     /** 
  39.      * 下载 
  40.      **/ 
  41.     String DOWNLOAD = "download"
  42.  

后续在使用的时候只需要在需要的方法上加上注解,填上相应的参数即可@OperationLog(desc = "查询单条记录", path = "/data")

总结

注解一个我们天天再用的东西,虽然不难,但是我们却很少自己去写注解的代码,通过这篇文章能给大家展示一下注解的使用逻辑,希望对大家有帮助。Spring 中的各种注解本质上也是这种逻辑都需要定义使用和解析。很多时候我们可以通过自定义注解去解决很多场景,比如日志,缓存等。

 

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2024-01-08 08:27:11

注解Bean代理

2023-06-08 11:57:15

Matter协议家庭智能

2024-08-22 08:17:55

C#工具循环

2019-09-02 08:39:02

路由器RAM内存

2023-05-29 08:11:42

@Value注解Bean

2017-12-07 15:00:00

笔记本OLED屏幕

2023-11-01 13:48:00

反射java

2022-07-26 00:00:22

HTAP系统数据库

2014-04-17 16:42:03

DevOps

2022-01-17 07:32:34

Java参数方法

2023-05-10 11:07:18

2021-01-15 07:44:21

SQL注入攻击黑客

2021-11-09 09:48:13

Logging python模块

2020-02-27 10:49:26

HTTPS网络协议TCP

2014-11-28 10:31:07

Hybrid APP

2023-03-16 10:49:55

2019-09-16 08:40:42

2018-12-21 11:24:55

Java时间处理编程语言

2021-11-26 08:07:16

MySQL SQL 语句数据库

2018-01-06 10:38:51

Ping抓包 ICMP协议
点赞
收藏

51CTO技术栈公众号