注解和反射一样,都是用来做框架的,我们这里学习注解的目的其实是为了以后学习框架或者做框架做铺垫的。
那注解该怎么学呢?和反射的学习套路一样,我们先充分的认识注解,掌握注解的定义和使用格式,然后再学习它的应用场景。
先来认识一下什么是注解?
Java注解是代码中的特殊标记,比如@Override、@Test等,作用是:让其他程序根据注解信息决定怎么执行该程序。
比如:Junit框架的@Test注解可以用在方法上,用来标记这个方法是测试方法,被@Test标记的方法能够被Junit框架执行。
再比如:@Override注解可以用在方法上,用来标记这个方法是重写方法,被@Override注解标记的方法能够被IDEA识别进行语法检查。
上面我们说的@Test注解、@Overide注解是别人定义好给我们用的,将来如果需要自己去开发框架,就需要我们自己定义注解。
接着我们学习自定义注解
比如:现在我们自定义一个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(){
}
}
到这里关于定义注解的格式、以及使用注解的格式就学习完了。
注解本质是什么呢?
想要搞清楚注解本质是什么东西,我们可以把注解的字节码进行反编译,使用XJad工具进行反编译。经过对MyTest1注解字节码反编译我们会发现:
1.MyTest1注解本质上是接口,每一个注解接口都继承子Annotation接口
2.MyTest1注解中的属性本质上是抽象方法
3.@MyTest1实际上是作为MyTest接口的实现类对象
4.@MyTest1(aaa="孙悟空",bbb=false,ccc={"Python","前端","Java"})里面的属性值,可以通过调用aaa()、bbb()、ccc()方法获取到。 【别着急,继续往下看,再解析注解时会用到】
各位小伙伴,刚才我们已经认识了注解以及注解的基本使用。接下来我们还需要学习几种特殊的注解,叫做元注解。
什么是元注解?
元注解是修饰注解的注解。这句话虽然有一点饶,但是非常准确。我们看一个例子
接下来分别看一下@Target注解和@Retention注解有什么作用,如下图所示
@Target是用来声明注解只能用在那些位置,比如:类上、方法上、成员变量上等
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
@Target(ElementType.TYPE) //声明@MyTest3注解只能用在类上
public @interface MyTest3{
}
接下来,我们把@MyTest3用来类上观察是否有错,再把@MyTest3用在方法上、变量上再观察是否有错
如果我们定义MyTest3注解时,使用@Target注解属性值写成下面样子
//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface MyTest3{
}
到这里@Target元注解的使用就演示完毕了。
@Retetion是用来声明注解保留周期,比如:源代码时期、字节码时期、运行时期
@Retetion(RetetionPloicy.SOURCE): 注解保留到源代码时期、字节码中就没有了
@Retetion(RetetionPloicy.CLASS): 注解保留到字节码中、运行时注解就没有了
@Retetion(RetetionPloicy.RUNTIME):注解保留到运行时期
【自己写代码时,比较常用的是保留到运行时期】
//声明@MyTest3注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
//控制使用了@MyTest3注解的代码中,@MyTest3保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest3{
}
各位小伙伴,通过前面的学习我们能够自己定义注解,也能够把自己定义的注解标记在类上或者方法上等位置,但是总感觉有点别扭,给类、方法、变量等加上注解后,我们也没有干什么呀!!!
接下来,我们就要做点什么。我们可以通过反射技术把类上、方法上、变量上的注解对象获取出来,然后通过调用方法就可以获取注解上的属性值了。我们把获取类上、方法上、变量上等位置注解及注解属性值的过程称为解析注解。
解析注解套路如下
1.如果注解在类上,先获取类的字节码对象,再获取类上的注解
2.如果注解在方法上,先获取方法对象,再获取方法上的注解
3.如果注解在成员变量上,先获取成员变量对象,再获取变量上的注解
总之:注解在谁身上,就先获取谁,再用谁获取谁身上的注解
按照需求要求一步一步完成
① 先定义一个MyTest4注解
//声明@MyTest4注解只能用在类上和方法上
@Target({ElementType.TYPE,ElementType.METHOD})
//控制使用了@MyTest4注解的代码中,@MyTest4保留到运行时期
@Retetion(RetetionPloicy.RUNTIME)
public @interface MyTest4{
String value();
double aaa() default 100;
String[] bbb();
}
② 定义有一个类Demo
@MyTest4(value="蜘蛛侠",aaa=99.9, bbb={"至尊宝","黑马"})
public class Demo{
@MyTest4(value="孙悟空",aaa=199.9, bbb={"紫霞","牛夫人"})
public void test1(){
}
}
③ 写一个测试类AnnotationTest3解析Demo类上的MyTest4注解
public class AnnotationTest3{
@Test
public void parseClass(){
//1.先获取Class对象
Class c = Demo.class;
//2.解析Demo类上的注解
if(c.isAnnotationPresent(MyTest4.class)){
//获取类上的MyTest4注解
MyTest4 myTest4 = (MyTest4)c.getDeclaredAnnotation(MyTest4.class);
//获取MyTests4注解的属性值
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(myTest4.bbb());
}
}
@Test
public void parseMethods(){
//1.先获取Class对象
Class c = Demo.class;
//2.解析Demo类中test1方法上的注解MyTest4注解
Method m = c.getDeclaredMethod("test1");
if(m.isAnnotationPresent(MyTest4.class)){
//获取方法上的MyTest4注解
MyTest4 myTest4 = (MyTest4)m.getDeclaredAnnotation(MyTest4.class);
//获取MyTests4注解的属性值
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(myTest4.bbb());
}
}
}
各位同学,关于注解的定义、使用、解析注解就已经学习完了。接下来,我们再学习一下注解的应用场景,注解是用来写框架的,比如现在我们要模拟Junit写一个测试框架,要求有@MyTest注解的方法可以被框架执行,没有@MyTest注解的方法不能被框架执行。
第一步:先定义一个MyTest注解
@Target(ElementType.METHOD) //只能注解方法
@Retetion(RetetionPloicy.RUNTIME)//注解要一直存活
public @interface MyTest{
}
第二步:写一个测试类AnnotationTest4,在类中定义几个被@MyTest注解标记的方法
public class AnnotationTest4{
@MyTest
public void test1(){
System.out.println("=====test1====");
}
@MyTest
public void test2(){
System.out.println("=====test2====");
}
public void test3(){
System.out.println("=====test2====");
}
public static void main(String[] args){
AnnotationTest4 a = new AnnotationTest4();
//1.先获取Class对象
Class c = AnnotationTest4.class;
//2.提取AnnotationTest4类中所有的方法对象
Method[] methods = c.getDeclaredMethods();
//3.遍历这个数组中的每个方法,看方法上是否存在@MyTest注解,存在就触发该方法执行
for(Method m: methods){
//判断方法上是否有MyTest注解,有就执行该方法
if(m.isAnnotationPresent(MyTest.class)){
m.invoke(a);
}
}
}
}
恭喜小伙伴们,学习到这里,关于注解的使用就学会了(* ^ ▽ ^* )
学习一个Java的高级技术叫做动态代理。首先我们认识一下代理长什么样?我们以大明星“杨超越”例。
假设现在有一个大明星叫杨超越,它有唱歌和跳舞的本领,作为大明星是要用唱歌和跳舞来赚钱的,但是每次做节目,唱歌的时候要准备话筒、收钱,再唱歌;跳舞的时候也要准备场地、收钱、再唱歌。杨超越越觉得我擅长的做的事情是唱歌,和跳舞,但是每次唱歌和跳舞之前或者之后都要做一些繁琐的事情,有点烦。于是杨超越就找个一个经济公司,请了一个代理人,代理杨超越处理这些事情,如果有人想请杨超越演出,直接找代理人就可以了。如下图所示
我们说杨超越的代理是中介公司派的,那中介公司怎么知道,要派一个有唱歌和跳舞功能的代理呢?
解决这个问题,Java使用的是接口,杨超越想找代理,在Java中需要杨超越实现了一个接口,接口中规定要唱歌和跳舞的方法。Java就可以通过这个接口为杨超越生成一个代理对象,只要接口中有的方法代理对象也会有。
接下来我们就先把有唱歌和跳舞功能的接口,和实现接口的大明星类定义出来。
public interface Star {
//代表明星的接口
//声明大明星有那些方法需要被代理
String sing(String name);
void dance();
}
public class BigStar implements Star{
private String name;
public BigStar(String name) {
this.name = name;
}
public String sing(String name)
{
System.out.println(this.name + "正在唱:" + name);
return "谢谢!谢谢!";
}
public void dance()
{
System.out.println(this.name + "正在优美的跳舞~~");
}
}
下面我们写一个为BigStar生成动态代理对象的工具类。这里需要用Java为开发者提供的一个生成代理对象的类叫Proxy类。
通过Proxy类的newInstance(…)方法可以为实现了同一接口的类生成代理对象。 调用方法时需要传递三个参数。
public class ProxyUtil {
public static Star createProxy(BigStar bigStar){
/* newProxyInstance(ClassLoader loader,
Class>[] interfaces,
InvocationHandler h)
参数1:用于指定一个类加载器
参数2:指定生成的代理长什么样子,也就是有哪些方法
参数3:用来指定生成的代理对象要干什么事情
*/
// Star starProxy = ProxyUtil.createProxy(s);
// starProxy.sing("好日子") starProxy.dance()
Star starProxy = (Star) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
new Class[]{Star.class}, new InvocationHandler() {
@Override // 回调方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 代理对象要做的事情,会在这里写代码
if(method.getName().equals("sing")){
System.out.println("准备话筒,收钱20万");
}else if(method.getName().equals("dance")){
System.out.println("准备场地,收钱1000万");
}
return method.invoke(bigStar, args);
}
});
return starProxy;
}
}
调用我们写好的ProxyUtil工具类,为BigStar对象生成代理对象
public class Test {
public static void main(String[] args) {
BigStar s = new BigStar("杨超越");
Star starProxy = ProxyUtil.createProxy(s);
String rs = starProxy.sing("好日子");
System.out.println(rs);
starProxy.dance();
}
}
现有如下代码
/**
* 用户业务接口
*/
public interface UserService {
// 登录功能
void login(String loginName,String passWord) throws Exception;
// 删除用户
void deleteUsers() throws Exception;
// 查询用户,返回数组的形式。
String[] selectUsers() throws Exception;
}
下面有一个UserService接口的实现类,下面每一个方法中都有计算方法运行时间的代码。
/**
* 用户业务实现类(面向接口编程)
*/
public class UserServiceImpl implements UserService{
@Override
public void login(String loginName, String passWord) throws Exception {
long time1 = System.currentTimeMillis();
if("admin".equals(loginName) && "123456".equals(passWord)){
System.out.println("您登录成功,欢迎光临本系统~");
}else {
System.out.println("您登录失败,用户名或密码错误~");
}
Thread.sleep(1000);
long time2 = System.currentTimeMillis();
System.out.println("login方法耗时:"+(time2-time1));
}
@Override
public void deleteUsers() throws Exception{
long time1 = System.currentTimeMillis();
System.out.println("成功删除了1万个用户~");
Thread.sleep(1500);
long time2 = System.currentTimeMillis();
System.out.println("deleteUsers方法耗时:"+(time2-time1));
}
@Override
public String[] selectUsers() throws Exception{
long time1 = System.currentTimeMillis();
System.out.println("查询出了3个用户");
String[] names = {"张全蛋", "李二狗", "牛爱花"};
Thread.sleep(500);
long time2 = System.currentTimeMillis();
System.out.println("selectUsers方法耗时:"+(time2-time1));
return names;
}
}
观察上面代码发现有什么问题吗?
在多个方法里面都写了相同的和业务无关的时间统计的代码。
我们会发现每一个方法中计算耗时的代码都是重复的,我们可是学习了动态代理的高级程序员,怎么能忍受在每个方法中写重复代码呢!况且这些重复的代码并不属于UserSerivce的主要业务代码。
所以接下来我们打算,把计算每一个方法的耗时操作,交给代理对象来做。
先在UserService类中把计算耗时的代码删除,代码如下
/**
* 用户业务实现类(面向接口编程)
*/
public class UserServiceImpl implements UserService{
@Override
public void login(String loginName, String passWord) throws Exception {
if("admin".equals(loginName) && "123456".equals(passWord)){
System.out.println("您登录成功,欢迎光临本系统~");
}else {
System.out.println("您登录失败,用户名或密码错误~");
}
Thread.sleep(1000);
}
@Override
public void deleteUsers() throws Exception{
System.out.println("成功删除了1万个用户~");
Thread.sleep(1500);
}
@Override
public String[] selectUsers() throws Exception{
System.out.println("查询出了3个用户");
String[] names = {"张全蛋", "李二狗", "牛爱花"};
Thread.sleep(500);
return names;
}
}
然后为UserService生成一个动态代理对象,在动态代理中调用目标方法,在调用目标方法之前和之后记录毫秒值,并计算方法运行的时间。代码如下
public class ProxyUtil {
public static UserService createProxy(UserService userService){
UserService userServiceProxy
= (UserService) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{UserService.class},
new InvocationHandler() {
@Override
public Object invoke( Object proxy,
Method method,
Object[] args) throws Throwable { if(
method.getName().equals("login") || method.getName().equals("deleteUsers")||
method.getName().equals("selectUsers")){
//方法运行前记录毫秒值
long startTime = System.currentTimeMillis();
//执行方法
Object rs = method.invoke(userService, args);
//执行方法后记录毫秒值
long endTime = System.currentTimeMillis();
System.out.println(method.getName() + "方法执行耗时:" + (endTime - startTime)/ 1000.0 + "s");
return rs;
}else {
Object rs = method.invoke(userService, args);
return rs; }
} });
//返回代理对象
return userServiceProxy;
}
}
在测试类中为UserService创建代理对象
/**
* 目标:使用动态代理解决实际问题,并掌握使用代理的好处。
*/
public class Test {
public static void main(String[] args) throws Exception{
// 1、创建用户业务对象。
UserService userService = ProxyUtil.createProxy(new UserServiceImpl());
// 2、调用用户业务的功能。
userService.login("admin", "123456");
System.out.println("----------------------------------");
userService.deleteUsers();
System.out.println("----------------------------------");
String[] names = userService.selectUsers();
System.out.println("查询到的用户是:" + Arrays.toString(names));
System.out.println("----------------------------------");
}
}
执行结果如下图所示
UserService userService = new UserServiceImpl();
动态代理对象的执行流程如下图所示,每次用代理对象调用方法时,都会执行InvocationHandler中的invoke方法。
UserService userService = ProxyUtil.createProxy(new UserServiceImpl());