这是第二天学习大纲:
传统的 SSH(Structs2、Spring、Hibernate)架构的项目基本都是使用的配置文件方式,很少用注解的方式,但其实 Struts2 有注解,Hibernate 也有注解。如果是 SSM(SpringMVC、Spring、Mybatis)这套架构的项目有可能使用到注解方式,所以重点会在这套架构学习中讲解注解方式,但 SSH 该架构也是用到了 Spring,所以还是会先讲解注解方式。
先想一想注解出现的目的是干嘛呢?其实注解最主要一个目的就是要替代传统 XML 文件方式,用注解会变得更简单,因为传统 XML 文件需要写很多很多配置,如果多了会变得很臃肿。
注解方式大概步骤如下:
1. 步骤一:导入注解开发所有需要的jar包 * 引入IOC容器必须的6个jar包 * 多引入一个:Spring框架的AOP的jar包,spring-aop的jar包 2. 步骤二:创建对应的包结构,编写Java的类 * UserService -- 接口 * UserServiceImpl -- 具体的实现类 3. 步骤三:在src的目录下,创建applicationContext.xml的配置文件,然后引入约束。注意:因为现在想使用注解的方式,那么引入的约束发生了变化 * 需要引入context的约束,具体的约束如下 <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- bean definitions here --> </beans> 4. 步骤四:在applicationContext.xml配置文件中开启组件扫描 * Spring的注解开发:组件扫描 <context:component-scan base-package="com.itheima.demo1"/> * 注意:可以采用如下配置 <context:component-scan base-package="com.itheima"/> 这样是扫描com.itheima包下所有的内容 5. 步骤五:在UserServiceImpl的实现类上添加注解 * @Component(value="userService") -- 相当于在XML的配置方式中 <bean id="userService" class="..."> 6. 步骤六:编写测试代码 public class SpringDemo1 { @Test public void run1(){ ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService us = (UserService) ac.getBean("userService"); us.save(); } } 复制代码
注:在导入如下 6 个包之外
还需导入 Spring 框架的 AOP 的 jar 包 spring-aop-4.2.4.RELEASE.jar
。
简单例子:这里简单用代码来演示下。
配置文件 applicationContext.xml
:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- bean definitions here --> <!-- 开启注解的扫描 --> <context:component-scan base-package="com.itheima.demo1"/> </beans> 复制代码
UserService.java:(业务层接口)
public interface UserService { public void sayHell(); } 复制代码
UserServiceImpl.java:(业务层实现类)
/** * 组件注解,标记类 * <bean id="userService" class="com.itheima.demo1.UserServiceImpl"> 等价于 @Component(value="userService") * @author Administrator */ @Component(value="userService") public class UserServiceImpl implements UserService { public void sayHell() { System.out.println("hello Spring!!"); userDao.save(); } } 复制代码
运行:
/** * 注解的方式 */ @Test public void run2(){ // 获取工厂,加载配置文件 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); // 获取对象 UserService us = (UserService) ac.getBean("userService"); us.sayHell(); } 复制代码
运行结果:hello Spring!!
可以看出注解方式与之前方式效果没什么区别。
以下是主要内容:
1. @Component:组件.(作用在类上) 2. Spring中提供@Component的三个衍生注解:(功能目前来讲是一致的) * @Controller -- 作用在WEB层 * @Service -- 作用在业务层 * @Repository -- 作用在持久层 * 说明:这三个注解是为了让标注类本身的用途清晰,Spring在后续版本会对其增强 3. 属性注入的注解(说明:使用注解注入的方式,可以不用提供set方法) * 如果是注入的普通类型,可以使用value注解 * @Value -- 用于注入普通类型 * 如果注入的是对象类型,使用如下注解 * @Autowired -- 默认按类型进行自动装配 * 如果想按名称注入 * @Qualifier -- 强制使用名称注入 * @Resource -- 相当于@Autowired和@Qualifier一起使用 * 强调:Java提供的注解 * 属性使用name属性 复制代码
继续用代码演示,承上面代码例子,我们修改代码:
UserServiceImpl.java:
@Component(value="userService") // @Scope(value="prototype") public class UserServiceImpl implements UserService { // 给name属性注入美美的字符串,setName方法还可以省略不写 @Value(value="美美") private String name; /*public void setName(String name) { this.name = name; }*/ // @Autowired 按类型自动装配 // @Autowired // @Qualifier(value="userDao") // 按名称注入 // 是Java的注解,Spring框架支持该注解 @Resource(name="userDao") private UserDao userDao; public void sayHell() { System.out.println("hello Spring!!"+name); userDao.save(); } } 复制代码
几点强调的:
@Component(value="userService")
可更改为 @Service(value="userService")
使用 @Value(value="美美")
给 name 属性注入美美的字符串,setName 方法还可以省略不写
@Autowired
按类型自动装配什么意思呢?用代码解释下:
UserDao.java:
public interface UserDao public void save(); } 复制代码
UserDaoImpl.java:
/** * UserDaoImpl交给IOC的容器 * @author Administrator */ @Repository(value="userDao") public class UserDaoImpl implements UserDao { public void save() { System.out.println("保存客户..."); } } 复制代码
若使用 @Autowired
@Autowired private UserDao userDao; 复制代码
进行属性注入,它会自动找 UserDao 接口的实现类对象,如下图为 UserDaoImpl 类对象,再解释下:
首先 IOC 容器解析完,会有很多对象,比如有 UserDaoImpl、UserServiceImpl 对象(可以观察代码,这两个类有进行注入),其中 UserServiceImpl 有成员属性 UserDao, @Autowired
属性注入的意思就是它会去找容器的 UserDao 实现类对象,发现有一个实现类 UserDaoImpl 对象,然后默认就把这个注入进来了,这就是按类型自动装配。但这种方式不好,因为如果 UserDao 有多个实现类,有可能装配其他的而不是想要的。(可以试着把 @Repository(value="userDao")
的 value 值改为其他值,可以发现还是能运行 UserDaoImpl 对象的 sava 方法,因为 @Autowired
按类型自动装配的。)
一般按名称来装配,即通过给 ID 名称注入。怎么做的呢?比如:
UserDaoImpl.java 改为:
@Repository(value="ud") public class UserDaoImpl implements UserDao { public void save() { System.out.println("保存客户..."); } } 复制代码
然后可以使用 @Qualifier
注入
@Qualifie(value="ud") //按名称 ud 注入 private UserDao userDao; 复制代码
另外可以使用 Java 提供的一个注解,Spring 框架支持该注解。
@Resource(name="userDao") private UserDao userDao; 复制代码
如下图,可以看到所在包的不同
1. Bean的作用范围注解 * 注解为@Scope(value="prototype"),作用在类上。值如下: * singleton -- 单例,默认值 * prototype -- 多例 2. Bean的生命周期的配置(了解) * 注解如下: * @PostConstruct -- 相当于init-method * @PreDestroy -- 相当于destroy-method 复制代码
如 UserServiceImpl.java:(多例)
@Component(value="userService") @Scope(value="prototype") public class UserServiceImpl implements UserService { .... @PostConstruct public void init(){ System.out.println("初始化..."); } } 复制代码
Spring 框架整合 JUnit 单元测试,什么意思呢?我们可以先看下之前都是怎么进行 JUnit 单元测试的,如下某个例子:
@Test public void run2(){ // 获取工厂,加载配置文件 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); // 获取对象 UserService us = (UserService) ac.getBean("userService"); us.sayHell(); } 复制代码
可以看到每次测试我们都得手动 new 这么个工厂,加载配置文件,这样很麻烦。为了方便我们测试,Spring 提供了整合 JUnit 单元测试方案。步骤如下:
1. 为了简化了JUnit的测试,使用Spring框架也可以整合测试 2. 具体步骤 * 要求:必须先有JUnit的环境(即已经导入了JUnit4的开发环境)!! * 步骤一:在程序中引入:spring-test.jar * 步骤二:在具体的测试类上添加注解 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class SpringDemo1 { @Resource(name="userService") private UserService userService; @Test public void demo2(){ userService.save(); } } 复制代码
代码演示:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class Demo2 { @Resource(name="userService") private UserService UserService; @Test public void run1(){ // 原来:获取工厂,加载配置文件,getBean() UserService.sayHell(); //直接调用就行 } } 复制代码
在讲什么 AOP 之前,我们先看例子,如下图:
如上图。在企业开发中我们可能需要写很多很多 DAO 层代码,可能有的是保存用户信息,有的是保存书籍信息,每个方法里面都写了很多代码,但假如有一天有新的需求,比如在保存用户信息的时候记录日志,那不得不打开 DAO 层源代码去修改,如果保存书籍信息也得记录日志,那也得去修改 BookDao 代码。这样的方式不是特别理想,万一不小心的修改,但一上线就报错了出问题了。
那有什么其他的方式,在不改变源代码的情况下,记录日志的功能加上?有的,AOP 可以。
另外,像上图,还可以看到每个 DAO 层都有提交事务这个步骤,每次都得写提交事务的代码,这样其实很麻烦,AOP 可以解决该问题。
1、什么是 AOP 的技术?
2、AOP:面向切面编程(思想。—— 解决OOP遇到一些问题)
3、AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)
4、为什么要学习AOP:可以在不修改源代码的前提下,对程序进行增强!!
我们举例来理解下 AOP。我们可以先了解下模块化手机,百度百科模块化手机。
简单讲就是把手机分成好几个不同模块,比如摄像头模块、电池模块、屏幕模块。这样可以明显看到有很多好处,解耦合了,并且可以自定义,比如摄像头换别的更好的摄像头。
其实 AOP 也是这个思想,如图:
把「获取session」、「开启事务」等等看做一个个模块。其中有的模块可以直接拿来用就行,也可以自定义。我们可以把很多重复模块直接拿出来编写就行,AOP 采取了横向抽取机制。(这就是 AOP 的宏观思想)
Srping 框架的 AOP 技术底层是采用的代理技术。如图:
解释下,如上图,代理对象可以控制目标对象的访问,原来没有代理对象,其他的请求可以直接访问目标对象,让目标对象代码执行,现在有了代理对象,其访问先要经过代理对象,代理对象中可以写任何代码,做很多功能,比如在目标对象执行之前执行记录日志。这样的方式可以看到,我们并没改变 save 方法,但仍能加上了想要的功能。AOP 底层就是这种思想。有两种代理方式:
1. Srping框架的AOP技术底层也是采用的代理技术,代理的方式提供了两种 1. 基于JDK的动态代理 * 必须是面向接口的,只有实现了具体接口的类才能生成代理对象 2. 基于CGLIB动态代理 * 对于没有实现了接口的类,也可以产生代理,产生这个类的子类的方式 2. Spring的传统AOP中根据类是否实现接口,来采用不同的代理方式 1. 如果实现类接口,使用JDK动态代理完成AOP 2. 如果没有实现接口,采用CGLIB动态代理完成AOP 复制代码
UserDao.java:
public interface UserDao { public void save(); public void update(); } 复制代码
UserDaoImpl.java:
public class UserDaoImpl implements UserDao { public void save() { System.out.println("保存用户..."); } public void update() { System.out.println("修改用户..."); } } 复制代码
MyProxyUtils.java:
/** * 使用JDK的方式生成代理对象 * @author Administrator */ public class MyProxyUtils { public static UserDao getProxy(final UserDao dao) { // 使用Proxy类生成代理对象 UserDao proxy = (UserDao) Proxy.newProxyInstance(dao.getClass().getClassLoader(), dao.getClass().getInterfaces(), new InvocationHandler() { // 代理对象方法一执行,invoke方法就会执行一次 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if("save".equals(method.getName())){ System.out.println("记录日志..."); // 开启事务 } // 提交事务 // 让dao类的save或者update方法正常的执行下去 return method.invoke(dao, args); } }); // 返回代理对象 return proxy; } } 复制代码
测试:
@Test public void run1(){ // 目标对象 UserDao dao = new UserDaoImpl(); dao.save(); dao.update(); System.out.println("============================="); // 使用工具类,获取到代理对象 UserDao proxy = MyProxyUtils.getProxy(dao); // 调用代理对象的方法 proxy.save(); proxy.update(); } 复制代码
运行结果:
CGLIB 其实本身是一个单独的项目,被 Spring 单独拿过来了作为它内在的一种技术。
在使用之前,需要引入CGLIB 的开发的 jar 包,在 Spring 框架核心包中已经引入了 CGLIB 的开发包了,所以直接导入 Spring 核心开发包 spring-core-4.2.4.RELEASE.jar
就导入了 CGLIB 开发的 jar 包。
BookDaoImpl.java:(实现类)
public class BookDaoImpl { public void save(){ System.out.println("保存图书..."); } public void update(){ System.out.println("修改图书..."); } } 复制代码
MyCglibUtils.java:
public class MyCglibUtils { /** * 使用CGLIB方式生成代理的对象 * @return */ public static BookDaoImpl getProxy() { Enhancer enhancer = new Enhancer(); // 设置父类 enhancer.setSuperclass(BookDaoImpl.class); // 设置回调函数 enhancer.setCallback(new MethodInterceptor() { // 代理对象的方法执行,回调函数就会执行 public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { if(method.getName().equals("save")){ System.out.println("记录日志..."); } // 正常执行 return methodProxy.invokeSuper(obj, args); } }); // 生成代理对象 BookDaoImpl proxy = (BookDaoImpl) enhancer.create(); return proxy; } } 复制代码
测试:
@Test public void run1(){ // 目标对象 BookDaoImpl dao = new BookDaoImpl(); dao.save(); dao.update(); System.out.println("============================"); // 使用CGLIB方式生成代理对象 BookDaoImpl proxy = MyCglibUtils.getProxy(); proxy.save(); proxy.update(); } 复制代码
运行结果:
////////////////////////////////////////////////////////////
来了解下企业中正常编写代码顺序:
如上,我们很多人写程序,就是直接干,从 JSP 页面到 Action 层,到 Service 层 再到 Dao 层。
在企业中一般都会是这样,先定义 service 接口,因为做一个系统,系统的业务是固定的,一般接口都是技术带头人定义的,然后在编写 service 实现类,再 Dao 层接口,再 Dao 层实现类,然后写完了进行测试,测试完成了再去写页面。一般大公司都是这个规范,如果是小公司可能就是直接干,遇到问题再说。所以在大公司工作是很枯燥的,因为有可能你只负责某一块。
1. Joinpoint(连接点) -- 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点 2. Pointcut(切入点) -- 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义 3. Advice(通知/增强) -- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能) 4. Introduction(引介) -- 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field 5. Target(目标对象) -- 代理的目标对象 6. Weaving(织入) -- 是指把增强应用到目标对象来创建新的代理对象的过程 7. Proxy(代理) -- 一个类被AOP织入增强后,就产生一个结果代理类 8. Aspect(切面) -- 是切入点和通知的结合,以后咱们自己来编写和配置的 复制代码
下图解释下:
大概步骤:
1. 步骤一:创建JavaWEB项目,引入具体的开发的jar包 * 先引入Spring框架开发的基本开发包 * 再引入Spring框架的AOP的开发包 * spring的传统AOP的开发的包 * spring-aop-4.2.4.RELEASE.jar * com.springsource.org.aopalliance-1.0.0.jar * aspectJ的开发包 * com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar * spring-aspects-4.2.4.RELEASE.jar 2. 步骤二:创建Spring的配置文件,引入具体的AOP的schema约束 <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"> 3. 步骤三:创建包结构,编写具体的接口和实现类 * com.itheima.demo2 * CustomerDao -- 接口 * CustomerDaoImpl -- 实现类 4. 步骤四:将目标类配置到Spring中 <bean id="customerDao" class="com.itheima.demo3.CustomerDaoImpl"/> 5. 步骤五:定义切面类 public class MyAspectXml { // 定义通知 public void log(){ System.out.println("记录日志..."); } } 6. 步骤六:在配置文件中定义切面类 <bean id="myAspectXml" class="com.itheima.demo3.MyAspectXml"/> 7. 步骤七:在配置文件中完成aop的配置 <aop:config> <!-- 引入切面类 --> <aop:aspect ref="myAspectXml"> <!-- 定义通知类型:切面类的方法和切入点的表达式 --> <aop:before method="log" pointcut="execution(public * com.itheima.demo3.CustomerDaoImpl.save(..))"/> </aop:aspect> </aop:config> 8. 完成测试 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class Demo3 { @Resource(name="customerDao") private CustomerDao customerDao; @Test public void run1(){ customerDao.save(); customerDao.update(); customerDao.delete(); } } 复制代码
CustomerDao.java:
public interface CustomerDao { public void save(); public void update(); } 复制代码
CustomerDaoImpl.java:
public class CustomerDaoImpl implements CustomerDao { public void save() { // 模拟异常 // int a = 10/0; System.out.println("保存客户..."); } public void update() { System.out.println("修改客户..."); } } 复制代码
MyAspectXml.java:
/** * 切面类:切入点 + 通知 * @author Administrator */ public class MyAspectXml { /** * 通知(具体的增强) */ public void log(){ System.out.println("记录日志..."); } /** * 最终通知:方法执行成功或者出现异常,都会执行 */ public void after(){ System.out.println("最终通知..."); } /** * 方法执行之后,执行后置通知。程序出现了异常,后置通知不会执行的。 */ public void afterReturn(){ System.out.println("后置通知..."); } /** * 环绕通知:方法执行之前和方法执行之后进行通知,默认的情况下,目标对象的方法不能执行的。需要手动让目标对象的方法执行 */ public void around(ProceedingJoinPoint joinPoint){ System.out.println("环绕通知1..."); try { // 手动让目标对象的方法去执行 joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("环绕通知2..."); } } 复制代码
applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?> <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"> <!-- bean definitions here --> <!-- 配置客户的dao --> <bean id="customerDao" class="com.itheima.demo3.CustomerDaoImpl"/> <!-- 编写切面类配置好 --> <bean id="myAspectXml" class="com.itheima.demo3.MyAspectXml"/> <!-- 配置AOP --> <aop:config> <aop:aspect ref="myAspectXml"> <!-- 切入点的表达式 1. execution() 固定的,不能不写 2. public 可以省略不写 3. void,返回值可以出现 * 表示任意的返回值,返回值类型不能不写 4. 可以使用 * 代替的,不能不编写的,简写方式:*..*方法 5. *DaoImpl 6. 方法 save* 7. 方法的参数: --> <!-- <aop:before method="log" pointcut="execution(public void com.itheima.demo3.CustomerDaoImpl.save())"/> --> <!-- public 可以省略不写 --> <!-- <aop:before method="log" pointcut="execution(void com.itheima.demo3.CustomerDaoImpl.save())"/> --> <!-- void,返回值可以出现 * 表示任意的返回值,返回值类型不能不写 --> <!-- <aop:before method="log" pointcut="execution(* com.itheima.demo3.CustomerDaoImpl.save())"/> --> <!-- 包名可以使用 * 代替,不能不写 --> <!-- <aop:before method="log" pointcut="execution(* com.itheima.*.CustomerDaoImpl.save())"/> --> <!-- 包的简写的方式,任意的包的结构 --> <!-- <aop:before method="log" pointcut="execution(* *..*.CustomerDaoImpl.save())"/> --> <!-- 编写类的写法 --> <!-- <aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save())"/> --> <!-- 方法编写 --> <!-- <aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save*())"/> --> <!-- 参数列表:出现一个*,表示一个参数,任意参数使用 .. --> <aop:before method="log" pointcut="execution(* *..*.*DaoImpl.save*(..))"/> </aop:aspect> </aop:config> </beans> 复制代码
1. 再配置切入点的时候,需要定义表达式,重点的格式如下:execution(public * *(..)),具体展开如下: * 切入点表达式的格式如下: * execution([修饰符] 返回值类型 包名.类名.方法名(参数)) * 修饰符可以省略不写,不是必须要出现的。 * 返回值类型是不能省略不写的,根据你的方法来编写返回值。可以使用 * 代替。 * 包名例如:com.itheima.demo3.BookDaoImpl * 首先com是不能省略不写的,但是可以使用 * 代替 * 中间的包名可以使用 * 号代替 * 如果想省略中间的包名可以使用 .. * 类名也可以使用 * 号代替,也有类似的写法:*DaoImpl * 方法也可以使用 * 号代替 * 参数如果是一个参数可以使用 * 号代替,如果想代表任意参数使用 .. 复制代码
1. 前置通知 * 在目标类的方法执行之前执行。 * 配置文件信息:<aop:after method="before" pointcut-ref="myPointcut3"/> * 应用:可以对方法的参数来做校验 2. 最终通知 * 在目标类的方法执行之后执行,如果程序出现了异常,最终通知也会执行。 * 在配置文件中编写具体的配置:<aop:after method="after" pointcut-ref="myPointcut3"/> * 应用:例如像释放资源 3. 后置通知 * 方法正常执行后的通知。 * 在配置文件中编写具体的配置:<aop:after-returning method="afterReturning" pointcut-ref="myPointcut2"/> * 应用:可以修改方法的返回值 4. 异常抛出通知 * 在抛出异常后通知 * 在配置文件中编写具体的配置:<aop:after-throwing method="afterThorwing" pointcut-ref="myPointcut3"/> * 应用:包装异常的信息 5. 环绕通知 * 方法的执行前后执行。 * 在配置文件中编写具体的配置:<aop:around method="around" pointcut-ref="myPointcut2"/> * 要注意:目标的方法默认不执行,需要使用ProceedingJoinPoint对来让目标对象的方法执行。 复制代码