spring框架的学习->从零开始学JAVA系列
阅读原文时间:2022年04月14日阅读:1

目录

Spring框架的学习

其实就是有人写好的代码,将其封装好了,形成一个比较统一的解决方案,称之为框架。我们直接拿来使用其他人已经写好的功能。

发明者:Rod Johnson -- 罗德·约翰逊

  • 引入对应的jar包
  • 查询其API文档进行学习
  • 配置框架的配置文件(框架运行时需要的常量)

概念

Spring是一个控制反转(IOC)和面向切面(AOP)的轻量级框架。

作用

  • 降低代码之间的耦合度,提高代码的扩展性和可维护性
  • 提升开发的效率

内容

  • SpringIOC 依赖注入

  • SpringAop 面向切面

  • SpringTX 事物

    Spring是一个框架集,内部集成了很多的功能,可以根据需求选择对应的子框架使用

概念

  • 在传统的系统开发中,层与层之间的耦合性太高,(例如业务层A直接创建了持久层B的对象)。而这个时候,一旦需要更换持久层的对象,则需要在业务层中一个个全部替换,可维护性极差。这种层与层的依赖方式称为:直接依赖。 A依赖B
  • IOC为控制反转,意思就是由曾经的直接依赖,变为间接依赖。例如上述中的,业务层不再直接依赖持久层的对象,而是通过(Spring的容器C)来间接依赖持久层的对象。A依赖C

作用

综上所述,IOC控制反转的作用主要是为了解决层与层之间的高耦合关系,由直接依赖转换为间接依赖。

基本使用流程

  • 去官方进行Spring的下载:spring下载地址。libs-release-local -> org -> springframework -> spring -> 选择版本后右侧有个下载的url,进入后就能下载了

  • 其中可以下载的文件:dist 为jar包等,docs为文档,schema为xsd约束文件

  • 在项目中引入SpringIOC所必要的jar包

  • 在src目录下添加一个配置文件,这里叫做:application.xml

  • 编写测试类进行测试

    package com.it.qxkj.test;

    import com.it.qxkj.mapper.StudentMapper;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;

    public class SpringIoc_01 {
    public static void main(String[] args) {
    // 直接依赖
    //StudentMapper studentMapper = new StudentMapper();

        // 间接依赖
        ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");
        StudentMapper studentMapper = (StudentMapper) ac.getBean("studentMapper");
        // 这里是随便定义的一个方法,方法中就是一个System.out.println()语句
        studentMapper.show();
    }

    }

SpringIOC创建对象的三种方式

问题引入:通过如上的小案例可以看出,虽然成功的将直接依赖变成了间接依赖,但是如果创建的对象中有属性需要初始化呢?

问题解决方案:通过如下三种方式

通过构造器方式(如下展示的代码片段均在 application.xml 中)

  • 无参构造器创建对象

    <!-- spring容器默认使用无参构造的方式创建对象
         id : 从spring容器(Map)中获取到当前对象的key
         class:需要创建的对象的全限定类名
         scope:默认为singleton,在Spring容器加载时就创建(只有一个对象)
                prototype :在使用getBean获取该对象时才创建(会创建多个对象)
     -->
    <bean id="stu1"  class="com.it.qxkj.pojo.Student" scope="prototype"/>
  • 有参构造器创建对象

    <!-- 有参构造
        index : 形式参数的下标位置,从0开始
        name: 属性名
        type:属性的全限定类名
        value: 需要赋的值
        ref:对当前spring容器的引用
     -->
    <bean id="stu2" class="com.it.qxkj.pojo.Student">
        <constructor-arg index="0" name="name" type="java.lang.String" value="zhangsan"/>
        <constructor-arg index="1" name="age" type="int" value="15"/>
    </bean>

通过属性注入的方式(对应属性的set方法)

<bean id="stu3" class="com.it.qxkj.pojo.Student">
        <property name="name" value="张三"/>
        <property name="age" value="18"/>
    </bean>

通过工厂方式创建对象

  • 动态工厂(非静态方法创建对象)

    <!-- 创建一个动态工厂对象 -->
    <bean id="actFactory" class="com.it.qxkj.factory.ActStudentFactory"/>
    
    <!-- 动态工厂创建对象 -->
    <bean id="stu4" factory-bean="actFactory" factory-method="getStudent" />
  • 静态工厂(静态方法创建对象)

依赖注入DI的使用

如上,我们已经明白了Spring如何帮我们创建对象,但是有另一个问题:

创建出来的对象如果有其他对象的引用怎么办?这个时候使用构造器、set属性注入的方式的value属性就不能满足需求了,因为value是一个常量值。

解决方案:使用ref属性,引入一个Spring容器中的对象

示例

1、创建一个Teacher类,里面可以什么都没有,也可以自定义一点属性

2、给Student对象增加一个引用类型为Teacher的属性(顺便加上set方法)

    <bean id="teacher1" class="com.it.qxkj.pojo.Teacher" />

    <bean id="stu1" class="com.it.qxkj.pojo.Student">
        <property name="name" value="张三"/>
        <property name="age" value="18"/>
        <property name="teacher" ref="teacher1"/>
    </bean>

Spring 整合 MyBatis实现用户登录

在mysql中创建一个t_user用户表(id,uname,pwd)

create table t_user(
    id int(11) primary key auto_increment,
    uname varchar(32) not null,
    pwd varchar(32) not null
);
insert into t_user values(default,'zhangsan','111111'),(default,'lisi','111111');

创建一个javaWeb项目

  • 在项目中引入Mybatis的jar包、Springjar包、gson、连接数据库的驱动等…

  • 创建一个login.jsp页面编写登录表单代码,并设置web.xml欢迎页为login.jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>

    ">

    ${requestScope.errorMsg}

    用户名:

    密码:

配置一个User实体类

public class User implements Serializable {

    private Integer id;

    private String uname;

    private String pwd;
    // 省略get、set、toString等方法
}

创建一个Servlet用于处理登录请求-> 依赖Service业务层

package com.it.controller;

import com.it.pojo.User;
import com.it.service.LoginService;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author dengqixing
 * @date 2021/7/22
 */
@WebServlet(urlPatterns = "/starlet/LoginServlet", loadOnStartup = 1)
public class LoginController extends HttpServlet {
    private ApplicationContext ac;
    private LoginService loginService;

    @Override
    public void init() throws ServletException {
        // 由于web.xml中配置了监听器,在项目启动时就将Spring容器加载进ServletContext作用域中了,所以这里可以直接获取到
        ac = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
        loginService = (LoginService) ac.getBean("loginService");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1、处理编码
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html;charset=UTF-8");
        // 2、接收数据
        String uname = req.getParameter("uname");
        String pwd = req.getParameter("pwd");
        // 3、处理业务
        User user = loginService.login(uname, pwd);
        // 4、返回结果
        if(user != null) {
            req.getSession().setAttribute("user",user);
            resp.sendRedirect(req.getContextPath() + "/main.jsp");
        }else {
            req.setAttribute("errorMsg","登录失败,请重试");
            req.getRequestDispatcher("/login.jsp").forward(req, resp);
        }
    }
}

创建一个Service用于处理登录业务 -> 依赖Dao 持久层

package com.it.service;

import com.it.mapper.LoginMapper;
import com.it.pojo.User;

/**
 * @author dengqixing
 * @date 2021/7/22
 */
public class LoginService {
    private LoginMapper loginMapper;

    public User login(String uname, String pwd) {
        User oldUser = new User(uname, pwd);
        return loginMapper.selectUser(oldUser);
    }

    public void setLoginMapper(LoginMapper loginMapper) {
        this.loginMapper = loginMapper;
    }
}

创建一个Dao用于完成登录请求的数据库访问

package com.it.mapper;

import com.it.pojo.User;
import org.apache.ibatis.annotations.Select;

/**
 * @author dengqixing
 * @date 2021/7/22
 */
public interface LoginMapper {

    @Select("select * from t_user where uname = #{uname} and pwd = #{pwd}")
    User selectUser(User user);
}

在src目录下创建application.xml,在其中配置Mybatis

<?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
                           https://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 引入配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties" />

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置SqlSessionFactoryBean,放入数据源,其实就是一个SqlSessionFactory -->
    <bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置Mapper扫描,加所有扫描到的Mapper都添加进Spring容器 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.it.mapper"/>
        <!-- 该属性被弃用,因为有时会出现加载MyBatis时,配置文件还没有被加载进来,导致数据源不存在。出现报错
            <property name="sqlSessionFactory" ref="sqlSessionFactoryBean"/>
        -->
        <!-- 改用了sqlSessionFactoryBeanName,传入容器的值,然后在spring初始化结束后用该value值从Spring容器中获取对象-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
    </bean>

    <bean id="loginService" class="com.it.service.LoginService">
        <property name="loginMapper" ref="loginMapper"/>
    </bean>
</beans>

jdbc.properties文件

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/demo1?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=root

mybatis-config.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 配置日志 -->
    <settings>
        <!-- 指定哪个日志实现,具体取值可查看帮助文档,会自动在类路径下寻找log4j.properties配置文件 -->
        <setting name="logImpl" value="LOG4J"></setting>
    </settings>

    <typeAliases>
        <package name="com.it.pojo"/>
    </typeAliases>

</configuration>

log4j.properties文件

## 全局日志输出,输出error级别的,输出 stdout,logfile(这2个名字是自己定义的)
log4j.rootLogger = error,stdout,logfile

# 指定单个类输出日志,这里指定通过xml文件动态生成的代理类,这样可以打印sql语句
# 可以理解为 com.it.mapper.*,会打印该命名空间下所有类的日志
log4j.logger.com.it.mapper=debug

### 输出信息到控制台
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %m%n

### 文件日志,只记录error日志到 D盘下的error.log文件
log4j.appender.logfile = org.apache.log4j.DailyRollingFileAppender
log4j.appender.logfile.File =D://error.log
log4j.appender.logfile.Append = true
# 指定日志消息输出的最低层次
log4j.appender.logfile.Threshold = ERROR
log4j.appender.logfile.layout = org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss}  [ %t:%r ] - [ %p ]  %m%n

配置web.xml (重要)

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <!-- 这个name属性值会被ContextLoaderListener监听器读取,必须固定 -->
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:application.xml</param-value>
    </context-param>

    <!--配置该监听器,将Spring容器存入到Application作用域中,也就是ServletContext-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <welcome-file-list>
        <welcome-file>login.jsp</welcome-file>
    </welcome-file-list>
</web-app>

项目的最终结构

关于SpirngIOC自动注入的三种方式

问题引出:虽然通过DI依赖注入可以完成引用类型的注入,但是一旦需要依赖注入的属性不断增加,男刀每一次都需要手动进行依赖注入吗?

解决方案:Spring自动注入

使用byName的方式完成注入(属性名与bean的id名一致)

<bean id="stu1" class="com.it.qxkj.pojo.Student" autowire="byName" />

使用byType的方式完成注入(通过判断类型进行注入)

<bean id="stu1" class="com.it.qxkj.pojo.Student" autowire="byType" />

使用默认注入的方式(默认值)

<bean id="stu1" class="com.it.qxkj.pojo.Student" autowire="default">

不使用自动注入

<bean id="stu1" class="com.it.qxkj.pojo.Student" autowire="no">

概念及问题引入

  • 问题引入:我们学习使用了SpringIoc之后,能够成功的完成层一层之间的解耦,但是有另一个问题出现了。那就是我们进行功能扩展时应该怎么办?例如有一个C接口,A实现了C,这个时候需要扩展A实现的功能。
  • 在实际开发中,我们是不能轻易的更改已经写好的源代码的,更何况A甚至可能不是我们写的,那我们扩展起来就会变得非常的麻烦。

APO的专业名词

  • 真实对象:要进行功能扩展的对象,相当于A对象

  • 代理对象:完成了扩能扩展的对象,相当于B对象

  • 切点:需要被扩展的方法称之为切点

  • 前置通知方法:在切点之前执行的方法

  • 后置通知方法:在切点后执行的方法

  • 环绕通知:环绕着切点进行执行的方法,在前置方法之后,在后置方法之前

  • 异常通知:在切点方法出现了异常时执行的方法

  • 切面:由前置通知 + 切点 + 后置通知形成的横向执行的面

  • 织入:由前置通知 + 切点 + 后置通知形成一个切面的过程成为织入

传统解决方案

  • 定义一个B类,来实现C接口,在B的实现方法中,创建A的对象

    class B implements C {
    @Override
    public String login() {
    // 扩展前的代码
    A a = new A();
    a.login();
    // 扩展后的代码
    }
    }

  • (重要),在获取对象的时候不直接获取A对象,而是获取C接口,通过多态的特性来获取到B

  • 这个时候C与B的依赖关系可以使用SpringIOC来完成解耦。

SpringAOP的解决方案(2种,分别为SchemaBase与Aspectj)

  • 引入springIOC以及SpringAOP的jar包
  • 定义指定的通知(前置、后置、环绕、异常)
  • 在Spring配置文件中进行配置

SpringAOP的使用(SchemaBase方式)

引入相对应的jar包

编写所需的通知

前置通知
package com.it.advice;

import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;

public class MyBefore implements MethodBeforeAdvice {

    /**
     *
     * @param method 当前切点的方法对象
     * @param objects 当前执行方法的参数
     * @param o 真实对象,相当于new A();
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知执行了呢");
    }
}
后置通知
package com.it.advice;

import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;

public class MyAfter implements AfterReturningAdvice {
    /**
     *
     * @param o 切点方法的返回值
     * @param method 切点方法对象
     * @param objects 方法参数
     * @param o1 真实对象
     * @throws Throwable
     */
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("后置通知执行了");
    }
}
异常通知
package com.it.advice;

import org.springframework.aop.ThrowsAdvice;

public class MyThrow implements ThrowsAdvice {
    /**
     * 该方法的格式固定,请务必这样书写
     * @param ex 异常对象
     * @throws Throwable
     */
    public void afterThrowing(Exception ex) throws Throwable {
        System.out.println("我是异常通知");
    }
}
环绕通知
package com.it.advice;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import java.lang.reflect.Method;

public class MyRound implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        // 参数获取
            // 真实对象
            Object aThis = methodInvocation.getThis();
            // 切点方法
            Method method = methodInvocation.getMethod();
            // 方法参数
            Object[] arguments = methodInvocation.getArguments();
        // 执行切点方法
        method.invoke(aThis,arguments);

        // 1、前置通知
        System.out.println("我是环绕通知切点方法执行之前");
        // 2、放行
        Object proceed = null;
        try {
            //该方法会返回切点方法的返回值
            proceed = methodInvocation.proceed();
        }catch (Throwable e) {
            // 3、异常通知
            System.out.println("切点方法异常了:" + e.getMessage());
        }
        // 4、后置通知
        System.out.println("我是环绕通知切点方法执行之后");

        return proceed;
    }
}

在spring的配置文件中进行配置

<?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
                           https://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--需要被代理的对象-->
    <bean id="a" class="com.it.utils.A"/>
    <!-- 前置通知 -->
    <bean id="myBefore" class="com.it.advice.MyBefore"/>
    <!-- 后置通知 -->
    <bean id="myAfter" class="com.it.advice.MyAfter"/>
    <!-- 异常通知 -->
    <bean id="myThrow" class="com.it.advice.MyThrow"/>
    <!-- 环绕通知 -->
    <bean id="myRound" class="com.it.advice.MyRound"/>

    <aop:config>
        <!-- 切入点表达式 -->
        <aop:pointcut id="p1" expression="execution(* com.it.utils.A.*(..))"/>
        <!-- 配置通知,会根据通知bean实现的接口来判断是何种通知 -->
        <aop:advisor advice-ref="myBefore" pointcut-ref="p1"/>
        <aop:advisor advice-ref="myAfter" pointcut-ref="p1"/>
        <aop:advisor advice-ref="myThrow" pointcut-ref="p1"/>
        <aop:advisor advice-ref="myRound" pointcut-ref="p1"/>
    </aop:config>
</beans>

执行流程解析

  1. 执行前置通知
  2. 执行环绕通知 ——> 切点方法出现异常:执行异常通知
  3. 执行后置通知

SpringAOP的使用(Aspectj)

引入jar包(跟SchemaBase一致)

编写一个通知类

package com.it.advice;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

public class MyAdvice {
    public void myBefore(JoinPoint joinPoint){
        joinPoint.getTarget();// 真实对象
        joinPoint.getThis(); // 代理对象
        joinPoint.getSignature(); // 切点方法的数据

        System.out.println("前置通知");
    }

    public void myAfter(JoinPoint joinPoint, Object returnVal) {
        System.out.println("切点方法的返回值:" + returnVal);
        System.out.println("后置通知");
    }

    public void myRound(ProceedingJoinPoint pjp) throws Throwable {
        // 放行,该方法会返回切点方法的返回值
        pjp.proceed();
    }

    public void myThrow(Exception ex) {
        System.out.println("异常通知:" + ex.getMessage());
    }
}

在spring配置文件中进行配置

<?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
                           https://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--需要被代理的对象-->
    <bean id="a" class="com.it.utils.A"/>
    <!-- 通知类的Bean -->
    <bean id="myAdvice" class="com.it.advice.MyAdvice"/>

    <aop:config>
        <!-- 切入点表达式 -->
        <aop:pointcut id="p1" expression="execution(* com.it.utils.A.*(..))"/>
        <!-- 配置通知 -->
        <aop:aspect ref="myAdvice">
            <aop:before method="myBefore" pointcut-ref="p1"/>
            <!-- 相当于finally,异常后仍然会执行 -->
            <aop:after method="myAfter" pointcut-ref="p1"/>
            <!-- 当切点方法正常执行结束后才会执行,returning必须与通知方法的形式参数相同,该参数为切点方法的返回值 -->
            <aop:after-returning method="myAfter" pointcut-ref="p1"  returning="returnVal" />
            <aop:around method="myRound" pointcut-ref="p1"/>
            <aop:after-throwing throwing="ex" method="myThrow" pointcut-ref="p1"/>
        </aop:aspect>
    </aop:config>
</beans>

SchemaBase与Aspectj的异同

相同点

  • 两者都能在不改变源码的情况下完成功能的扩展

异同点

  • 区分通知的方式不同

    • SchemaBase是基于实现的接口来识别是前置还是后置通知等
    • Aspectj是在配置文件中通过标签识别的
  • 切点表达式的作用范围不同

    • SchemaBase配置的切入点表达式是全局的
    • Aspectj在 aop:aspect 标签中配置的切入点表达式只在当前 aop:aspect 标签生效
  • 获取方法参数的方式不同

    • SchemaBase通过实现的方法中的自带参数获取到方法参数
    • Aspectj通过JoinPoint的getArgs()方法来获取到方法参数

SpringAOP的第三种实现方式(了解)

引入SpringAOP所需的jar包

编写一个需要被功能扩展的类

package com.it.service.impl;

import org.springframework.stereotype.Component;

@Component("testBean")
public class Demo {
    public void test() {
        System.out.println("我是一个测试注解的test方法 ===");
    }
}

配置一个通知类

package com.it.advice;

import com.it.service.impl.Demo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class AnnAdvice {
    // 定义一个切入点表达式
    @Pointcut("execution(* com.it.service.impl.Demo.test(..))")
    private void p1() {
    }

    ;

    @Before(value = "p1()")
    public void before(JoinPoint joinPoint) {
        System.out.println("注解的前置通知");
    }

    @AfterReturning(value = "p1()", returning = "returnVal")
    public void afterReturning(JoinPoint joinPoint, Object returnVal) {
        System.out.println("注解的后置通知");
    }

    @Around(value = "p1()")
    public void around(ProceedingJoinPoint pp) throws Throwable {
        System.out.println("注解的环绕通知");
        pp.proceed();
    }

    @AfterThrowing(value = "p1()", throwing = "e")
    public void afterThrowing(Exception e) {
        System.out.println("注解的异常通知" + e);
    }

    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");
        Demo demo = (Demo) ac.getBean("testBean");
        demo.test();
    }
}

在spring配置文件中开启包扫描以及注解的支持

    <!--扫描注解,将其加入到spring容器中-->
    <context:component-scan base-package="com.it"/>
    <!--
        expose-proxy : 解决切点自调用时通知不生效
        proxy-target-class:如果被代理的目标对象至少实现了一个接口,则会使用JDK动态代理,所有实现该目标类实现的接口都将被代理
        如果该目标对象没有实现任何接口,则创建CGLIB动态代理。但是可以通过proxy-target-class属性强制指定使用CGLIB代理。
        如果指定了proxy-target-class="true"则将强制开启CGLIB动态代理
    -->
    <aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true" />

SpringAOP的设计模式:代理设计模式

什么是代理设计模式

将某个功能的调用以及使用的代码封装起来,避免调用方式及使用代码的直接暴露

代理设计模式的相关专业名词

  • 真实对象:要进行功能扩展的对象
  • 真实方法:要进行功能扩展的方法
  • 代理对象:调用真实对象并完成功能扩展的对象。
  • 代理方法:调用真实方法的扩展方法。

代理设计模式的分类

静态代理模式

指的是代理对象、代理方法都由开发者编写

例如:A实现了C接口,现在需要扩展A的test()方法。

我们需要干的事:编写一个代理对象B,在代理对象中编写代理方法(前置、后置等)

动态代理模式

指代理对象、代理方法都是通过动态生成的。

JDK动态代理(基于实现的接口完成的代理)

返回的代理对象:是实现的接口的子类,需要用接口来接收

  • 编写一个接口

    package jdk;

    public interface MyInterface {
    void show();
    }

  • 编写一个需要实现类

    package jdk;

    public class Student implements MyInterface{

    @Override
    public void show() {
        System.out.println("Student:学生");
    }

    }

  • 编写一个测试类生成代理对象

    package jdk;

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;

    public class Test {
    public static void main(String[] args) {

        // 创建一个真实对象
        Student student = new Student();
        /**
         * ClassLoader loader 类加载器
         * Class<?>[] interfaces 代理对象需要实现的接口
         * InvocationHandler h 代理方法的处理
         * 必须使用接口来接收,因为生成的代理对象是接口的子类
         */
        MyInterface myInterface =
                (MyInterface) Proxy.newProxyInstance(student.getClass().getClassLoader()
                                                    ,student.getClass().getInterfaces()
                                                    ,new MyHandler(student));
    myInterface.show();
    }

    }
    class MyHandler implements InvocationHandler{
    private Object realObject;

    public MyHandler(Object realObject) {
        this.realObject = realObject;
    }
    
    /**
     *
     * @param proxy 代理对象
     * @param method 代理对象的方法
     * @param args 方法参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("jdk代理 扩展前");
    // 使用一个真实对象来调用真实方法
    Object invoke = method.invoke(realObject, args);
    
    System.out.println("jdk代理 扩展后");
    return invoke;
    }

    }

Cglib动态代理(第三方),基于继承完成的动态代理

返回的代理对象:与需要被扩展的类是同一个类型的对象

  • 引入cglib所需的jar包

  • 创建一个需要被扩展的功能类

    package cglib;

    public class Dog {
    public void play() {
    System.out.println("Dog 只会跑来跑去,不会玩游戏");
    }
    }

  • 创建一个Test类使用Cglib生成代理对象

    package cglib;

    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;

    import java.lang.reflect.Method;

    public class Test {
    public static void main(String[] args) {
    // Cglib生成代理对象需要用到的类
    Enhancer enhancer = new Enhancer();
    // 指定生成的代理对象的父类
    enhancer.setSuperclass(Dog.class);
    // 指定代理方法如何处理
    enhancer.setCallback(new MyHandle());
    // 生成代理对象
    Dog proxyDog = (Dog)enhancer.create();
    // 执行代理方法
    proxyDog.play();
    }
    }

    class MyHandle implements MethodInterceptor{

    /**
     *
     * @param o 代理对象
     * @param method 真实对象的方法
     * @param objects 方法参数
     * @param methodProxy 代理对象的方法
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("cglib 扩展前");
        // 调用真实方法
        Object invoke = methodProxy.invokeSuper(o, objects);
        System.out.println("cglib 扩展后");
        return invoke;
    }

    }

为什么需要用到Spring事物

  • 问题引出:在同一个业务中进行多个数据库操作。例如在银行转账的例子中,要减少转账人的余额,并且需要增加收款人的余额。这2个操作必须在同一个事物之内(也就是要么一起成功,要么一起失败)

  • 问题解决:在业务方法的执行之前开启事物,如果方法正确执行,则提交事物,否则回滚事物。

  • 未使用mybatais前的事物解决: 获得数据库的连接对象,对事物进行控制

  • 使用mybatis后的事物解决:使用sqlSession对事物进行控制(需要用到ThreadLocal保证一个线程中只有一个SqlSession)

  • 使用Spring后的事物解决:在学了SpringAop后,理解到了代理模式,而事物是否也可以使用代理设计模式实现呢? 答案是可以的,Spring为我们提供了事物管理器,使用SpringAop面向切面编程的技术完成了对事物的控制,我们只需要使用就可以了

事物的分类

编程式事物

事物管理的代码,例如开启事务,事物提交、事物回滚,等等的代码都需要由程序员自行编写。

声明式事物

事物管理由第三方直接提供,由程序员完成组装操作(配置)后直接使用

Spring事物的使用

引入所需的jar包

  • 引入SpringIOC的jar包

  • SpringAOP的jar包

  • SpringTx的jar包

  • mybaits的jar包

  • 数据库连接的jar包

  • mybaits与spring整合jar

搭建基本开发环境

  • 在web.xml中配置ContextLoaderListener监听器和contextConfigLocation全局属性

  • 搭建项目的基本文件夹结构(按照自己的要求)

  • 在src或者已经指定好的类路径下编写spring的配置文件(我这里名字叫application.xml)

    • 配置包扫描的路径

    • 开启Spring对AOP注解的支持

    • 配置DataSource数据源

    • 配置SqlSessionFactoryBean(指定dataSource和、mapper.xml扫描路径、mybatis全局配置文件路径)。作用是为了生产SqlSession

    • 配置MapperScannerConfigurer(指定mapper的路径、指定sqlSessionFactory)作用是为了将Mapper对象扫描并添加到Spring容器

    • 配置事物bean(指定dataSource),DataSourceTransactionManager

    • 配置事物管理的方法,tx:advice,传入事物bean

    • 配置AOP切面aop:config

  • 在类路径下配置一个log4j的日志文件

  • 在类路径下配置mybaits全局配置文件

  • 在类路径下编写jdbc.properties数据库连接配置文件

基于xml的Spring事务使用

    <!-- 配置spring提供的事物管理bean -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事物管理的方法,指定事物的规则-->
    <tx:advice transaction-manager="transactionManager" id="advice">
        <!-- 只对该标签配置的方法生效 -->
        <tx:attributes>
            <!--
                propagation:事物的传播行为
                isolation:事物的隔离级别
                rollback-for:在遇到这些异常时回滚
                no-rollback-for:在遇到异常时不回滚
                timeout:事物的超时时间,单位为秒,-1为永不超时
                read-only:该事物是否只读,查询操作建议设置为true,会增加效率
                name:需要进行匹配的方法,*代表通配符
            -->
            <tx:method
                    propagation="REQUIRED"
                    isolation="DEFAULT"
                    rollback-for="java.lang.ClassNotFoundException,"
                    no-rollback-for="java.lang.NullPointerException"
                    timeout="-1"
                    read-only="true"
                    name="transfer*"/>

        </tx:attributes>
    </tx:advice>

    <!-- 配置一个aop切面 -->
    <aop:config>
        <aop:pointcut id="mp" expression="execution( * com.it.service.impl.*.*(..))"/>
        <aop:advisor advice-ref="advice" pointcut-ref="mp" />
    </aop:config>

基于注解的Spring事务使用

在spring配置文件中进入如下配置
    <!-- 配置spring提供的事物管理bean -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--开启注解事务控制 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
在context:component-scan 扫描的路径中的类使用注解完成事务的配置
  • 在需要进行事务控制的上添加@Transactional注解(该类中的所有方法都会进行事物控制)

  • 在需要进行事务控制的类方法上添加@Transactional注解

  • 由于Spring事务是基于AOP实现的,而对真实对象创建代理对象时,需要拥有真实对象。所以进行事物控制的类必须交给Spring管理(配置bean或者使用对应的注解)

事务的传播行为

事务的传播行为发生在:一个业务方法中调用另外一个业务方法

  • REQUIRED (默认值): 如果当前有事务,就在事务中执行,如果当前没有事务,新建一个事务

  • SUPPORTS:如果当前有事务就在事务中执行,如果当前没有事务,就在非事务状态下执

  • MANDATORY:必须在事务内部执行,如果当前有事务,就在事务中执行,如果没有事务,报错.

  • REQUIRES_NEW:必须在事务中执行,如果当前没有事务,新建事务,如果当前有事务,把当前事务挂起

  • NOT_SUPPORTED:必须在非事务下执行,如果当前没有事务,正常执行,如果当前有事务,把当前事务挂起

  • NEVER:必须在非事务状态下执行,如果当前没有事务,正常执行,如果当前有事务,报错.

  • NESTED:必须在事务状态下执行.如果没有事务,新建事务,如果当前有事务,创建一个嵌套事务

事物的隔离级别

作用:在多线程中保证数据库数据完整性

数据库使用中出现的完整性问题
  • 脏读

    一个事务(A)读取到另一个事务(B)中未提交的数据,另一个事务中数据可能进行了改变,此时A事务读取的数据可能和数据库中数据是不一致的,此时认为数据是脏数据,读取脏数据过程叫做脏读。

  • 不可重复读

    主要针对某行数据的修改操作、两次读取在同一个事务

当事务A第一次读取事务后,事务B对事务A读取的数据进行修改,事务A中再次读取的数据和之前读取的数据不一致,过程不可重复读

  • 幻读

    针对新增或者删除操作

事务A按照特定条件查询出结果,事务B新增了一条符合条件的数据.事务A中查询的数据和数据库中的数据不一致的,事务A好像出现了幻觉,这种情况称为幻读

五种隔离级别
  • DEFAULT: 默认值,由底层数据库自动判断应该使用什么隔离级别

  • READ_UNCOMMITTED:可以读取未提交数据,可能出现脏读,不重复读,幻读。(效率最高、不安全)

  • READ_COMMITTED:只能读取其他事务已提交数据,可以防止脏读,可能出现不可重复读和幻读

  • REPEATABLE_READ: 读取的数据被添加锁,防止其他事务修改此数据,可以防止不可重复读,脏读,可能出现幻读

  • SERIALIZABLE: 排队操作,对整个表添加锁.一个事务在操作数据时,另一个事务等待事务操作完成后才能操作这个表(最安全、效率最低)

SpringMVC的学习和使用

概念及问题引入

目前现阶段未使用SpringMVC的开发弊端:

  • 每一个功能点都需要一个Servlet
  • 在Servlet中获取请求数据比较麻烦,每次都要getParament,如果有几十个数据,就会变得很麻烦
  • 相应给浏览器的代码冗余,永远都是 转发、重定向、getWriter返回数据 三种

解决方案

  • 定义一个公共的Servlet,让其处理所有请求,根据请求的url调用对应的逻辑方法进行请求的处理
  • 但是逻辑方法都写在公共的Servlet中,代码不方便管理,所以需要编写新的逻辑类来写逻辑方法(Controller)

几个小问题

  • 如何在Servlet中获取逻辑对象(逻辑类)

通过Spring的容器的子容器,在子容器中存储所有的逻辑类Controller的逻辑对象,然后Servlet在init初始化的时候一次性从子容器中获取所有的逻辑对象。

  • 如何动态的通过url调用指定的逻辑对象的逻辑方法

    通过注解 + 反射的技术

SpringMVC的使用流程

  • 创建一个Web项目
  • 引入SpringMVC的jar包
  • 创建web项目的包结构,dao、service、controller(放逻辑对象)
  • 在类路径下增加一个spring子容器配置文件,进行包扫描和注解驱动的开启
  • web.xml中配置SpringMVC
    • 配置Servlet的访问路径 为 /(拦截除了.jsp结尾的所有资源) 或者 /*
    • 配置Spring子容器的配置文件路径
  • 创建逻辑对象
    • 添加@Controller注解
    • 在逻辑方法中添加@RequestMapping注解完成url的映射
  • 在spring配置文件中配置资源放行
  • 正常访问项目

Spring的开发环境搭建

  • 创建一个Web项目

  • 引入SpringMVC的jar包

  • 创建web项目的包结构

  • 在类路径下增加一个spring子容器配置文件

    <context:component-scan base-package="com.it" />
    <!-- 其实是因为spring也有一些需要加入容器的bean,但是包名太长,所以提供了这个标签-->
    <mvc:annotation-driven />
  • 在web.xml中配置SpringMVC这个Servlet

    <servlet>
        <servlet-name>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
  • 编写单元方法

    @Controller
    public class MyController {

    @RequestMapping("aa") // 相当于Servlet的urlPatterns
    public String method1(){
        System.out.println("我是第一个springMVC处理的逻辑方法");
        return "aa";
    }

    }

SpringMVC的几种获取请求参数的方式

问题:在学习了SpringMVC的请求流程后,我们知道SpringMVC所提供的DispatcherServlet是根据请求的url来动态使用反射和注解技术调用的对应的逻辑方法,那么逻辑方法如何获取到请求的参数呢?

解决方案:DispatcherServlet在service方法中处理逻辑时能够获取到Request对象,只要DispatcherServlet将Request对象传入到逻辑单元方法中进行反射执行,那么就可以获取到了。单元方法必须声明形参来进行参数的接收。

紧耦方式获取请求参数
    @RequestMapping("reqParam1")
    public String method2(HttpServletRequest req) {
        String name = req.getParameter("name");
        System.out.println(name);
        return null;
    }

本质:

  • DispatcherServlet在接收到url处理逻辑时找到该单元方法执行
  • 获取到单元方法上的形式参数类型
  • 根据类型传入实参(在这里是HttpServletRequest)
解耦方式获取请求参数
通过声明形式参数 并指定键名的方式获取请求参数
    @RequestMapping("reqParam2")
    public String method2(String uname, int age) {
        System.out.println(uname);
        System.out.println(age);
        return null;
    }
当键名与形式参数的名字不同时
    @RequestMapping("reqParam3")
    /**
     * @RequestParam 有如下几个参数
     *      name : 指定传递过来的键名
     *      value:效果同上
     *      required:是否为必须的,true(默认值)为必须(如果未赋值则会抛出异常),false为非必须
     *      defaultValue : 如果为传递,给其的默认值
     *          注意:required与defaultValue 不要共同使用
     */
    public String method3(@RequestParam("name") String uname, int age) {
        System.out.println(uname);
        System.out.println(age);
        return null;
    }
使用实体类对象获取请求数据(需提供set方法)
  • 实体类

    package com.it.entity;

    public class Student {
    private String name;
    private int age;

    public void setName(String name) {
        this.name = name;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    }

  • 提供单元方法

    @RequestMapping("reqParam4")
    public String method4(Student student) {
        System.out.println(student);
        return null;
    }

本质:通过获取Student类中的属性,通过属性作为键来进行getParameter获取到数据后存入该Student对象中。

获取同键不同值的数据(例如一个人有多个爱好)
    @RequestMapping("reqParam5")
    public String method5(String[] hobbys) {
        for (String hobby : hobbys) {
            System.out.println(hobby);
        }
        return null;
    }
RestFul风格url获取数据
  • 什么是restFul风格?

    传统url: localhost:8080/project/req1?name=zhangsan&age=18

    restFul:localhost:8080/project/req1/zhangsan/age

  • 为什么要使用restFul风格

    原因:因为前台和后台的代码不一定是同一个人写的,所以可能出现形式参数与键名的不一致性。

    例如:前台传递参数的键名一旦修改,后台也需要做对应的修改,麻烦。

    而使用restFul风格则不用指定键名,只是将数据放入到url中

  • 那么如果只是一个url,那么dispatcherServlet也只会查找一个@RequestMapping对应的url地址作为逻辑方法的入口,所以SpringMVC提供了对RestFul风格的请求方式的支持

  • 实现代码

    @RequestMapping("reqParam6/{name}/{age}")
    /**
     * @PathVariable 有如下属性
     *  name:指定url中占位符的名称
     *  value:同上
     *  required: 是否必须(默认为true)
     */
    public String method6(@PathVariable String name,@PathVariable("age") Integer myAge) {
        System.out.println(name);
        System.out.println(myAge);
        return null;
    }
注意事项

在使用SpringMVC来进行参数的接收时,最好使用包装类

本质原因:例如单元方法一个形式参数 int age,但是在访问该单元方法时,没有传递该age。

DispatcherServlet在调用逻辑方法时,会通过getParameter("age")来获取值,为该形式参数age赋值。

但是如果没有传递age,那么getParameter就会获取到一个null,然后强转给一个int类型的age,就会报错

编码问题(乱码)的解决方案

请求编码的过滤
  • get请求

    • request.setCharacterEncoding();

    • 配置Tomcat服务器配置文件server.xml

      或者

  • post请求

    • request.setCharacterEncoding();
响应编码的过滤
  • response.setCharacterEncoding();// 告诉tomcat响应的编码
  • response.setContentType();// 告诉浏览器以什么方式解析
SpringMVC提供的编码过滤器的使用
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <!--设置编码格式-->
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
        <!--设置编码格式的生效范围-->
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

配置SpringMVC的静态资源放行

问题:由于配置的DispatcherServlet的拦截路径是拦截除jsp结尾的所有请求,所以也会将js、css等等静态资源也给拦截,因此我们需要指定放行规则

解决:在springmvc的配置文件中进行资源放行的规则配置

    <!-- 放行静态资源
    mapping:相当于浏览器在访问静态资源时的路径地址匹配
    location:代表从服务器的部署目录的哪个文件夹中获取资源
    -->
    <mvc:resources mapping="/js/**" location="/WEB-INF/js/" />
    <mvc:resources mapping="/css/**" location="/css/" />
    <!-- 放行一整个static文件夹,文件夹中可以放css,js等等,这样就不用配置多个路径了 -->
    <mvc:resources mapping="/static/**" location="/static/"/>

注意:

DispatcherServlet处理一个请求的时候,会先查找其对应的单元方法进行处理。如果找不到单元方法才会根据SpringMVC配置的静态资源放行规则查找静态资源。

所以说不要设置某逻辑单元方法的路径与任何一个静态资源的访问url相同。

SpringMVC响应的三种方式

使用紧耦合的方式进行响应,response对象
@RequestMapping("resp1")
    public String method7(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 转发
        request.getRequestDispatcher("index.jsp").forward(request, response);
        // 重定向
        response.sendRedirect(request.getContextPath() + "/index.jsp");
        // 返回数据
        response.getWriter().write("你好呀");
        return null;
    }
使用转发的方式(默认) 解耦合方式
    @RequestMapping("resp2")
    public String method7(){
        System.out.println("即将转发到index.jsp");
        // 如果不写forward: 默认就是转发
//        return "index.jsp";
        return "forward:index.jsp";
    }
使用重定向的方式 解耦合方式
    @RequestMapping("resp3")
    public String method8(){
        System.out.println("即将重定向到index.jsp");
        return "redirect:index.jsp";
    }

SpringMVC 数据传递的2种方式

直接使用HttpServletRequest对象,将数据存储到request作用域或者session作用域
    @RequestMapping("dataPass1")
    public String method10(HttpServletRequest request) {
        // 存入request作用域
        request.setAttribute("myName","codeStar");
        // 存入session作用域
        request.getSession().setAttribute("myName2","codeStar2");

        return "forward:myDataPass.jsp";
    }
Model对象的使用(只能存储到request作用域)
    @RequestMapping("dataPass2")
    public String method11(Model model) {
        // 存入request作用域
        model.addAttribute("myName","codeStar");
        return "forward:myDataPass.jsp";
    }
注意,Model对象存储的数据如果通过重定向,会发生什么?

我们知道,HttpServletRequest存储的requset作用域中的数据,一旦通过重定向,那么数据就会消失。

可是Model的数据如果重定向,则会将本次请求中的存储的reqeust作用域的对象作为下次请求携带的参数

    @RequestMapping("dataPass3")
    public String method12(Model model) {
        // 存入request作用域
        model.addAttribute("myName","codeStar");
        return "redirect:myDataPass.jsp";
    }

SpringMVC的视图解析器(逐步迭代过程)

本质:就是也就是用于处理逻辑单元方法返回值的类

最初代,使用View作为视图解析器的接口
  • 该接口拥有两个实现类

    • 一个是InternalResourceView:用于进行请求转发
    • RedirectView :用于进行重定向
  • 使用示例

    @RequestMapping("view1")
    public View method13(HttpServletRequest request) {
        // 请求转发

    // return new InternalResourceView("/myView.jsp");

        // 重定向
        return new RedirectView(request.getContextPath() + "/myView.jsp");
    }
经过更新,使用ModelAndView,顾名思义,也就是View和Model的整合

出现的原因:我们既需要使用View完成视图的请求转发或者重定向,又需要Model来帮助我们完成数据的传递,所以ModelAndView就出现了。(其实主要还是因为以前直接使用View对象的时候需要new对象比较麻烦)

  • 使用示例

    @RequestMapping("view2")
    public ModelAndView method14(HttpServletRequest request) {
        // 创建ModelAndView对象
        ModelAndView mv = new ModelAndView();
        // 将数据存储到request作用域
        mv.addObject("myName","codeStar");
        // 设置请求转发

    // mv.setViewName("/myView.jsp");// 默认就是转发

        // 设置重定向
        mv.setViewName("redirect:/myView.jsp");// 重定向(注意了,会自动加上项目名,不需要request.getContextPath());
        return mv;
    }
再次经过更新,直接返回String字符串,通过String字符串来识别

其实只是写法变得简便了,底层使用的还是ModelAndView。

DispatcherServlet在接收到方法的String字符串返回值之后,传递给ModelAndView来帮助进行解析,通过判断是字符串中的forward(不写就是默认)还是redirect,来决定是请求转发还是重定向。

    @RequestMapping("view3")
    public String method15() {
        // 请求转发
//        return "forward:/myView.jsp";
//        return "/myView.jsp";

        // 重定向
        return "redirect:/myView.jsp";
    }
自定义视图解析器(只针对于请求转发)
  • 问题引出:可以看到,Spring为我们提供的视图解析器虽然可以做到请求转发与重定向功能的实现。

    但是有点死板,必须要我们写完整的资源路径,如果该资源在层层迭代的一个文件夹中,难道我们每次做资源跳转的时候都要写完整的路径吗,麻烦。

  • 解决:所以Spring给我们提供了一个自定义视图解析器,我们可以通过配置前缀与后缀等参数完成我们自定义视图解析器的配置。

  • 在Springmvc的配置文件中添加如下代码

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 你返回的String字符串的前缀,例如 return ”aa“,那么则会变成/WEB-INF/jsp/aa -->
        <property name="prefix" value="/WEB-INF/jsp/" />
        <!-- 你返回的String字符串的后缀 -->
        <property name="suffix" value=".jsp" />
    </bean>
  • 使用示例

    @RequestMapping("view4")
    public String method16() {
        return "view4";
    }
使用restFul风格的单元方法(基本配置了自定义视图解析器之后,该公共方法必不可少)

问题引出:我们的资源目录下有许许多多的资源,难道我们访问每个资源都需要一个对应的单元方法吗?麻烦。

解决:使用restFul风格的单元方法模糊匹配资源

    @RequestMapping("{url}")
    public String resourceUrl(@PathVariable String url) {
        return url;
    }

注意:(以下都是基于浏览器发起的请求能够匹配该公共方法时)

如果配置了该公共方法。浏览器发起的请求会先匹配是否有对应的单元方法,如果没有。则会匹配到该公共方法。

还有一个重要的点: 如果对应的单元方法没有@ResponseBody注解,也没有返回对应的视图资源路径,则还是会走一次该公共方法。

自定义视图解析器中为什么配置了/WEB-INF下的目录

因为WEB-INF目录相当于一个对外不开放的文件夹。只能通过服务器内部的请求转发才能获取到其中的资源。为了安全起见。

注意:必须返回一个不带forward关键字的字符串,才会走自定义视图解析器
  • DispatcherServlet会根据返回的String字符串来决定是使用ModelAndView还是使用自定义视图解析器。
  • 在未配置自定义视图解析器之前,返回一个String字符串:都会走ModelAndView
  • 配置了自定义视图解析器之后:返回一个String字符串,如果其中没有携带forward或者redirect关键字,则会走自定义视图解析器。否则会走ModelAndView。
  • 自定义视图解析器仅针对于请求转发

SpringMVC的上传与下载

传统项目的上传功能实现步骤
  • 前台需要一个type为file的input,调整上传的格式为二进制
  • 后台会将该文件二进制流数据存储到request对象中
  • 然后通过fileupload中的ServletFileUpload 来对request对象中的文件二进制流进行解析
  • 解析后得到一个FileItem的List集合,然后将其存入到服务器中
SpringMVC的上传实现步骤
引入FileUpload和commons-io的jar包
在springmvc的配置文件中配置一个bean,当然,也可以配置一些属性
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" />
记住页面中文件提交的name属性值(我这里使用的是ajax上传文件,如果是同步请参照之前发布的FileUpload上传下载)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <base href="<%=request.getContextPath() + "/"%>">
    <script type="text/javascript" src="js/jquery-1.12.3.min.js"></script>
    <script type="text/javascript">
        function uploadPhoto() {
            var file = $("#photo")[0].files[0];
            var data = new FormData();
            // 后台接收时候,需要以该key:photo为形参名接收
            data.append("photo",file);
            $.ajax({
                type:"post",
                url: "regUpload",
                //在 ajax 中 contentType 设置为 false 是为了避免 JQuery 对其操作,从而失去分界符,而使服务器不能正常解析文件。
                contentType: false,
                //默认情况下会将发送的数据序列化以适应默认的内容类型application/x-www-form-urlencoded
                //如果想发送不想转换的的信息的时候需要手动将其设置为false
                processData: false,
                data: data,
                dataType: 'json',
                success: function (result) {
                    if (result.state){
                        alert("上传成功")
                        // 上传图片的回显
                        $("#pageImg").attr("src","upload/" + result.url).css("display","");
                    }   else {
                        alert("上传失败:" + result.msg)
                    }
                }
            })
        }
    </script>
</head>
<body>
    <form action="">
        <p>选择头像:<input id="photo" type="file">
                <input type="button" onclick="uploadPhoto()" value="点击上传">
                <img id="pageImg" src="" width="200px" style="display: none">
        </p>
        <input type="hidden" name="img" id="img">
    </form>
</body>
</html>
在声明的处理上传请求的单元方法的形参直接接收(DispatcherServlet会解析文件后传递给单元方法)
    @RequestMapping("regUpload")
    @ResponseBody
    public MyResult regUpload(MultipartFile photo, HttpServletRequest request) throws IOException {
        // 1、获取文件名
        String oldName = photo.getOriginalFilename();
        // 2、获得文件后缀
        String suffix = oldName.substring(oldName.lastIndexOf("."));
        String newName = UUID.randomUUID() + suffix;
        // 3、获得文件存储路径
        File path = new File(request.getServletContext().getRealPath("/upload"));
        // 4、判断文件是否存在
        if(!path.exists()){
            path.mkdirs();
        }
        // 5、将文件存在服务器中
        photo.transferTo(new File(path, newName));
        // 返回友好提示
        MyResult myResult = new MyResult();
        myResult.setState(true);
        myResult.setUrl(newName);
        return myResult;
    }
SpringMVC的下载实现
前台代码示例
    <a href="download?fileName=a38733de-2226-426b-8e0c-acef9200d541.png">啦啦啦</a>
后台代码示例
    @RequestMapping("download")
    public void downloadImage(String fileName, HttpServletResponse response, HttpServletRequest request) throws IOException {
        // 告诉浏览器,下载该文件,并指定了文件名
        response.setHeader("Content-Disposition", "attachment;filename="+fileName);
        // 从服务器中读取该文件
        File file = new File(request.getServletContext().getRealPath("/upload") + "/" + fileName);
        // 将该文件转换为byte数组
        byte[] bytes = FileUtils.readFileToByteArray(file);

        // 相应给浏览器
        response.getOutputStream().write(bytes);
    }

SpringMVC拦截器介绍与使用

拦截器的介绍
  • 之前在介绍过滤器的时候,可以知道过滤器是保护请求的服务器资源,在请求资源之前,如果符合过滤器的拦截范围,则会先执行过滤器。过滤器的执行时机:在Servlet之前。

  • 但是,现在我们使用了SpringMVC,只有一个Servlet,那就是DispatcherServlet,这个时候过滤器虽然能用,但是过滤器的拦截范围不管多小,都会拦截到DispatcherServlet,也就是会拦截所有的DispatcherServlet的请求。

  • 解决方案:

    • 使用过滤器

    • 过滤器的执行时机:Servlet之后,单元方法之前

拦截器的使用
编写一个需要被拦截的Controller
package com.it.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class InterceptorTestController {

    @RequestMapping("inter1")
    public void inter1() {
        System.out.println("我是inter1单元方法");
    }

    @RequestMapping("inter2")
    public void inter2() {
        System.out.println("我是inter2单元方法");
    }

    @RequestMapping("demo")
    public void demo() {
        System.out.println("我是demo单元方法");
    }
}
配置一个java类,实现HandlerInterceptor
package com.it.config;

import com.it.controller.InterceptorTestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class MyInterceptor implements HandlerInterceptor {
    @Autowired
    private InterceptorTestController obj;
    /**
     * 触发时机:执行单元方法执行
     * @param request 请求对象
     * @param response 响应对象
     * @param handler 实参的类型是HandlerMethod,也就是对应的单元方法,所以可以在这里调用一次单元方法
     *                因为单元方法平时都是由DispatcherServlet调用的,在这里自己调用就可以使单元方法调用变得可控
     * @return 返回true表示放行, flase表示不放行
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 调用了一次单元方法
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        method.invoke(obj,null);

        System.out.println("执行单元方法之前调用");
        // 放行
        return true;
    }

    /**
     * 执行时机: 在执行单元方法之后, 相应给浏览器页面之前,也就是视图解析器之前
     * @param request 请求对象
     * @param response 响应对象
     * @param handler 同preHandle一致
     * @param modelAndView 视图解析器,可以变更传递的数据,也可以变更需要跳转的资源
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("在执行单元方法之后, 资源相应给浏览器之前");
        // 可以对数据进行过滤后再重新存入作用域,例如去除敏感词
        // 可以重新设置页面的资源跳转地址
    }

    /**
     * 触发时机: 在视图解析器执行完毕,响应给用户资源后执行
     * @param request
     * @param response
     * @param handler
     * @param ex 接收此次请求处理过程中出现的异常
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (ex != null) {
            System.out.println("本次请求出现的异常: " + ex.getMessage());
        }
        // 也可以做一些关闭资源的操作
        // 一些记录用户信息的操作(在这里进行操作时,不会影响到用户)
    }
}
在SpringMVC中配置该拦截器
    <!-- 配置拦截器 -->
    <mvc:interceptors>
        <!--声明全局拦截器,表示拦截所有单元方法-->
        <!--<bean id="all" class=""></bean>-->

        <!-- 配置局部拦截器, 注意,bean标签需要放在最后-->
        <mvc:interceptor>
            <!-- 拦截范围 -->
            <mvc:mapping path="/inter*"/>
            <!-- 不拦截的范围 -->
            <mvc:exclude-mapping path="/inter2"/>
            <bean id="interceptor" class="com.it.config.MyInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>
多重拦截时的触发时机

配置在前的拦截器设置变量为 A,

配置在后的拦截器设置为:B

  • 执行顺序为:

    • A的preHandle(配置靠上)
    • B的preHandle(配置靠下)
    • 单元方法
    • B的postHandle(配置靠下)
    • A的postHandle(配置靠上
    • B的afterCompletion(配置靠下)
    • A的afterCompletion(配置靠上)

SpringMVC对ajax请求时响应数据的支持

传统解决方案

之前我们使用传统Servlet处理ajax请求时,将需要转换为json的实体类对象或者List对象使用Gson进行转换,并使用response对象getWriter获得打印流对象来转换后的json字符串响应给前端。

SpringMVC解决方案
引入jackson的jar包(因为Spring对ajax响应的支持会使用到)

在需要处理ajax的单元方法上加上一个@ResponseBody注解

提示:这个时候如果返回一个实体类对象或List等java类对象,会直接将该实体对象转换为json数据

    @RequestMapping("regUpload")
    @ResponseBody
    public MyResult regUpload(MultipartFile photo, HttpServletRequest request) throws IOException {}
前台ajax获取时,无需再使用eval等方式将其转换为json对象

Spring中的常用注解

  • @Component 一般用于entity类上,相当于bean标签

  • @Controller 一般用于控制器上(SpringMVC),功能如上

  • @Service 一般用于service层上,功能如上

  • @Repository 一般用于dao层上,功能如上

  • @Resource 相当于依赖注入,默认使用byName方式,找不到则会使用beType,是jdk提供的注解(可以不用提供set方法)

  • @Autowired 作用如上,默认使用byType方式依赖注入,是spring提供的注解。(可以不用提供set方法)

  • @RequestParam 作用在单元方法形参上,用于指定传递参数的键名

  • @PathVariable 作用在单元方法形参上,用于获取url中的占位符{占位符名}

    @RequestMapping("demo/{name}/{age}")
    public String demo(@PathVariable String name,@PathVariable("age") String myAge){
        return "";
    }
  • @Value 作用于成员属性上,用于进行普通属性注入

    • 如果需要使用表达式动态获取配置文件参数,需要在spring配置文件中

      <context:property-placeholder location="classpath:你的配置文件路径" />

    • 在java中使用@Value注解获取

      @Value("${配置文件中的键名}")
      @Value("zhangsan")