总结了一些实际开发过程中需要注意的一些细节。(持续更新…)

MySQL

  1. 使用st_distance_sphere函数获取指定地点周边一定范围内的所有地点。

    例如:district表中保存了各个城市(city_code)的经纬度信息(longitude、latitude),使用以下sql查询上海市(121.797447, 31.166809)周边300公里内的城市

    1
    2
    3
    select distinct city_code, st_distance_sphere(Point(longitude, latitude), Point(121.797447, 31.166809)) as distance
    from district
    where st_distance_sphere(Point(longitude, latitude), Point(121.797447, 31.166809)) < 300000;
  2. SQL join语法速记:

    • A inner join B on ... 取A和B的交集
    • A left join B on ... 取A全部,B没有的对应的字段值为null
    • A right join B on ... 取B全部,A没有的对应的字段值为null
    • A full outer join B on ... 取并集,彼此没有的对应的字段值为null
  3. SQL的where语句中,使用=(或!=)去匹配给定的字段值时,匹配的结果中不包含表中该字段为null的数据。

    例如:当tt_data表中存在一条数据的字段data_type为null时,使用以下sql都无法查询到该条数据:

    1
    2
    select * from tt_data where data_type = 'A';
    select * from tt_data where data_type != 'A';
  4. 使用多线程分页同步数据时,会使用到常用的分页查询sql: select * from table order by id asc limit n, m。在数据量较大时,该sql在执行到最后几页的时候耗时会明显增加,原因是limit语句会从表的第1行扫描到第n行,n越大,扫描的时间越长。下面是针对这种场景的一种优化方式:

    • 假设主键ID是自增的,查出带同步数据的最小主键ID(minId)和最大主键ID(maxId)
    • 通过sql: select * from table where id >= startId and id < endId order by id asc查询得到每页需要同步的数据。其中startId目标数据页的起始行ID,endId为目标数据页的结束行ID,并且endId = startId + m。特殊的,第一页的startId=minId,最后一页的endId=maxId。
  5. 在执行数据清理任务是,如果MySQL单表需要清除的数据超过全表数据的50%时,可采用以下方案:

    • 创建临时表,表结构与原表结构相同
    • 拷贝需要保留的数据到临时表
    • 重命名临时表为原表名
    • 删除原表

    具体sql为:

    1
    2
    3
    4
    5
    6
    7
    8
    -- 创建临时表
    CREATE TABLE `temp_table` LIKE `ori_table`;
    -- 拷贝原表数据到临时表
    INSERT INTO `temp_table` SELECT * FROM `ori_table` WHERE ...;
    -- 重命名表
    RENAME TABLE `ori_table` TO `ori_table_bak`, `temp_table` TO `ori_table`;
    -- 移除原表
    DROP TABLE `ori_table_bak`;
  6. 配置MySQL连接参数rewriteBatchedStatements=true可启用批量插入优化,将多条数据插入语句批量提交只执行一次,可在一定程度上优化写入性能降低TPS。

  7. MySQL随机数sql:

    1
    2
    -- 获取500~1500之间的随机数
    select round(rand() * 1000 + 500);
  8. 在查询sql中使用force index(idx)强制此次查询使用指定索引,例如:

    1
    select * from tt_data force index(idx) where ...; 
  9. MySQL执行sql报错:You can't specify target table 'tt_data' for update in FROM clause,改写sql之后解决。但是改写在后的sql中,delete from tt_data where id in (ids)语句并没有并没有走主键索引进行查找删除,换成join临时表的方式使id in (ids)正常走主键索引查询删除,效率提升明显。具体语句如下:

    1
    2
    3
    4
    5
    6
    -- 原sql,会报错
    delete from tt_data where id in (select id from tt_data where ...);
    -- 改写后的sql
    delete from tt_data where id in (select id from (select id from tt_data where ...));
    -- 优化后的sql
    delete a from tt_data a inner join (select id from tt_data where ...) b on a.id = b.id;

Java

  1. RandomUtils.nextInt() 生成随机整数,RandomStringUtils 生成随机字符串。

  2. 实现随机抽取集合里面的部分元素 Collections.shuffle(list) 将 list 元素循序打乱 list.subList(0, loreResource.getQuesNum()); // subList(fromIndex, toIndex)的实际范围是[fromIndex, toIndex) 获取指定数量的元素。

  3. ListUtils.select() 方法,类似于 JQuery 数组的 filter 方法。

  4. System.out.println(Arrays.toString(someList.toArray())); 方法可以方便地打印List内容。

  5. Arrays.asList(T... a) 无法将基本类型转换为 List,原因是 asList() 方法接收的是泛型的可变长参数,而基本类型(如int,char等)是无法泛型化的。使用 asList() 对基本数据类型进行操作时需要使用基本数据类型的包装类。asList() 返回的 ArrayList 类型是 Arrays 的一个内部类,没有实现 add()remove() 等用于操作 ArrayList 的方法,当我们需要对 asList() 返回的列表进行常用操作时需要对其进行转换,List list = new ArrayList(Arrays.asList(testArray));

  6. Java类启动顺序,static 静态代码先于构造方法。

  7. ThreadLocal变量一般使用static修饰。使用时,为了避免内存泄漏,在当前线程执行完之后需要调用ThreadLocal.remove()方法清除线程关联对象。

SpringBoot

  1. 如果项目启用了事务管理,Service 的实现类在增删改数据时需要加上 @Transactional 注解标明该类或方法加入了事务管理。

  2. 使用 shiro 权限框架时,需要检查需要权限控制的 Controller 类或方法是否加上了 @RequiresRoles@RequiresPermissions 注解。

  3. Controller 的方法传入 @RequestBody 参数时,method 只能是 POST 或 PUT。

  4. Controller 通过 Model 对象实例通过 setAttribute 方法传值到前端 jsp 页面或是其他模板页面时,如果是在 js 里获取的话需要通过 var param = ‘${param}’ 方式获取,用’’引起来可以避免当 ${param} 为空时 js 代码报错的问题,但同时也将参数类型强制转换为 string 类型了;当参数类型是 List 集合时,需要使用 eval(‘${param}’) 方法。

  5. 后台在增删改数据时,记得更新和该数据相关的缓存;特别是在更改记录时要更新更改之前和之后会包含该记录的缓存。

  6. 跨域访问时,可以在全局 Interceptor 中使用 httpServletResponse.setHeader("Access-Control-Allow-Origin", ACCESS_CONTROL_ALLOW_ORIGIN) 限制可跨域访问的域名,类似的,使用 httpServletResponse.setHeader("Access-Control-Allow-Methods", ACCESS_CONTROL_ALLOW_METHODS) 限制请求方法,httpServletResponse.setHeader("Access-Control-Max-Age", ACCESS_CONTROL_MAX_AGE) 设置可跨域访问的时限, httpServletResponse.setHeader("Access-Control-Allow-Headers", ACCESS_CONTROL_ALLOW_HEADERS) 限制请求头参数。

  7. SpringBoot 中可以使用 @EnableScheduling 注解启用定时任务功能,在方法上使用 @Scheduled 注解设置任务启动的时间。

  8. 访问日志功能的实现:定义一个 LogInterceptor ,使用 logger.info() 等方法记录下 httpServletRequest 中相关属性。

  9. shiro 的 reaml 中保存的 Principal 为 User 对象时,页面上想要获取 User 对象的 username 属性可以使用 <shiro:principal property="username"/> 标签,注意这里的 property 即是对应的属性名。

  10. Spring 框架自带的 BeanUtils.copyProperties(Object source, Object target, String... ignoreProperties) 方法可以方便的做类属性的拷贝。

  11. SpringBoot security 在使用 .antMatchers("/management/**").hasAnyRole("SuperAdmin", "admin") 要注意数据库里保存的角色名称必须要以 'ROLE_' 开头,这里的角色在数据库的名称应为 "ROLE_SuperAdmin""ROLE_admin"

  12. SpringBoot security 的注解 @PreAuthorize 可以用在类或方法上来进行权限控制,其中 hasRole()hasAuthority() 的区别在于前者可以忽略角色信息的前缀(默认是 “ROLE_”),而后者则必须要包含前缀。例如当保存的角色信息为”ROLE_admin”时, hasRole('admin')hasAuthority('ROLE_admin') 是等效的。

  13. SpringBoot 在整合 shiro 时无法在使用 rememberMeCookie 实现”记住我”(即 rememberMe)功能的同时实现使用 sessionIdCookie 单独管理用户 session 信息。而使用 sessionIdCookie 可以解决出现404之后刷新页面直接跳转到登录页面的问题(问题详情:http://jinnianshilongnian.iteye.com/blog/1999182)。

  14. SpringBoot JPA 可以很简单的集成分页和排序功能,支持的 request 参数如下:

    1
    2
    3
    page,第几页,从 0 开始,默认为第 0 页;  
    size,每一页的数量,默认为 10;
    sort,排序相关的信息,以 `property,direction(,ASC|DESC)` 的方式组织,例如 `sort=firstname&sort=lastname,desc` 表示在按 firstname 增序排列的基础上按 lastname 的降序排列。

    例如请求链接为:
    http://host:port/blogs?page=0&size=3&sort=category,asc&sort=description,desc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @SuppressWarnings("unchecked")
    @GetMapping("")
    public Page<Blog> findAll(@PageableDefault(size = 10, page = 0, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
    long startTime = System.currentTimeMillis();
    Page<Blog> blogPage = (Page<Blog>) redisTemplate.opsForValue().get(blogCachePrefix + JSON.toJSONString(pageable));
    if (blogPage == null) {
    blogPage = blogRepository.findAll(pageable);
    redisTemplate.opsForValue().set(blogCachePrefix + JSON.toJSONString(pageable), blogPage, 3, TimeUnit.MINUTES);
    }
    long endTime = System.currentTimeMillis();
    logger.info("获取分页博客博客,pageable :{}, 耗时:{}ms", JSON.toJSONString(pageable), (endTime - startTime));
    return blogPage;
    }
  15. 使用 jackson 将实体对象转成 JSON ,在属性上加上 @JsonInclude(JsonInclude.Include.NON_NULL) 注解,当该属性NULL或者为空时将不参加序列化。JsonInclude 可选项有:

    1
    2
    3
    4
    JsonInclude.Include.ALWAYS      //默认总是参加序列化
    JsonInclude.Include.NON_DEFAULT //属性为默认值不序列化
    JsonInclude.Include.NON_EMPTY //属性为空或NULL都不序列化
    JsonInclude.Include.NON_NULL //属性为NULL不序列化
  16. 实体类里的Enum类型的属性映射数据库字段的时候可以使用 @Convert(converter = Status.class, attributeName = "status") 注解;jackson 在序列化Enum类型的时候可以在Enum类对应的getter方法使用 @JsonValue 注解来定义向前端输出的内容,而前端传过来的值同样可以通过 @JsonCreator 注解反序列化得到对应的Enum类型对象。

  17. 1.5.6.RELEASE 版本的SpringBoot在使用 ElasticsearchRespository 类时会产生以下异常:

    1
    Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException:                 Could not write JSON document: (was java.lang.NullPointerException) (through reference chain: org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl["facets"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain:     org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl["facets"])

    推荐使用 1.4.7.RELEASE 版本。

  18. Json 序列化的时候会将 Long 等类型强转成类似于 Javascript 中的 number 类型(内部实现可能是转成 Double 类型),这样会导致大数值丢失精度的问题。jackson 可以使用 @JsonSerialize(using = ToStringSerializer.class) 注解将 Long 类型转成String 再进行序列化,但这样会导致前端接收到的数据类型变成了 String 而不是 number。

  19. 在进行批量更新操作时,需要将待更新的数据进行排序之后再进行批量操作,这样可以避免并发场景下的死锁问题。

  20. @RequestBody注解的实体对象如果需要将前端传过来的 array 转成实体中相应的 String 类型的属性时,可以在该属性的 setter 方法进行转换操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Data
    @Entity
    @Table(name = "welcome_message")
    public class WelcomeMessage implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    private Long id;

    @Column(name = "articles")
    private String articles;

    public void setArticles(List<WxMpXmlOutNewsMessage.Item> articles) {
    this.articles = JSON.toJSONString(articles);
    }

    public List<WxMpXmlOutNewsMessage.Item> getArticles() {
    return JSON.parseArray(this.articles, WxMpXmlOutNewsMessage.Item.class);
    }
    }

    相应的,如果需要将数据库中存储的 String 转成 List 传回前端或者应用于后台逻辑,可以在字段对应属性的 getter 方法进行转换。
    这里使用的是 fastjson 的 JSON 类。

  21. 过长的 Long 类型主键转 Json 传回前端会使用js处理会丢失精度,可以在实体的 @Id 上配置转成 Json 的序列化及反序列化处理类,在将 id 传回前端或者接收 前端传过来的 id 时使用 String 类型来进行转换保证精度,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 解决Long类型的主键转json传回前端丢失精度的问题,将Long转成String
    * @author yupaits
    * @date 2017/12/18
    */
    public class LongJsonSerializer extends JsonSerializer<Long> {
    @Override
    public void serialize(Long value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
    if (value != null) {
    jsonGenerator.writeString(String.valueOf(value));
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 将前端String类型的主键转成Long类型
    * @author yupaits
    * @date 2017/12/18
    */
    public class LongJsonDeserializer extends JsonDeserializer<Long> {

    @Override
    public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
    String value = jsonParser.getText();
    try {
    return value == null ? null : Long.parseLong(value);
    } catch (NumberFormatException e) {
    return null;
    }
    }
    }
    1
    2
    3
    4
    @Id
    @JsonSerialize(using = LongJsonSerializer.class)
    @JsonDeserialize(using = LongJsonDeserializer.class)
    private Long id;
  22. SpringAop的Order顺序遵循先进后出的原则。如Aop1(order=1),Aop2(order=2),则切面的执行顺序为Aop1.doAround.beforeProceed -> Aop1.doBefore -> Aop2.doAround.beforeProcced -> Aop2.doBefore -> Aop2.doAround.afterProcced -> Aop2.doAfter -> Aop2.doAfterReturning -> Aop1.doAround.afterProcced -> Aop1.doAfter -> Aop1.doAfterReturning

  23. Spring Boot启动时指定环境。

    • IDEA中进入 Run/Debug Configurations 设置 Environment->VM options-Dspring.profiles.active=test,或者在 Environment variables 中添加参数 spring.profiles.active 并指定参数的值为 test
    • 命令行使用 java -jar *.jar 启动时,在命令的后面增加 --spring.profiles.active=test
    • application.yml 中配置 spring.profiles.active 的值为 test
  24. 通过zuul上传包含中文名的文件时,需要在上传文件url的path加上 /zuul 前缀即可。

    例如:当上传url为 http://localhost:10060/upload/image,上传名为 头像.jpg 的文件,会出现报错 java.io.FileNotFoundException: E:\upload\image\2018-5\??.jpg (文件名、目录名或卷标语法不正确。)。如果将url直接改为http://localhost:10060/zuul/upload/image 则可以上传成功。

  25. Spring Boot Jpa实现实体联合主键:实体类加上 @IdClass(XxxKey.class) 注解,实体类中多个联合注解的属性上都加上 @Id 注解。示例:

    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
    @Data
    @AllArgsConstructor
    @Entity
    @IdClass(UserRoleKey.class)
    @Table(name = "shop_user_role")
    public class UserRole implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @Column(nullable = false)
    private Long userId;

    @Id
    @Column(nullable = false)
    private Long roleId;
    }

    @Data
    @EqualsAndHashCode
    @NoArgsConstructor
    @AllArgsConstructor
    public class UserRoleKey implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long userId;

    private Long roleId;
    }
  26. 使用Spring Cloud Sleuth的日志中会包含链路追踪的信息,具体体现为:

    1
    2
    2017-04-08 23:56:50.459 INFO [bootstrap,38d6049ff0686023,d1b8b0352d3f6fa9,false] 8764 — [nio-8080-exec-1] demo.JpaSingleDatasourceApplication : Step 2: Handling print
    2017-04-08 23:56:50.459 INFO [bootstrap,38d6049ff0686023,d1b8b0352d3f6fa9,false] 8764 — [nio-8080-exec-1] demo.JpaSingleDatasourceApplication : Step 1: Handling home

    比一般的日志多出了 [bootstrap,38d6049ff0686023,d1b8b0352d3f6fa9,false] 这些内容,对应 [appname,traceId,spanId,exportable]

    • appname:服务名称
    • traceId\spanId:链路追踪的两个术语
    • exportable: 是否是发送给zipkin
  27. SpringBoot中使用 logback-spring.xml 进行日志打印的配置。示例:

    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
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="30 seconds" debug="false">
    <property name="appName" value="app-name"/>
    <property name="logPattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} - [${appName}] - ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %c - %m%n"/>

    <appender name="amqp" class="org.springframework.amqp.rabbit.logback.AmqpAppender">
    <host>172.17.0.1</host>
    <port>5672</port>
    <username>guest</username>
    <password>guest</password>
    <applicationId>okbuy</applicationId>
    <routingKeyPattern>logstash</routingKeyPattern>
    <declareExchange>true</declareExchange>
    <exchangeType>direct</exchangeType>
    <exchangeName>okbuy.log</exchangeName>
    <generateId>true</generateId>
    <maxSenderRetries>2</maxSenderRetries>
    <charset>UTF-8</charset>
    <durable>true</durable>
    <deliveryMode>PERSISTENT</deliveryMode>
    <layout>
    <pattern>${logPattern}</pattern>
    </layout>
    </appender>

    <appender name="stdoutAppender" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
    <pattern>${logPattern}</pattern>
    <charset>utf8</charset>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
    <level>WARN</level>
    </filter>
    </appender>

    <appender name="asyncStdoutAppender" class="ch.qos.logback.classic.AsyncAppender">
    <discardingThreshold>0</discardingThreshold>
    <queueSize>1024</queueSize>
    <appender-ref ref="stdoutAppender"/>
    <includeCallerData>true</includeCallerData>
    </appender>

    <root>
    <level value="WARN"/>
    <appender-ref ref="amqp"/>
    <appender-ref ref="asyncStdoutAppender"/>
    </root>
    </configuration>
  28. Spring的事务控制和数据库的事务控制之间的关系:

    • 数据库开启事务、提交事务、回滚事务对应jdbc的三个api,Spring事务控制的本质是通过AOP把这三个方法增强在不同的地方调用,实现Spring的事务在方法之间的传播。
    • 数据库在开启事务到提交的过程中,数据库本身有异常都会回滚。
    • 业务异常导致数据库回滚,是Spring通过调用 jdbc rollback 的api实现的。
  29. Spring Security OAuth2 Client项目中@EnableOAuth2Sso注解修饰Application启动类时不起作用,需要放在@Configuration修饰的类上(通常是继承WebSecurityConfigurerAdapter的配置类)才能正常工作。

  30. 检查幂等(是否重复请求)伪代码:

    1
    2
    3
    4
    5
    6
    CREATE TABLE `tb_unique` (
    `request_id` varchar(64) NOT NULL COMMENT 'request id',
    `gmt_create` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '创建时间',
    `gmt_modified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '修改时间',
    PRIMARY KEY (`request_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='幂等表';
    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
    bool checkUnique(String requestId) {
    try {
    UniqueDO uniqueDO = buildUniqueDO(request);

    //reqeustId唯一索引,重复则抛异常DataIntegrityViolationException
    uniqueDao.insert(uniqueDO);

    //插入成功说明之前没有数据,返回没有被幂等
    return false;

    } catch (DataIntegrityViolationException ex) {

    //出现异常不一定时由于db里面有数据,可能是事务隔离级别导致,
    //所以要再查一次,确认数据再db里面存在
    UniqueDO uniqueDO = uniqueDao.selectByRequestId(requestId);

    if (uniqueDO == null) {
    //不存在数据则抛异常让方法重试
    throw ex;
    }

    //存在返回被幂等
    return true;
    }
    }

    requestId一般由客户端sdk生成和具体业务相关联。

  31. profile为default时会读取 application.yml 的配置,而当profile不是default时,会读取对应的 applicaiton-{profile}.yml 的配置。需要注意的是,如果 application.ymlapplication-{profile}.yml 中存在相同的配置项时,application.yml 的优先级更高,所以一般在 application.yml 中设置共用的配置项。

  32. 在SpringMVC中 @RequestBody 注解修饰的对象如果存在 @DateTimeFormat 注解修饰的属性,而且使用 jackson 进行反序列,那么 @DateTimeFormat 注解实际上是不起作用的,此时需要使用 @JsonFormat 注解进行代替。示例如下:

    1
    2
    3
    //@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startTime;
  33. Spring AOP 的切入点(PointCut)表达式使用规则:

    1
    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
    • ? 的部分表示可省略
    • * 用来代表任意字符
    • .. 用来表示任意个参数
    • modifiers-pattern 表示修饰符如 public、protected 等
    • ret-type-pattern 表示方法返回类型
    • declaring-type-pattern 表示特定的类
    • name-pattern 表示方法名称
    • param-pattern 表示参数
    • throws-pattern 表示抛出的异常
  34. fastjson序列化之后如果出现$ref说明启用了“重复应用/循环引用”特性,可以通过以下两种方式处理:

    1. 通过全局或局部序列化配置SerializerFeature.DisableCircularReferenceDetect来展示实际内容
    2. 将存在重复引用的对象使用该对象的副本(new一个同类型对象并复制属性值)而不是原对象
  35. Mybatis的if标签中判断字符串相等要使用如下格式:

    1
    2
    <if test='field == "value"'>
    </if>

    注意外层用单引号,字符串的值使用双引号。

Bootstrap

  1. 页面的modal元素记得加上 data-backdrop='static'data-keyboard='false',禁用非 modal 内点击和点击键盘 ESC 键 取消 modal。

JQuery

  1. 批量删除并返回批量删除的结果时,ajax 方法中 async 一定要配置成 false,否则页面无法正确响应批量删除的结果。

  2. 使用 qrcodejs 生成二维码图片之后直接使用 $('img').attr('src') 返回的值是undefined,这时需要使用 setTimeout(function(), delay_time) 来拿到图片的 src 。

  3. js数组遍历删除元素的方法:

    1
    2
    3
    4
    5
    6
    for (let i = 0; i < arr.length; i++) {
    if (...) {
    arr.splice(i, 1);
    i--;
    }
    }

CSS

  1. 局部列表滚动查看css,height和max-height二选一。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    .grid-list {
    height: 190px; //固定高度的列表
    max-height: 500px; //列表最大高度
    overflow-x: hidden;
    overflow-y: scroll;
    }
    .grid-list::-webkit-scrollbar {
    display: none;
    }
  2. 一个基于网格的响应式布局简单示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    .grid-container {
    display: grid;
    grid-template-rows: 1fr;
    grid-gap: 1rem;
    }
    @media screen and (max-width: 719px) {
    .grid-container {
    grid-template-columns: 1fr;
    }
    }
    @media screen and (min-width: 719px) {
    .grid-container {
    grid-template-columns: 1fr 1fr;
    }
    }

    浏览器窗口宽度大于719px时,元素呈2列排布,反之呈1列排布。

HTML

  1. 使用原生HTML进行表单开发时,如果没有指定 <button>type 属性,则默认 type="submit",最终导致点击之后 window.location.href 的路径中会自动多一个 ?,这是因为原生的表单提交是以path后拼接form的参数进行请求的,所以在进行异步请求时需要指定提交的按钮为 <button type="button">

Vue.js

  1. 使用局部的 filter 代替 methods 中的方法格式化数据显示。

  2. vue 在绑定 date 类型的 input 输入框时,设置默认值要用 :value="date | toDate",toDate 为过滤器;当 date 的值是 timestamp 类型时,toDate 可以为以下方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function dateTime(date) {
    if (date) {
    date = new Date(date);
    var month = date.getMonth() + 1;
    var day = date.getDate();
    var hour = date.getHours();
    var minute = date.getMinutes();
    var second = date.getSeconds();
    return date.getFullYear() + '-' +
    (month < 10 ? '0' + month : month) + '-' +
    (day < 10 ? '0' + day : day) + ' ' +
    (hour < 10 ? '0' + hour : hour) + ':' +
    (minute < 10 ? '0' + minute : minute) + ':' +
    (second < 10 ? '0' + second : second);
    }
    return date;
    }
  3. vue.js 2.5.2版本在 v-for 循环中使用 v-model 绑定数组元素时会报如下错误:

    1
    You are binding v-model directly to a v-for iteration alias. This will not be able to modify the v-for source array because writing to the alias is like modifying a function local variable. Consider using an array of objects and use v-model on an object property instead.

    从提示信息可以看出 vue 建议使用 array 的某个元素的属性值作为 v-model 绑定的主体,如果元素并不是 Object 类型而是 String 类型时,需要通过 index 来实现元素绑定。具体代码如下:

    1
    2
    3
    <div v-for="(el, index)  in array">
    <input type="text" v-model="array[index]">
    </div>
  4. 将数组中位于 index1 的元素移动至 index2 的位置:

    1
    list.splice(index2, 0, list.splice(index1, 1)[0]);

    交换数组中位于 index1 和 index2 的两个元素:

    1
    list[index1] = list.splice(index2, 1, list[index1])[0];

    vue 组件中实时交换位于 index1 和 index2 的两个元素:

    1
    this.$set(list, index1, list.splice(index2, 1, list[index1])[0]);
  5. Vue的 filters 块中的过滤器不能加载自身 data() 块中的变量,如果需要使用 data() 中的变量对数据进行转换时可以将过滤器的逻辑写在 methods 块中。

  6. 在使用基于Vue的前端框架进行开发时,有些框架组件无法对点击事件进行监听和处理(如 ant-design-vuepopconfirm 组件,详见 Could you add a property in PopConfirm to stop click event propagation?),而如果此时正好需要阻止某个组件的点击事件向上层元素传播时,可以使用如下方式:

    1
    2
    3
    <div @click="e => e.stopPropagation()">
    <a-popconfirm></a-popconfirm>
    </div>
  7. Vue的 style scoped 中的样式不起作用时,可以新增一个无scoped修饰的 style 来定义css样式,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <style scoped>
    .scoped-style {
    background-color: black;
    }
    </style>

    <style>
    .global-style {
    color: white;
    }
    </style>
  8. 在Vue中使用JSX语法时,需要注意以下方面:

    • 所有的运算赋值操作都需要在 {} 中,如获取变量值,调用方法等。
    • 不支持Vue的过滤器,需要使用方法来代替。
    • 事件监听 @click="handleClick(param)" => onClick={this.handleClick.bind(this, param)}@click.native="handleNativeClick(param)" => nativeOnClick={this.handleNativeClick.bind(this, param)},这里需要使用js原生的bind方法来进行方法调用。
    • 不支持Vue的指令,常用的指令的备选解决方案:v-if="condition" => v-show="condition" 或者 {condition ? <div>JSX</div> : ''}<li v-for="item in items" :key="item">{item}</li> => {items.map((item, index) => {return <li>{item}</li>})}
  9. win7系统在安装高版本node.js(v14.x.x)时,会提示“仅支持更高的win8、win10系统”。但是node.js的生态里,很多新技术必须安装高版本node.js环境才能正常使用和开发(如electron的最新版本要求node.js版本不低于v14.x.x)。可以通过以下方式解决:

    • 下载nvm(node.js版本管理器)并安装
    • 安装完成后,使用nvm list available查看可安装的node.js版本
    • 选择最新的CURRENT版本或者LTS版本进行安装,安装完成后使用nvm use 14切换至高版本node.js即可正常使用

缓存

  1. 使用 redis-cli 进入 redis 的命令行模式时,可以使用 keys ** 查看所有 key 值,使用 get [key] 查看 key 对应的 value 值。需要注意的是,使用 keys ** 查看到的 key 值如果使用 “” 包括,那么 get [key] 的 key 也需要用 “” 包括起来。

构建

  1. 使用 frontend-maven-plugin 整合前后端项目构建时,需要注意web模块需要放在server模块之前,保证web先构建,这样才能将web构建好的文件复制到server的 resources/public 目录下。

    1
    2
    3
    4
    <modules>
    <module>xxx-web</module>
    <module>xxx-server</module>
    </modules>

    同时要注意后台不要对 @RequestMapping("/") 做处理,否则有可能会使得访问 "/" 无法显示前端构建好的index.html页面。

  2. maven引用私服的jar包时,需要在 pom.xml 文件的 <repositories> 标签下中指定私服的 <id><name><url>

  3. gitlab-runner的 config.toml[runners.docker] 中配置 extra_hosts = ["xxx.xxx.com:172.17.0.1"]可实现runner容器的ip和host映射关系的配置。

  4. 设置maven的 setting.xml 中 mirror 节点的 mirrorOf 有以下配置策略:

    • *:everything
    • external:*:everything not on the localhost and not file based
    • repo,repo1:repo or repo1
    • *,!repo1:everything except repo1

部署

  1. windows 下使用 Nginx 相关命令如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #启动
    点击 Nginx 目录下的 nginx.exe;cmd 运行 start nginx

    #关闭
    nginx -s stop #立即停止nginx,不保存相关信息
    nginx -s quit #正常退出nginx,并保存相关信息

    #重启(重新加载配置)
    nginx -s reload
  2. nginx 的配置文件 root 路径不识别 \ 只识别 /

  3. Letsencrypt通配符证书的申请和在nginx配置:

  4. 使用 nginx 解决 CORS 问题

    使用反向代理将服务端(接口提供方)与客户端(接口调用方)部署在同一个域下。

  5. 使用 nginx 解决 X-Frame-Options: deny 问题

    使用headers-more插件,推荐直接下载带headers-more模块的openresty,下载地址

    在 nginx.conf 中(一般是在server下)配置:

    1
    2
    3
    4
    # 删除 "X-Frame-Options" header
    more_clear_headers 'X-Frame-Options';
    # 设置 "X-Frame-Options: SAMEORIGIN" 同域可嵌入iframe
    more_set_headers 'X-Frame-Options: SAMEORIGIN';
  6. 指定MySQL时区,在mysqld.conf中增加 default-time-zone='+8:00' 配置。

  7. 在做微信公众号开发时,需要在公网上调用开发机器的接口,除了使用花生壳等软件进行内网穿透之外,如果有富余的公网服务器资源,可以使用一些简单方便的内网穿透工具,推荐 ichWebpass

  8. gitlab pg数据库配置

    修改/var/opt/gitlab/postgresql/data/pg_hba.conf文件,增加下面的配置:

    1
    host all all 192.168.1.10/22 trust

    上述的修改会在使用gitlab-ctl reconfigure命令之后失效,通过修改gitlab配置文件中pg数据库的entries可以避免这种情况。具体为:

    在gitlab.rb中增加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    postgresql['custom_pg_hba_entries'] = {
    APPLICATION: [{ # APPLICATION should identify what the settings are used for
    type: 'host',
    database: 'all',
    user: 'all',
    cidr: '192.168.1.0/24',
    method: 'trust',
    #option: 0
    }]
    }

    注意: 这里的APPLICATION对象是一个数组,有些gitlab版本默认不是数组,需要手动修改。

Git

  1. GitHub 现在支持创建私有代码仓库了,但使用时需要注意:将 GitHub 的 repository 从 public 切换成 private 再切回 public 之后,需要 push 代码到 master 分支才能让已经404的 Github Pages 页面恢复正常。

Windows

  1. 为了在windows系统上安装docker,需要将win10系统升级到专业版开启HyperV虚拟机才行。

    win10专业版升级密钥:DR9VN-GF3CR-RCWT2-H7TR8-82QGT

Linux

  1. Ubuntu/Debian开机启动脚本 spider.sh 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #!/bin/bash

    ### BEGIN INIT INFO
    # Provides: spider
    # Required-Start:
    # Required-Stop:
    # Default-Start: 2 3 4 5
    # Default-Stop: 0 1 6
    # Short-Description: spider
    # Description: spider service start
    ### END INIT INFO

    [shell content]
    • 开机启动:update-rc.d spider.sh defaults
    • 移除开机启动:update-rc.d spider.sh remove
  2. 使用netstat查看系统网络端口情况:netstat -tunlp;如果发现没有 PID (进程号),需要使用 sudo netstat -tunlp

  3. 对于新安装的官方Ubuntu-server等系统,可以通过指令 sudo apt-get install build-essential 安装gcc环境。

  4. 取消sudo密码:sudo visudo 进入编辑界面;添加行 username ALL=(ALL) NOPASSWD:ALL,username为登录用户名,如果想作用于某个用户组则使用 %usergroup ALL=(ALL:ALL) NOPASSWD:ALL

  5. linux系统添加ssh公钥的方式:

    • 本地

      1
      2
      3
      4
      5
      # 覆盖
      cat [ssh_pub_key] > ~/.ssh/authorized_keys

      # 追加
      cat [ssh_pub_key] >> ~/.ssh/authorized_keys
    • 远程

      1
      scp ~/.ssh/id_rsa.pub root@[remote-server-ip]:/root/.ssh/authorized_keys

      注: 需要输入远程服务器的登录密码进行认证。

  6. ssh对目录的权限有要求,具体如下:

    目录/文件 权限
    ~/.ssh 700
    ~/.ssh/* 600
    ~/.ssh/config 700
  7. 在终端中执行curl -X GET http://www.sample.com/api?a=1&b=2指令时,实际上只会传入a=1参数,想要同时传入a=1b=2,可使用以下两种方式:

    • &符号转义: curl -X GET http://www.sample.com/api?a=1\&b=2
    • url加上双引号: curl -X GET "http://www.sample.com/api?a=1&b=2"

Docker

  1. 使用docker-compose的build参数构建docker镜像时,要注意指定的context目录下默认的Dockerfile文件名必须为 Dockerfile。如果想使用其他文件名,可以通过 -f filename 指定。

  2. docker环境为docker for windows,有效避免host为windows系统时 docker run -v 绑定的目录映射关系在系统重启之后失效的方法为:使用 docker volume create path1 创建volume,再使用 -v path1:/container/path。简单地说就是使用 “自定义数据卷” 代替 “容器自动创建的临时数据卷” 进行映射,完成docker容器的数据持久化。

  3. 指定docker的dns,防止网络环境变化时导致容器网络的不稳定。
    编辑docker的daemon.json:

    1
    2
    3
    {
    "dns": ["114.114.114.114", "8.8.8.8"]
    }
  4. 使用docker-compose来部署redash时,注意需要先创建redash数据库才能启动:

    1
    2
    sudo docker-compose run --rm server create_db
    sudo docker-compose up -d
  5. 使用docker方式部署consul时,由于consul在docker虚拟网络中无法正确识别注册服务的hostname,因此需要在注册服务中配置 spring.cloud.consul.discovery.hostname=192.168.1.xxx (注册服务的局域网ip,要注意的是必须是docker宿主机所在地局域网ip,使用172.17.0.1等docker内网ip也不行),才能确保consul正确地进行健康检查。