Spring AOP
概念
AOP全稱為Aspect Oriented Programming,表示面向切面編程。切面指的是將那些與業務無關,但業務模塊都需要使用的功能封裝起來的技術。
AOP基本術語
**連接點(Joinpoint):**連接點就是被攔截到的程序執行點,因為Spring只支持方法類型的連接點,所以在Spring中連接點就是被攔截到的方法。連接點由兩個信息確定:
- 方法( 表示程序執行點,即在哪個目標方法)
- 相對點(表示方位,即目標方法的什么位置,比如調用前,后等)
切入點(Pointcut): 一般認為,所有的方法都可以認為是連接點,但是我們并不希望在所有的方法上都添加通知,而切入點的作用就是提供一組規則來匹配連接點,給滿足規則的連接點添加通知。
**通知、增強(Advice) : **可以為切入點添加額外功能,分為:前置通知、后置通知、異常通知、環繞通知、最終通知等。
**目標對象(Target)**目標對象指將要被增強的對象,即包含主業務邏輯的類對象。或者說是被一個或者多個切面所通知的對象。
**織入(Weaving):**織入是將切面和業務邏輯對象連接起來, 并創建通知代理的過程。織入可以在編譯時,類加載時和運行時完成。在編譯時進行織入就是靜態代理,而在運行時進行織入則是動態代理。
**代理(Proxy):**被AOP織入通知后,產生的結果類。
**切面(Aspect):*切面是一個橫切關注點的模塊化,一個切面能夠包含同一個類型的不同增強方法,比如說事務處理和日志處理可以理解為兩個切面。切面由切入點和通知組成。Spring AOP就是負責實施切面的框架,它將切面所定義的橫切邏輯織入到切面所指定的連接點中。
應用
配置pom文件:
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.11</version> </dependency> <!-- 切面相關的包 --> <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.9</version> </dependency>
編寫業務層:
接口:
public interface UserService {int saveUser(Map<String,Object> params); }
實現類:
public class UserServiceImpl implements UserService{@Overridepublic int saveUser(Map<String, Object> params) {System.out.println("保存用戶信息" + params);return 0;} }
配置業務層:
spring-aop.xml:
<!--業務層對象--><bean id="userService" class="com.qf.aop.service.UserServiceImpl"/>
編寫通知類:
通知分為前置通知、后置通知、異常拋出通知、環繞通知、最終通知(沒什么用這里不實現)。前置通知:
接口為MethodBeforeAdvice,其底層實現如下:
public interface MethodBeforeAdvice extends BeforeAdvice {/*** Callback before a given method is invoked.*/void before(Method method, Object[] args, @Nullable Object target) throws Throwable; }
使用前置通知需要實現這個接口:
public class BeforeAdvice implements MethodBeforeAdvice{@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("準備執行方法:" + className + "." + methodName + "參數:" + Arrays.toString(args));} }
寫完通知類后需要配置通知:
spring-aop.xml:
<!--業務層對象--> <bean id="userService" class="com.qf.aop.service.UserServiceImpl"/><!--配置通知對象--> <bean id="before" class="com.qf.aop.advice.BeforeAdvice"/>
當通知對象和業務層對象都納入IOC容器管理之后,需要將通知對象作用在業務層對象上。Spring提供了aop標簽來完成這一功能。
<!--aop配置--><aop:config><!--pointcut表示切點,也就是通知會在哪些位置觸發expression表示切點表達式,切點表達式必須是execution(), execution()方法中的參數必須配置到方法上比如 * com.qf.spring.aop.service..*(..)第一個 * 表示任意訪問修飾符com.qf.spring.aop.service.. 最后的兩個..表示service包下面的所有子包中的類*(..) 表示任意方法, 如果()中沒有..,則表示不帶參數的方法;有,就表示帶任意參數--><!--切入點配置--><aop:pointcut id="pc" expression="execution(* com.qf.aop.service..*(..))"/><!--通知配置--><aop:advisor advice-ref="before" pointcut-ref="pc"/></aop:config> </beans>
測試:
public class AopTest {@Testpublic void saveUserTest(){ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");UserService userService = context.getBean("userService", UserService.class);HashMap<String, Object> map = new HashMap<>();map.put("name","愛德華");map.put("sex","男");int i = userService.saveUser(map);} }
注:利用ClassPathXmlApplicationContext拿到配置文件上下文對象,進而拿到bean對象。
后置通知接口:AfterReturningAdvice.
剩下的流程和前置接口相同,編寫通知類,配置通知類對象,配置通知。
<!--配置通知對象--><bean id="before" class="com.qf.aop.advice.BeforeAdvice"/><bean id="after" class="com.qf.aop.advice.AfterAdvice"/><!--aop配置--><aop:config><!--切入點配置--><aop:pointcut id="pc" expression="execution(* com.qf.aop.service..*(..))"/><!--通知配置--><aop:advisor advice-ref="before" pointcut-ref="pc"/><aop:advisor advice-ref="after" pointcut-ref="pc"/></aop:config> </beans>
異常拋出通知
異常拋出接口為ThrowsAdvice。
注意:異常通知類接口沒有要重寫的方法,而是自定義。
public class ExceptionAdvice implements ThrowsAdvice {public void afterThrowing(Method method, Object[] args, Object target, Exception ex){String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("執行方法時:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",發生了異常:" + ex.getMessage());} }
配置和前面相同:
<bean id="exception" class="com.qf.aop.advice.ExceptionAdvice" /><aop:advisor advice-ref="exception" pointcut-ref="pc"/>
環繞通知
接口:MethodInterceptor
注意:1.這里重寫的方法參數為MethodInvocation invocation,可以通過invocation.getMethod();//獲取被攔截的方法。
public class AroundAdvice implements MethodInterceptor {@Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {Method method = invocation.getMethod();//獲取被攔截的方法對象Object[] args = invocation.getArguments();//獲取方法的參數Object target = invocation.getThis();//獲取代理對象String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("準備執行方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args));Object returnVal = method.invoke(target, args);System.out.println("執行完方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",得到返回值:" + returnVal);return returnVal;} }
環繞通知可以實現前置通知、后置通知、異常拋出通知的功能,所以配置文件中只需要配置環繞通知即可。
<!--業務層對象--> <bean id="userService" class="com.qf.aop.service.UserServiceImpl"/><!--配置通知對象--><bean id="around" class="com.qf.aop.advice.AroundAdvice"/><!--aop配置--> <aop:config><!--切入點配置--><aop:pointcut id="pc" expression="execution(* com.qf.aop.service..*(..))"/><!--通知配置--><aop:advisor advice-ref="around" pointcut-ref="pc"/> </aop:config>
AspectJ
簡介:
AspectJ是一個面向切面的框架,它擴展了Java語言,定義了AOP 語法,能夠在編譯期提供代碼的織入。Spring通過集成AspectJ實現了以注解的方式定義增強類,大大減少了配置文件中的工作量
注解:
- @Aspect 切面標識
- @Pointcut 切入點
- @Before 前置通知
- @AfterReturning 后置通知
- @Around 環繞通知
- @AfterThrowing 異常拋出通知
通知類編寫:
package com.qf.aop.advice;import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.aspectj.lang.reflect.MethodSignature;import java.lang.reflect.Method; import java.util.Arrays;@Aspect public class AspectJAdvice {@Before(value = "execution(* com.qf.aop.service..*(..))")public void before(JoinPoint jp){Signature signature = jp.getSignature();//獲取簽名Object[] args = jp.getArgs();//獲取方法參數if(signature instanceof MethodSignature){//如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod();//獲取方法String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("準備執行方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args));}}@AfterReturning(value = "execution(* com.qf.aop.service..*(..))", returning = "returnValue")public void after(JoinPoint jp, Object returnValue){Object[] args = jp.getArgs(); //獲取方法參數Signature signature = jp.getSignature(); //獲取簽名if(signature instanceof MethodSignature){ //如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod(); //獲取方法String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("執行完方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",得到返回值:" + returnValue);}}@AfterThrowing(value = "execution(* com.qf.aop.service..*(..))", throwing = "t")public void exception(JoinPoint jp, Throwable t){Object[] args = jp.getArgs(); //獲取方法參數Signature signature = jp.getSignature(); //獲取簽名if(signature instanceof MethodSignature){ //如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod(); //獲取方法String methodName = method.getName();String className = method.getDeclaringClass().getName();System.out.println("執行方法時:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",發生了異常:" + t.getMessage());}}@Around("execution(* com.qf.aop.service..*(..))")public Object around(ProceedingJoinPoint pjp) throws Throwable {Object[] args = pjp.getArgs();//獲取方法的參數Object target = pjp.getTarget(); //獲取代理對象Signature signature = pjp.getSignature(); //獲取簽名if(signature instanceof MethodSignature) { //如果簽名是方法簽名Method method = ((MethodSignature) signature).getMethod(); //獲取被攔截的方法對象String methodName = method.getName();String className = method.getDeclaringClass().getName();try {System.out.println("準備執行方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args));Object returnValue = method.invoke(target, args);System.out.println("執行完方法:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",得到返回值:" + returnValue);return returnValue;} catch (Throwable t){System.out.println("執行方法時:" + className + "." + methodName + ",參數:" + Arrays.toString(args) + ",發生了異常:" + t.getMessage());throw t;}}return null;} }
啟用注解支持:
<!--配置通知對象--> <bean class="com.qf.aop.advice.AspectJAdvice"/> <!--啟動AspectJ注解 自動為類生成代理--><aop:aspectj-autoproxy proxy-target-class="true"/>
SpringMVC
簡介
1. Spring MVC
SpringMVC是一個Java 開源框架, 是Spring Framework生態中的一個獨立模塊,它基于 Spring 實現了Web MVC(數據、業務與展現)設計模式的請求驅動類型的輕量級Web框架,為簡化日常開發,提供了很大便利。
2. Spring MVC 核心組件
DispatcherServlet 前置控制器
負責接收請求、分發請求
Handler 處理器
處理器包括了攔截器、控制器中的方法等,主要負責處理請求
HandlerMapping 處理器映射器
解析配置文件、掃描注解,將請求與處理器進行匹配
HandlerAdpter 處理器適配器
根據請求來找到匹配的處理器,這個過程稱為適配
ViewResolver 視圖解析器
處理器執行后得到的結果可能是一個視圖,但這個視圖屬于邏輯視圖(頁面中存在邏輯代碼,比如循環、判斷),需要使用視圖解器行處理,這個過程稱為渲染視圖
Spring MVC工作原理
mvc工作原理
前端發送的請求由DispatcherServlet接收到,然后根據提供的HandlerMapping來分發過去,在分發請求過程中使用到HandlerAdapter來適配處理器(因為處理的類型無法確定),找到對應的處理器適配器之后就會執行這個處理器,執行會得到一個ModelAndView,然后交給ViewResolver進行解析得到視圖位置,然后對視圖進行渲染,渲染完成后將渲染好的視圖交給DispatcherServlet,然后傳回前端展示。
Spring MVC發展演變
1.Bean的名字或ID匹配URL請求
由于版本更新,使用新版本會無法完成過時的功能,但是為了更好地理解演變的過程,這里使用低版本:
<!--低版本--> <dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>4.3.9.RELEASE</version> </dependency> <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>4.3.9.RELEASE</version> </dependency> <dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version><scope>provided</scope> </dependency>
首先需要再web.xml配置文件中配置DispatcherServlet,包括初始化參數(全局上下文,自己項目的配置文件路徑以及使得項目啟動時就創建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:spring-mvc.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>
然后是自己寫的spring-mvc.xml配置,這里需要寫視圖解析器、處理器映射器(處理器適配器采用默認的,在底層mvc框架中會根據處理器類型尋找合適的處理器適配器)。具體的邏輯為前端發送請求->處理器映射方式、配置控制器找到控制器->控制器返回modelandview->視圖解析器解析路徑找到jsp文件:
<!--視圖解析器--><bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/"/><property name="suffix" value=".jsp"/></bean><!--處理器映射的方式:使用bean的名字或者id的值來與請求匹配--><bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/><!--通過id值匹配請求的URL--><bean id="/user" class="com.qf.controller.UserController"/>
處理器映射器給出映射的方式:使用bean的名字或者id,然后DispatcherServlet找到處理器適配器,處理器適配器提供id和處理器的路徑。然后底層會根據路徑找到處理器。
處理器:
public class UserController extends AbstractController {@Overrideprotected ModelAndView handleRequestInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {return new ModelAndView("user");} }
處理器返回ModelAndView給適配器,DispatcherServlet根據id拿到返回的ModelAndView,并交給視圖解析器進行處理,最終將處理好的路徑進行渲染。
但是這樣做有一個問題,每一個請求都需要一個控制器與之對應,如果有很多請求,那么就要寫很多個控制器。開發效率極為低下,而Spring提供了方法名匹配請求來解決這個問題。
2.Bean方法名匹配請求
方法名解析器:InternalPathMethodNameResolver,將方法名作為匹配URL請求的依據,與控制器關聯起來。
這樣一來請求就可以直接與控制器中的方法關聯,那么控制器中的方法就應該有多個。
多操作控制器:
MultiActionController控制器類,供其他控制器類繼承,在其子類中可以編寫多個處理請求的方法,然后使用方法名解析器去匹配請求。
控制器:
public class UserMultiController extends MultiActionController {//這個方法匹配/login請求public ModelAndView login(HttpServletRequest request, HttpServletResponse response){return new ModelAndView("login");}//這個方法匹配/register請求public ModelAndView register(HttpServletRequest request,HttpServletResponse response){return new ModelAndView("register");} }
編寫完控制器后需要寫相應的控制器映射器(視圖解析器不變):
spring-mvc.xml:
<!--方法名解析器,處理映射的方式:使用方法名--><bean id="methodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver"/><!--/login 請求使用該bean對象處理--><bean id="/login" class="com.qf.controller.UserMultiController"><property name="methodNameResolver" ref="methodNameResolver"/></bean><!--/register 請求使用該bean對象處理--><bean id="/register" class="com.qf.controller.UserMultiController"><property name="methodNameResolver" ref="methodNameResolver"/></bean>
按照這種匹配請求的方式,如果一個控制器要處理多個請求,就會導致此配置文件無限擴展,變得冗雜,后期難以維護,這時如何解決?
Spring提供了SimpleUrlHandlerMapping映射器,該映射器支持一個控制器與多個請求匹配的同時也解決了配置信息繁多的問題。
3.簡單URL處理器映射
在Bean方法名匹配請求方式的控制器不變的基礎上,只需要改動控制器映射器即可:
spring-mvc.xml:
<!--使用簡單URL處理器映射--><bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"><property name="mappings"><props><prop key="/view">userController</prop><prop key="/user/*">userMultiController</prop></props></property></bean><bean id="userController" class="com.qf.controller.UserController"/><bean id="userMultiController" class="com.qf.controller.UserMultiController"/>
隨著業務的增加,控制器的數量也為增加,請求的匹配也會增多,xml文件里雖然減少了冗余,但每次增加方法也會增加代碼量,如何解決?
-Spring提供了DefaultAnnotationHandlerMapping映射器,支持使用注解來匹配請求,這樣就解決了請求匹配導致配置信息繁多的問題,同時還提升了開發效率。
注解匹配請求
控制器中通過@Controller注解說明這是一個處理器,方法中通過@RequestMapping注解注明映射信息。
controller:
@Controller public class UserAnnotationController {@RequestMapping(value = "/login",method = RequestMethod.GET)public String login(){return "login";}@RequestMapping(value = "register",method = RequestMethod.GET)public String register(){return "register";} }
注意:這次的controller不需要實現接口或者繼承抽象類了,也就意味著可以自定義方法,只需要在方法上加注解就可以達到映射的效果。
寫好處理器后需要配置(視圖解析器還是不用變,只需要改變處理器映射器就行):
spring-mvc.xml:<!--類上的注解處理器--><bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/><!--方法上的注解處理器--><bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"><!--掃描包,使得該包下類以及類中定義的方法上所使用的注解生效--><context:component-scan base-package="com.qf.controller" />
新的版本配置
<!--較新的版本使用該標簽啟動注解支持--> <mvc:annotation-driven/> <!--掃描包,使類和類方法注解生效--> <context:component-scan base-package="com.qf.controller"/>
相當于使用一句話代替了原來對類和方法上注解處理器的聲明。
Spring MVC常用注解
@Controller
控制器的標識
@Controller public class UserController{}
@RequestMapping
該注解用于匹配請求(注明URI)
@Controller @RequestMapping("/user") public class UserController{@RequestMapping(value="/login", method=RequestMethod.POST)public int login(){return 1;} }
@RequestBody
該方法只能用在方法的參數上,用于從請求體中獲取數據并注入參數中,并且獲取的數據只能是JSON格式的數據。
@Controller @RequestMapping("/user") public class UserController{@RequestMapping(value="/login", method=RequestMethod.POST)public int login(@RequestBody User user){return 1;} }
@ResponseBody
該注解用于向頁面傳遞數據,如果沒有該注解,那么controller方法中返回的任何數據都會被認為是一個頁面字符串。
@Controller @RequestMapping("/user") public class UserController{@RequestMapping(value="/login", method=RequestMethod.POST)@ResponseBodypublic int login(@RequestBody User user){return 1;} }
@RequestParam
該注解只能用在方法的參數上, 用于從 URL 查詢字符串或表單參數中提取參數。
@Controller @RequestMapping("/user") public class UserController{@RequestMapping(value="/search", method=RequestMethod.GET)@ResponseBodypublic List<User> searchUsers(@RequestParam(value="name") String name){return new ArrayList<>();} }
注意:@RequestParam和@PathVariable的區別:
@PathVariable:
- 用于從 URL 路徑中提取參數。
- 例如:提取
http://example.com/user/john
中的john
。- 用于 RESTful 風格的 URL。
@RequestParam:
- 用于從 URL 查詢字符串或表單參數中提取參數。
- 例如:提取
http://example.com/user/search?name=john
中的name
,或提取表單提交的數據。- 適用于查詢字符串參數和表單參數。
@PathVariable
該注解只能應用在方法的參數上,用于從請求路徑中獲取數據并注入至參數中
@Controller @RequestMapping("/user") public class UserController{// /user/admin@RequestMapping(value="/{username}", method=RequestMethod.GET)@ResponseBodypublic User queryUser(@PathVariable("username") String username){return new User();} }
注意: 花括號
{}
用于定義路徑變量,表示 URL 中的動態部分,這些部分將被提取并傳遞給控制器方法的參數。 前端在發送請求時,必須用具體的username
替換路徑變量 。
@RequestHeader
該注解只能應用在方法的參數上,用于從請求頭中獲取數據
@RequestMapping("/find") public void findUsers(@RequestHeader("Content-Type") String contentType) {//從請求頭中獲取Content-Type的值 }
@CookieValue
該注解只能應用在方法的參數上,用于從請求中獲取cookie的值
@RequestMapping("/find") public void findUsers(@CookieValue("JSESSIONID") String jsessionId) {//從請cookie中獲取jsessionId的值 }
@ControllerAdvice
該注解只能應用在類上,表示這個類就是處理異常的控制器
/*** 異常處理的控制器*/ @ControllerAdvice //這個注解就是spring mvc提供出來做全局異常統一處理的 public class ExceptionController { }
@ExceptionHandler
該注解只能應用在@ControllerAdvice或者@RestControllerAdvice標識的類的方法上用來處理異常
/*** 異常處理的控制器*/ @ControllerAdvice //這個注解就是spring mvc提供出來做全局異常統一處理的 public class ExceptionController {@ExceptionHandler //異常處理器@ResponseBody //響應至頁面public String handleException(Exception e){return e.getMessage();} }
Spring 對 RESTFUL的支持
@RestController
相當于@Controller 和 @ResponseBody 注解的組合。表示該類中的所有方法執行完成后所返回的結果直接向頁面輸出。
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
靜態資源處理
靜態資源無法訪問的原因
靜態資源包含html、js、css、圖片、字體文件等。靜態文件沒有url-pattern,所以默認是無法訪問的。之所以可以訪問,是因為tomcat中有一個全局的servlet:org.apache.catalina.servlets.DefaultServlet,它的url-pattern是 “/”, 所以項目中不能匹配的靜態資源請求,都由這個Servlet來處理。但在SpringMVC中DispatcherServlet也采用了"/" 作為url-pattern, 那么項目中不會再使用全局的Serlvet,這樣就造成了靜態資源不能完成訪問。
處理方案
方案一:修改DispatcherServlet對應的url-pattern修改為"/"以外的其他匹配樣式。
方案二(建議):將所有的靜態資源放進一個static包中,如果需要訪問,則將defaultServlet的url-pattern的url-mapping改為/static/*
<!-- web.xml --> <servlet-mapping><servlet-name>default</servlet-name><url-pattern>/static/*</url-pattern></servlet-mapping>
方案三:利用default-servlet-handler 將處理靜態資源的請求轉發給容器的默認 Servlet ,而不是給DispatcherServlet。
<!-- spring-mvc.xml --> <!-- 這個handler就是處理靜態資源的,它的處理方式就是將請求轉會到tomcat中名為default的Servlet --> <mvc:default-servlet-handler/> <!-- mapping是訪問路徑,location是靜態資源存放的路徑 --> <mvc:resources mapping="/static/**" location="/static/" />
中文亂碼處理
在web.xml中配置字符編碼過濾器CharacterEncodingFilter
<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>