苏三说技术

苏三说技术 查看完整档案

成都编辑  |  填写毕业院校平安集团  |  资深java工程师 编辑 www.susan.net.cn 编辑
编辑

工作问题总结,技术分享,架构设计,底层原理。日常随笔
请关注微信公众账号:苏三说技术 ,有更多干货分享 和 学习资料。欢迎大家访问我的技术博客:www.susan.net.cn

个人动态

苏三说技术 赞了文章 · 10月17日

spring-boot-route(十七)使用aop记录操作日志

在上一章内容中——使用logback管理日志,我们详细讲述了如何将日志生成文件进行存储。但是在实际开发中,使用文件存储日志用来快速查询问题并不是最方便的,一个优秀系统除了日志文件还需要将操作日志进行持久化,来监控平台的操作记录。今天我们一起来学习一下如何通过apo来记录日志。

为了让记录日志更加灵活,我们将使用自定义的注解来实现重要操作的日志记录功能。

一 日志记录表

日志记录表主要包含几个字段,业务模块,操作类型,接口地址,处理状态,错误信息以及操作时间。数据库设计如下:

CREATE TABLE `sys_oper_log` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
   `title` varchar(50) CHARACTER SET utf8 DEFAULT '' COMMENT '模块标题',
   `business_type` int(2) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',
   `method` varchar(255) CHARACTER SET utf8 DEFAULT '' COMMENT '方法名称',
   `status` int(1) DEFAULT '0' COMMENT '操作状态(0正常 1异常)',
   `error_msg` varchar(2000) CHARACTER SET utf8 DEFAULT '' COMMENT '错误消息',
   `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB CHARSET=utf8mb4 CHECKSUM=1 COMMENT='操作日志记录'

对应的实体类如下:

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

    /** 日志主键 */
    private Long id;

    /** 操作模块 */
    private String title;

    /** 业务类型(0其它 1新增 2修改 3删除) */
    private Integer businessType;

    /** 请求方法 */
    private String method;

    /** 错误消息 */
    private String errorMsg;

    private Integer status;

    /** 操作时间 */
    private Date operTime;
}

二 自定义注解及处理

自定义注解包含两个属性,一个是业务模块title,另一个是操作类型businessType

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块
     */
    String title() default "";

    /**
     * 功能
     */
    BusinessType businessType() default BusinessType.OTHER;
}

使用aop对自定义的注解进行处理

@Aspect
@Component
@Slf4j
public class LogAspect {

    @Autowired
    private AsyncLogService asyncLogService;

    // 配置织入点
    @Pointcut("@annotation(com.javatrip.aop.annotation.Log)")
    public void logPointCut() {}

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     * 
     * @param joinPoint 切点
     * @param e 异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // 获得注解
            Log controllerLog = getAnnotationLog(joinPoint);
            if (controllerLog == null) {
                return;
            }

            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(0);
            if (e != null) {
                operLog.setStatus(1);
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog);
            // 保存数据库
            asyncLogService.saveSysLog(operLog);
        } catch (Exception exp) {
            log.error("==前置通知异常==");
            log.error("日志异常信息 {}", exp);
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     * 
     * @param log 日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private Log getAnnotationLog(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null) {
            return method.getAnnotation(Log.class);
        }
        return null;
    }
}

操作类型的枚举类:

public enum BusinessType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,
}

使用异步方法将操作日志存库,为了方便我直接使用jdbcTemplate在service中进行存库操作。

@Service
public class AsyncLogService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 保存系统日志记录
     */
    @Async
    public void saveSysLog(SysOperLog log) {

        String sql = "INSERT INTO sys_oper_log(title,business_type,method,STATUS,error_msg,oper_time) VALUES(?,?,?,?,?,?)";
        jdbcTemplate.update(sql,new Object[]{log.getTitle(),log.getBusinessType(),log.getMethod(),log.getStatus(),log.getErrorMsg(),new Date()});
    }
}

三 编写接口测试

将自定义注解写在业务方法上,测试效果

@RestController
@RequestMapping("person")
public class PersonController {
    @GetMapping("/{name}")
    @Log(title = "system",businessType = BusinessType.OTHER)
    public Person getPerson(@PathVariable("name") String name, @RequestParam int age){
        return new Person(name,age);
    }

    @PostMapping("add")
    @Log(title = "system",businessType = BusinessType.INSERT)
    public int addPerson(@RequestBody Person person){
        if(StringUtils.isEmpty(person)){
            return -1;
        }
        return 1;
    }

    @PutMapping("update")
    @Log(title = "system",businessType = BusinessType.UPDATE)
    public int updatePerson(@RequestBody Person person){
        if(StringUtils.isEmpty(person)){
            return -1;
        }
        return 1;
    }

    @DeleteMapping("/{name}")
    @Log(title = "system",businessType = BusinessType.DELETE)
    public int deletePerson(@PathVariable(name = "name") String name){
        if(StringUtils.isEmpty(name)){
            return -1;
        }
        return 1;
    }
}

当然,还可以在数据库中将请求参数和响应结果也进行存储,这样就能看出具体接口的操作记录了。


本文示例代码已上传至github,点个star支持一下!

Spring Boot系列教程目录

spring-boot-route(一)Controller接收参数的几种方式

spring-boot-route(二)读取配置文件的几种方式

spring-boot-route(三)实现多文件上传

spring-boot-route(四)全局异常处理

spring-boot-route(五)整合swagger生成接口文档

spring-boot-route(六)整合JApiDocs生成接口文档

spring-boot-route(七)整合jdbcTemplate操作数据库

spring-boot-route(八)整合mybatis操作数据库

spring-boot-route(九)整合JPA操作数据库

spring-boot-route(十)多数据源切换

spring-boot-route(十一)数据库配置信息加密

spring-boot-route(十二)整合redis做为缓存

spring-boot-route(十三)整合RabbitMQ

spring-boot-route(十四)整合Kafka

spring-boot-route(十五)整合RocketMQ

spring-boot-route(十六)使用logback生产日志文件

spring-boot-route(十七)使用aop记录操作日志

spring-boot-route(十八)spring-boot-adtuator监控应用

spring-boot-route(十九)spring-boot-admin监控服务

spring-boot-route(二十)Spring Task实现简单定时任务

spring-boot-route(二十一)quartz实现动态定时任务

spring-boot-route(二十二)实现邮件发送功能

spring-boot-route(二十三)开发微信公众号

这个系列的文章都是工作中频繁用到的知识,学完这个系列,应付日常开发绰绰有余。如果还想了解其他内容,扫面下方二维码告诉我,我会进一步完善这个系列的文章!

查看原文

赞 2 收藏 1 评论 2

苏三说技术 发布了文章 · 10月17日

这8种保证线程安全的技术你都知道吗?

并发情况下如何保证数据安全,一直都是开发人员每天都要面对的问题,稍不注意就会出现数据异常,造成不可挽回的结果。笔者根据自己的实际开发经验,总结了下面几种保证数据安全的技术手段:

  1. 无状态
  2. 不可变
  3. 安全的发布
  4. volatile
  5. synchronized
  6. lock
  7. cas
  8. threadlocal

一.无状态

我们都知道只有多个线程访问公共资源的时候,才可能出现数据安全问题,那么如果我们没有公共资源,是不是就没有这个问题呢?

public class NoStatusService {

    public void add(String status) {
        System.out.println("add status:" + status);
    }

    public void update(String status) {
        System.out.println("update status:" + status);
    }
}

二.不可变

如果多个线程访问公共资源是不可变的,也不会出现数据的安全性问题。

public class NoChangeService {

    public static final String DEFAULT_NAME = "abc";

    public void add(String status) {
        System.out.println("add status:" + status);
    }
}

三.安全的发布

如果类中有公共资源,但是没有对外开放访问权限,即对外安全发布,也没有线程安全问题

public class SafePublishService {

    private String name;

    public String getName() {
        return name;
    }

    public void add(String status) {
        System.out.println("add status:" + status);
    }
}

四.volatile

如果有些公共资源只是一个开关,只要求可见性,不要求原子性,这样可以用volidate关键字定义来解决问题。

public class FlagService {

    public volatile boolean flag = false;


    public void change() {
        if (flag) {
            System.out.println("return");
            return;
        }
        flag = true;
        System.out.println("change");
    }
}

五.synchronized

使用JDK内部提供的同步机制,这也是使用比较多的手段,分为:方法同步 和 代码块同步,我们优先使用代码块同步,因为方法同步的范围更大,更消耗性能。每个对象内部都又一把锁,只有抢答那把锁的线程,才能进入代码块里,代码块执行完之后,会自动释放锁。

public class SyncService {

    private int age = 1;

    public synchronized void add(int i) {
        age = age + i;
        System.out.println("age:" + age);
    }

    public void update(int i) {
        synchronized (this) {
            age = age + i;
            System.out.println("age:" + age);
        }
    }
}

六.lock

除了使用synchronized关键字实现同步功能之外,JDK还提供了lock显示锁的方式。它包含:可重入锁、读写锁 等更多更强大的功能,有个小问题就是需要手动释放锁,不过在编码时提供了更多的灵活性。

public class LockService {
    private ReentrantLock reentrantLock = new ReentrantLock();

    public int age = 1;


    public void add(int i) {
        try {
            reentrantLock.lock();
            age = age + i;
            System.out.println("age:" + age);
        } finally {
            reentrantLock.unlock();
        }
    }
}

七.cas

JDK除了使用锁的机制解决多线程情况下数据安全问题之外,还提供了cas机制。这种机制是使用CPU中比较和交换指令的原子性,JDK里面是通过Unsafe类实现的。cas需要四个值:旧数据、期望数据、新数据 和 地址,比较旧数据 和 期望的数据如果一样的话,就把旧数据改成新数据,当前线程不断自旋,一直到成功为止。不过可能会出现aba问题,需要使用AtomicStampedReference增加版本号解决。其实,实际工作中很少直接使用Unsafe类的,一般用atomic包下面的类即可。

public class AtomicService {

    private AtomicInteger atomicInteger = new AtomicInteger();


    public int add(int i) {
        return atomicInteger.getAndAdd(i);
    }
}

八.threadlocal

除了上面几种解决思路之外,JDK还提供了另外一种用空间换时间的新思路:threadlocal。它的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本,对另外的线程没有影响。特别注意,使用threadlocal时,使用完之后,要记得调用remove方法,不然可能会出现内存泄露问题。

public class ThreadLocalService {

    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();


    public void add(int i) {
        Integer integer = threadLocal.get();
        threadLocal.set(integer == null ? 0 : integer + i);
    }
}

总结

本文介绍了8种多线程情况下保证数据安全的技术手段,当然实际工作中可能会有其他。技术没有好坏之分,主要是看使用的场景,需要在不同的场景下使用不同的技术。

查看原文

赞 1 收藏 0 评论 0

苏三说技术 发布了文章 · 10月15日

天天在用volatile,你知道它的底层原理吗?

前言

对于从事java开发工作的朋友来说,在工作中可能会经常接触volatile关键字。即使有些朋友没有直接使用volatile关键字,但是如果使用过:ConcurrentHashMap、AtomicInteger、FutureTask、ThreadPoolExecutor等功能,它们的底层都使用了volatile关键字,你就不想了解一下它们为什么要使用volatile关键字,它的底层原理是什么?

从双重检查锁开始

面试时被要求写个单例模式的代码,很多朋友可能写的是双重检查锁。代码如下:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {

    }

    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton4();
                }
            }
        }
        return INSTANCE;
    }
}

有些朋友看到这里觉得有点熟悉,平时可能就是这个写的。

但是,我要告诉你的是,这个代码有问题,它在有些时候不是单例的。为什么会出现问题呢?

答案,在后面揭晓。

JMM(java内存模型)

在介绍volatile底层原理之前,让我们先看看什么是JMM(即java内存模型)。

天天在用volatile,你知道它的底层原理吗?

java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

java内存模型会带来三个问题:

1.可见性问题

线程A和线程B同时操作共享数据C,线程A修改的结果,线程B是不知道的,即不可见的

2.竞争问题

刚开始数据C的值为1,线程A和线程B同时执行加1操作,正常情况下数据C应该为3,但是在并发的情况下,数据C却还是2

3.重排序问题

JVM为了优化指令的执行效率,会对一下代码指令进行重排序。

那么如何解决问题呢?

volatile的底层原理

java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类 型的处理器重排序,从而让程序按我们预想的流程去执行。

1、保证特定操作的执行顺序。

2、影响某些数据(或则是某条指令的执行结果)的内存可见性。

java的内存屏障指令如下:

天天在用volatile,你知道它的底层原理吗?

对于volatile的写操作,在其前后分别加上 StoreStore 和 StoreLoad指令

天天在用volatile,你知道它的底层原理吗?

对于volatile的读操作,在其后加上 LoadLoad 和 LoadStore指令

天天在用volatile,你知道它的底层原理吗?

由上图可以看到,内存屏障是可以保证volatile变量前后读写顺序的。

此外,对volatile变量写操作时,使用store指令会强制线程刷新数据到主内存,读操作使用load指令会强制从主内存读取变量值。

再看看这个例子:

public class DataTest {
  
    private volatile int count = 0;

    public int getCount() {
       return count;
    }  

    public void setCount(int count) {
       this.count = count;
    }   
  
    public void incr() {
       count++;
    }  
} 

上面列子中的getCount和setCount方法这种单操作是可以保证原子性的,但是像incr方法无法保证原子性。

由此可见,volatile关键字可以解决可见性 和 重排序问题。但是不能解决竞争问题,无法保证操作的原子性,解决竞争问题需要加锁,或者使用cas等无锁技术。

再看双重检查锁问题

从上面可以看出JMM会有重排序问题,之前双重检查锁为什么有问题呢?

public static SimpleSingleton4 getInstance() {
  if (INSTANCE == null) {
     synchronized (SimpleSingleton4.class) {
        if (INSTANCE == null) {

          //1.分配内存空间
          //2.初始化引用
          //3.将实际的内存地址赋值给当前引用
          INSTANCE = new SimpleSingleton4();
        }
    }
  }
  return INSTANCE;
}

从代码中的注释可以看出,INSTANCE = new SimpleSingleton4();这一行代码其实经历了三个过程:

1.分配内存空间

2.初始化引用

3.将实际的内存地址赋值给当前引用

正常情况下是按照1、2、3的顺序执行的,但是指令重排之后也不排除按照1、3、2的顺序执行的可能性,如果按照1、3、2的顺序。

天天在用volatile,你知道它的底层原理吗?

上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

解决这个问题,可以把INSTANCE定义成volatile的。

private volatile static SimpleSingleton4 INSTANCE;

其实,创建单例的方法有很多,最好的还是静态内部类。

public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
}

总结

volatile的底层是通过:store,load等内存屏障命令,解决JMM的可见性和重排序问题的。但是它无法解决竞争问题,要解决竞争问题需要加锁,或使用cas等无锁技术。单例模式不建议使用双重检查锁,推荐使用静态内部类的方式创建。

彩蛋

使用volatile保证线程间的可见性和重排序问题,相对于synchronized等加锁机制更轻量级,但是对性能还是有一定的消耗,如何优化性能呢?

可以参考spring中DefaultNamespaceHandlerResolver类的getHandlerMappings方法

@Nullable
private volatile Map<String, Object> handlerMappings;

image.png

该方法就使用了双重检查锁,可以看到方法内部使用局部变量,首先将实例变量值赋值给该局部变量,然后再进行判断。最后内容先写入局部变量,然后再将局部变量赋值给实例变量。使用局部变量相对于不使用局部变量,可以提高性能。主要是由于 volatile 变量创建对象时需要禁止指令重排序,这就需要一些额外的操作。

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下:苏三说技术,或者点赞,转发一下,坚持原创不易,您的支持是我前进最大的动力,谢谢。

查看原文

赞 1 收藏 0 评论 0

苏三说技术 发布了文章 · 10月8日

springboot自动配置的秘密

​前言

       随着互联网越来越流行,springboot已经成为我们无论是工作,还是面试当中,不得不掌握的技术。说起springboot笔者认为最重要的功能非自动配置莫属了,为什么这么说?如果参与过以前spring复杂项目的朋友肯定,有过这样的经历,每次需要一个新功能,比如事务、AOP等,需要大量的配置,需要导出找jar包,时不时会出现jar兼容性问题,可以说苦不堪言。

      springboot的出现得益于“习惯优于配置”的理念,没有繁琐的配置、难以集成的内容(大多数流行第三方技术都被集成),这是基于Spring 4.x以上的版本提供的按条件配置Bean的能力。有了springboot的自动配置的功能,我们可以快速的开始一个项目。

一 什么是自动配置

不知道朋友们在工作当中有没有这样的经历:

1.1 引入redisTemplate

只要我们在pom.xml文件中引入spring-boot-starter-data-redis-xxx.jar包,然后只要在配置文件中配置redis连接,如:

spring.redis.database = 0
spring.redis.timeout = 10000
spring.redis.host = 10.72.16.9
spring.redis.port = 6379
spring.redis.pattern = 1

就可以在service方法中直接注入StringRedisTemplate对象的实例,可以直接使用了。朋友们有没有想过这是为什么?

@Autowired
private StringRedisTemplate stringRedisTemplate;

1.2  引入transactionTemplate

在项目中只要引入spring-boot-starter-xxx.jar,事务就自动生效了,并且可以直接在service方法中直接注入TransactionTemplate,用它开发编程式事务代码。是不是很神奇?这又是为什么?

1.3  使用@ConfigurationProperties

使用@ConfigurationProperties可以把指定路径下的属性,直接注入到实体对象中,看看下面这个例子:

@Data
@Component
@ConfigurationProperties("jump.threadpool")
public class ThreadPoolProperties {
​
 private int corePoolSize;
 private int maxPoolSize;
 private int keepAliveSeconds;
 private int queueCapacity;
}

只要application.properties这样配置,就可以自动注入到上面的实体中

jump.threadpool.corePoolSize=8
jump.threadpool.maxPoolSize=16
jump.threadpool.keepAliveSeconds=10
jump.threadpool.queueCapacity=100

没错,这三个例子都是springboot自动配置在起作用,我们分为两种情况:bean的自动配置 和 属性的自动配置。

二 工作原理


2.1 bean的自动配置

Spring Boot的启动类上有一个@SpringBootApplication注解,这个注解是Spring Boot项目必不可少的注解。

我们先看看@SpringBootApplication注解

它上面定义了另外一个注解:@EnableAutoConfiguration

该注解的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包下面key是EnableAutoConfiguration全名的,所有自动配置类。

我们看看springboot的spring-boot-autoconfigure-xxx.jar

该jar包里面就有META-INF/spring.factories文件。

这个spring.factories文件是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔。

@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(...)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。

SpringApplication.run(...)方法怎么调到selectImports()方法的

加载过程大概是这样的:

SpringApplication.run(...)方法  》 

AbstractApplicationContext.refresh()方法  》 

invokeBeanFactoryPostProcessors(...)方法  》 

PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(...) 方法  》

ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(..)方法  》

AutoConfigurationImportSelector.selectImports

该方法会找到自动配置的类,并给打了@Bean注解的方法创建对象。

postProcessBeanDefinitionRegistry方法是最核心的方法,它负责解析@Configuration、@Import、@ImportSource、@Component、@ComponentScan、@Bean等,完成bean的自动配置功能。

回到刚刚第二个例子TransactionTemplate为什么可以直接引用?

是因为在spring-boot-autoconfigure-xxx.jar的spring.factories配置文件中,EnableAutoConfiguration全类名下配置了TransactionAutoConfiguration全类名,springboot在启动的时候会加载这个类。

而TransactionAutoConfiguration类是一个配置类,它里面创建TransactionTemplate类的实例。

这样在其他地方就可以直接注入TransactionTemplate类的实例。

2.2 属性的自动配置

属性的自动配置是通过ConfigurationPropertiesBindingPostProcessor类的postProcessBeforeInitialization方法完成,

public Object postProcessBeforeInitialization(Object bean, String beanName)
 throws BeansException {
 ConfigurationProperties annotation = getAnnotation(bean, beanName,
 ConfigurationProperties.class);
 if (annotation != null) {
 bind(bean, beanName, annotation);
 }
 return bean;
}

它会解析@ConfigurationProperties注解上的属性,将配置文件中对应key的值绑定到属性上。

三 自动配置的生效条件

每个xxxxAutoConfiguration类上都可以定义一些生效条件,这些条件基本都是从@Conditional派生出来的。

常用的条件如下:

@ConditionalOnBean:当容器里有指定的bean时生效
@ConditionalOnMissingBean:当容器里不存在指定bean时生效
@ConditionalOnClass:当类路径下有指定类时生效
@ConditionalOnMissingClass:当类路径下不存在指定类时生效
@ConditionalOnProperty:指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。

举个比较常用的例子看看TransactionAutoConfiguration,是如何使用条件的

我们可以看到,条件用的是:@ConditionalOnClass,表示TransactionAutoConfiguration类只有在PlatformTransactionManager类存在时才会生效。

如何自定义自动配置类?

请阅读《老司机手把手教你编写自己的springboot starter》里面有详细步骤。

总结

本篇文章从什么是自动配置,工作原理 和 自动配置的生效条件 三个方面介绍了自动配置的相关知识点。自动配置又分为:bean的自动配置 和 属性的自动配置,二者的实现原理不一样。自动配置的生效条件用得非常多,建议朋友们好好研究一下。至于如何自定义自动配置类,本篇没有讲,是因为我在另外一篇文章《老司机手把手教你编写自己的springboot starter》中仔细介绍过的,有需要的朋友可以自行查阅。

如果这篇文档对您有所帮助的话,麻烦关注一下我的公众账号:苏三说技术,或者帮忙点赞或转发,坚持原创不易,您的支持是我坚持最大的动力。后面我会分享更多更实用的干货,谢谢大家的支持。

查看原文

赞 1 收藏 0 评论 0

苏三说技术 发布了文章 · 9月21日

别再用swagger了,给你推荐几个文档生成神器

最近公司打算做一个openapi开放平台,让我找一款好用的在线文档生成工具,具体要求如下:

1.必须是开源的

2.能够实时生成在线文档

3.支持全文搜索

4.支持在线调试功能

5.界面优美

说实话,这个需求看起来简单,但是实际上一点的都不简单。

我花了几天时间到处百度,谷歌,技术博客 和 论坛查资料,先后调研了如下文档生成工具:

一、gitbook
github地址:https://github.com/GitbookIO/...

开源协议:Apache-2.0 License

Star: 22.9k

开发语言:javascript

用户:50万+

示例地址:https://www.servicemesher.com...

file

GitBook是一款文档编辑工具。它的功能类似金山WPS中的Word或者微软Office中的Word的文档编辑工具。它可以用来写文档、建表格、插图片、生成pdf。当然,以上的功能WPS、Office可能做得更好,但是,GitBook还有更最强大的功能:它可以用文档建立一个网站,让更多人了解你写的书,另外,最最核心的是,他支持Git,也就意味着,它是一个分布式的文档编辑工具。你可以随时随地来编写你的文档,也可以多人共同编写文档,哪怕多人编写同一页文档,它也能记录每个人的内容,然后告诉你他们之间的区别,也能记录你的每一次改动,你可以查看每一次的书写记录和变化,哪怕你将文档都删除了,它也能找回来!这就是它继承Git后的厉害之处!

优点:使用起来非常简单,支持全文搜索,可以跟git完美集成,对代码无任何嵌入性,支持markdown格式的文档编写。

缺点:需要单独维护一个文档项目,如果接口修改了,需要手动去修改这个文档项目,不然可能会出现接口和文档不一致的情况。并且,不支持在线调试功能。

个人建议:如果对外的接口比较少,或者编写之后不会经常变动可以用这个。

二、smartdoc
gitee地址:https://gitee.com/smart-doc-t...

开源协议:Apache-2.0 License

Star: 758

开发语言:html、javascript

用户:小米、科大讯飞、1加

示例地址:https://gitee.com/smart-doc-t...

file

smart-doc是一个java restful api文档生成工具,smart-doc颠覆了传统类似swagger这种大量采用注解侵入来生成文档的实现方法。smart-doc完全基于接口源码分析来生成接口文档,完全做到零注解侵入,只需要按照java标准注释的写就能得到一个标准的markdown接口文档。

优点:基于接口源码分析生成接口文档,零注解侵入,支持html、pdf、markdown格式的文件导出。

缺点:需要引入额外的jar包,不支持在线调试

个人建议:如果实时生成文档,但是又不想打一些额外的注解,比如:使用swagger时需要打上@Api、@ApiModel等注解,就可以使用这个。

三、redoc
github地址:https://github.com/Redocly/redoc

开源协议:MIT License

Star: 10.7K

开发语言:typescript、javascript

用户:docker、redocly

示例地址:https://docs.docker.com/engin...

file

redoc自己号称是一个最好的在线文档工具。它支持swagger接口数据,提供了多种生成文档的方式,非常容易部署。使用redoc-cli能够将您的文档捆绑到零依赖的 HTML文件中,响应式三面板设计,具有菜单/滚动同步。

优点:非常方便生成文档,三面板设计

缺点:不支持中文搜索,分为:普通版本 和 付费版本,普通版本不支持在线调试。另外UI交互个人感觉不适合国内大多数程序员的操作习惯。

个人建议:如果想快速搭建一个基于swagger的文档,并且不要求在线调试功能,可以使用这个。

四、knife4j
gitee地址:https://gitee.com/xiaoym/knife4j

开源协议:Apache-2.0 License

Star: 3k

开发语言:java、javascript

用户:未知

示例地址:http://swagger-bootstrap-ui.x...

file

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望她能像一把匕首一样小巧,轻量,并且功能强悍。

优点:基于swagger生成实时在线文档,支持在线调试,全局参数、国际化、访问权限控制等,功能非常强大。

缺点:界面有一点点丑,需要依赖额外的jar包

个人建议:如果公司对ui要求不太高,可以使用这个文档生成工具,比较功能还是比较强大的。

五、yapi
github地址:https://github.com/YMFE/yapi

开源协议:Apache-2.0 License

Star: 17.8k

开发语言:javascript

用户:腾讯、阿里、美团、百度、京东等大厂

示例地址:http://swagger-bootstrap-ui.x...

file

yapi是去哪儿前端团队自主研发并开源的,主要支持以下功能:

可视化接口管理

数据mock

自动化接口测试

数据导入(各种,包括swagger、har、postman、json、命令行)

权限管理

支持本地化部署

支持插件

支持二次开发

优点:功能非常强大,支持权限管理、在线调试、接口自动化测试、插件开发等,BAT等大厂等在使用,说明功能很好。

缺点:在线调试功能需要安装插件,用户体检稍微有点不好,主要是为了解决跨域问题,可能有安全性问题。不过要解决这个问题,可以自己实现一个插件,应该不难。

个人建议:如果不考虑插件安全的安全性问题,这个在线文档工具还是非常好用的,可以说是一个神器,笔者在这里强烈推荐一下。

如果这篇文档对您有所帮助的话,麻烦关注一下我的公众账号:苏三说技术,或者帮忙点赞或转发,坚持原创不易,您的支持是我坚持最大的动力。后面我会分享更多更实用的干货,谢谢大家的支持。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 1 收藏 0 评论 1

苏三说技术 赞了回答 · 9月20日

jdk的多线程处理机制各有什么优缺点,实用于什么场景

synchronized:单纯的锁,由jdk底层维护并优化,类似于悲观锁,安全,但是效率偏低
cas:比较并交换,用副本和内存值比较,如果相同就直接交换地址,效率高,但容易产生aba问题
threadlocal: 和线程绑定的变量,没啥难理解的吧
volidate: 保持可见性,由于jmm针对于volidate的特殊规则,use,load双向绑定,load,read连续绑定,,assgin,store 双向绑定store,write连续绑定,所以具有可见性,针对于多线程一写多读,不存在读取缓存导致结果错误的问题,针对于 read,load,use 或者 assgin,store,write 必须按照先后顺序执行,所以禁止重排序。
lock: synchronized 的 增强版,包括公平性,非块状结构,尝试获取等功能,参照api,开始效率高于 synchronized,但synchronized在近版本中由于多次优化,效率已经差不多了,缺点是必须注意释放。

关注 4 回答 4

苏三说技术 发布了文章 · 9月18日

11张图让你彻底明白jdk1.7 hashmap的死循环是如何产生的

jdk1.7 hashmap的循环依赖问题是面试经常被问到的问题,如何回答不好,可能会被扣分。今天我就带大家一下梳理一下,这个问题是如何产生的,以及如何解决这个问题。

一、hashmap的数据结构

先一起看看jdk1.7 hashmap的数据结构

数组 + 链表

hashmap会给每个元素的key生成一个hash值,然后根据这个hash值计算一个在数组中的位置i。i不同的元素放在数组的不同位置,i相同的元素放在链表上,最新的数据放在链表的头部。

往hashmap中保存元素会调用put方法,获取元素会调用get方法。接下来,我们重点看看put方法。

二、put方法

重点看看put方法

public V put(K key, V value) {
     if (table == EMPTY_TABLE) {
        inflateTable(threshold);
     }
     if (key == null)
         return putForNullKey(value);
     //根据key获取hash 
     int hash = hash(key);
     //计算在数组中的下表
     int i = indexFor(hash, table.length);
     //变量集合查询相同key的数据,如果已经存在则更新数据
     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
         Object k;
             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
             V oldValue = e.value;
             e.value = value;
             e.recordAccess(this);
             //返回已有数据
             return oldValue;
         }
     }
     modCount++;
     //如果不存在相同key的元素,则添加新元素
     addEntry(hash, key, value, i);
     return null;
}

再看看addEntry方法

void addEntry(int hash, K key, V value, int bucketIndex) {
     // 当数组的size >= 扩容阈值,触发扩容,size大小会在createEnty和removeEntry的时候改变
     if ((size >= threshold) && (null != table[bucketIndex])) {
         // 扩容到2倍大小,后边会跟进这个方法
         resize(2 * table.length);
         // 扩容后重新计算hash和index
         hash = (null != key) ? hash(key) : 0;
         bucketIndex = indexFor(hash, table.length);
     }
     // 创建一个新的链表节点,点进去可以了解到是将新节点添加到了链表的头部
     createEntry(hash, key, value, bucketIndex);
 }

看看resize是如何扩容的

 void resize(int newCapacity) {
     Entry[] oldTable = table;
     int oldCapacity = oldTable.length;
     if (oldCapacity == MAXIMUM_CAPACITY) {
     threshold = Integer.MAX_VALUE;
     return;
     }
     // 创建2倍大小的新数组
     Entry[] newTable = new Entry[newCapacity];
     // 将旧数组的链表转移到新数组,就是这个方法导致的hashMap不安全,等下我们进去看一眼
     transfer(newTable, initHashSeedAsNeeded(newCapacity));
     table = newTable;
     // 重新计算扩容阈值(容量*加载因子)
     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

出问题的就是这个transfer方法

void transfer(Entry[] newTable, boolean rehash) {
     int newCapacity = newTable.length;
     // 遍历旧数组
     for (Entry<K,V> e : table) {
         // 遍历链表
         while(null != e) {
             //获取下一个元素,记录到一个临时变量,以便后面使用
             Entry<K,V> next = e.next;
             if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
             }
             // 计算节点在新数组中的下标
             int i = indexFor(e.hash, newCapacity);
             // 将旧节点插入到新节点的头部
             e.next = newTable[i];
             //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next
             //这两行代码决定了倒序插入
             //比如:以前同一个位置上是:3,7,后面可能变成了:7、3
             newTable[i] = e;
             //将下一个元素赋值给当前元素,以便遍历下一个元素
             e = next;  
         }  
     }
}

我来给大家分析一下,为什么这几个代码是头插法,网上很多技术文章都没有说清楚。

三、头插法

我们把目光聚焦到这几行代码:

 //获取下一个元素,记录到一个临时变量,以便后面使用
 Entry<K,V> next = e.next;
 // 计算节点在新数组中的下标
 int i = indexFor(e.hash, newCapacity);
 // 将旧节点插入到新节点的头部
 e.next = newTable[i];
 //这行才是真正把数据插入新数组中,前面那行代码只是设置当前节点的next
 newTable[i] = e;
 //将下一个元素赋值给当前元素,以便遍历下一个元素
 e = next;

假设刚开始hashMap有这些数据

调用put方法需要进行一次扩容,刚开始会创建一个空的数组,大小是以前的2倍,如图所示:

开始第一轮循环:

 //next= 7   e = 3  e.next = 7
 Entry<K,V> next = e.next;
 // i=3
 int i = indexFor(e.hash, newCapacity);
 //e.next = null ,刚初始化时新数组的元素为null
 e.next = newTable[i];
 //给新数组i位置 赋值 3
 newTable[i] = e;
 // e = 7
 e = next;

执行完之后,第一轮循环之后数据变成这样的

image.png

再接着开始第二轮循环:

 //next= 5   e = 7  e.next = 5
 Entry<K,V> next = e.next;
 // i=3
 int i = indexFor(e.hash, newCapacity);
 //e.next = 3 ,此时相同位置上已经有key=3的值了,将该值赋值给当前元素的next
 e.next = newTable[i];
 //给新数组i位置 赋值 7
 newTable[i] = e;
 // e = 5
 e = next;

上面会构成一个新链表,连接的顺序正好反过来了。

image.png

由于第二次循环时,节点key=7的元素插到相同位置上已有元素key=3的前面,所以说是采用的头插法。

四、死循环的产生

接下来重点看看死循环是如何产生的?

假设数据跟元素数据一致,有两个线程:线程1 和 线程2,同时执行put方法,最后同时调用transfer方法。

线程1 先执行,到  Entry<K,V> next = e.next; 这一行,被挂起了。

 //next= 7   e = 3  e.next = 7
 Entry<K,V> next = e.next;
 int i = indexFor(e.hash, newCapacity);
 e.next = newTable[i];
 newTable[i] = e;
 e = next;

此时线程1 创建的数组会创建一个空数组

接下来,线程2开始执行,由于线程2运气比较好,没有被中断过,执行完毕了。

过一会儿,线程1被恢复了,重新执行代码。

 //next= 7   e = 3  e.next = 7
 Entry<K,V> next = e.next;
 // i = 3
 int i = indexFor(e.hash, newCapacity);
 // e.next = null,刚初始化时新数组的元素为null
 e.next = newTable[i];
 // 给新数组i位置 赋值 3
 newTable[i] = e;
 // e = 7
 e = next;

这时候线程1的数组会变成这样的

image.png

再执行第二轮循环,此时的e=7

 //next= 3   e = 7  e.next = 3
 Entry<K,V> next = e.next;
 // i = 3
 int i = indexFor(e.hash, newCapacity);
 // e.next = 3,此时相同位置上已经有key=3的值了,将该值赋值给当前元素的next
 e.next = newTable[i];
 // 给新数组i位置 赋值 7
 newTable[i] = e;
 // e = 3
 e = next;

这里特别要说明的是 此时e=7,而e.next为什么是3呢?

因为hashMap的数据是公共的,还记得线程2中的生成的数据吗?

image.png

此时e=7,那么e.next肯定是3。

经过上面第二轮循环之后,线程1得到的数据如下:

此时由于循环判断还没有退出,判断条件是: while(null != e),所以要开始第三轮循环:

 //next= null   e = 3  e.next = null
 Entry<K,V> next = e.next;
 // i = 3
 int i = indexFor(e.hash, newCapacity);
 // e.next = 7,关键的一步,由于第二次循环是 key:7 .next = key:3,现在key:3.next = key:7
 e.next = newTable[i];
 // 给新数组i位置 赋值 3
 newTable[i] = e;
 // e = null
 e = next;

由于e=null,此时会退出循环,最终线程1的数据会是这种结构:

image.png

key:3 和 key:7又恢复了刚开始的顺序,但是他们的next会相互引用,构成环形引用。

注意,此时调用hashmap的get方法获取数据时,如果只是获取循环链上key:3 和 key:7的数据,是不会有问题的,因为可以找到。就怕获取循环链上没有的数据,比如:key:11,key:15等,会进入无限循环中导致CPU使用率飙升。

五、如何避免死循环

为了解决这个问题,jdk1.8把扩容是复制元素到新数组由 头插法 改成了 尾插法 。此外,引入了红黑树,提升遍历节点的效率。在这里我就不过多介绍了,如果有兴趣的朋友,可以关注我的公众号,后面会给大家详细分析jdk1.8的实现,以及 jdk1.7、jdk1.8 hashmap的区别。

此外,HashMap是非线程安全的,要避免在多线程的环境中使用HashMap,而应该改成使用ConcurrentHashMap。

所以总结一下要避免发生死循环的问题的方法:改成ConcurrentHashMap

如果这篇文档对您有所帮助的话,麻烦关注一下我的公众账号:苏三说技术,或者帮忙点赞或转发,坚持原创不易,您的支持是我坚持最大的动力。后面我会分享更多更实用的干货,谢谢大家的支持。

image.png

查看原文

赞 2 收藏 1 评论 2

苏三说技术 提出了问题 · 9月16日

jdk的多线程处理机制各有什么优缺点,实用于什么场景

synchronized:同步机制
cas:比较交互
threadlocal:创建变量副本
volidate:保持可见性
lock:显示锁

关注 4 回答 4

苏三说技术 回答了问题 · 9月16日

解决docker 如何查看已存在容器的创建命令

docker ps -a

关注 7 回答 5

认证与成就

  • 获得 35 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 9月1日
个人主页被 406 人浏览