精华内容
下载资源
问答
  • java重入锁与不可重入

    万次阅读 多人点赞 2018-08-28 11:08:58
    所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。 synchronized 和 ReentrantLock 都是可重入锁。 可重入锁的意义在于防止死锁。 实现...

    所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

    synchronized 和   ReentrantLock 都是可重入锁。

    可重入锁的意义在于防止死锁。

    实现原理是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。

    如果同一个线程再次请求这个锁,计数将递增;

    每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。

    关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有重入的锁,那么这段代码将产生死锁(很好理解吧)。

    例子:

    比如说A类中有个方法public synchronized methodA1(){

            methodA2();

    }

    而且public synchronized methodA2(){

                        //具体操作

    }

    也是A类中的同步方法,当当前线程调用A类的对象methodA1同步方法,如果其他线程没有获取A类的对象锁,那么当前线程就获得当前A类对象的锁,然后执行methodA1同步方法,方法体中调用methodA2同步方法,当前线程能够再次获取A类对象的锁,而其他线程是不可以的,这就是可重入锁。

     

    代码演示:

    不可重入锁:

    public class Lock{
        private boolean isLocked = false;
        public synchronized void lock() throws InterruptedException{
            while(isLocked){    
                wait();
            }
            isLocked = true;
        }
        public synchronized void unlock(){
            isLocked = false;
            notify();
        }
    }

    使用该锁:

    public class Count{
        Lock lock = new Lock();
        public void print(){
            lock.lock();
            doAdd();
            lock.unlock();
        }
        public void doAdd(){
            lock.lock();
            //do something
            lock.unlock();
        }
    }

    当前线程执行print()方法首先获取lock,接下来执行doAdd()方法就无法执行doAdd()中的逻辑,必须先释放锁。这个例子很好的说明了不可重入锁。

    可重入锁:

    接下来,我们设计一种可重入锁

    public class Lock{
        boolean isLocked = false;
        Thread  lockedBy = null;
        int lockedCount = 0;
        public synchronized void lock()
                throws InterruptedException{
            Thread thread = Thread.currentThread();
            while(isLocked && lockedBy != thread){
                wait();
            }
            isLocked = true;
            lockedCount++;
            lockedBy = thread;
        }
        public synchronized void unlock(){
            if(Thread.currentThread() == this.lockedBy){
                lockedCount--;
                if(lockedCount == 0){
                    isLocked = false;
                    notify();
                }
            }
        }
    }

    所谓可重入,意味着线程可以进入它已经拥有的锁的同步代码块儿。

    我们设计两个线程调用print()方法,第一个线程调用print()方法获取锁,进入lock()方法,由于初始lockedBy是null,所以不会进入while而挂起当前线程,而是是增量lockedCount并记录lockBy为第一个线程。接着第一个线程进入doAdd()方法,由于同一进程,所以不会进入while而挂起,接着增量lockedCount,当第二个线程尝试lock,由于isLocked=true,所以他不会获取该锁,直到第一个线程调用两次unlock()将lockCount递减为0,才将标记为isLocked设置为false。

    可重入锁的概念和设计思想大体如此,Java中的可重入锁ReentrantLock设计思路也是这样。

     

    synchronized和ReentrantLock 都是可重入锁。ReentrantLock与synchronized比较:

    1.前者使用灵活,但是必须手动开启和释放锁

    2.前者扩展性好,有时间锁等候(tryLock( )),可中断锁等候(lockInterruptibly( )),锁投票等,适合用于高度竞争锁和多个条件变量的地方

    3.前者提供了可轮询的锁请求,可以尝试去获取锁(tryLock( )),如果失败,则会释放已经获得的锁。有完善的错误恢复机制,可以避免死锁的发生。

    摘自:JAVA可重入锁与不可重入锁   和   Java不可重入锁和可重入锁理解

     

    展开全文
  • 看到一个问题,Java的可重入锁为什么可以防止死锁呢?网上看了看资料,虽然有答案说出了正确答案,但是分析的不够详细,对初学者不够友好。这里我再做一个更清晰的分析。首先是示例代码:1 public classWidget {2 ...

    看到一个问题,Java的可重入锁为什么可以防止死锁呢?网上看了看资料,虽然有答案说出了正确答案,但是分析的不够详细,对初学者不够友好。这里我再做一个更清晰的分析。

    首先是示例代码:

    1 public classWidget {2 public synchronized voiddoSomething(){3 //do something

    4 }5 }6 public class LoggingWidget extendsWidget {7 public synchronized voiddoSomething() {8 super.doSomething();9 }10 }

    这是《java并发编程实例》一书中的例子,并且书中说:“如果synchronized 不是可重入锁,那么LoggingWidget 的super.dosomething();无法获得Widget对象的锁,因为会死锁。”

    乍一看好像不是这么回事,就算synchronized不是可重入锁,可是synchronized关键字一个在父类Widget 的方法上,另一个在子类LoggingWidget 的方法上,怎么会有死锁产生呢。

    这里其实牵涉到了Java的重写。我们看子类LoggingWidget 的doSomething方法,重写了父类Widget 的doSomething方法,但是子类对象如果要调用父类的doSomething方法,那么就需要用到super关键字了。因为实例方法的调用是Java虚拟机在运行时动态绑定的,子类LoggingWidget 的对象调用doSomething方法,一定是绑定到子类自身的doSomething方法,必须用super关键字告诉虚拟机,这里要调用的是父类的doSomething方法。

    实际上,如果我们分析运行时的LoggingWidget 类,那我们看到的应该是这样子的(这里只是为了分析,真实情况肯定和下面的例子不同):

    1 public class LoggingWidget extendsWidget {2 public synchronized voidWidget.doSomething() {3 //do something

    4 } //父类的doSomething方法

    5

    6 public synchronized voiddoSomething() {7 super.doSomething();8 }9 }

    子类对象,其实是持有父类Widget 的doSomething方法的,只需要使用super关键字告诉虚拟机要运行的是父类的doSomething方法,虚拟机会去调用子类对象中的父类Widget 的doSomething方法的。所以,super关键字并没有新建一个父类的对象,比如说widget,然后再去调用widget.doSomething方法,实际上调用父类doSomething方法的还是我们的子类对象。

    那么这样就很好理解了,如果一个线程有子类对象的引用loggingWidget,然后调用loggingWidget.doSomething方法的时候,会请求子类对象loggingWidget 的对象锁;又因为loggingWidget 的doSomething方法中调用的父类的doSomething方法,实际上还是要请求子类对象loggingWidget 的对象锁,那么如果synchronized关键字不是个可重入锁的话,就会在子类对象持有的父类doSomething方法上产生死锁了。正因为synchronized关键字的可重入锁,当前线程因为已经持有了子类对象loggingWidget 的对象锁,后面再遇到请求loggingWidget 的对象锁就可以畅通无阻地执行同步方法了。

    更进一步,将上面的示例代码改写一下,那么就算synchronized不是可重入锁,也不会产生死锁的问题了。代码如下:

    1 public classWidget {2 public synchronized voiddoSomething(){3 //do something

    4 }5 }6 public class LoggingWidget extendsWidget {7 public synchronized voiddoSomething() {8 Widget widget = newWidget();9 widget.doSomething();10 }11 }

    在子类的doSomething方法中,直接新建了一个父类的对象widget,然后用这个父类对象来调用父类的doSomething方法,实际上请求的是这个父类对象widget的对象锁,就不涉及到可重入锁的问题了。

    展开全文
  • 一、刷单原理:防止一个方法,在方法参数值相同的情况下,短时间频繁调用,这里根据spring中的AOP原理来实现的,自己定义了一个注解,这个注解主要用来判断哪些方法上面加了这个注解,就做参数请求处理,先配置...

       最近公司商城订单出现重复订单数据问题,比较棘手,一直在找原因,没有发现问题,太坑了,后来决定在原有的业务基础上面加上防刷单处理和redis分布式锁,双重保证应用的安全和稳定性。


    一、防刷单原理:防止一个方法,在方法参数值相同的情况下,短时间频繁调用,这里根据spring中的AOP原理来实现的,自己定义了一个注解,这个注解主要用来判断哪些方法上面加了这个注解,就做参数请求处理,先配置具体的aop切面路径扫描类中的方法,处理是根据这个请求的路径获取相应的方法中的参数做具体分析。

    实现的步骤:

    1. 定义一个注解(主要用来判断哪些方法要做防重复提交处理)
    2. 通过spring中的AOP进行扫描,方法处理。
    3. 设置一个过期时间来处理redis分布式锁处理(这里会在redis分布式锁中实现

     

     

    /*********定义防重复请求方法注解*********/
    
    package com.lolaage.common.annotations;
    import java.lang.annotation.*;
    /**
     * 定义一个注解(主要用来判断哪些方法要做防重复提交处理)
     * @Description 防止同一个方法被频繁执行(是否需要频繁执行看参数params是否不一样)
     * @Date 19:35 2019/4/9
     * @Param
     * @return
     **/
    @Target({ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface SameMethodFrequentlyRun {
    	/**
    	 * @Description 当方法的参数是实体对象,对象必须对象重写equal和hashcode方法
    	 **/
    	String params()  default "";
    	String description()  default "";
    	/**
    	 * @Description
    	 **/
    	long milliseconds()  default 30000L;
    }    
    
      
    /*************下面是具体的方法处理请求参数过程***************/
    
    
    package com.lolaage.common.aop;
    import com.lolaage.base.po.JsonModel;
    import com.lolaage.common.annotations.SameMethodFrequentlyRun;
    import com.lolaage.helper.util.RedisLockTemplate;
    import com.lolaage.util.StringUtil;
    import org.apache.log4j.Logger;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * @Description  防止同一个方法被频繁执行AOP(是否需要频繁执行看参数params是否不一样)
     **/
    
    @Aspect
    @Component
    public class SameMethodFrequentlyRunAop {
    	private static Logger logger = Logger.getLogger(SameMethodFrequentlyRunAop.class);
    
    
    	// 配置接入点,即为所要记录的action操作目录
    	@Pointcut("execution(* com.lolaage.helper.web.controller..*.*(..))")
    	private void controllerAspect() {
    
    	}
    
    	@Around("controllerAspect()")
    	public Object around(ProceedingJoinPoint pjp) {
    		Object returnObj=null;
    		StringBuilder sb=new StringBuilder();
    
    		// 拦截的实体类,就是当前正在执行的controller
    		Object target = pjp.getTarget();
    		//获取全类名
    		String className=target.getClass().getName();
    		// 拦截的方法名称。当前正在执行的方法
    		String methodName = pjp.getSignature().getName();
    		// 拦截的方法参数
    		Object[] args = pjp.getArgs();
    
    		// 拦截的放参数类型
    		Signature sig = pjp.getSignature();
    		MethodSignature msig = (MethodSignature) sig ;
    
    		Class[] parameterTypes = msig.getMethod().getParameterTypes();
    		sb.append(className);
    		for (Object o : args) {
    			if(o==null){
    				continue;
    			}
    			int i = o.hashCode();
    			sb.append(":");
    			sb.append(i);
    		}
    		// 获得被拦截的方法
    		Method method = null;
    		try {
    			method = target.getClass().getMethod(methodName, parameterTypes);
    			SameMethodFrequentlyRun sameMethodFrequentlyRun = method.getAnnotation(SameMethodFrequentlyRun.class);
    			if (sameMethodFrequentlyRun != null) {
    				String description = sameMethodFrequentlyRun.description();
    				String params = sameMethodFrequentlyRun.params();
    				if(StringUtil.isEmpty(params)){
    					params=sb.toString();
    				}
    				long milliseconds = sameMethodFrequentlyRun.milliseconds();
    				Boolean isGetLock = RedisLockTemplate.distributedLock_v2(params, description, milliseconds, false);
    				if(!isGetLock){
    					//提示不要重复操作
    					JsonModel result = new JsonModel();
    					return result.setErrCode(5004);
    				}
    			}
    		} catch (NoSuchMethodException e) {
    			logger.error("分布式防重复操作异常:AOP只会拦截public方法,非public会报异常,如果你要将你的方法加入到aop拦截中,请修改方法的修饰符:"+e.getMessage());
    		}
    		try {
    			  returnObj = pjp.proceed();
    		} catch (Throwable e) {
    			logger.error("分布式防重复操作异常Throwable:"+e.getMessage());
    			e.printStackTrace();
    		}
    		return returnObj;
    	}
    
    
    }
    
    
    /**
    * 分布式锁压力测试,和防重复测试
    * @return
    */
    @SameMethodFrequentlyRun(description="查询操作日志",milliseconds = 10000L)
    @RequestMapping("/pressureLock")
    public void pressureLock(String key,QuitParam quitParam) {
     System.out.println(this.hashCode()+"---"+Thread.currentThread().getName()+":测试开始");
     System.out.println(this.hashCode()+"---"+Thread.currentThread().getName()+"测试结束");
    }

    二、redis分布式对象锁的原理:

     解释:  针对某种资源,需要被整个系统的各台服务器共享访问,但是只允许一台服务器同时访问。比如说订单服务是做成集群的,当两个以上结点同时收到一个相同订单的创建指令,这时并发就产生了,系统就会重复创建订单。而分布式共享锁就是解决这类问题 

    原理:对高并发请求的时候,我们使用redis分布式共享锁来处理,通过set方法设置对应的key-value和milliseconds过期时间,在规定的时间内保证锁可以释放出来,通过eval来解锁。

    实现代码:

    /**
     * @Description 分布式锁模板
     * @Date 10:39 2019/4/9
     * @Param [key, actionLog, expireSecond]
     * @return java.lang.Boolean
     **/
    public static  Boolean distributedLock_v2(String key,String actionLog, long milliseconds,boolean isDelLock){
    	RedisBaseDao redisDao = RedisUtil.getRedisDao();
    	boolean isGetLock=false;
       String requestId = UUID.randomUUID().toString();
    	try {
    		isGetLock = redisDao.getDistributedLock(key,requestId , milliseconds);
    		if(!isGetLock){
    			logger.error("分布式锁拦截,不能重复操作,"+key+",actionLog="+actionLog);
    		}
    		return isGetLock;
    	} catch (Exception e) {
    		e.printStackTrace();
    		if(e instanceof RedisException){
    			logger.error("redis 分布式锁异常,可能存在重复操作的的可能性,key="+key+",actionLog="+actionLog+",e="+e);
    			return true;
    		}
    	}finally {
    		if(isGetLock&&isDelLock){
    			try {
    				redisDao.releaseDistributedLock(key,requestId);
    			} catch (Exception e) {
    				e.printStackTrace();
    				logger.error("分布式锁释放锁失败,key="+key+",actionLog="+actionLog+","+e);
    			}
    		}
    	}
    	return false;
    }
    
    /**
     * 尝试获取分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param milliseconds 超期时间
     * @return 是否获取成功
     */
    
    private static final Long RELEASE_SUCCESS = 1L;
    public   boolean getDistributedLock(String lockKey, String requestId, Long milliseconds) {
    	  return   this.setNx(lockKey, requestId, milliseconds);
    }
    /**
     * 释放分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public   boolean releaseDistributedLock( String lockKey, String requestId) {
    	return this.deleteKeyForSameValue(lockKey,requestId);
    }
    
    
     public Boolean setNx(  String key,   String value,Long expireTime) {
    	Boolean isSet = redisTemplate.execute(new RedisCallback<Boolean>() {
    		@Override
    		public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
    			//过期时间好处:即使服务器宕机了,也能保证锁被正确释放。
    			//setNx原子性操作,防止同一把锁在同一时间可能被不同线程获取到
    			Jedis jedis = (Jedis) redisConnection.getNativeConnection();
    			String result = jedis.set(key, value, "nx", "px", expireTime);
    			if("OK".equals(result)){
    				return true;
    			}
    			return false;
    		}
    	});
    	return isSet;
    }
    public Boolean deleteKeyForSameValue(  String key,   String value) {
      return  redisTemplate.execute(new RedisCallback<Boolean>() {
    	 @Override
    	public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
    		Jedis jedis = (Jedis) redisConnection.getNativeConnection();
    			//删除key的时候,先判断该key对应的value是否等于先前设置的随机值,只有当两者相等的时候才删除该key
    	//防止释放其他客户端获取到的锁
    	//原子性操作
    	String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return 
      redis.call('del', KEYS[1]) else return 0 end";
    Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
    			if (RELEASE_SUCCESS.equals(result)) {
    				return true;
    			}
    			return false;
    		}
    	});
    }
    
    

    ## 方案优点
    > 多个服务器竞争资源,需要排队,解决类似一个订单被多个服务器提交问题。

    ## 方案缺点 
    - 试用与一主多从的redis集群,如果多主多从,不能解决共享锁问题   
        -这个问题解决方案[https://yq.aliyun.com/articles/674394](https://yq.aliyun.com/articles/674394),[https://blog.csdn.net/chen_kkw/article/details/81433470](https://blog.csdn.net/chen_kkw/article/details/81433470)
    - 同时当一主多从服务器,主机宕机,有丢失锁的风险,概率很小。 
        - **场景**
        - 在Redis的master节点上拿到了锁,但是这个加锁的key还没有同步到slave节点,master故障,发生故障转移,slave节点升级为master节点; 导致锁丢失。概率很小,可以不考虑。
    实例代码下载:https://download.csdn.net/my/downloads

    展开全文
  • 提交了多少代码、提交了多少方法、有单元测试吗、影响了那些流程链路、有没有夹带上线。 大部分时候这些问题的汇总都是人为的方式进行提供,以依赖相信研发为主。剩下的就需要依赖有经验的测试进行白盒验证。所以...

    作者:小傅哥
    博客:bugstack.cn
    沉淀、分享、成长,让自己和他人都能有所收获!

    一、前言

    在我们实际的业务开发到上线的过程中,中间都会经过测试。那么怎么来保证测试质量呢?比如;提交了多少代码、提交了多少方法、有单元测试吗、影响了那些流程链路、有没有夹带上线。

    大部分时候这些问题的汇总都是人为的方式进行提供,以依赖相信研发为主。剩下的就需要依赖有经验的测试进行白盒验证。所以即使是这样测试也会在上线后发生很多未知的问题,毕竟流程太长,影响面太广。很难用一个人去照顾到所有流程。

    所以,我很希望使用技术手段来解决这一问题,通过服务质量监控来在研发提测后,自动报告相关数据,例如;研发代码涉及流程链路展示、每个链路测试次数、通过次数、失败次数、当时的出入参信息以及对应的代码块在当前提测分支修改记录等各项信息。最终测试在执行验证时候,分配验证渠道扫描到所有分支节点,可以清晰的看到全链路的影响。那么,这样的测试才是可以保证系统的整体质量的。

    好!接下来到后续一段时间,我会不断的去完善和开发这些功能。也欢迎你的加入!

    二、技术目标

    技术行为都是为目标服务的,也就是实现务产品功能

    而我们这个文章的目标是需要使用固定的技术栈 JavaAgent+ ASM,来抓取方法执行时候的信息,包括:类名称、方法名称、入参信息和入参值、出参信息和出参值以及当前方法的耗时。

    JavaAgent,是一种探针技术可以通过 premain 方法,在类加载的过程中给指定的方法进行字节码增强。其实你的每一个类最终都是字节码指令的执行,而这种增强后的方法就可以输出我们想要的信息。这就相当于你硬编码时候输出了一些方法的耗时,日志等信息。

    ASM,是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。说白了asm是直接通过字节码来修改class文件。另外除了 asm 可以操作字节码,还有javassist和Byte-code等,他们比 asm 要简单,但是执行效率还是 asm 高。因为 asm 是直接使用指令来控制字节码。

    三、实现方案

    字节码增强实现方案

    按照图中我们使用 javaAgentprimain 方法,使用 asm 进行字节码增强,以便于输出我们的监控信息。最终在我们把字节码增强后,程序所执行的就是我们的新的方法字节码,从而也就可以获取到我们需要的信息。那么,接下来我们开始一步步上线这些功能。

    关于实现方案中的所有源码,可以通过关注公众号:bugstack虫洞栈,回复源码下载进行获取

    1. 定义测试方法

    public class ApiTest {
    
        public static void main(String[] args) throws InterruptedException {
            ApiTest apiTest = new ApiTest();
            String res01 = apiTest.queryUserInfo(111, 17);
            System.out.println("测试结果:" + res01 + "\r\n");;
        }
    
        public String queryUserInfo(int uId, int age) throws InterruptedException {
            return "你好,bugstack虫洞栈 | 精神小伙!";
        }
    
    }
    
    • 这里我们定义了一个查询用户信息的测试方法,后续不断将这个方法进行字节码增强。

    2. 监控类入口

    PreMain.java & 入口方法

    public class PreMain {
    
        //JVM 首先尝试在代理类上调用以下方法
        public static void premain(String agentArgs, Instrumentation inst) {
            inst.addTransformer(new ProfilingTransformer());
        }
    
        //如果代理类没有实现上面的方法,那么 JVM 将尝试调用该方法
        public static void premain(String agentArgs) {
        }
    
    }
    

    MANIFEST.MF & 配置

    Manifest-Version: 1.0
    Premain-Class: org.itstack.sqm.asm.PreMain
    Can-Redefine-Classes: true
    
    • 以上是固定的基础模板代码,所有的 JavaAgent 程序都需要从这里开始。

    3. 字节码方法处理

    
    public class ProfilingTransformer implements ClassFileTransformer {
    
        @Override
        public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
            try {
    
            	// 排除一些不需要处理的方法
                if (ProfilingFilter.isNotNeedInject(className)) {
                    return classfileBuffer;
                }
    
                return getBytes(loader, className, classfileBuffer);;
            } catch (Throwable e) {
                System.out.println(e.getMessage());
            }
            return classfileBuffer;
        }
    
        ...
    
    }
    
    
    • 这里主要通过传入进行的类加载器、类名、字节码等,负责字节码的增强操作。而这里会使用 ASM 方式进行处理,如下;
    
    private byte[] getBytes(ClassLoader loader, String className, byte[] classfileBuffer) {
          ClassReader cr = new ClassReader(classfileBuffer);
          ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
          ClassVisitor cv = new ProfilingClassAdapter(cw, className);
          cr.accept(cv, ClassReader.EXPAND_FRAMES);
          return cw.toByteArray();
      }
    
    

    4. 字节码方法解析

    字节码方法解析

    • 当程序启动加载的时候,每个类的每一个方法都会被监控到。类的名称、方法的名称、方法入参出参的描述等,都可以在这里获取。
    • 为了可以在后续监控处理不至于每一次都去传参(方法信息)浪费消耗性能,一般这里都会给每个方法生产一个全局防重的 id ,通过这个 id 就可以查询到对应的方法。
    • 另外从这里可以看到的方法的入参和出参被描述成一段指定的码,(II)Ljava/lang/String;,为了我们后续对参数进行解析,那么需要将这段字符串进行拆解。

    4.1 解析方法入参和出参

    asm 文档中说明过关于字节码结构和方法的信息,I;int、Ljava/lang/String;String,所以我们可以分析出这个方法的是两个 int 类型的入参和一个 String 类型的出参。也就是;String queryUserInfo(int uId, int age)

    那么这个方法的入参除了这么简单的,还会很复杂的,比如:(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)Ljava/lang/String; 对于这样的字符串内容需要使用到正则表达式进行解析。

    正则解析方法描述

    @Test
    public void test_desc() {
        String desc = "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;IJ[I[[Ljava/lang/Object;Lorg/itstack/test/Req;)Ljava/lang/String;";
    
        Matcher m = Pattern.compile("(L.*?;|\\[{0,2}L.*?;|[ZCBSIFJD]|\\[{0,2}[ZCBSIFJD]{1})").matcher(desc.substring(0, desc.lastIndexOf(')') + 1));
    
        while (m.find()) {
            String block = m.group(1);
            System.out.println(block);
        }
    
    }
    

    测试结果

    Ljava/lang/String;
    Ljava/lang/Object;
    Ljava/lang/String;
    I
    J
    [I
    [[Ljava/lang/Object;
    Lorg/itstack/test/Req;
    
    Process finished with exit code 0
    
    • 可以看到我们将所有的参数类型已经解析出来,因为只有通过这样的解析我们才能去处理方法中入参。这主要是8个基本类型需要进行类型转换为对象,填充到数组中,方便我们输出结果。

    4.2 提取类和方法生产标识ID

    接下来我们将解析的方法信息包括入参、出参结果生产方法的标识ID,这个ID是一个全局唯一的,每一个方法都有一个固定的标识。如下;

    methodId = ProfilingAspect.generateMethodId(new MethodTag(fullClassName, simpleClassName, methodName, desc, parameterTypeList, desc.substring(desc.lastIndexOf(')') + 1)));
    
    public static int generateMethodId(MethodTag tag) {
        int methodId = index.getAndIncrement();
        if (methodId > MAX_NUM) return -1;
        methodTagArr.set(methodId, tag);
        return methodId;
    }
    
    • 这是一个原子性用户自增的ID,AtomicInteger,同时也提供了一个对应的集合;AtomicReferenceArray<MethodTag>
    • 当我们每添加一个方法就会使用这个工具生产一个对应的ID,同时存放到集合中,并返回。这个生成的过程是一次性的,所以也不会影响执行时候的耗时。

    5. 字节码增强「方法进入」

    ProfilingMethodVisitor extends AdviceAdapter中,可以重写方法 onMethodEnter 。也就是当方法进入时候设置开始时间和收集入参到数组中。而收集入参的过程相对会复杂一些,需要使用字节码指令创建数据,之后把每一个入参在使用字节码加载到数组中。这个过程有点像我们写代码,定义数组设置参数。

    5.1 在方法里设置开始时间

    这段代码我们需要使用字节码指令插桩到方法的开始处

    long var3 = System.nanoTime();
    

    字节码插桩处理

    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
    startTimeIdentifier = newLocal(Type.LONG_TYPE);
    mv.visitVarInsn(LSTORE, startTimeIdentifier);	
    
    字节码描述
    INVOKESTATIC调用静态方法
    LSTORE将栈顶long类型值保存到局部变量indexbyte中

    5.2 初始化入参装填数组

    使用字节码的方式去初始化一个参数数量的数组

    Object[] var6 = new Object[](x);
    

    通过字节码的方式进行创建数组

    if (parameterCount >= 4) {
        mv.visitVarInsn(BIPUSH, parameterCount);//初始化数组长度
    } else {
        switch (parameterCount) {
            case 1:
                mv.visitInsn(ICONST_1);
                break;
            case 2:
                mv.visitInsn(ICONST_2);
                break;
            case 3:
                mv.visitInsn(ICONST_3);
                break;
            default:
                mv.visitInsn(ICONST_0);
        }
    }
    mv.visitTypeInsn(ANEWARRAY, Type.getDescriptor(Object.class));
    
    字节码描述
    BIPUSHvaluebyte值带符号扩展成int值入栈
    ANEWARRAY创建引用类型的数组

    这里有一个数组大小的判断,如果小于4会使用 ICONST 初始化长度。

    5.3 给数组赋值

    给数组赋值相当于如下效果,只不过需要经过一些字节码的方式进行处理

    Object[] var6 = new Object[]{var1, var2};
    

    通过字节码的方式进行初始化

     // 给数组赋参数值
    for (int i = 0; i < parameterCount; i++) {
        mv.visitInsn(DUP);
        mv.visitVarInsn(BIPUSH, i);
        String type = parameterTypeList.get(i);
    	if ("Z".equals(type)) {
    	    mv.visitVarInsn(ILOAD, ++cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
    	} else if ("C".equals(type)) {
    	    mv.visitVarInsn(ILOAD, ++cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false);
    	} else if ("B".equals(type)) {
    	    mv.visitVarInsn(ILOAD, ++cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false);
    	} else if ("S".equals(type)) {
    	    mv.visitVarInsn(ILOAD, ++cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false);
    	} else if ("I".equals(type)) {
    	    mv.visitVarInsn(ILOAD, ++cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
    	} else if ("F".equals(type)) {
    	    mv.visitVarInsn(FLOAD, ++cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false);
    	} else if ("J".equals(type)) {
    	    mv.visitVarInsn(LLOAD, ++cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
    	} else if ("D".equals(type)) {
    	    cursor += 2;
    	    mv.visitVarInsn(DLOAD, cursor);  //获取对应的参数
    	    mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false);
    	} else {
    	    ++cursor;
    	    mv.visitVarInsn(ALOAD, cursor);  //获取对应的参数
    	}
    	mv.visitInsn(AASTORE);
    
    	mv.visitVarInsn(ASTORE, parameterIdentifier);
    }
    

    这里在赋值的过程中,包括了对基本类型的转换,否则是不能放入到的 Object 数组中的。因为它们 int long … 都不是对象类型

    字节码描述
    ILOAD从局部变量indexbyte中装载int类型值入栈
    INVOKESTATIC调用静态方法
    AASTORE将栈顶引用类型值保存到指定引用类型数组的指定项

    到这为止,我们就已经将参数初始化到数组中了,后面就可以将参数通过方法传递出去。

    6. 字节码增强「方法退出」

    在方法结束后这里还提供给我们一个退出的方法 onMethodExit ,我们可以通过这个方法的重写,使用字节码获取出参并一起输出到外部。

    6.1 获取 return 出参值

    通过字节码的方式,实现下面出参赋值给一个属性,并最终把值给 return

    Object var7 = "你好,bugstack虫洞栈 | 精神小伙!";
    ProfilingAspect.point(var3, 0, var6, var7);
    return uId;
    

    通过字节码方式进行处理

    switch (opcode) {
        case RETURN:
            break;
        case ARETURN:
            mv.visitVarInsn(ASTORE, ++localCount); // 6
            mv.visitVarInsn(ALOAD, localCount);    // 6
            break;
    }
    

    6.2 最终将方法信息输出给外部

    mv.visitVarInsn(LLOAD, startTimeIdentifier);
    mv.visitLdcInsn(methodId);
    if (parameterTypeList.isEmpty()) {
        mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(ProfilingAspect.class), "point", "(JI)V", false);
    } else {
        mv.visitVarInsn(ALOAD, parameterIdentifier);  // 5
        mv.visitVarInsn(ALOAD, localCount);           // 6
        mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(ProfilingAspect.class), "point", "(JI[Ljava/lang/Object;Ljava/lang/Object;)V", false);
    }
    
    • LLOAD ,从局部变量indexbyte中装载long类型值入栈。这里加载的就是方法的启动时间。

    • LDC , 常量池中的常量值(int, float, string reference, object reference)入栈。这里是加载方法ID;methodId

    • ALOAD ,parameterIdentifier ,从局部变量indexbyte中装载引用类型值入栈。此时加载参数数组信息。

    • ALOAD ,localCount ,加载的是返回值信息,也就是 return 的结果。

    • INVOKESTATIC ,最后就是调用静态方法输出结果信息,这个静态方法是我们已经预设好的,如下;

      public static void point(final long startNanos, final int methodId, Object[] requests, Object response) {
          MethodTag method = methodTagArr.get(methodId);
          System.out.println("监控 - Begin");
          System.out.println("类名:" + method.getFullClassName());
          System.out.println("方法:" + method.getMethodName());
          System.out.println("入参类型:" + JSON.toJSONString(method.getParameterTypeList()));
          System.out.println("入数[值]:" + JSON.toJSONString(requests));
          System.out.println("出参类型:" + method.getReturnParameterType());
          System.out.println("出参[值]:" + JSON.toJSONString(response));
          System.out.println("耗时:" + (System.nanoTime() - startNanos) / 1000000 + "(s)");
          System.out.println("监控 - End\r\n");
      }
      

    四、测试验证

    1. 需要测试的方法

    public class ApiTest {
    
        public static void main(String[] args) throws InterruptedException {
            ApiTest apiTest = new ApiTest();
            String res01 = apiTest.queryUserInfo(111, 17);
            System.out.println("测试结果:" + res01 + "\r\n");;
        }
    
        public String queryUserInfo(int uId, int age) throws InterruptedException {
            return "你好,bugstack虫洞栈 | 精神小伙!";
        }
    
    }
    

    2. 配置javaagent

    -javaagent:/Users/xiaofuge/itstack/git/github.com/SQM/target/SQM-1.0-SNAPSHOT.jar
    
    • IDEA 运行时候配置到 VM options 中,jar包地址按照自己的路径进行配置。

    3. 被字节码增强后的方法

    public String queryUserInfo(int var1, int var2) throws InterruptedException {
        long var3 = System.nanoTime();
        Object[] var6 = new Object[]{var1, var2};
        Object var7 = "你好,bugstack虫洞栈 | 精神小伙!";
        ProfilingAspect.point(var3, 0, var6, var7);
        return var7;
    }
    
    • 通过编译后的方法可以看到,方法的执行信息全部通过静态方法输出到外部。这样就可以很方便的监控一个方法的执行信息。

    4. 输出结果

    ASM类输出路径:/Users/xiaofuge/itstack/git/github.com/SQM/target/test-classes/org/itstack/test/ApiTest$1SQM.class
    监控 - Begin
    类名:org.itstack.test.ApiTest
    方法:queryUserInfo
    入参类型:["I","I"]
    入数[][111,17]
    出参类型:Ljava/lang/String;
    出参[]"你好,bugstack虫洞栈 | 精神小伙!"
    耗时:95(s)
    监控 - End
    
    测试结果:你好,bugstack虫洞栈 | 精神小伙!
    

    五、总结

    展开全文
  • 企业短信防火墙【新昕科技】+短信验证码【中昱维信】Java应用实例一、企业短信防火墙的实现1.1 简介1.2 第一步:获取防火墙帐号密钥1.3 第二步:下载防火墙服务器1.4 第三步:业务系统前后端接1.5丰富可视化实时...
  • 模板方法模式简介模板方法(Template method),...模板方法是指写一个操作中的算法框架,而将一些步骤延迟到子类中去实现,这样就使得子类可以不改变一个算法的结构即可定义该算法的某些特定步骤。模板方法的...
  • 深入理解 Java 反射:Method (成员方法

    万次阅读 多人点赞 2017-01-18 20:03:08
    读完本文你将了解到: ...varargs variable arguments methodJava 可变参数方法 bridge method桥接方法 反射调用方法 调用含有可变参数的方法 常见错误 1 泛型擦除导致的 NoSuchMethodException 常见错误 2 访问不可见
  • 解决方案 1、使用lock(Object)的方法来防止重入,表示一个Timer处理正在执行,下一个Timer发生的时候发现上一个没有执行完就等待执行,适用重入很少出现的场景(具体也没研究过,可能比较占内存吧)。 代码跟上面...
  • 本文转自:防止数据重复提交的6种方法(超简单) 模拟用户场景 根据朋友的反馈,大致的场景是这样的,如下图所示: 简化的模拟代码如下(基于 Spring Boot): import org.springframework.web.bind.annotation....
  • 【单选题】关于Java程序的构造方法,说法错误的是( )。更多相关问题[单选] 关于点支承玻璃幕墙支承钢结构加工要点的说法,正确的是()。[单选] 塔机在工作时,司机室在门窗关闭的状态下噪声不应大于()[单选] 当设计无...
  • Java重发机制的实现

    2021-07-23 14:11:05
    在上一篇文章 用rabbitmq实现消息重发功能 中,使用了外部的rabbitmq来实现了消息重发的功能,但是使用rabbitmq来实现并不适用于所有的场景,在这篇文章中,我再扩展两种仅用java本身就能实现的方法。 Retryer @...
  • java导入Excel中数据查重的方法

    千次阅读 2017-11-17 15:17:17
    今天查数据库数据的时候发现一个问题,明明添加了数据库查重的方法,如果导入文件中存在和数据库相同值的时候会提示并拒绝提交,但数据库还是出现了重复字段,一开始有点纳闷,后来发现原来是因为一个excel文件中...
  • API重放机制

    2021-03-15 16:18:29
    说说API的重放机制我们在设计接口的时候,最怕一个接口被用户截取用于重放攻击。重放攻击是什么呢?就是把你的请求原封不动地再发送一次,两次...n次,一般正常的请求都会通过验证进入到正常逻辑中,如果这个正常...
  • 如标题所示,我的个人背景非常简单,Java开发经验1年半,学历普通,2本本科毕业,毕业后出来就一直在Crud,在公司每天重复的工作对我的技术提升并没有什么帮助,但小镇出来的我也深知自我努力的重要性,想要改变...
  • 检查应该在sleep方法后(而不是先于),以便当线程恢复的时候窗口被立即绘。修改后的run方法如下: public void run() { while (true) { try { Thread.sleep(interval); synchronized(this) { while ...
  • Java异常处理 try catch finally 多重catch 异常分类处理 输入两个数进行求商 使用if-else语句实现实现处理异常 import java.util.Scanner; public class Test { public static void main(String[] args) { ...
  • 牛逼!Java 从入门到精通,超全汇总版

    万次阅读 多人点赞 2021-05-06 19:40:33
    文章目录Java 基础Head First JavaJava 核心技术卷一Java 编程思想设计模式Head First 设计模式图解设计模式设计模式Java 设计模式Java 进阶Java 并发编程实战Java 并发编程艺术Java 并发编程之美图解Java多...
  • Java

    千次阅读 2020-08-01 09:56:23
    Java 1、Java 简介 Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表...
  • * 锁超时时间,防止线程在锁以后,防止阻塞后面的线程无法获取锁 */ private int expireMsecs = 60 * 1000; /** * 线程获取锁的等待时间 */ private int timeoutMsecs = 10 * 1000; /** * 是否锁定标志 ...
  • Java面试题大全(2021版)

    万次阅读 多人点赞 2020-11-25 11:55:31
    为了使计算机能够理解人的意图,人类就必须要将需解决的问题的思路、方法、和手段通过计算机能够理解的形式告诉计算机,使得计算机能够根据人的指令一步一步去工作,完成某种特定的任务。这种人和计算机
  • Java面试题大全(2020版)

    万次阅读 多人点赞 2019-11-26 11:59:06
    发现网上很多Java面试题都没有答案,所以花了很长时间搜集整理出来了这套Java面试题大全,希望对大家有帮助哈~ 本套Java面试题大全,全的不能再全,哈哈~ 一、Java 基础 1. JDK 和 JRE 有什么区别? JDK:Java ...
  • java中的几种单例模式

    2021-02-12 20:28:10
    构造方法私有化,实例属性私有化。2.必须仅在类的内部完成实例的初始化过程。3.提供公共静态方法,用以返回已经初始化完成的实例。4.不可通过反射,反序列化方式获得新的实例。1.饿汉模式:进行类初始化时就完成实例...
  • Java工程师成神之路 | 2020正式版

    千次阅读 2020-04-13 09:50:27
    指令重排和有序性问题 线程安全和内存模型的关系 happens-before as-if-serial 锁 可重入锁 阻塞锁 乐观锁与悲观锁 数据库相关锁机制 分布式锁 无锁 CAS CAS的ABA问题 锁优化 偏向锁 轻量级锁 重量级锁 锁消除 锁粗...
  • Java并发

    2021-04-18 01:51:41
    Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。何为线程?线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的...
  • java并发总结

    2021-10-11 22:10:22
    ㅤ 5.9、五种状态 初始状态 可运行状态(就绪状态) 运行状态 阻塞状态 终止状态 ㅤ 5.10、六种状态(java层面) NEW 线程刚被创建,但是还没有调用 start() 方法 RUNNABLE 当调用了 start() 方法之后,注意,Java ...
  • 分三个方面回答:加锁和释放锁的原理,可重入原理,保证可见性原理。Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的。Synchronized和Lock的对比,和选择?Synchronized在使用时有何注意事...
  • Java并发——显示锁

    2020-12-24 08:54:08
    Java提供一系列的显示锁类,均位于java.util.concurrent.locks包中。锁的分类: 排他锁,共享锁排他锁又被称为独占锁,即读写互斥、写写互斥、读读互斥。Java的ReadWriteLock是一种共享锁,提供读读共享,但读写和写...
  • 更为 要的是,乐观锁没有因竞争造成的系统开销,所以在性能上 也是更胜一筹 乐观锁的实现原理 相信你对上面的内容是有一定的了解的,下面我们来看看乐 观锁的实现原理,有助于我们从根本上总结优化方法。...
  • 各大公司Java后端开发面试题总结

    万次阅读 多人点赞 2017-03-01 11:31:11
    Synchronized 与Lock都是可重入锁,同一个线程再次进入同步代码的时候.可以使用自己已经获取到的锁。 Synchronized是悲观锁机制,独占锁。而Locks.ReentrantLock是,每次不加锁而是假设没有冲突而去完成某项操作...
  • 10万字208道Java经典面试题总结(附答案)

    万次阅读 多人点赞 2021-08-01 16:05:55
    JDK(Java Development Kit),Java开发工具包 JRE(Java Runtime Environment),Java运行环境 JDK中包含JRE,JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 17,097
精华内容 6,838
关键字:

java防重入的方法

java 订阅