使用AOP防止请求重复提交
- 首先定义注解NoPepeatSubmit
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {long value() default 1000*10;
}
- 定义AOP相关方法
public class RepeatSubmitAspect {@Autowiredprivate StringRedisService stringRedisService;@Pointcut("@annotation(xxx.xxx.NoRepeatSubmit)")public void pointCut() {}@Around("pointCut()")public Object around(ProceedingJoinPoint pjp) throws Throwable {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();Assert.notNull(request, "request can not null");// 此处可以用token--作为用户唯一标识String token = request.getHeader("Authorization-admin");String key = token + "-" + request.getServletPath();// 获取注解NoRepeatSubmit annotation = ((MethodSignature) pjp.getSignature()).getMethod().getAnnotation(NoRepeatSubmit.class);// 获取注解相关参数:这里是时间,表示同一用户多久可以请求一次long expire = annotation.value();//超时时间:10秒,最好设为常量String time=String.valueOf(System.currentTimeMillis() + expire);//加锁 --这里需要考虑并发问题,详情见下文boolean islock = stringRedisService.secKilllock(key, time);if (islock) {Object result;try {//执行请求result = pjp.proceed();} finally {//解锁stringRedisService.unlock(key,time);}return result;}else {// 重复请求return new Result(CoReturnFormat.REPEAT_REQUEST);}}
}
- 请求上添加NoPepeatSubmit注解
注:相关概念
防止重复提交涉及到的锁相关概念
防止重复提交的基本思路是对一个请求,将它存储到redis中作为key,value是保持时间---这个时间内不对相同的请求做响应
首先是确保同一用户的同一请求,这里使用token + "-" + request.getServletPath()保证
其次,redis中锁设置如下:
public boolean secKilllock(String key,String value){/*** setIfAbsent就是setnx* 将key设置值为value,如果key不存在,这种情况下等同SET命令。* 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写* */if(redisTemplate.opsForValue().setIfAbsent(key,value)){//加锁成功返回truereturn true;}//避免死锁,且只让一个线程拿到锁//走到这里的线程都加锁失败了,有两种情况,//一种是在重复提交的时间范围内,//一种是不在重复提交的范围内,这是这可能是因为加入redis的请求出现异常,导致没有删除keyString currentValue = (String) redisTemplate.opsForValue().get(key);/*** 下面这几行代码的作用:* 1、防止死锁* 2、防止多线程抢锁* */if(! StringUtils.isEmpty(currentValue)&& Long.parseLong(currentValue) < System.currentTimeMillis()){//如果锁过期了,获取上一个锁的时间String oldValue = (String) redisTemplate.opsForValue().getAndSet(key,value);//只会让一个线程拿到锁----这里有个问题,多个线程到这里的时候,不能确保通过的线程是上锁的线程//这里应该保证的是立即重复点击的请求放行一个即可//如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了if(! StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)){return true;}}return false;}/*** 解锁* @param key* @param value* */@Overridepublic void unlock(String key,String value){try{String currentValue = (String) redisTemplate.opsForValue().get(key);if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {redisTemplate.opsForValue().getOperations().delete(key);}}catch (Exception e){e.printStackTrace();log.error("『redis分布式锁』解锁异常,{}", e);}}
注解--黑马
Java注解是代码中的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。
比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。
再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。
注解不光可以用在方法上,还可以用在类上、变量上、构造器上等位置。
自定义注解的格式如下图所示:
比如:现在我们自定义一个MyTest注解
public @interface MyTest{String aaa();boolean bbb() default true; //default true 表示默认值为true,使用时可以不赋值。String[] ccc();
}
定义好MyTest注解之后,我们可以使用MyTest注解在类上、方法上等位置做标记。注意使用注解时需要加@符号,如下
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{@MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})public void test1(){}
}
注意:注解的属性名如何是value的话,并且只有value没有默认值,使用注解时value名称可以省略。比如现在重新定义一个MyTest2注解
public @interface MyTest2{String value(); //特殊属性int age() default 10;
}
定义好MyTest2注解后,再将@MyTest2标记在类上,此时value属性名可以省略,代码如下
@MyTest2("孙悟空") //等价于 @MyTest2(value="孙悟空")
@MyTest1(aaa="牛魔王",ccc={"HTML","Java"})
public class AnnotationTest1{@MyTest(aaa="铁扇公主",bbb=false, ccc={"Python","前端","Java"})public void test1(){}
}
注解本质是什么呢?
1.MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口
2.MyTest1注解中的属性本质上是抽象方法
3.@MyTest1实际上是作为MyTest接口的实现类对象
4.@MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"})里面的属性值,可以通过调用aaa()、bbb()、ccc()方法获取到。
什么是元注解?
元注解是修饰注解的注解。这句话虽然有一点饶,但是非常准确。我们看一个例子
接下来分别看一下@Target注解和@Retention注解有什么作用,如下图所示
@Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
解析注解
我们把获取类上、方法上、变量上等位置注解及注解属性值的过程称为解析注解。
解析注解套路如下
1.如果注解在类上,先获取类的字节码对象,再获取类上的注解
2.如果注解在方法上,先获取方法对象,再获取方法上的注解
3.如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解
总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解
接口--黑马
当定义接口是默认成员变量使用public static final修饰,方法使用public abstract修饰,可以省略
public interface A{//这里public static final可以加,可以不加。public static final String SCHOOL_NAME = "黑马程序员";//这里的public abstract可以加,可以不加。public abstract void test();
}
JDK8的新特性
增加了默认方法,私有方法,静态方法三种方法
public interface A {/*** 1、默认方法:必须使用default修饰,默认会被public修饰* 实例方法:对象的方法,必须使用实现类的对象来访问。*/default void test1(){System.out.println("===默认方法==");test2();}/*** 2、私有方法:必须使用private修饰。(JDK 9开始才支持的)* 实例方法:对象的方法。*/private void test2(){System.out.println("===私有方法==");}/*** 3、静态方法:必须使用static修饰,默认会被public修饰*/static void test3(){System.out.println("==静态方法==");}void test4();void test5();default void test6(){}
}
1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会优先使用父类的方法
4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。
综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。
public class Test implements A,B {//对应情况2,一个类实现多个同名方法的接口只会实现一个方法,确保唯一,idea中这个方法显示是多个同名方法的实现@Overridepublic void test() {System.out.println("hello");}
}
interface A{void test();
}
interface B{void test();
}
interface C extends A,B{}
class D implements C{// 对应情况1,接口继承了多个同名方法接口,实现时只有一个同名方法@Overridepublic void test() {System.out.println("D");}
}
类似的,类比到情况4,当出现接口同名方法冲突时,java会保证只实现一个,从而确保唯一性,避免冲突