如何伪装成一个服务端开发(五)
2019独角兽企业重金招聘Python工程师标准>>>
目录
如何伪装成一个服务端开发(一)
如何伪装成一个服务端开发(二)
如何伪装成一个服务端开发(三)
如何伪装成一个服务端开发(四)
如何伪装成一个服务端开发(五)
代理模式
在继续之前最好先了解一下GoF中的代理模式,这里不再详细展开。
代理模式的核心思想就是客户不与最终对象交互,而和一种中间代理交互,中间代理再和最终对象交互。这样的好处是,中间代理有机会干预整个交互流程。
使用Proxy.newProxyInstance
java中有一种动态代理的东西,和上面代理模式中的静态代理最大的不同点就在于在编译阶段代理类的.class文件是否已经产生。动态代理是在运行阶段才会产生一个代理类并且加载到classload中的。
假设我们有一个简单的接口和实现
public interface HelloService {public void sayHello(String name);
}public class HelloServiceImpl implements HelloService {@Overridepublic void sayHello(String name) {if (name == null || name.trim() == "") {throw new RuntimeException ("parameter is null!!");}System.out.println("hello " + name);}}
现在我可以在client中定义一个HelloServiceImpl进行调用,这是正常逻辑。但是现在我想要能够介入这个流程,所以这里就需要放入代理模式,这没毛病。
在定义一个拦截器接口和实现
public interface Interceptor {// 事前方法public boolean before();// 事后方法public void after();/*** 取代原有事件方法* @param invocation -- 回调参数,可以通过它的 proceed 方法,回调原有事件* @return 原有事件返回对象* @throws InvocationTargetException* @throws IllegalAccessException*/public Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException;// 事后返回方法。事件没有发生异常执行public void afterReturning();// 事后异常方法,当事件发生异常后执行public void afterThrowing();// 是否使用 around 方法取代原有方法boolean useAround();
}public class MyInterceptor implements Interceptor {@Overridepublic boolean before() {System.out.println("before ......");return true;}@Overridepublic boolean useAround() {return true;}@Overridepublic void after() {System.out.println("after ......");}@Overridepublic Object around(Invocation invocation) throws InvocationTargetException, IllegalAccessException {System.out.println("around before ......");Object obj = invocation.proceed();System.out.println("around after ......");return obj;}@Overridepublic void afterReturning() {System.out.println("afterReturning......");}@Overridepublic void afterThrowing() {System.out.println("afterThrowing......");}}
其中 Invocation是spring中的一个类,实际上并复杂,我们看下源码
public class Invocation {private Object[] params;private Method method;private Object target;public Invocation(Object target, Method method, Object[] params) {this.target = target;this.method = method;this.params = params;}// 反射方法public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, params);}/**** setter and getter ****/
}
当调用proceed是,就通过反射方法直接调用target的指定方法。
现在我们来谈谈我们的目的,我们想要在调用HelloServiceImpl.sayHello的时候被拦截,并且按约定好的流程调用MyInterceptor中对应的方法。
在上面整个代码中,我们发现缺失了代理模式中最重要的东西——代理。
public class ProxyBean implements InvocationHandler {private Object target = null;private Interceptor interceptor = null;/*** 绑定代理对象* @param target 被代理对象* @param interceptor 拦截器* @return 代理对象*/public static Object getProxyBean(Object target, Interceptor interceptor) {ProxyBean proxyBean = new ProxyBean();// 保存被代理对象proxyBean.target = target;// 保存拦截器proxyBean.interceptor = interceptor;// 生成代理对象Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),proxyBean);// 返回代理对象return proxy;}/*** 处理代理对象方法逻辑* @param proxy 代理对象* @param method 当前方法* @param args 运行参数* @return 方法调用结果* @throws Throwable 异常*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) {// 异常标识boolean exceptionFlag = false;Invocation invocation = new Invocation(target, method, args);Object retObj = null; try {if (this.interceptor.before()) {retObj = this.interceptor.around(invocation);} else {retObj = method.invoke(target, args);}} catch (Exception ex) {// 产生异常exceptionFlag = true;}this.interceptor.after();if (exceptionFlag) {this.interceptor.afterThrowing();} else {this.interceptor.afterReturning();return retObj;}return null;}}
ProxyBean是一个工具方法,我们当然还有其他很多形式的写法,但是不论怎么写,都绕不开关键的一步
newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
该方法的作用就是,在运行时生成一个继承了interfaces类的类 A,并且使用loader进行加载,然后使用这个类A生成一个对象 a。当调用a中的方法时,都会调用h.invoke方法(InvocationHandler实际上是个接口,只有一个invoke方法)。
然后我们使用下面代码测试
private static void testProxy() {HelloService helloService = new HelloServiceImpl();// 按约定获取proxyHelloService proxy = (HelloService) ProxyBean.getProxyBean(helloService, new MyInterceptor());proxy.sayHello("zhangsan");System.out.println("\n###############name is null!!#############\n");proxy.sayHello(null);
}
就能够打印如下Log
before ......
around before ......
hello zhangsan
around after ......
after ......
afterReturning......###############name is null!!#############before ......
around before ......
after ......
afterThrowing......
AOP
通过上面demo,我们成功在运行的代码中插入我们想额外运行的方法(MyInterceptor)。Spring AOP的代码核心实现就是使用和上面相同的动态代理模式。而所谓的AOP就是我们在上面做的这件事情,将我们的代码(MyInterceptor),通过一定方法介入了原有流程(Client对target的调用)。
下面我们先来讲解 AOP 术语。
- 连接点(join point):对应的是具体被拦截的对象,因为 Spring 只能支持方法,所以被拦截的对象往往就是指特定的方法,例如,我们前面提到的 HelloServiceImpl 的 sayHello 方法就是一个连接点,AOP 将通过动态代理技术把它织入对应的流程中。
- 切点(point cut):有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样一个功能的概念。
- 通知(advice):就是按照约定的流程下的方法,分为前置通知(before advice)、后置通知(after advice)、环绕通知(around advice)、事后返回通知(afterReturning advice)和异常通知(afterThrowing advice),它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件。
- 目标对象(target):即被代理对象,例如,约定编程中的 HelloServiceImpl 实例就是一个目标对象,它被代理了。
- 引入(introduction):是指引入新的类和其方法,增强现有 Bean 的功能。
- 织入(weaving):它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。
- 切面(aspect):是一个可以定义切点、各类通知和引入的内容,Spring AOP将通过它的信息来增强Bean的功能或者将对应的方法织入流程。
上述的描述还是比较抽象的
Spring AOP编程
1.确定连接点
比如这里还是使用之前的demo,这里就用 GameUser.printUserInfo来作为连接点
public interface User{...public void printUserInfo();
}@Component("gameUser")
public class GameUser implements User , BeanNameAware,BeanFactoryAware, ApplicationContextAware, InitializingBean, DisposableBean {....@Overridepublic void printUserInfo() {log.info("I am Game User ,type is "+userInfo.getUserType());}....
}
2.开发切面
首先需要引入三方包
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Aspect
@Component
public class MyAspect {// before消息@Before("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")public void before() {System.out.println("before ......");}//不论执行对错,after一定会被执行@After("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")public void after() {System.out.println("after ......");}@AfterReturning("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")public void afterReturning() {System.out.println("afterReturning ......");}@AfterThrowing("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")public void afterThrowing() {System.out.println("afterThrowing ......");}
}
切面的开发首先需要使用@Aspect,另外切面本身也必须是一个可以被注入的Bean,所以这里使用了@Component(当然也可以在容器配置中使用@Bean注入)。
然后我们会选择我们主要的消息,在消息发生时运行相应的方法。关于其中正则式的含义我们稍晚再解释,总的来说作用就是用来指定连接点的,例子中的意思就表示选择了User中的printUserInfo的执行作为连接点。
AOP注入
这里是笔者自己添加的流程,因为实在是遇到了一个坑,花了一整天的时间才算弄明白。
通过上面两步,我们已经具有了aop的所有物料。我们尝试测试。
@SpringBootApplication
public class FirstSpringBootApplication {private static Logger log = Logger.getLogger(FirstSpringBootApplication.class.getName());public static void main(String[] args) {SpringApplication.run(FirstSpringBootApplication.class, args);ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);User user = applicationContext.getBean(GameUser.class);user.printUserInfo();((AnnotationConfigApplicationContext) applicationContext).close();}
}
我们发现注入aop没有生效。为什么,因为这里我们使用了 AnnotationConfigApplicationContext 来创建一个AOP容器,但是这个容器默认是不支持aop的,所以我们需要让该容器支持aop,只需要在 AppConfig 类上添加注解进行配置即可。
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(value = "com.guardz.first_spring_boot.model")
public class AppConfig {
}
运行,这个时候发现竟然启动报错了
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.guardz.first_spring_boot.model.GameUser' availableat org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1101)at com.guardz.first_spring_boot.FirstSpringBootApplication.main(FirstSpringBootApplication.java:24)
说是找不到 GameUser 这个bean,但是我们看到我们的GameUser已经使用了@Component注解,并且AppConfig的扫描路径中确实包含了GameUser.
这是什么原因呢?在Spring中有两种实现动态代理的方法,一种是我们上面介绍的Proxy,还有一种是CGLib。Proxy的代理模式是通过获取目标类的接口(如果没有实现接口,那么会切换成CGLib的模式),然后动态实现接口。而CGLib的方式是生成一个该类的子类。默认情况下,spring会使用Proxy的模式。我们可以使用 @EnableAspectJAutoProxy(proxyTargetClass=true) 强制全部使用CGLIB模式。
一旦开启了AOP模式,并且我们目标类GameUser已经被代理。那么我们就无法在容器中获取GameUser对象,所以当调用getBean(GameUser.class)尝试获取GameUser对象的时,如果使用了CGLib模式(存在GameUser的子类,就是它的代理类),那么能够正常运行。如果使用了Proxy模式,就会出现错误。
所以总结来说,解决这个问题的方法是,使用 getBean(User.class) 或者使用 getBean("gameUser") .
猜测另外,XXXApplication类调用main的时候回自动生成一个容器,这个容器的配置文件就是XXXApplication类本身。@SpringBootApplication 默认就包含了@ComponentScan,会扫描当前包以及它的子包,并且默认启用了aop,所以如果我们不使用 AnnotationConfigApplicationContext , 而直接新建类,使用@Autowired 注入gameUser,并且调用printUserInfo方法也能够运行。
切点
回到被我们省略了的切面上面,我们使用@Before这样的注解定义了连接点,但是我们发现我冲重复的谢了很多次连接点的正则式,这个时候就引入了切点@Pointcut . 修改切面文件
@Aspect
@Component
public class MyAspect {@Pointcut("execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))")public void pointCut(){}@Before("pointCut()")public void before() {System.out.println("before ......");}.....
}
使用@Pointcut定义一个连接点,然后再@Before这些消息处直接使用这个连接点即可。
然后我们再来分析下这个正则式到底是什么意思
execution(* com.guardz.first_spring_boot.model.User.printUserInfo(..))
execution() 表示在执行的时候,链接内部正则式匹配的方法
* 表示任意返回类型
com.guardz.first_spring_boot.model.User.printUserInfo 指定具体方法
(..) 表示任意参数
其实这里的写法还是很多的,比如常用的@annotation()就表示当连接点带有指定注解时进行拦截。这里不再详述,可以网上查找,后面的学习中应该也会逐步出现。
@Around
我们已经使用了@Befor @After等,但是还少了一个@Around通知,只有当我们需要大幅修改原来代码的时候才会使用,否则尽量不要使用。
在原来的切面中添加
@Around("pointCut()")
public void around(ProceedingJoinPoint jp) throws Throwable {System.out.println("around before......");// 回调目标对象的原有方法jp.proceed();System.out.println("around after......");
}
它拥有一个 ProceedingJoinPoint 类型的参数。这个参数的对象有一个 proceed 方法,通过这个方法可以回调原有目标对象的方法。
这里有个问题,就是 around before和 before的调用顺序,按照我们这种写法来的话,调用顺序是这样的
around before......
before ......
2018-12-29 15:29:13.245 INFO 96357 --- [ main] com.guardz.first_spring_boot.model.User : I am Game User ,type is admin
around after......
after ......
afterReturning ......
注意是先调用了 around before 后调用了 before。 但如果使用xml进行配置aop时,运行顺序就变成了先调用before,在调用around before。
所以这个一定要注意,尽量不要使用around.
@DeclareParents
public interface DeclareUser{void doSomething();
}public class DeclareUserImpl implements DeclareUser {@Overridepublic void doSomething() {System.out.println("就是要出狂战斧");}
}
然后修改MyAspect
@Aspect
public class MyAspect {@DeclareParents(value= "com.guardz.first_spring_boot.model.GameUser",defaultImpl=DeclareUserImpl.class)public DeclareUser declareUser;....
}
然后,当我们通过getBean(User.class) 获取到GameUser的代理对象之后,我们可以将它强行转成DeclareUser类型,并且调用其中的doSomething方法。
实现原理就是
我们的代理对象会额外继承我们使用@DeclareParents 引入的接口。这个接口的实现方法就是defaultImpl制定的类。
@DeclareParents 的value制定了需要被引入新接口的对象(注意这里如果使用User对象会报错,需要使用GameUser,因为是GameUser被代理了)。
我能想到的一个用法是,让DeclareUser接口继承User接口(不继承也可以),然后再DeclareUserImpl中重写GameUser当中的方法
public class DeclareUserImpl implements DeclareUser {....@Overridepublic void printUserInfo() {log.info("我就是要出狂战斧");}@Overridepublic void doSomething() {System.out.println("就是要出狂战斧");}
}
比如上面重写printUserInfo方法,然后当我们调用GameUser代理对象中的printUserInfo,实际上会调用到上面方法。但是非常不推荐这样使用,因为逻辑隐藏太深,如果别人看你代码可能会打你。
通知获取参数
首先我们修改下User中的printUserInfo方法,简单添加一个参数 void printUserInfo(String ext);
然后修改我们的切面
@Aspect
public class MyAspect {......//args(ext)表示获取连接点方法中名称为ext的参数//JoinPoint jp 是可选的,可有可无@Before("pointCut() && args(ext)")public void before(JoinPoint jp,String ext) {//可以通过JoinPoint获取所有参数Object[] args = jp.getArgs();System.out.println("before ...... " + ext);}
}
我们的before中已经带上了ext的参数。
另外@Around是么有JoinPoint参数的,因为它自带了ProceedingJoinPoint参数,可以通过它获取方法参数。
多个切面
对于同一个连接点,我们可以注册多个切面。多个切面之间的执行顺序是随机的,我们可以通过在切面上添加@Order()注解确定运行顺序,比如@Order(1) @Order(2) 数字越小,越早运行。
总结
至此我们基本了解了spring中的aop,并且学习了使用方法,但是这里只介绍了使用注解的方式,aop也能够使用xml进行配置,但是既然已经知道了工作流程,相信如果遇到别人代码使用xml进行了aop,我们也能够通过查阅资料很快上手
参考&引用
《深入浅出 Spring Boot 2.X》
转载于:https://my.oschina.net/zzxzzg/blog/2995225
如何伪装成一个服务端开发(五)相关推荐
- 伪装成mysql的备_如何伪装成一个服务端开发(六) -- 数据库操作
目录 如何伪装成一个服务端开发(六) 前言 本篇开始学习Spring 的数据库连接. 术语 数据库连接涉及到一些术语,如果在学习之前没有搞清楚,很容易在业务理解上出现偏差. JDBC : Java D ...
- 计算机网络拓跋结构,实战 | 服务端开发与计算机网络结合的完美案例
前言 大家好,我是阿秀 后端,可以说是仅次于算法岗之外竞争最为激烈的岗位,而其中的服务端开发也是很多人会选择在秋招中投递的一个岗位,我想对于很多人来说,走上服务端开发之路的起点就是一个回声服务器了. ...
- 一、服务端开发基础(搭建Web服务器、网络基础概念、请求响应流程、配置Apache、静态网站与动态网站)
一.建立你的第一个网站(目标) 前端开发 最终还是属于 Web 开发 中的一个分支,想要成为一名合格的前端开发人员,就必须要 充分理解Web 的概念. 构建一个专业的网站是一项巨大的工作!对于新手我们 ...
- 送给即将春秋招的同学--一名服务端开发工程师的校招面经总结
前言:作为一名21年大学毕业的Java服务端开发工程师,从19年10月份(大三上)开始进行日常实习面试,期间获得小米.快手.领英.Tencent等offer,因疫情爆发无法准时入职,20年3月份春招成 ...
- 百万在线:大型游戏服务端开发
进入手游时代,服务端技术也在向前演进.现代游戏服务端既要承载数以万计的在线玩家,又要适应快速变化的市场需求,因此,如何设计合适的架构就成了重中之重.服务端技术并不简单,作为服务端新人,全面掌握服务端技 ...
- 服务端Skynet(五)——如何搭建一个实例
服务端Skynet(五)--如何搭建一个实例 文章目录 服务端Skynet(五)--如何搭建一个实例 1.配置文件 2.服务消息分发与回应(call/send) 3.通信(server/client) ...
- 抖音、腾讯、阿里、美团春招服务端开发岗位硬核面试(二)
在上一篇 文章中,我们分享了几大互联网公司面试的题目,本文就来详细分析面试题答案以及复习参考和整理的面试资料,小民同学的私藏珍品????. 首先是面试题答案公布,在讲解时我们主要分成如下几块:语言的基 ...
- 我问你这篇保熟不?! -- 做服务端开发,不懂网络层,真的可以吗?
文章目录 唠嗑两句·网络层 网络层简介 网际协议IP 常见的三类IP地址 A类 B类.C类 IP地址与硬件地址 地址解析协议ARP IP层转发分组 子网划分 子网划分的背景意义 什么是子网划分? 子网 ...
- 5年客户端开发的程序员如何转型服务端开发?
最近一位老哥一直有一个困惑,为啥全网都在劝退客户端开发?作为从事客户端开发5年的老鸟,在过去的一段时间还是享受到了对应的差别福利.正如网上所说的,如果是想在十级之前压人,就选择客户端开发,想在大后期发 ...
最新文章
- 强化学习(五) - 时序差分学习(Temporal-Difference Learning)及其实例----Sarsa算法, Q学习, 期望Sarsa算法
- 机械制图手册_42条机械制图基础常识,带徒师傅必备!
- android 稳定性测试工具,APP 稳定性测试工具-Fastbot_Android详解
- 蓝牙a2dp硬件卸载是什么意思_索尼这项音频黑科技 让蓝牙音质从此不输有线
- 反走样和OpenGL多重采样
- 还在用全部token训练ViT?清华UCLA提出token的动态稀疏化采样,降低inference时的计算量...
- java futuretask get reject异常_FutureTask的get()方法之异常处理
- Linux磁盘分区/格式化/挂载目录
- dos命令行设置网络优先级_网络工程师必知的Linux命令,精品!
- 解构控制反转(IoC)和依赖注入(DI)
- 分布式爬虫(一)------------------分布式爬虫概述
- 金山卫士开源代码_官方下载地址
- 如何添加使用微信小程序,教程在这里,微信小程序怎样添加使用
- Xmarks Hosts
- 49个excel常用技巧(一)
- 阿里云服务器和虚拟主机之间的区别
- mysql 2002_解决MySQL报错ERROR 2002 (HY000)
- CAD怎么切换角度标注对象?CAD切换角标操作技巧
- 企业人才测评结果的三种导向分析
- word两个不同表格合并,防止自动调整