反射Reflection
Student.java
public class Student {
public String name;
protected String gender;
String id;
private int grade;
public Student(){
}
public Student(String name) {
this.name = name;
}
private Student(String name, String gender){
this.name = name;
this.gender = gender;
}
public void study(){
System.out.println("Studying...");
}
public void study(int hours){
System.out.println("Studying " + hours + "hours");
}
private void sleep(){
System.out.println("Sleeping...");
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", gender='" + gender + '\'' +
", id='" + id + '\'' +
", grade=" + grade +
'}';
}
}
public class ClassTest {
public static void main(String[] args) throws Exception {
/**
* 三种方式
* forName(String className)
* 类名.class
* 对象.getClass()
*/
//forName(String className)
Class<?> aClass = Class.forName("Note.JavaEE.reflect.Student");
//类名.class
Class<Student> studentClass = Student.class;
//对象.getClass()
Student mark = new Student("Mark");
Class<? extends Student> aClass1 = mark.getClass();
System.out.println(aClass == studentClass);
System.out.println(aClass == aClass1);
}
}
注意:
- 无参构造方法可直接用Class对象的newInstance()方法进行构造
- 当获取到的访问权限不是public时,需要使用setAccessible(true)进行暴力反射
- 成员变量使用get,set进行取值和赋值
- 构造方法采用newInstance()使用
- 方法使用通过invoke()完成
public class FieldTest {
public static void main(String[] args) throws Exception {
Class<Student> studentClass = Student.class;
/**
* getFields()
* 获取所有public属性
*/
Field[] fields = studentClass.getFields();
for(Field field : fields){
System.out.println(field);
}
/**
* getDeclaredFields()
* 获取所有属性
*/
Field[] declaredFields = studentClass.getDeclaredFields();
for(Field field : declaredFields){
System.out.println(field);
}
/**
* getField(String name)
* 获取指定public属性
*/
Student student = new Student("Mark");
Field name = studentClass.getField("name");
Object o = name.get(student);
System.out.println(o);
/**
* getDeclaredField(String name) 获取指定属性
* setAccessible(true) 忽略安全检查(暴力反射)
*/
Field age = studentClass.getDeclaredField("gender");
age.setAccessible(true);
Object o1 = age.get(student);
System.out.println(o1);
/**
* set(Object o, Object value) 更改指定属性值
*/
name.set(student, "Mike");
System.out.println(student);
}
}
public class ConstructorTest {
public static void main(String[] args) throws Exception {
Class<Student> studentClass = Student.class;
//获取所有public构造器
Constructor<?>[] constructors = studentClass.getConstructors();
//获取所有构造器
Constructor<?>[] declaredConstructors = studentClass.getDeclaredConstructors();
//获取指定public构造器,传入参数类型和个数
Constructor<Student> constructor = studentClass.getConstructor(String.class);
Student mark = constructor.newInstance("Mark");
//获取指定构造器,传入参数类型和个数
Constructor<Student> declaredConstructor = studentClass.getDeclaredConstructor(String.class, String.class);
declaredConstructor.setAccessible(true);
Student student = declaredConstructor.newInstance("Mark", "Female");
//使用无参构造器(必须是public,否则需要使用暴力反射)
Student student1 = studentClass.newInstance();
}
}
public class MethodTest {
public static void main(String[] args) throws Exception {
Class<Student> studentClass = Student.class;
//获取所有public方法
Method[] methods = studentClass.getMethods();
//获取所有方法
Method[] declaredMethods = studentClass.getDeclaredMethods();
Student mark = new Student("Mark");
//通过方法名以及参数获取指定public方法并使用
Method study = studentClass.getMethod("study");
study.invoke(mark);
Method study1 = studentClass.getDeclaredMethod("study", int.class);
study1.invoke(mark, 10);
//通过方法名以及参数获取任意指定方法并使用
Method sleep = studentClass.getDeclaredMethod("sleep");
sleep.setAccessible(true);
sleep.invoke(mark);
}
}
配置文件 reflectTest.Properties
className=Note.JavaEE.reflect.Student
methodName =study
public class Test {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
ClassLoader classLoader = Test.class.getClassLoader();
InputStream resourceAsStream = classLoader.getResourceAsStream("reflectTest.properties");
//加载配置文件
properties.load(resourceAsStream);
//获取配置文件中的内容
String className = properties.getProperty("className");
String methodName = properties.getProperty("methodName");
//反射
Class<?> aClass = Class.forName(className);
Method method = aClass.getMethod(methodName);
method.invoke(aClass.newInstance());
}
}
问题1:
如何使用配置文件完成对有参数的方法的调用?
答案1:自己创建一个转换方法
private static Class getPrimitiveClass(String str) throws ClassNotFoundException {
switch (str) {
case "int":
return int.class;
case "double":
return double.class;
case "float":
return float.class;
case "char":
return char.class;
case "String":
return String.class;
default:
return Class.forName(str);
}
}
注解Annotation
@SuppressWarnings("all")
public class SimpleAnnotationTest {
@Override
public String toString() {
return super.toString();
}
@Deprecated
public void show1(){
}
public void say(){
Date date = new Date();
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnnotation {
//基本数据类型
String className();
String methodName();
int value() default 0;
//枚举类型
MyEnum myEnum();
//注解类型
MyAnnotation2 myAnnotation();
//以上类型数组
int[] numbers();
}
@MyAnnotation(className = "Note.JavaEE.reflect.Student",methodName = "study",myEnum = MyEnum.S1,numbers = {1,2})
public class MyAnnotationTest {
public static void main(String[] args) throws Exception {
//采用注解替代配置文件获取类名和方法名
Class<MyAnnotationTest> myAnnotationTestClass = MyAnnotationTest.class;
MyAnnotation annotation = myAnnotationTestClass.getAnnotation(MyAnnotation.class);
String className = annotation.className();
String methodName = annotation.methodName();
//反射
Class<?> aClass = Class.forName(className);
Method method = aClass.getMethod(methodName);
method.invoke(aClass.newInstance());
}
}
问题2:
如何理解解析程序?
答案2:
下面的代码中,CheckExceptionTest就是一个解析程序
public class Calculator {
@CheckException
public String add(){
String s = null;
return s.substring(1,2);
}
@CheckException
public void sub(){
}
@CheckException
public void multiply(){
}
@CheckException
public int divide(){
return 2/0;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckException {
}
public class CheckExceptionTest {
public static void main(String[] args) {
//异常次数
int count = 0;
Calculator calculator = new Calculator();
Class<Calculator> calculatorClass = Calculator.class;
Method[] methods = calculatorClass.getMethods();
for (Method method : methods) {
if(method.isAnnotationPresent(CheckException.class)){
try{
method.invoke(calculator);
}catch (Exception e){
System.out.println(e.getCause().getMessage());
count++;
}
}
}
System.out.println("一共出现"+count+"次异常");
}
}
JDBC
Tips:不要忘记Add as Library
public class JDBCDemo {
public static void main(String[] args) {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root", "root");
String sql = "select * from user where username = ?";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setString(1,"Joe");
resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
System.out.println(resultSet.getString("id")+" "+resultSet.getString("username"));
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if(preparedStatement!=null){
try {
preparedStatement.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if(connection!=null){
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
}
2 Joe
Process finished with exit code 0
数据库链接池
<c3p0-config>
<default-config>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/mybatis</property>
<property name="user">root</property>
<property name="password">root</property>
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">10</property>
<property name="checkoutTimeout">3000</property>
</default-config>
<named-config name="tododb">
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/tododb</property>
<property name="user">root</property>
<property name="password">root</property>
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">8</property>
<property name="checkoutTimeout">1000</property>
</named-config>
</c3p0-config>
其中如下部分配置可以缺省,存在默认值
<property name="initialPoolSize">5</property>
<property name="maxPoolSize">8</property>
<property name="checkoutTimeout">1000</property>
#初始化时获取连接数,取值应在minPoolSize与maxPoolSize之间。默认为: 3
c3p0.initialPoolSize=5
#连接池中保留的最小连接数,默认为:3
c3p0.minPoolSize=3
#连接池中保留的最大连接数。默认值: 15
c3p0.maxPoolSize=8
#获取一个connection需要的时间,单位毫秒。超过这个时间则超时。
c3p0.checkoutTimeout=3000
public class c3p0Demo {
public static void main(String[] args) throws SQLException {
//如果configName为空,或者不存在名字,则创建默认链接
DataSource ds = new ComboPooledDataSource("tododb");
Connection conn;
for (int i=0; i<=11; i++){
conn = ds.getConnection();
System.out.println(conn);
if(i==5){
conn.close();
}
}
}
}
输出:第6、8个connection是一样的,因为在第6个close归还了
由于配置文件中设置最大连接数为8,所以共有8个不同的连接,第9个超时
com.mchange.v2.c3p0.impl.NewProxyConnection@5fe5c6f [wrapping: com.mysql.jdbc.JDBC4Connection@6979e8cb]
com.mchange.v2.c3p0.impl.NewProxyConnection@5c0369c4 [wrapping: com.mysql.jdbc.JDBC4Connection@2be94b0f]
com.mchange.v2.c3p0.impl.NewProxyConnection@17ed40e0 [wrapping: com.mysql.jdbc.JDBC4Connection@50675690]
com.mchange.v2.c3p0.impl.NewProxyConnection@3ac42916 [wrapping: com.mysql.jdbc.JDBC4Connection@47d384ee]
com.mchange.v2.c3p0.impl.NewProxyConnection@22a71081 [wrapping: com.mysql.jdbc.JDBC4Connection@3930015a]
com.mchange.v2.c3p0.impl.NewProxyConnection@1bc6a36e [wrapping: com.mysql.jdbc.JDBC4Connection@1ff8b8f]
com.mchange.v2.c3p0.impl.NewProxyConnection@71e7a66b [wrapping: com.mysql.jdbc.JDBC4Connection@2ac1fdc4]
com.mchange.v2.c3p0.impl.NewProxyConnection@1c53fd30 [wrapping: com.mysql.jdbc.JDBC4Connection@1ff8b8f]
com.mchange.v2.c3p0.impl.NewProxyConnection@75412c2f [wrapping: com.mysql.jdbc.JDBC4Connection@282ba1e]
Exception in thread "main" java.sql.SQLException: An attempt by a client to checkout a Connection has timed out.
at com.mchange.v2.sql.SqlUtils.toSQLException(SqlUtils.java:118)
at com.mchange.v2.sql.SqlUtils.toSQLException(SqlUtils.java:77)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:690)
at com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource.getConnection(AbstractPoolBackedDataSource.java:140)
at c3p0Demo.main(c3p0Demo.java:14)
Caused by: com.mchange.v2.resourcepool.TimeoutException: A client timed out while waiting to acquire a resource from com.mchange.v2.resourcepool.BasicResourcePool@6f79caec -- timeout at awaitAvailable()
at com.mchange.v2.resourcepool.BasicResourcePool.awaitAvailable(BasicResourcePool.java:1505)
at com.mchange.v2.resourcepool.BasicResourcePool.prelimCheckoutResource(BasicResourcePool.java:644)
at com.mchange.v2.resourcepool.BasicResourcePool.checkoutResource(BasicResourcePool.java:554)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutAndMarkConnectionInUse(C3P0PooledConnectionPool.java:758)
at com.mchange.v2.c3p0.impl.C3P0PooledConnectionPool.checkoutPooledConnection(C3P0PooledConnectionPool.java:685)
... 2 more
配置文件
创建DBUtils工具类
创建成员属性DataSource ds,将加载配置文件放在静态代码块内对ds赋值,创建静态方法getConnection()、close()、getDataSource()
释放资源还可以重载一个方法,当需要prepareStatement时,此处省略
JDBCTemplate
将不再需要手动释放资源,提供了方便的数据库操作的接口,只需要将sql作为参数传递即可
JDBCTemplate详细示例
IOC
bean的作用范围:
默认为单例
scope属性:
指定bean的作用范围
singleton:单例
prototype:多例
request: 作用于web应用的请求范围
session: 作用于web应用的会话范围
global-session:作用域集群环境的会话范围,当不是集群环境,就是session
bean对象的生命周期:
单例:
生命周期和容器相同
多例:
出生:使用对象时才创建
活着:对象在使用过程中一直活着
死亡:垃圾回收
DI
基于注解的IOC
XML | 注解 |
---|---|
配置简单,维护方便 (我们找到类,就相当于找到了对应的配置) | 修改时,不用改源码。不涉及重新编译和部署 |
目标:
通过注解达到和XML一样的配置效果
XML配置的基本格式:
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"
scope="" init-method="" destroy-method="">
<property name="" value="" | ref=""></property>
</bean>
创建对象 | 依赖注入 | 作用范围 | 生命周期 | |
---|---|---|---|---|
XML | bean标签 | property标签 | scope属性 | init/destroy-method |
Annotation | @Component @Repository @Controller @Service | @Autowired @Qualifier @Resource @Value | @Scope | @PostConstruct @PreDestroy |
创建对象:
@Component
作用:把当前类对象存入spring容器
属性:value=”“ 指定bean的id,默认为类名首字母小写
出现位置:类的开头
- 如果只存在有参构造方法,参数会按照@Autowired方式赋值
- 如果同时存在有参和无参构造方法,默认调用无参构造方法
以下三个注解与Component功能一样,用于区分三层结构
@Controller:表现层
@Service:业务层
@Repository:持久层
数据注入:
@Autowired
作用:按照类型注入,容器中有唯一的对象与注入类型相同,则注入成功
支持类型:其他bean类型
出现位置:属性之前
@Qualifier
作用:当容器中有多个相同类型的bean时,可指定id
支持类型:其他bean类型
属性:value:指定bean的id
出现位置:
- 属性之前:必须与@Autowired搭配使用
- 方法参数:可单独存在
@Resource
作用:直接按照 Bean 的 id 注入。
支持类型:其他bean类型
属性:name:指定 bean 的 id。
出现位置:类之前
- 该注解并非spring框架中的
import javax.annotation.Resource;
@Value
作用:注入基本数据类型和 String 类型数据的关键字
属性:value:用于指定值
出现位置:
- 属性之前
- 方法参数
作用范围:
@Scope
作用:指定bean的作用范围
属性:value:指定范围的值
- singleton
- prototype
- request
- session
- global-session
出现位置:类之前
生命周期:
@PostConstruct
作用:指定初始化方法
出现位置:方法之前
执行时间:构造方法之后
@PreDestroy
作用:指定销毁方法
出现位置:方法之前
执行时间:对象销毁之前
纯注解配置
上述基于注解的IOC仍存在bean.xml配置文件,并且在数据库配置时也不得不使用XML
bean.xml
<context:component-scan base-package="com.itheima"></context:component-scan>
数据库配置
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/mybatis</property>
<property name="user">root</property>
<property name="password">root</property>
</bean>
我们希望可以不依赖于bean.xml,完全采用注解来完成全部的配置。
XML | bean.xml | bean标签 | <context:component-scan base-package=“com.itheima”></context:component-scan> |
---|---|---|---|
注解 | @Configuration | @Bean | @ComponentScan |
@Configuration
作用:指定当前类是一个 spring 配置类
位置:类之前
@ComponentScan
作用:指定 spring 在初始化容器时要扫描的包
属性:basePackages/value:指定要扫描的包
位置:类之前
@Bean
作用:创建一个对象,并且放入 spring 容器。
属性:name:指定bean的id
位置:方法之前
@PropertySource
作用:加载.properties文件中的配置
属性:指定 properties 文件位置,如果是在类路径下,需要写上"classpath:"
位置:类之前
@Import
作用:加载其他java文件中的配置(都写在一个配置文件中太长)
属性:value:指定其他java配置类模板
位置:类之前
示例:
#jdbcConfig.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy
jdbc.username=root
jdbc.password=1234
/**
* 和spring连接数据库相关的配置类
*/
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 用于创建一个QueryRunner对象
* @param dataSource
* @return
*/
@Bean(name="runner")
@Scope("prototype")
public QueryRunner createQueryRunner(@Qualifier("ds2") DataSource dataSource){
return new QueryRunner(dataSource);
}
/**
* 创建数据源对象
* @return
*/
@Bean(name="ds2")
public DataSource createDataSource(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl(url);
ds.setUser(username);
ds.setPassword(password);
return ds;
}catch (Exception e){
throw new RuntimeException(e);
}
}
@Bean(name="ds1")
public DataSource createDataSource1(){
try {
ComboPooledDataSource ds = new ComboPooledDataSource();
ds.setDriverClass(driver);
ds.setJdbcUrl("jdbc:mysql://localhost:3306/eesy02");
ds.setUser(username);
ds.setPassword(password);
return ds;
}catch (Exception e){
throw new RuntimeException(e);
}
}
}
@Configuration
@ComponentScan("com.itheima")
@Import(JdbcConfig.class)
@PropertySource("classpath:jdbcConfig.properties")
public class SpringConfiguration {
}
获取容器:
ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
注意:在实际写代码的过程中,我们发现即使省略@Configuration注解也能正常运行,但实际上是有区别的,具体情况可参考https://cloud.tencent.com/developer/article/1657369
Spring 整合 Junit
经过了上述的各种测试后,我们会发现测试方法中总会出现这样一段代码:
ApplicationContext ac = new AnnotationConfigApplicationContext(JdbcConfig.class);
as = ac.getBean("accountServiceImpl", IAccountService.class);
对于测试者来说,这段代码不应该由他们来完成,这应该是开发者的职责。因此Spring框架提供了一个运行器,可以读取配置文件(或注解)来创建容器。
1. 导入依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
2. 使用@RunWith 注解替换原有运行器
在测试类的前面加上这段注解:
@RunWith(SpringJUnit4ClassRunner.class)
3. 使用@ContextConfiguration 指定 spring 配置文件或注解的位置
在测试类前面加上这段注解:
配置文件
@ContextConfiguration(locations = "classpath:bean.xml")
注解
@ContextConfiguration(classes = JdbcConfig.class)
4. 使用@Autowired 给测试类中的变量注入数据
示例:
@RunWith(SpringJUnit4ClassRunner.class)
//@ContextConfiguration(locations = "classpath:bean.xml")
@ContextConfiguration(classes = JdbcConfig.class)
public class AccountServiceTest {
@Autowired
private IAccountService as;
@Test
public void testFindAll() throws SQLException {
as.findAll();
}
}
代理
代理模式在 Java 开发中是一种比较常见的设计模式。设计目的旨在为服务类与客户类之间插入其他功能,插入的功能对于调用者是透明的,起到伪装控制的作用。如租房的例子:房客、中介、房东。对应于代理模式中即:客户类、代理类 、委托类(被代理类)。
- JAVA静态代理
是指由程序员创建或工具生成的代理类,这个类在编译期就已经是确定了的,存在的。
- JAVA动态代理
运行期利用JVM的反射机制生成代理类,这里是直接生成类的字节码,然后通过类加载器载入JAVA虚拟机执行。
相比于静态代理,动态代理在创建代理对象上更加的灵活,动态代理类的字节码在程序运行时,由Java反射机制动态产生。它会根据需要,通过反射机制在程序运行期,动态的为目标对象创建代理对象,无需程序员手动编写它的源代码。动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性,因为反射机制可以生成任意类型的动态代理类。代理的行为可以代理多个方法,即满足生产需要的同时又达到代码通用的目的。
图例:
JDK动态代理
基于接口的动态代理;被代理对象至少存在一个接口
- 创建被代理对象接口以及被代理对象
public interface IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterService(float money);
}
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
- 创建代理对象,并实现匿名内部类
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
CGLIB动态代理
cglib是针对类来实现代理的,它的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
- 导入cglib坐标
由于使用的是第三方库cglib,需要maven坐标导入
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
- 动态创建代理对象,采用匿名内部类
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(10000f);
}
}
两种动态代理的区别:
JDK动态代理(目标对象存在接口时)执行效率高于Cglib
如果目标对象有接口实现,选择JDK代理,如果没有接口实现选择Cglib代理
JDK与CGLIB效率对比以及分析 https://blog.csdn.net/bibiboyx/article/details/109045613
我们来分析一下两种动态代理的底层实现逻辑:
- JDK动态代理
动态生成的代理类$Proxy0.class继承自Proxy.class,Proxy类的构造方法对成员变量invocationHandler进行初始化,而自定义的匿名内部类对象对其进行初始化。
而$Proxy0.class在创建时利用的是producer.getClass().getInterfaces(),即接口的类模板。
也就是说动态创建的代理对象是该接口的一个实现类。
由此我们可以猜测,在动态创建代理时,接口的每个方法实现了以下这段代码:
invocationHandler.invoke(this,当前执行方法,参数);
为了验证这个猜想,我们可以反编译$Proxy0.class:
具体详细内容可参考https://cloud.tencent.com/developer/article/1638472
这篇文章把两种动态代理讲得很清楚!!!
- CGLIB动态代理
生成一个被代理对象的子类,该子类的每一个方法调用MethodInterceptor对象的intercept方法
MethodProxy是intercept()方法中的第四个参数的类型,它是实际类方法的代理引用,使用methodProxy比使用jdk自身的method在效率上会有提升
MethodProxy的使用可以参考https://blog.csdn.net/psd0503/article/details/107116881
此外,cglib可以采用回调过滤器 CallbackFilter,它在拦截目标对象的方法时,可以有选择性的执行方法拦截,也就是选择被代理方法的增强处理。
public class TargetCallbackFilter implements CallbackFilter {
@Override
public int accept(Method method) {
if ("hobby".equals(method.getName()))
return 1;
else
return 0;
}
}
具体详细内容可参考https://cloud.tencent.com/developer/article/1638472
CGLIB 动态代理是通过配置目标类信息,然后利用 ASM 字节码框架进行生成目标类的子类。
ASM字节码操纵框架https://www.jianshu.com/p/a1e6b3abd789
学习完CGLIB,我们可以回头再去分析@configuration配置类https://blog.csdn.net/qq_15719169/article/details/119579612
Spring中的AOP
相关术语
- 连接点
被代理类中可以被增强的方法
- 切入点
被代理类中被增强了的方法
- 通知
拦截到切入点后要做的事情,即增强的内容 通知类型:前置通知,后置通知,异常通知,最终通知,环绕通知
- 目标对象(Target)
被代理的对象
- 织入(Weaving)
是指把增强应用到目标对象来创建新的代理对象的过程。
- 代理(Proxy)
一个类被 AOP 织入增强后,就产生一个结果代理类。
- 切面(Aspect)
是切入点和通知(引介)的结合
基于XML的AOP配置
- 创建接口和被代理类
public interface IAccountService {
void saveAccount();
void updateAccount(int i);
int deleteAccount();
}
public class AccountServiceImpl implements IAccountService{
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新"+i);
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
通知类:
public class Logger {
/**
* 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的pringLog方法开始记录日志了。。。");
}
}
- 导入maven坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
- 配置bean.xml
命名空间
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
配置Ioc
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
配置Aop
- 配置通知类
- 配置切面以及切入点
<!-- 配置Logger类 -->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id。
在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知
aop:before:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* ....AccountServiceImpl.saveAccount())
包名可以使用…表示当前包及其子包
* …AccountServiceImpl.saveAccount()
类名和方法名都可以使用来实现通配
* ….()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用…表示有无参数均可,有参数可以是任意类型
全通配写法:
* ….(…)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl..(…)
其他通知类型切面配置
aop:pointcut:
作用:
用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
属性:
expression:用于定义切入点表达式。
id:用于给切入点表达式提供一个唯一标识
<aop:pointcut expression=“execution(
public void com.itheima.service.impl.AccountServiceImpl.transfer(
java.lang.String, java.lang.String, java.lang.Float) )” id=“pt1”/>
aop:after-returning
作用:
用于配置后置通知
属性:
method:指定通知中方法的名称。
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
执行时间点:
切入点方法正常执行之后。它和异常通知只能有一个执行
<aop:after-returning method=“commit” pointcut-ref=“pt1”/>
aop:after-throwing
作用:
用于配置异常通知
属性:
method:指定通知中方法的名称。
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
执行时间点:
切入点方法执行产生异常后执行。它和后置通知只能执行一个
<aop:after-throwing method=“rollback” pointcut-ref=“pt1”/>
aop:after
作用:
用于配置最终通知
属性:
method:指定通知中方法的名称。
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
执行时间点:
无论切入点方法执行时是否有异常,它都会在其后面执行。
<aop:after method=“release” pointcut-ref=“pt1”/>
环绕通知
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
在环绕通知方法中手动插入代码,表明切入点所在位置:
接口ProceedingJoinPoint可以作为环绕通知的方法参数,并且通过proceed()方法表明切入点位置
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
- 编写测试类
public class AOPTest {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService)ac.getBean("accountService");
as.saveAccount();
as.updateAccount(1);
as.deleteAccount();
}
}
- 运行结果
Logger类中的pringLog方法开始记录日志了。。。
执行了保存
Logger类中的pringLog方法开始记录日志了。。。
执行了更新1
Logger类中的pringLog方法开始记录日志了。。。
执行了删除
基于注解的AOP配置
- 导入maven坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
- 配置beam.xml
命名空间(aop,context)
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
配置spring创建容器时要扫描的包
<context:component-scan base-package="com.itheima"></context:component-scan>
配置spring开启注解AOP的支持
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
- 对被代理类添加注解配置
@Service("accountService")
public class AccountServiceImpl implements IAccountService{
@Override
public void saveAccount() {
System.out.println("执行了保存");
int i=1/0;
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新"+i);
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
-
对通知类进行注解配置
对类进行注解配置|对增强方法注解配置
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}
// @Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}
// @AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
// @AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
// @After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}
@Around("pt1()")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
- 编写测试类
public class AOPTest {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
IAccountService as = (IAccountService)ac.getBean("accountService");
as.saveAccount();
}
}