常用问题整理

概述

近期关于开发过程中常见问题整理。

consul 命令

查看consul健康状态

curl http://localhost:8500/v1/agent/health/service/name/safety-api

删除无用consul实例

 curl  http://123.206.197.169:8500/v1/agent/service/deregister/safety-user-18027

ws连接异常

尝试去掉protocol参数

page-helper 和mybatis-plus冲突解决

<dependency>
      <groupId>com.github.pagehelper</groupId>
      <artifactId>pagehelper-spring-boot-starter</artifactId>
      <!-- 解决mybatis-plus冲突 -->
      <exclusions>
        <exclusion>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
        </exclusion>
        <exclusion>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

spring-boot异常解决

org.safety safety-common ${project.version} org.springframework spring-webmvc org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-tomcat

重置GIT配置,包括checkStyle

重置GIT 配置
git config –global –unset core.hookspath

swagger 失效原因

mvc:
static-path-pattern: /media/**

如果非要配置静态配置,可以做兼容性处理,增加配置类。

@Configuration
public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");

  }
}

如何使用JS复制DIV或者p标签文本

使用曲线救国方式

<style type="text/css">
   .wrapper {position: relative;}
   #input {position: absolute;top: 0;left: 0;opacity: 0;z-index: -10;}
</style>

<div class="wrapper">
   <p id="text">我把你当兄弟你却想着复制我?</p>
   <textarea id="input">这是幕后黑手</textarea>
   <button onclick="copyText()">copy</button>
</div>

JS

<script type="text/javascript">
    function copyText() {
      var text = document.getElementById("text").innerText;
      var input = document.getElementById("input");
      input.value = text; // 修改文本框的内容
      input.select(); // 选中文本
      document.execCommand("copy"); // 执行浏览器复制命令
      alert("复制成功");
    }
  </script>

2021

2021 遇到的一些问题和总计

mybatis plus 问题

limit 问题

    // 可以使用lambdaQuery查询
    QueryWrapper<ContractTemplate> wrapper = new QueryWrapper<>();
    wrapper.lambda().orderByDesc(ContractTemplate::getDownCount);
    wrapper.last("limit 10");
    List<ContractTemplate> list = contractTemplateService.list(wrapper);

带分页组合查询

    LambdaQueryChainWrapper<UserCollect> lambdaQuery = userCollectService.lambdaQuery();
    lambdaQuery.eq(UserCollect::getUserCode, startQueryReqVo.getUserCode());
    lambdaQuery.eq(UserCollect::getType, startQueryReqVo.getType());
    lambdaQuery.eq(!StringUtils.isEmpty(startQueryReqVo.getSubType()), UserCollect::getSubType, startQueryReqVo.getSubType());
    lambdaQuery.like(!StringUtils.isEmpty(startQueryReqVo.getTags()), UserCollect::getTags, startQueryReqVo.getTags());
    // 使用lambda表达式拼装查询语句
    lambdaQuery.and(!StringUtils.isEmpty(startQueryReqVo.getSearchKey()), 
    wapper -> wapper.like(!StringUtils.isEmpty(startQueryReqVo.getSearchKey()), UserCollect::getTitle, startQueryReqVo.getSearchKey()).or().like(!StringUtils.isEmpty(startQueryReqVo.getSearchKey()), UserCollect::getContent, startQueryReqVo.getSearchKey()));

    Page<UserCollect> page = lambdaQuery.page(new Page<UserCollect>(startQueryReqVo.getCurrentPage(), startQueryReqVo.getPageSize()));
    return ResultVo.success(page);

定时器模式下的事务支持

在定时器中直接执行数据插入等操作是不会被spring管理事务的,所以需要将数据插入更新操作提成单独的service并加上@transactional.

  @Scheduled(cron = "0 0/4 * * * ?") // 定时任务
  public void syncDataTask() {
    save();
  }

  public void save() {
    service.doSomeInsert();
  }

  // 单独的service
  @service
  public class service {
    /**
    * 此事务会被spring管理,但注意定时任务中插入数据最好在一个整体中。
    * 以免其他插入或更新操作没有被spring管理而导致数据异常。
    **/
    @transactional
    public void doSomeInsert(){
        // insert some record
    }
  }

AOP中支持事务

可以参考配置全局的事务处理

https://blog.csdn.net/yxw908186797/article/details/90319643

以下情况事务会失效

  @Async
  @Transactional
  public void saveInterfaceLog(Record rec) {
      recordMapper.insertRecord(rec);
  }

解决方式是将insertRecord(rec)提取到另一个service中, 并由事务管理起来。

  @Async
  public void saveInterfaceLog(Record rec) {
      recordService.insert(rec);
  }

  // 将异步方法中的执行由事务进行管理
  @Transactional
  public void insert(Record rec) {
    recordMapper.insertRecord(rec);
  }

事务总结

在开发过程中我们经常遇到AOP ,异步,多线程,定时器等的使用,但是如果这几类操作中携带了对数据的插入或更新以及删除操作。
正常情况下spring是不支持对这几种情况进行事务管理的,而我们又希望在程序错误的情况下让事务回滚,那么都可以考虑将需要事务操作的内容提为一个单独的服务,单独由事务进行管理。

security框架

spring 提供了现成的security框架机制,其优点类似于拦截器,常常我们将其和JWT进行一些组合来完成权限验证工作,但是使用过程经常遇到一些问题。
比如,某些链接被限制了,但是本身又放开了权限,为什么呢? 先看JWT + security框架实现机制如何。

@EnableWebSecurity
public class SecurityConfig extends JWTSecurityConfig {

    @Value("${spring.security.allowCorsOrigin}")
    private String allowCorsOrigin;

    @Value("${jwt.auth.secret}")
    private String authJwtSecret;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 继承jwt配置
        super.configure(http);
        // 禁用csrf
        http.csrf().disable();
        http.cors();
        // 根据角色校验url
        http.authorizeRequests().antMatchers("/admin/**").hasAnyAuthority("ADMIN");
        http.antMatcher("/**");

    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 配置不需要鉴权的url
        web.ignoring().antMatchers( // 测试
                "/swagger-ui.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/api/**"
        );
    }


    @Override
    protected String getJWTSecret() {
        return authJwtSecret;
    }


    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(allowCorsOrigin));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

// JWT权限配置类,指定过滤器、权限拦截类
@Component
public abstract class JWTSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private JWTUtils jwtUtils;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint()) // 处理未登录提示
                .accessDeniedHandler(new JWTAccessDeniedHandler()).and() // 处理权限不足AccessDeniedException
                .addFilterAfter(new JWTAuthenticationFilter(), BasicAuthenticationFilter.class) // 做一些简单过滤处理 
                .authenticationProvider(new JWTAuthenticationProvider(getJWTSecret(), jwtUtils)) // 指定权限校验者
                .authorizeRequests().filterSecurityInterceptorOncePerRequest(true).anyRequest() // 开始鉴权
                .authenticated();
    }
    // 标注由子类实现
    protected abstract String getJWTSecret();
}

// 此部分为filter中部分内容,过滤器主要设置验证参数Authentication
SecurityContextHolder.clearContext();
if (StringUtils.isNotBlank(token)) {
  SecurityContextHolder.getContext().setAuthentication(new JWTAuthenticationToken(token)); // 设置用户全局变量
}


// 获取全局变量
  public static JWTAuthenticationToken getJWTAuthentication() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    AssertUtils.assertTrue(
        authentication != null && authentication.isAuthenticated()
            && authentication instanceof JWTAuthenticationToken,
        ErrorCode.NOT_LOGIN, "用户未登录");

    return (JWTAuthenticationToken) authentication;
}
// 权限校验类
public class JWTAuthenticationProvider implements AuthenticationProvider {

  private String jwtSecret;
  private JWTUtils jwtUtils;

  public JWTAuthenticationProvider(String jwtSecret, JWTUtils jwtUtils) {
    this.jwtSecret = jwtSecret;
    this.jwtUtils = jwtUtils;
  }

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 自定义JWT权限验证对象
    JWTAuthenticationToken jwtAuthentication = (JWTAuthenticationToken) authentication;
    // 获取前端表单中输入后返回的用户名、密码
    AssertUtils.assertNotNull(jwtAuthentication, ErrorCode.ILLEGAL_PARAMETER, "非法参数");
    String jwtToken = jwtAuthentication.getJwtToken();
    return jwtUtils.generateAuthenticationToken(jwtToken, jwtSecret); // 生成对应token
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return JWTAuthenticationToken.class.isAssignableFrom(authentication);
  }
}


// 自定义JWT权限验证对象
@data
public class JWTAuthenticationToken extends AbstractAuthenticationToken {

  private static final long serialVersionUID = 689177934199815232L;
  private String userCode;
  private String userPhone;
  private String userEmail;
  private Integer status;
  private String companyName;
  private Map<String, Object> attributes = new HashMap<>();

  private String jwtToken;

  public JWTAuthenticationToken(String jwtToken) {
    super(null);
    this.jwtToken = jwtToken;
    setAuthenticated(false);
  }

  public JWTAuthenticationToken(String userCode, String userPhone,String userEmail, Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes) {
    super(authorities);
    this.userCode=userCode;
    this.userPhone = userPhone;
    this.userEmail = userEmail;
    if (!CollectionUtils.isEmpty(attributes)) {
      this.attributes = attributes;
    }
    super.setAuthenticated(true);
  }

  public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
    if (isAuthenticated) {
      throw new IllegalArgumentException(
          "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
    }
    super.setAuthenticated(false);
  }

  @Override
  public Object getCredentials() {
    return null;
  }

  @Override
  public Object getPrincipal() { // 用于设置设置全局信息
    return userEmail;
  }

 // JWT UTIL 列举部分信息用于理解
@Component
public class JWTUtils {

  private static final String TOKEN_KEY_USER_CODE = "userCode";
  private static final String TOKEN_KEY_USER_PHONE = "phone";
  /**
   * 存储用户code与token的对应关系的前缀
   */
  public static final String USERCODE_AND_JWTTOKEN_RELATIONSHIP = "USERCODE_AND_JWTTOKEN_RELATIONSHIP_";

  @Resource
  private RedisTemplate redisTemplate;

  /**
   * 生成jwt token
   *
   * @param expireTime 过期时间
   * @param phone 手机号
   */
  @SuppressWarnings("unchecked")
  public String generateJWTToken(Date expireTime,String userCode,String phone,String email,Integer status,String companyName,
                                 Map<String, Object> attributes, String jwtSecret, Long timeout,
      TimeUnit timeUnit, JWTTokenTypeEnums jwtTokenTypeEnums) {
    Map<String, Object> claims = new HashMap<>();
    attributes.put(JWTTokenTypeEnums.TOKEN.toString(), jwtTokenTypeEnums);
    claims.put(TOKEN_KEY_ATTRIBUTES, attributes);
    String jwtToken = Jwts.builder().addClaims(claims).setExpiration(expireTime).signWith(SignatureAlgorithm.HS512, resetJwtSecret(jwtSecret)).compact();
    // 保存用户和对应token的key关系
    redisTemplate.opsForValue().set(getUserCodeAndTokenRelationshipKey(jwtTokenTypeEnums, getCtype(attributes), userCode), jwtToken, timeout, timeUnit);
    return jwtToken;
  }
  /**
   * 解析jwtToken
   */
  public Claims parseJWTToken(String jwtToken, String jwtSecret) {
    return Jwts.parser().setSigningKey(resetJwtSecret(jwtSecret)).parseClaimsJws(jwtToken)
        .getBody();
  }

  /**
   * 生成认证实体,基于spring security authenticationToken机制
   *
   * @param authToken token
   * @param jwtSecret 秘钥
   */
  @SuppressWarnings("unchecked")
  public JWTAuthenticationToken generateAuthenticationToken(String authToken, String jwtSecret) {
    Claims claims = null;
    claims = parseJWTToken(authToken, jwtSecret);
    String userCode = claims.get(TOKEN_KEY_USER_CODE);
    Map<String, Object> attributes = (Map<String, Object>) claims.get(TOKEN_KEY_ATTRIBUTES);
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    return new JWTAuthenticationToken(userCode, userName, personType, authorities,attributes);
  }






}

JWT权限验证流程总结

准备好JWT相关配置类和自定义token类,并且和spring security进行关系绑定。
在用户登录成功后为用户生成一个合乎JWT格式的token和refreshToken。(登录接口不需鉴权)
调用其他接口时,则获取filter设置全局token对象JWTAuthenticationToken(包含token值)。
provider负责解析和校验token并从token中获取用户信息,且是否超时,并设置全局用户参数。(JWT属于暴力破解,和Redis关系不大)
如果需要刷新token 则从新生产refreshtoken。

JWT和Redis token的区别

jwt token属于暴力破解模式,直接解析加密串获取用户信息和token时限。
redis 存储token模式相对安全,但是效率稍低。
尝试在JWT中增加自有的加密方式,比如JWT + timestamp + 等再做一次SHA256加密解密等。
用户退出可以设置JWT参数,让其失效。

JAVA 观察者模式

java 自带了观察者模式的实现,通知类实现Observable,实现类创建 noticeAllWatchers(obj)来通知所有观察者。
具体实现类实现Observer,通过update通知观察者处理对应的逻辑。
应用初始化时需要向通知类Observable注册,并在需要的时候调用Observable实现类方法noticeAllWatchers(obj)通知所有观察者;

总结

后面将会继续补充,关于web开发中的一些问题。


   Reprint policy


《常用问题整理》 by jackromer is licensed under a Creative Commons Attribution 4.0 International License
 Previous
使用swwager生成离线API文档 使用swwager生成离线API文档
概述 本文主要介绍如何使用swagger生成离线API。 项目架构 spring-boot-2.0 + swagger-2.8.0 . 需要哪些要素 3个配置文件 静态文件StaticResourceFile,将静态文件中的文件夹放到
2020-06-09 jackromer
Next 
mybatis-plus实现自定义分页 mybatis-plus实现自定义分页
概述 本文主要介绍如何使用mybatis-plus实现复杂自定义分页 使用说明 接口定义 /** * 分页查询 * // 此处最好返回IPAGE */ APIResult<IPage<ResDTO>>
2020-05-06 jackromer
  目录