丶木叶

丶木叶 查看完整档案

深圳编辑西安邮电大学  |  集成电路设计 编辑  |  填写所在公司/组织填写个人主网站
编辑

我始终相信比你优秀的人过着比你更努力的生活。

个人动态

丶木叶 发布了文章 · 2018-04-12

Spring自定义注解不生效原因解析及解决方法

自定义注解不生效原因解析及解决方法

背景:

项目中,自己基于spring AOP实现了一套java缓存注解。但是最近出现一种情况:缓存竟然没有生效,大量请求被击穿到db层,导致db压力过大。现在我们看一下具体代码情形(代码为伪代码,只是为了说明一下具体情况)。

interface A {
    int method1(..);
    int method2(..);
    ... ...
}

class AImpl implements A {
    @Override
    @CacheMM(second=600)      //这里的@CacheMM就是我实现的自定义缓存注解
    public int method1(..) {
        ... ...
        method2(..);
        ... ...
    }
    
    @Override
    @CacheMM(second=600)
    public int method2(..) {
        ... ...
    }
}

如上代码,当调用method1时,发现method2注解并没有生效。

分析:

这是为什么呢?别急,我们带着这个问题去看了一下注解的实现类。(这里就不贴缓存注解的实现代码了)我的自定义注解是直接extends AbstractBeanFactoryPointcutAdvisor类然后实现其中的getPointcut() 和 getAdvice() 实现的。(其实这里可以直接使用aop环绕通知的,原理都差不多,我是为了熟悉源码才这样写的)。

接下来,我们继续往下分析,我们都知道基于spring aop实现的注解,在spring 中,如果有aop实现,那么容器注入的是该类的代理类,这里的代理类是aop 动态代理生成的代理类。Spring aop 的动态代理有两种:一种是jdk的动态代理,一种是基于CGLIB的。这两个的区别我就不多说了,如果你的业务类是基于接口实现的,则使用jdk动态代理,否则使用CGLIB动态代理。 我这里使用的是接口实现,所以我们就顺着思路去看一下jdk动态代理的具体实现。

上边的业务代码类我已经贴出。而需要生成代理对象(proxy),分成两步:

  1. 生成代理对象需要建立代理对象(proxy)和真实对象(AImpl)的代理关系
  2. 实现代理方法

在JDK动态代理中需要实现接口:java.lang.reflect.InvocationHandler.

import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;   
import java.lang.reflect.Proxy;   
  
public class AProxy implements InvocationHandler   
{   
    private Object target;   
      
    /**  
     * 生成代理对象,并和真实服务对象绑定.  
     * @param target 真实服务对象  
     * @return 代理对象 */   
    public Object bind(Object target)   
    {   
        this.target = target;   
          
        //生成代理对象,并绑定.   
        Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), //类的加载器   
                              target.getClass().getInterfaces(), //对象的接口,明确代理对象挂在哪些接口下   
                              this);//指明代理类,this代表用当前类对象,那么就要求其实现InvocationHandler接口的invoke方法   
          
        return proxy;   
    }   
          
    /**  
     * 当生成代理对象时,第三个指定使用AProxy进行代理时,代理对象调用的方法就会进入这个方法。  
     * @param proxy 代理对象  
     * @param method  被调用的方法  
     * @param args 方法参数  
     * @return 代理方法返回。  
     * @throws Throwable  异常处理 */   
     @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable   
     {   
        System.err.println("反射真实对象方法前");   
        Object obj = method.invoke(target, args);//相当于AImpl类中对应方法调用.   
        System.err.println("反射真实对象方法后");   
          
        return obj;   
    }  
}  

代码中,Object obj = method.invoke(target,args) 通过反射调度真实对象的方法,这个很重要。我们知道其实虽然aop是通过代理对象去实现一些附加的操作的,但是真正的类方法调用还是通过反射调用真实对象的。这个时候,我们回头看一下问题,我们AImpl中有两个方法,其中method2是在method1内部调用的。当调用method1时,spring内部其实调用的是代理类AProxy类的invoke,这个时候在执行真实对象方法钱去执行method1中的一些附加操作。然后,在通过反射进入对应AImpl类中调用method1方法。注意,这个时候,已经不在代理对象中操作了,由于method2的调用是在method1内部调用的,所以在这里实际调用method2的是真实对象,并不是代理对象。 所以,就导致method2上的缓存注解没有生效。

解决:

好了,现在知道问题的原因后(动态代理的坑啊,内部调用不走代理类,所以实现的附加操作肯定不会执行了),我们来针对性的解决。我们现在知道这个其实是因为实际执行的不是代理类而导致的,那我们解决的思路就想办法让method2的调用走代理类就可以了。(就是这么简单)

AProxy类我们是可以在spring容器中得到的。下面是修改后的解决方案:

method1(..) {
    ... ...
     // 如果希望调用的内部方法也被拦截,那么必须用过上下文获取代理对象执行调用,而不能直接内部调用,否则无法拦截  
        if(null != AopContext.currentProxy()){  
            AopContext.currentProxy().method2();  
        }else{  
            method2();  
        }      

}

这里的AopContext.currentProxy() 拿到的实际就是代理对象了,这样通过代理对象去调用method2肯定就没有问题了。

还有一种解决方法就是不使用 动态代理织入,使用aspectJ织入,aspectJ直接在源类上进行字节码的插入,而不是以代理的方式进行。

这里可以参考一下
AspectJ 编译时织入(Compile Time Weaving, CTW)

因为这样改动比较大,所以目前我还是采用第一种方案解决问题了。至此,问题得到解决。

总结:

结合Spring aop动态代理的实现原理,提供两种动态代理:JDK代理和CGLIB代理

JDK代理只能对实现了接口的类生成代理,而不能针对类;
CGLIB是针对类实现代理的,主要对指定的类生成一个子类,并覆盖其中的方法,
因为是继承,所以不能使用final来修饰类或方法。所以该类或方法最好不要声明成final

更加详细的解释可以参考这篇博文 哪些方法不能实施Spring AOP事务

查看原文

赞 0 收藏 0 评论 0

丶木叶 发布了文章 · 2018-03-08

Nginx--proxy cache使用

Nginx--proxy cache使用


项目中采用Nginx作为代理服务器,静态接口的数据都缓存在nginx中,这样可以有效减小源服务器的负载。在这里整理一下Nginx proxy cache的配置。

nginx proxy cache 原理

image

  1. nginx.conf中配置proxy_cache:
proxy_temp_path   /data/nginx_cache/proxy_cache/proxy_temp_dir;

proxy_cache_path  /dev/shm/proxy_cache_dir levels=1:2 keys_zone=cache_one:200m inactive=2d max_size=2g;

proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504 http_404;
  • proxy_cache_path 缓存文件路径
  • levels 设置缓存文件目录层次;levels=1:2 表示两级目录
  • keys_zone 设置缓存名字和共享内存大小
  • inactive 在指定时间内没人访问则被删除
  • max_size 最大缓存空间,如果缓存空间满,默认覆盖掉缓存时间最长的资源。
  • proxy_temp_path : 使用temp_path存储,如果不使用,则配置在max_size后 use_temp_path=off;
  1. 在location中使用proxy cache:
    location ^~ /static/  {
                ... ...
                
                #定制proxy_cache的key,去除imei和sn等个性化参数。 
                set $custom_proxy_cache_key $host$uri$is_args$args;
                include vhosts/customize_proxy_cache_key;
                
                #忽略Expires、Set-Cookie头部
                proxy_ignore_headers Expires Set-Cookie;
                proxy_cache cache_one;
                proxy_cache_valid 200 304 10m;
                proxy_cache_key $custom_proxy_cache_key;
                add_header X-Proxy-Cache $upstream_cache_status;
                expires 10m;
                
                ... ...
                }

这里有几个要注意的地方:

定制cache的key时,一定要注意:**谨慎使用值变化范围比较大的参数**。因为,这里使用参数的值作为cache的key的,当值变化返回很大的时候,一方面会导致缓存文件变得很大,另一方面缓存也就失去意义。    


这里有一个知识点:
源服务器是通过Set-Cookie来告诉浏览器cookie的信息,包括cookie值,path,以及域。只要浏览器查看请求满足本地cookie的域,就把这个cookie携带入头部传给server。这里忽略掉这个头部才能使nginx proxy_cache 生效。

上边customize_proxy_cache_key具体配置如下:

set $custom_params $query_string;
#nginx缓存key去除imei
if ( $custom_params ~ ^(.*)(&imei=[^&]+)(.*)$) {
        set $a $1;
        set $c $3;
        set $custom_params "${a}${c}";
}
set $custom_proxy_cache_key $host$uri$is_args$custom_params;

这里,就将参数中的imei和sn用户唯一值的参数去除,保证cache健康。

配置完成后,重启nginx,至此,nginx cache已经启用。

查看原文

赞 1 收藏 1 评论 0

丶木叶 关注了用户 · 2018-03-08

liwd @liwd

逗逗逗逗逗比一枚~~

关注 2

丶木叶 发布了文章 · 2018-02-11

spring 自定义注解(annotation)与 aop获取注解

知识点: Java自定义注解、spring aop @aspect的使用


首先我们先介绍Java自定义注解。

在开发过程中,我们实现接口的时候,会出现@Override,有时还会提示写@SuppressWarnings。其实这个就是Java特有的特性,注解。

注解就是某种注解类型的一种实例,我们可以把它用在某个类上进行标注。下面这张图解释注解都是什么?

image

上图可以看出注解大体分为三种:元注解,标记注解,一般注解;

这一块其他的我就不多做介绍,我们这里主要说一下如何定义自己的注解,在这之前我们必须了解标准元注解和相关定义注解的语法。
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:

1.@Target,
2.@Retention,
3.@Documented,
4.@Inherited

@Target
@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)有:
  1. CONSTRUCTOR:用于描述构造器
  2. FIELD:用于描述符
  3. LOCAL_VARIABLE:用于描述局部变量
  4. METHOD:用于描述方法
  5. PACKAGE:用于描述包
  6. PARAMETER: 用于描述参数
  7. TYPE: 用于描述类、接口(包括注解类型)或者enum声明
@Retention
@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
  1. SOURCE:在源文件中有效(即源文件保留)
  2. CLASS:在class文件中有效(即class保留)
  3. RUNTIME:在运行时有效(即运行时保留)
 @Documented
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
 @Inherited
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

自定义注解

使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
定义注解格式:
public @interface 注解名 {定义体}

   注解参数的可支持数据类型:
   1. 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
   2. String类型
   3. Class类型
   4. enum类型
   5. Annotation类型
   6. 以上所有类型的数组
  
  Annotation类型里面的参数该怎么设定:
  第一,只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型;
  第二,参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数成员就为String;
  第三,如果只有一个参数成员,最好把参数名称设为"value",后加小括号.例:下面的例子FruitName注解就只有一个参数成员。


使用示例:

CacheRedis.java

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheRedis {
    String key();

    int expireTime() default 600;
}

CacheService.java

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class CacheService {

    Logger logger = LoggerFactory.getLogger(CacheService.class);

    @Pointcut(value = "@annotation(com.meizu.bro.service.test.CacheRedis)")
    public void pointCut(){}

    @Before(value = "pointCut() && @annotation(cacheRedis)")
    public void before(CacheRedis cacheRedis) {
        logger.info("the result of this method will be cached.");
    }

    @AfterReturning(value = "pointCut() && @annotation(cacheRedis)",returning = "result")
    public void after(CacheRedis cacheRedis,Object result) {
        String key = cacheRedis.key();
        int expireTime = cacheRedis.expireTime();
        //do something...
        logger.info("-----redis-----[key = " + key + "]"+"[expireTime = " + expireTime + "]");
        logger.info("the result of this method is" + result + ",and has been cached.");
    }

    //@Around("pointCut() && @annotation(cacheRedis)")
    //public Object setCache(ProceedingJoinPoint joinPoint,CacheRedis cacheRedis) {
    //    Object result = 1;
    //
    //    Method method = getMethod(joinPoint);//自定义注解类
    //    //CacheRedis cacheRedis = method.getAnnotation(CacheRedis.class);//获取key值
    //    String key = cacheRedis.key();
    //    int expireTime = cacheRedis.expireTime();
    //    //获取方法的返回类型,让缓存可以返回正确的类型
    //    Class returnType =((MethodSignature)joinPoint.getSignature()).getReturnType();
    //
    //    logger.info("[key = " + key + "]"+"[expireTime = " + expireTime + "]");
    //
    //    return result;
    //}
    //
    //private Method getMethod(ProceedingJoinPoint joinPoint) {
    //    //获取参数的类型
    //    Method method = null;
    //    try {
    //        Signature signature = joinPoint.getSignature();
    //        MethodSignature msig = null;
    //        if (!(signature instanceof MethodSignature)) {
    //            throw new IllegalArgumentException("该注解只能用于方法");
    //        }
    //        msig = (MethodSignature) signature;
    //        method = joinPoint.getTarget().getClass().getMethod(msig.getName(), msig.getParameterTypes());
    //    } catch (NoSuchMethodException e) {
    //        logger.error("annotation no sucheMehtod", e);
    //    } catch (SecurityException e) {
    //        logger.error("annotation SecurityException", e);
    //    }
    //    return method;
    //}
}

测试接口TestController.java

@Controller
public class TestController {
    
    @Autowired
    private TestService testService;

    @RequestMapping(value = "/test")
    public ModelAndView myTest() {
        int test = testService.test(10);
        return ViewUtil.buildStandardJsonViewByObj(test);
    }

    @RequestMapping(value = "/test1")
    public ModelAndView myTest1() {
        String yanyi = testService.test1("yanyi");
        return ViewUtil.buildStandardJsonViewByObj(yanyi);
    }
}

TestService.java

public interface TestYanyiService {
    int test(int i);

    String test1(String i1);
}

TestServiceImpl.java

import org.springframework.stereotype.Service;

@Service
public class TestYanyiServiceImpl implements TestYanyiService {
    @Override
    @CacheRedis(key = "test",expireTime = 10)
    public int test(int i) {
        return 0;
    }

    @Override
    @CacheRedis(key = "test1")
    public String test1(String i1) {
        return i1;
    }
}

代码完成后,需要在pom文件中导入以下依赖:

            <dependency>
                <groupId>aopalliance</groupId>
                <artifactId>aopalliance</artifactId>
                <version>1.0</version>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjweaver</artifactId>
                <version>1.8.13</version>
            </dependency>
            <dependency>
                <groupId>org.aspectj</groupId>
                <artifactId>aspectjrt</artifactId>
                <version>1.8.13</version>
            </dependency>       
            <dependency>
                <groupId>cglib</groupId>
                <artifactId>cglib</artifactId>
                <version>2.2.2</version>
            </dependency>

这一块要注意:aspectjweaver包的版本和JDK版本有关联。如果是JDK 1.7及以上的用户,需要aspectjweaver包的版本不能过低(JDK1.7 —— aspectJ1.7.3+)。否则会报错:报错error at ::0 can't find referenced pointcut。

接下来,在Spring配置文件里进行以下配置

<!-- 启动对@AspectJ注解的支持 -->  
<aop:aspectj-autoproxy/> 

<!--通知spring使用cglib而不是jdk的来生成代理方法 AOP可以拦截到Controller->  
<aop:aspectj-autoproxy proxy-target-class="true" />  

ok! 完成后,调用测试接口,可以看到在控制窗口打印切面类前置和后置的相关log。这时候,我们就可以在里边实现自己的功能咯。(我这里准备实现redis缓存功能。)

查看原文

赞 9 收藏 6 评论 0

丶木叶 关注了用户 · 2018-02-06

咚子 @zi_597d64ce14187

一个前端

关注 12542

丶木叶 关注了用户 · 2018-02-06

缘自世界 @birenyangguangcanlan

心态很重要,我始终相信没有不会做的,只有不想做的,在这个人人都聪明的今天,你不凭智慧,只需努力就能打败90%的对手,如果你再展现出你50%的智慧,我想没有什么问题可以难倒你。

关注 7721

丶木叶 关注了用户 · 2018-02-06

justjavac @justjavac

会写点 js 代码

关注 14480

丶木叶 关注了用户 · 2018-02-06

边城 @jamesfancy

从事软件开发 20 年,在软件分析、设计、架构、开发及软件开发技术研究和培训等方面有着非常丰富的经验,近年主要在研究 Web 前端技术、基于 .NET 的后端开发技术和相关软件架构。

关注 10984

丶木叶 关注了用户 · 2018-02-06

keke @keke233

好好学习,天天向上~

关注 8403

丶木叶 关注了标签 · 2018-02-06

segmentfault

SegmentFault (www.sf.gg) 是一个面向中文开发者的专业技术社区。社区采用良性、合理的机制来让开发者自由生长,希望通过最干净、简洁、优质的产品体验,来吸引国内优秀的开发者和技术人员,一起打造一个纯粹的技术交流社区。

我们希望为中文开发者提供一个纯粹、高质的技术交流平台,与开发者一起学习、交流与成长,创造属于开发者的时代!

网站产品

问答平台 专注高效地解决技术问题。确保内容质量的投票机制,合理区分的答案与回馈信息,用户参与改进的维基化内容,SegmentFault 帮你快捷地找到答案。

文章平台 简洁安静的技术经验分享。简约干净的界面,让你专注于内容的撰写;好用到爆的 Markdown 编辑器,和你的思维速度匹配无间。让你重新爱上写博客。

活动平台 在活动中找到志同道合的好基友。黑客马拉松、开发竞赛、线下沙龙、知识讲座、线上活动…… 总有一款适合你。

技术笔记 一个方便快捷的代码笔记本,使用 CodeMirror 编辑器,支持纯文本、Markdown、Java、CSS 等多种类型的文本渲染,还有神奇的笔记传送门。

程序员招聘平台 我们针对企业推出了面向开发者的专属招聘功能,企业组织可以展示自己的资料、团队成员、技术背景等内容。最重要的是,还能招募到契合的团队成员。

获得成就

  1. 3人创始团队创业初期利用1年时间独立开发底层框架,上线问答、博客、活动等社区平台,聚集十多万开发者。
  2. SegmentFault 团队将黑客马拉松活动引入中国,至今,已经在国内一线互联网城市以及台北、新加坡、硅谷等地区举办了超过 20 场黑客马拉松。SegmentFault 是目前中国最大的黑客马拉松组织方。
  3. SegmentFault 在 2013 中国新媒体创业大赛中获得全国决赛第二名,并入选微软创投加速器第 4 期,,并获得 IDG 资本数百万天使投资。
  4. SegmentFault 在 2014 年获得 IDG资本 数百万天使投资。
  5. SegmentFault 在 2015 年获得顶级 VC 赛富亚洲基金(软银赛富)领投、IDG资本 跟投的数千万 A 轮融资。

网站架构

SegmentFault 基于我们自己开发的 Typecho Framework 开源框架,这是一个简单、轻量、可扩展的 PHP 框架。其中引入了类似 JAVA 注入变量的概念,解决了 PHP 项目中模块的自由引用问题。存储采用 Redis、MySQL,搜索引擎选用 xunsearch,前端为响应式设计,使用了 Sass、Compass、jQuery 等技术。整个项目通过 GitHub、BaseCamp、Gmail 进行协作。

关注 53046

认证与成就

  • 获得 22 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-10-21
个人主页被 430 人浏览