1. 程式人生 > 其它 >spring框架的學習->從零開始學JAVA系列

spring框架的學習->從零開始學JAVA系列

目錄

Spring框架的學習

框架的概念

其實就是有人寫好的程式碼,將其封裝好了,形成一個比較統一的解決方案,稱之為框架。我們直接拿來使用其他人已經寫好的功能。
發明者:Rod Johnson -- 羅德·約翰遜

框架的使用

  • 引入對應的jar包
  • 查詢其API文件進行學習
  • 配置框架的配置檔案(框架執行時需要的常量)

Spring框架的引入

概念

Spring是一個控制反轉(IOC)和麵向切面(AOP)的輕量級框架。

作用

  • 降低程式碼之間的耦合度,提高程式碼的擴充套件性和可維護性
  • 提升開發的效率

內容

  • SpringIOC 依賴注入
  • SpringAop 面向切面
  • SpringTX 事物
    Spring是一個框架集,內部集成了很多的功能,可以根據需求選擇對應的子框架使用

SpringIOC的學習

概念

  • 在傳統的系統開發中,層與層之間的耦合性太高,(例如業務層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" />
  • 靜態工廠(靜態方法建立物件)
<!-- 靜態工廠建立物件 -->
 <bean id="stu5" class="com.it.qxkj.factory.StaticStudentFactory" 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" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <base href="<%=request.getContextPath() + "/"%>">
</head>
<body>
    <p><font color="red">${requestScope.errorMsg}</font></p>
    <form action="starlet/LoginServlet" method="post">
        <p>使用者名稱:<input name="uname" type="text"/></p>
        <p>密碼:<input name="pwd" type="password"/></p>
        <p><button type="submit">點選登入</button> </p>
    </form>
</body>
</html>

配置一個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">

SpringAOP的學習與使用

概念及問題引入

  • 問題引入:我們學習使用了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;
    }
}

SpringTX事物的學習與使用

為什麼需要用到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)

    • 配置包掃描的路徑 <context:component-scan base-package="填自己的"/>

    • 開啟Spring對AOP註解的支援<aop:aspectj-autoproxy />

    • 配置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")