假如现在有一个需求, 在对数据库进行增删改查的时候, 假如执行每个操作之前都要求把数据备份一下. 这个时候怎么做比较好呢, 难道要在每个方法之前都写一个 save() 方法吗, 如果用到增删改查的地方非常多, 这时候就非常麻烦了.
通过 java 中的动态代理就可以很方便的实现. 比如
首先有个操作数据库的类
- public interface DBOperation {
- int save();
- int delete();
- int insert();
- Object get();
- }
定义一个 activity, 实现数据库操作接口, 通过 Proxy.newProxyInstance 方法创建出 DBOperation 的代理实现类, 这个方法需要一个 InvocationHandler 参数,
自定义一个 InvocationHandler, 在其 invoke 方法中我们就可以在执行每个方法之前和之后做一些自己的操作了.
- public class ProxyActivity extends AppCompatActivity implements DBOperation{
- private final static String TAG = "myTag>>>";
- DBOperation db;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_proxy);
- db = (DBOperation) Proxy.newProxyInstance(DBOperation.class.getClassLoader()
- ,new Class[]{DBOperation.class},new DBHandler(this));
- }
- public void action(View view) {
- db.delete();
- }
- class DBHandler implements InvocationHandler{
- DBOperation db;
- public DBHandler(DBOperation db) {
- this.db = db;
- }
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- if (db != null) {
- Log.e("TAG","before");
- save();
- Log.e("TAG","after");
- return method.invoke(db,args);
- }
- return null;
- }
- }
- @Override
- public int save() {
- Log.e(TAG, "保存数据");
- return 0;
- }
- @Override
- public int delete() {
- Log.e(TAG, "删除数据");
- return 0;
- }
- @Override
- public int insert() {
- return 0;
- }
- @Override
- public Object get() {
- return null;
- }
- }
上面的代码点击执行 action 方法, 执行结果如果下
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: before
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/myTag>>>: 保存数据
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: after
2019-07-02 22:49:55.297 7516-7516/com.chs.architecturetest E/myTag>>>: 删除数据
在项目开发中, 我们经常会遇到这样的需求
统计用户的点击行为
在进入某些页面之前先判断是否登录, 如果没登录就去登录页面
我们不可能去每个方法中都写相关的统计代码, 如果类很多的情况下会麻烦死还容易出错, 如果使用动态代理也是比较麻烦的, 这时候我们可以使用 AspectJ.
AspectJ 是一个面向切面的框架, 它扩展了 Java 语言. AspectJ 定义了 AOP 语法, 它有一个专门的编译器用来生成遵守 Java 字节编码规范的 Class 文件.
下面使用它来解决前面的两个问题
首先配置 AspectJ
App 下的 build.gralde 中添加依赖
implementation 'org.aspectj:aspectjrt:1.8.13'
工程的 build.gralde 中添加 classpath AspectJ 还需要添加 maven 的依赖
- dependencies {
- classpath 'com.android.tools.build:gradle:3.4.1'
- classpath 'org.aspectj:aspectjtools:1.8.10'
- classpath 'org.aspectj:aspectjweaver:1.8.10'
- }
- buildscript {
- repositories {
- mavenCentral()
- }
最后 App 下的 build.gralde 中添加 AspectJ 的编译代码, 在 dependencies 同级添加.
- import org.aspectj.bridge.iMessage
- import org.aspectj.bridge.MessageHandler
- import org.aspectj.tools.ajc.Main
- final def log = project.logger
- final def variants = project.Android.applicationVariants
- variants.all { variant ->
- if (!variant.buildType.isDebuggable()) {
- log.debug("Skipping non-debuggable build type'${variant.buildType.name}'.")
- return;
- }
- JavaCompile javaCompile = variant.javaCompile
- javaCompile.doLast {
- String[] args = ["-showWeaveInfo",
- "-1.8",
- "-inpath", javaCompile.destinationDir.toString(),
- "-aspectpath", javaCompile.classpath.asPath,
- "-d", javaCompile.destinationDir.toString(),
- "-classpath", javaCompile.classpath.asPath,
- "-bootclasspath", project.Android.bootClasspath.join(File.pathSeparator)]
- log.debug "ajc args:" + Arrays.toString(args)
- MessageHandler handler = new MessageHandler(true);
- new Main().run(args, handler);
- for (iMessage message : handler.getMessages(null, true)) {
- switch (message.getKind()) {
- case iMessage.ABORT:
- case iMessage.ERROR:
- case iMessage.FAIL:
- log.error message.message, message.thrown
- break;
- case iMessage.WARNING:
- log.warn message.message, message.thrown
- break;
- case iMessage.INFO:
- log.info message.message, message.thrown
- break;
- case iMessage.DEBUG:
- log.debug message.message, message.thrown
- break;
- }
- }
- }
- }
OK 配置完毕下面开始解决第一个行为统计的问题
首先定义一个注解 ClickBehavior, 运行时注解, 作用在方法上, 并且有一个参数代表需要统计的行为的名称
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.METHOD)
- public @interface ClickBehavior {
- String value();
- }
然后定义一个切面类 ClickBehaviorAspectJ
- @Aspect// 定义切面类
- public class ClickBehaviorAspectJ {
- private final static String TAG = "myTag>>>";
- //execution 定义切入点
- //* *(..)) 通配符 可以处理所有 ClickBehavior 注解的方法
- @Pointcut("execution(@com.chs.architecturetest.annotation.ClickBehaviorAspectJ * *(..))")
- public void methodPointCut() {}
- // 对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
- @Around("methodPointCut()")
- public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
- // 获取签名方法
- MethodSignature signature = (MethodSignature) joinPoint.getSignature();
- // 获取方法名
- String methodName = signature.getName();
- // 获取 class 名
- String className = signature.getDeclaringType().getSimpleName();
- // 获取需要统计的 value 值
- String funName = signature.getMethod().getAnnotation(com.chs.architecturetest.annotation.ClickBehavior.class).value();
- // 当前时间
- long begin = System.currentTimeMillis();
- Log.e(TAG,"ClickBehaviorAspectJ Method Before");
- Object proceed = joinPoint.proceed();
- Log.e(TAG,"ClickBehaviorAspectJ Method End");
- // 执行时间
- long duration = System.currentTimeMillis() - begin;
- Log.e(TAG, String.format("统计了:%s 功能, 在 %s 类的 %s 方法, 用时 %d ms",
- funName, className, methodName, duration));
- return proceed;
- }
- }
这里面有几个注解, 一般用前三个就能完成
@Aspect 代表这是一个切面类
@Pointcut 设置需要切入的方法, 这里设置的所有的有 ClickBehavior 注解的方法. 我们也可以指定某一个类下的所有方法
("execution(com.chs.architecturetest.MainActivity *(..))")
, 或者整个工程中的所有方法
- ("execution(* *(..))")
- //execution(< 修饰符模式 >? < 返回类型模式 > < 方法名模式 >(< 参数模式 >) < 异常模式 >?)
@Around 对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
@Before("methodPointCut()") 切入之前执行
@After("methodPointCut()") 切入之后执行
@AfterReturning(value = "methodPointCut()", returning = "returnValue") 返回通知, 切点方法返回结果之后执行
@AfterThrowing(value = "methodPointCut()", throwing = "throwable") 异常通知, 切点抛出异常时执行
在 Activity 中整 3 个按钮分别为登录, VIP, 账户, 并设置点击方法. 给这几个点击方法设置行为点击注解
- @ClickBehavior("VIP 页面")
- public void goToVip(View view) {
- Log.e(TAG,"去 VIP 页面");
- startActivity(new Intent(this,OtherActivity.class));
- }
- @ClickBehavior("账户页面")
- public void goToZh(View view) {
- Log.e(TAG,"去账户页面");
- startActivity(new Intent(this,OtherActivity.class));
- }
- @ClickBehavior("登录页面")
- public void goToLogin(View view) {
- Log.e(TAG,"去登录页面");
- }
OK 完成到这里行为统计就完成了, 执行带 @ClickBehavior 注解的方法都会执行统计的代码, 比如点击登录按钮打印日志
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag>>>: ClickBehaviorAspectJ Method Before
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag>>>: 去登录页面
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag>>>: ClickBehaviorAspectJ Method End
2019-07-02 23:23:48.640 8640-8640/com.chs.architecturetest E/myTag>>>: 统计了: 登录页面功能, 在 ProxyActivity 类的 goToLogin 方法, 用时 0 ms
检查登录的功能
首先写一个注解 ClickBehavior. 它不需要有值
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.METHOD)
- public @interface LoginBehavior {
- }
定义登录的 AspectJ 类
- @Aspect// 定义切面类
- public class LoginAspectJ {
- private final static String TAG = "myTag>>>";
- //execution 定义切入点
- //* *(..)) 通配符 可以处理所有 ClickBehavior 注解的方法
- @Pointcut("execution(@com.chs.architecturetest.annotation.LoginBehavior * *(..))")
- public void methodPointCut() {}
- // 对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
- @Around("methodPointCut()")
- public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
- // 是否登录真实项目中去 sharedprefrence 中去那
- Context context = (Context) joinPoint.getThis();
- if(false){
- Log.e(TAG, "检测到已登录!");
- return joinPoint.proceed();
- }else {
- Log.e(TAG, "检测到没有登录!");
- context.startActivity(new Intent(context,LoginActivity.class));
- return null;
- }
- }
- }
在 around 方法中就可以执行判断是否登录的逻辑了, 真实项目中一般都是从 SharedPreferences 中拿到数据判断是否登录.
最后给需要判断登录状态的地方添加 @LoginBehavior 注解
- @LoginBehavior
- @ClickBehavior("VIP 页面")
- public void goToVip(View view) {
- Log.e(TAG,"去 VIP 页面");
- startActivity(new Intent(this,OtherActivity.class));
- }
- @LoginBehavior
- @ClickBehavior("账户页面")
- public void goToZh(View view) {
- Log.e(TAG,"去账户页面");
- startActivity(new Intent(this,OtherActivity.class));
- }
- @ClickBehavior("登录页面")
- public void goToLogin(View view) {
- Log.e(TAG,"去登录页面");
- }
比如这里将前面代码 if 判断中直接改为 false, 点击去 VIP 页面的按钮测试结果如下, 会跳转到到登录页面
2019-07-02 23:26:00.057 8640-8640/com.chs.architecturetest E/myTag>>>: ClickBehaviorAspectJ Method Before
2019-07-02 23:26:00.058 8640-8640/com.chs.architecturetest E/myTag>>>: 检测到没有登录!
2019-07-02 23:26:00.067 8640-8640/com.chs.architecturetest E/myTag>>>: ClickBehaviorAspectJ Method End
2019-07-02 23:26:00.068 8640-8640/com.chs.architecturetest E/myTag>>>: 统计了: VIP 页面功能, 在 ProxyActivity 类的 goToVip 方法, 用时 10 ms
OK 完成啦
来源: http://www.tuicool.com/articles/ZFzqMbE