14-注解&类加载器&动态代理

1. 注解

(1) 注解的介绍

注解:jdk5 之后提供的一个特性,和类、接口同级。格式:@interface 注解名{}

程序中有注释和注解。

  • 注释给开发人员看

  • 注解给计算机看的

注解作用:

  • 编译检查

  • 替代配置文件

  • 定义注解(元注解:注解上的注解)

  • 分析代码(用到反射)

了解下 Java 中的 3 个注解:

  • @Override :声明该方法是从分类上继承过来的,执行编译期的检查

    @Override
    public String toString() {
    return super.toString();
    }
  • @SuppressWarnings:抑制警告,值有好多。只需要知道一个 all 抑制所有的警告

    @SuppressWarnings("all")
    private String username;
    @SuppressWarnings("all")
    private List list;
  • @Deprecated:声明,该方法不赞成使用

    @Deprecated
    public static void add(){
    //加上 @Deprecated 之后,即声明了该add方法过时,在别的地方调用该方法会出现横线
    }
    public static void add(int ... args){
    //上面那个add方法过时,这个没有
    }

(2) 自定义注解

先看一个自定义注解例子:

public @interface MyAnnotation {
//注解属性
int i();
String s();
//Date d(); //该类型不行
String[] ss();
Class cl();
MyAnnotation1 m1();
Color RED(); //其中,Color 为枚举类型
}

注解属性:注解本质就是一个接口,接口中可以有常量和抽象方法,抽象方法在注解中就称之为注解属性。

注解属性的类型有如下:

  • 基本类型

  • String

  • Class

  • Annotation

  • Enum(枚举类型)

  • 以及以上类型对应的一维数组,例如 String []

注意:一旦注解有属性了,使用注解的时候必须赋值(除非这个注解属性有默认值)

注解本质上就是一个接口,我们先看下面自定义的注解:

public @interface aa {
int i();
String s();
String[] ss();
}

再 CMD 下使用 javac aa.java 编译该程序得到 aa.class 字节码文件,然后使用 jdk 自带的反编译工具 javap aa 可以看到反编译得到的结果,如下:

可以看到注解实质就是一个接口。

在注解上赋值的格式: @注解名(属性名=属性值)

(1)只有一个注解的时候

public @interface MyAnnotaion {
int i;
}

注解的使用:

@MyAnnotaion(i=0)
public class Demo3{
}

(2)多个注解的时候

public @interface MyAnnotaion {
int i;
String s();
}

注解的使用:

@MyAnnotaion(i=0 , s="qwe")
public class Demo3{
}

(3)注解属性类型为数组,且只有一个值的时候,可以有两种写法

比如该注解:

public @interface MyAnnotaion {
String[] ss();
}
  • 注解使用方式1:属性名= { 值 }

    @MyAnnotaion(ss = {"qwe", "123"})
    public class Demo3 {
    }
  • 注解使用方式2:属性名=属性值

    @MyAnnotaion(ss = "qwe")
    public class Demo3 {
    }

(4)若属性名为value的时候,且只需要为这个value属性赋值的时候,value可以省略,例如:

public @interface MyAnnotaion {
String value();
}

使用该注解:

@MyAnnotaion("qwe")
public class Demo3 {
}

(5)注解属性类型为数组,且有多个注解属性

public @interface MyAnnotaion {
String[] ss();
int i;
}

使用该注解:

@MyAnnotaion(ss = {"qwe", "123"}, int i=0)
public class Demo3 {
}

(3) 元注解

自己自定义的注解到底作用在方法上,还是类上,亦或可以作用在两者?——这里就需要使用到元注解了。

元注解:定义在注解上的注解。

@Retention 规定注解保留到什么阶段 值为RetentionPolicy的三个枚举值
SOURCE:只在代码中保留,在字节码文件中就删除了
CLASS:在代码和字节码文件中保留
RUNTIME:所有阶段都保留
@Target 规定注解作用在什么上面 值为ElementType的枚举值
TYPE:作用在类 接口 等上面
METHOD:作用方法上面
FIELD:作用字段上面

什么意思呢?比如 SOURCE 可以用来检查编译时候是否出现异常或者什么问题。

举例:

@Target(ElementType.METHOD)
public @interface MyAnnotation4 {
}

表示只能作用在方法上面,作用在类上面是不合法的。

(4) 案例

先看一个案例,以下是步骤分析:

1.定义一个注解 @MyTest
2.在一个测试类 MyTestTest 上的添加几个方法
在方法上添加@MyTest
3.在另一个有主方法的类上添加main方法
运行main方法的时候,需要将带有 @MyTest注解的方法执行

自定义注解:MyTest.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//注解在运行时保留
@Retention(RetentionPolicy.RUNTIME)
//注解只能作用在方法上
@Target(ElementType.METHOD)
public @interface MyTest {
}

MyTestTest.java

public class MyTestTest {
@MyTest
public void f1(){
System.out.println("f1方法执行了~~~~");
}
public void f2(){
System.out.println("f2方法执行了~~~~");
}
@MyTest
public void f3(){
System.out.println("f3方法执行了~~~~");
}
}

MainTest.java

public class MainTest {
public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException {
//运行这个类的时候 需要将 测试类中带有@mytest所有方法执行
//获取字节码对象
Class clazz=MyTestTest.class;
//获取所有的方法
Method[] arr = clazz.getMethods();
//让带有注解的方法执行
for (Method m : arr) {
//获取有注解的方法
//判断方法是否有指定的注解
boolean flag=m.isAnnotationPresent(MyTest.class);
if(flag){
//System.out.println(m.getName());
m.invoke(clazz.newInstance());
}
}
}

可以看到执行了加有 @MyTest 注解的方法,未加注解的方法没执行。可以看出该案例达到了分析代码的作用。

注意: 该案例中添加的注解 @Retention(RetentionPolicy.RUNTIME) 是在所有阶段保留,若没在方法上添加 @Retention 的注解或添加的不是所有阶段保留的注解:

//注解只能作用在方法上
@Target(ElementType.METHOD)
public @interface MyTest {
}

则在程序运行后结果是什么都没出现。 m.isAnnotationPresent(MyTest.class); 这里需要的是所有阶段保留的注解。

再看一个案例:获取连接的工具类,通过配置四个参数,以下为步骤分析:

1.自定义一个注解JDBCInfo
添加元注解:
在程序运行的时候使用 @Retention
只能作用在方法上 @Target
添加注解属性
String driverClass() default "com.mysql.jdbc.Driver";
String url();
String username() default "root";
String password();
2.在jdbcutils工具类中提供一个getConnection,在方法上面添加一个注解 @JDBCInfo(...)
getConnection方法需要进行的操作:获取注解上的四个属性值
获取字节码文件
获取该方法上的注解
获取注解的值
3.运行的时候可以通过getConnection获取一个连接

JdbcInfo.java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface JdbcInfo {
String driverClass() default "com.mysql.jdbc.Driver";
String url();
String user() default "root";
String password() default "root";
}

JdbcUtils.java

public class JdbcUtils {
@JdbcInfo(url = "jdbc:mysql://localhost:3306/day16",password="1234")
public static Connection getConnection() throws Exception{
//1.获取字节码文件
Class clazz=JdbcUtils.class;
//2.获取getConnenction
Method m = clazz.getMethod("getConnection");
//3.判断该方法上是否有 jdbcinfo注解 若有的话获取
if(m.isAnnotationPresent(JdbcInfo.class)){
JdbcInfo info = m.getAnnotation(JdbcInfo.class);
//4.获取注解四个属性
String driverClass = info.driverClass();
String url = info.url();
String user = info.user();
String password = info.password();
//5.注册驱动
Class.forName(driverClass);
//6.获取连接
return DriverManager.getConnection(url, user, password);
}
return null;
}
// 测试
public static void main(String[] args) throws Exception {
System.out.println(getConnection());;
}
}

案例 - 完成文件上传

技术分析,可以使用下面几种方式:

  • 第一种方式:servlet3.0

  • 第二种方式:commons-fileupload

  • 第三种:框架

这里我们看看第一种方式怎实现的!

servlet3.0:支持注解开发,没有 web.xml 这个文件了,内嵌了文件上传功能。

eclipse 下具体怎么操作呢?

前面我们都是在 eclipse 下新建 Dynamic Web Project 的 2.5 版本,这次我们新建 3.0 版本,如图:

可以看到该 web 项目的 WEB-INF 目录下是没有 web.xml 文件的。然后可以新建 servlet3.0 了。

如图,也可勾选

WEB-INF 目录下会存在 web.xml 文件。(关于2.x 与 3.x 区别参考网上博文了解下吧:dynamic web module version 2.5与3.0的主要区别

例如:
创建servlet
在类上面添加 @WebServlet(urlPatterns={ "/demo2", "/demo21" },loadOnStartup=2)
创建listener
在类上添加 @WebListener
创建filter
在类上添加 @WebFilter(urlPatterns="/*")

文件上传的一些要求:

浏览器端的要求:
表单的提交方法必须是post
必须有一个文件上传组件 <input type="file" name=""/>
必须设置表单的enctype=multipart/form-data
服务器端的要求:
servlet3.0中
需要在servlet中添加注解
@MultipartConfig
接受普通上传组件 (除了文件上传组件):request.getParameter(name属性的值)
接受文件上传组件 request.getPart(name属性的值);
getName():获取的name的属性值
获取文件名:
part.getHeader("Content-Disposition"):获取头信息 然后截取

文件上传的注意的问题:

名字重复 随机名称
在数据库中提供两个字段,
一个字段用来存放文件的真实名称 1.jpg
另一个字段用来存放文件存放路径 g:/sdfasdf.jpg
随机名称:
uuid
时间戳
文件安全
重要的文件存放在 web-inf 或者 meta-inf 或者 服务器创建一个路径
不是很重要的文件 项目下
文件存放目录
方式1:日期
方式2:用户
方式3:文件个数
方式4:随机目录
mkdirs

直接看代码,form2.jsp

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<form action="/day1701/upload2" method="post" enctype="multipart/form-data">
用户名:<input name="username"><br/>
文件:<input type="file" name="f"><br/>
<input type="submit">
</form>
</body>
</html>

Demo2Servlet.java

@WebServlet(urlPatterns={ "/demo2", "/demo21" },loadOnStartup=2)
public class Demo2Servlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("demo2 servlet初始化了");
}
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("demo2 servlet 执行了~~");
}
}

Upload2Servlet.java

@WebServlet("/upload2")
@MultipartConfig
public class Upload2Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//0.设置编码
request.setCharacterEncoding("utf-8");
//1.获取普通的上传组件 username
String username = request.getParameter("username");
System.out.println(username);
//2.获取文件上传组件
Part part = request.getPart("f");
//2.1获取文件的名称
String sss = part.getHeader("content-disposition");
String realName=sss.substring(sss.indexOf("filename=")+10, sss.length()-1);
System.out.println("文件的名称:"+realName);
//2.2 获取随机名称
String uuidName = UploadUtils.getUUIDName(realName);
System.out.println("文件随机名称:"+uuidName);
//2.3 获取文件存放的目录
String dir = UploadUtils.getDir(uuidName);
String realPath = this.getServletContext().getRealPath("/upload"+dir);
File file = new File(realPath);
if(!file.exists()){
file.mkdirs();
}
System.out.println("文件的目录:"+realPath);
//3.对拷流
InputStream is = part.getInputStream();
FileOutputStream os = new FileOutputStream(new File(file,uuidName));
IOUtils.copy(is, os);
os.close();
is.close();
//4.删除临时文件
part.delete();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}

其中 UploadUtils.java

public class UploadUtils {
/**
* 获取随机名称
* @param realName 真实名称
* @return uuid
*/
public static String getUUIDName(String realName){
//realname 可能是 1.jpg 也可能是 1
//获取后缀名
int index = realName.lastIndexOf(".");
if(index==-1){
return UUID.randomUUID().toString().replace("-", "").toUpperCase();
}else{
return UUID.randomUUID().toString().replace("-", "").toUpperCase()+realName.substring(index);
}
//return null;
}
/**
* 获取文件真实名称
* @param name
* @return
*/
public static String getRealName(String name){
// c:/upload/1.jpg 1.jpg
//获取最后一个"/"
int index = name.lastIndexOf("\\");
return name.substring(index+1);
}
/**
* 获取文件目录
* @param name 文件名称
* @return 目录
*/
public static String getDir(String name){
int i = name.hashCode();
String hex = Integer.toHexString(i);
int j=hex.length();
for(int k=0;k<8-j;k++){
hex="0"+hex;
}
// System.out.println(hex);
return "/"+hex.charAt(0)+"/"+hex.charAt(1);
}
public static void main(String[] args) {
//String s="G:\\day17-基础加强\\resource\\1.jpg";
String s="1.jgp";
String realName = getRealName(s);
//System.out.println(realName);
String uuidName = getUUIDName(realName);
//System.out.println(uuidName);
String dir = getDir(realName);
System.out.println(dir);
}
}

注:这个封装的工具类可以留着以后用到

2. 类加载器

类加载:我们编写的 .java 文件,JVM 会将它变成 .class 文件。该文件要想运行,必须加载内存中,然后会生成一个对象 .Class 对象。

类加载器层次结构:

  • 引导类加载器rt.jar下的类

  • 扩展类加载器ext/*.jar 下的类

  • 应用类加载器: 我们自己编写类

先看如下图:

如下可以看到 rt.jar 包下很多类的字节码文件。

全盘负责委托机制:

  • 当一个类运行的时候,有可能有其他类,应用类加载器询问扩展类加载器:你加载过这些类吗?

  • 扩展类加载器在向上问(引导类加载器):你加载过这些类吗?

  • 引导类加载器:我查查,有一个是我负责,我来加载

  • 扩展类加载器:接下来我来查,有几个是我负责,我加载,还有几个类我已经加载完成了,你可以直接使用

  • 应用类加载器:收到了,剩下的我来。

看代码:

public class Test {
public static void main(String[] args) throws Exception {
ClassLoader ap = Test.class.getClassLoader();
System.out.println(ap);
ClassLoader ex = DNSNameService.class.getClassLoader();
System.out.println(ex);
ClassLoader co = String.class.getClassLoader();
System.out.println(co);
}
}

结果:

能观察到依次由应用类加载器扩展类加载器加载。注:看到打印的为 null ,表示使用的引导加载器

3. 动态代理

(1) 动态代理代码学习

静态代理书写步骤:

1.要求被装饰者和装饰者实现同一个接口或者继承同一个类
2.在装饰者中要有被装饰者的引用
3.对需要加强的方法进行加强
4.对不需要加强的方法调用原来的方法

动态代理:

动态代理:
在项目运行的时候才创建一个代理对象,对方法进行增强(控制)
方式1:
jdk中Proxy类,前提:实现接口
方式2:
spring中cglib,前提:继承类

直接看代码学习,Car.java

public interface Car {
void run();
void stop();
}

QQ.java

public class QQ implements Car{
@Override
public void run() {
System.out.println("qq在跑");
}
@Override
public void stop() {
System.out.println("qq刹车");
}
}

TT.java

public class TT {
public static void main(String[] args) {
final QQ qq=new QQ();
//qq.run();
//qq.stop();
//创建代理对象
Car qqProxy=(Car) Proxy.newProxyInstance(QQ.class.getClassLoader(), new Class[]{Car.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//System.out.println("哈哈哈哈");
//System.out.println(method.getName());
//obj 就是代理对象
//Object obj=method.invoke(qq, args);
//return obj;
//对所有方法进行加强
/*System.out.println("加上电池");
Object obj = method.invoke(qq, args);
System.out.println("5秒破百");
return obj;*/
//只对run方法进行加强
if("run".equals(method.getName())){
System.out.println("加上电池");
Object obj = method.invoke(qq, args);
System.out.println("5秒破百");
return obj;
}
return method.invoke(qq, args);
}
});
//qqProxy.run();
qqProxy.stop();
}
}

为什么 final QQ qq=new QQ(); QQ 对象声明为 final 呢?

A:这是因为创建完代理对象后还需要做其操作,需要保证代理对象不能被删除,即 QQ 对象 qq 存在。

总结:

动态的在内存中创建一个代理对象
Object Proxy.newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
参数说明:
ClassLoader:代理对象类加载器 一般我们使用的是被代理对象的类加载器
Class[]:代理对象需要实现接口 一般我们使用的是被搭理对象所实现的所有接口
InvocationHandler:执行处理类.在这里面对方法进行加强
invocationHandler中只有一个方法
Object invoke(Object proxy, Method method, Object[] args)
参数说明:
proxy:代理对象
method:当前执行的方法
args:当前方法执行的时候所需要的参数
返回值:就是当前method对象执行的返回值

(2) 案例-统一编码

步骤分析:

过滤器
doFilter(Request request,Response response)
将代理request传递过去
doFilter(Request requestPrxoy,Response response)

EncodingFilter.java

public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
//1.强转
final HttpServletRequest request=(HttpServletRequest) req;
HttpServletResponse response=(HttpServletResponse) resp;
//创建代理对象
HttpServletRequest requestProxy=(HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(), request.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("getParameter".equals(method.getName())){
//获取请求方式
String m = request.getMethod();
if("get".equalsIgnoreCase(m)){
String s = (String) method.invoke(request, args);//相当于 request.getParameter(args);
return new String(s.getBytes("iso8859-1"),"utf-8");
}else if("post".equalsIgnoreCase(m)){
request.setCharacterEncoding("utf-8");
return method.invoke(request, args);
}
}
//不需要加强的方法
return method.invoke(request, args);
}
});
//2.放行
chain.doFilter(requestProxy, response);
}
@Override
public void destroy() {
}
}