目錄
AspectJ
| 1
2
 | graph TD;
AOP ---> SpringAop & AspectJ
 | 
AOP (概念)
面向切面編程,利用AOP可以對業務邏輯的各個部分進行隔離,使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高開發的效率
不修改原始碼,從而擴充新功能
Filter(過濾器)Interceptor(攔截器)AspectJ(AOP)之差異
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 | flowchart LR;
1((使用者))--發送請求
-->Filter\n+統一設置編碼\n+過濾敏感字\n+登入驗證\n+URL級別的訪問權限控制\n+數據壓縮
-->dispatcher
-->Interceptor\n+權限驗證\n+登入驗證\n+性能檢測
-->AOP\n+日誌紀錄
-->2(Controller)
-1[粗糙]--能處理request的精細程度---->-2[細緻]
 | 



Filter
| 1
2
3
4
5
6
 | 
flowchart LR;
1[瀏覽器]--->2{過濾器}--->3[Web資源]
3[Web資源]-->2{過濾器}-->1[瀏覽器]
 | 
在HttpServletRequest到達Servlet之前,過濾、處理一些資訊,本身依賴Sevlet容器,不能獲取SpringBean的一些資訊,它是javax.servlet.FilterChain的項目,不是Springboot
可以做什麼
- 修改Request, Response
- 防止xss(Cross-Site-SCripting跨網站指令碼)攻擊
- 包裝二進制流
自定義Filter
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 | /**
* 網路上教學蠻多都是implenments filter,但我建議extend GenericFilterBean
* 會比較方便一點,省去implenments init(), distory()的麻煩 
*/
@Slf4j
@Component
@WebFilter(filterName = "f1",urlPatterns = {"*.html","*.jsp","/"})  //filterName就只是一個名稱可以,隨意就好,urlPattern是用來指定哪些url要經過這個過濾器
public class HiFilter extends GenericFilterBean {
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      log.info("Hello Hoxton");
      chain.doFilter(request,response); //代表這個Filter已經作用完畢,可以把request,response交給下一個Filter了
    }
}
 | 

結果如上
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 | @Slf4j
/**
* 網路上教學蠻多都是implenments filter,但我建議extend GenericFilterBean
* 會比較方便一點,省去implenments init(), distory()的麻煩 
*/
public class HiFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
      log.info("Hello Hoxton");
      chain.doFilter(request,response);  //代表這個Filter已經作用完畢,可以把request,response交給下一個Filter了
    }
}
 | 
| 1
2
3
4
5
6
7
8
9
 | @Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean heFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean(new HiFilter());
        registration.addUrlPatterns("/*"); //配置相關的路徑
        return registration;
    }
}
 | 
一些其他的config設置,僅供參考,與上面釋例無關
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 | @Configuration
public class FilterConfig {
//test
@Bean
public FilterRegistrationBean<Filter> logProcessTimeFilter() {
  FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
  bean.setFilter(new LogProcessTimeFilter()); //設定想要使用哪一個Filter
  bean.addUrlPatterns("/*"); //設置哪些url會觸發Filter,設置成/* 就代表全部都會吃到,/user/*就代表/user開頭的都會吃到
  bean.setName("logProcessTimeFilter"); //設置要叫什麼名字
  bean.setOrder(0); //設定過濾器的執行順序
  return bean;
}
@Bean
public FilterRegistrationBean<Filter> logApiFilter() {
  FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
  bean.setFilter(new LogApiFilter()); //設定想要使用哪一個Filter
  bean.addUrlPatterns("/*"); //設置哪些url會觸發Filter,設置成/* 就代表全部都會吃到,/user/*就代表/user開頭的都會吃到
  bean.setName("logApiFilter"); //設置要叫什麼名字
  bean.setOrder(1); //設定過濾器的執行順序
  return bean;
}
@Bean
public FilterRegistrationBean<Filter> printResponseRequestFilter() {
  FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
  bean.setFilter(new PrintResponseRequest()); //設定想要使用哪一個Filter
  bean.addUrlPatterns("/*"); //設置哪些url會觸發Filter,設置成/* 就代表全部都會吃到,/user/*就代表/user開頭的都會吃到
  bean.setName("printResponseRequestFilter"); //設置要叫什麼名字
  bean.setOrder(2); //設定過濾器的執行順序
  return bean;
}
}
 | 
SpringBoot本身也提供了許多不同的Filter供使用,參考如下

常用的有以下幾個
- CharacterEncodingFilter(用於處理編碼問題)
- HiddenHttpMethodFilter(隱藏Http函數)
- HttpPutFormContentFilter(form表單處理)
- RequesrtContextFilter(請求上下文)
其他資訊可以詳閱Spring MVC中各个filter的用法
其中以OncePerRequestFilter最常被使用,這個Filter會去過濾每一個Request請求,且不會重複執行,且這個Filter有一個doFilterInternal()的方法,供我們撰寫Filter邏輯因doFilter()的方法已在OncePerRequestFilter裡面實現了,可以用來做Jwtoken的登入驗證,程式如下:
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 | @Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtService jwtService;
    @Autowired
    private UserDetailsService userDetailsService;
    //注入JwtService UserDetailsService,分別用來解析Token與查詢使用者詳情
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws          ServletException, IOException {
        String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (authHeader != null) {
            String accessToken = authHeader.replace("Bearer ", "");
            //從請求標頭中取得Authorization欄位中的值
            Map<String, Object> claims = jwtService.parseToken(accessToken);
            //擷取出後面的JWT字串,接著解析它
            String username = (String) claims.get("username");
            //從claims物件中取得username欄位的值
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            //並透過userDetailService查詢使用者詳情。這也代表JWT的內容(payload)必須包含username這個欄位
            //在filter中查詢使用者的目的,是為了將該次請求所代表的驗證後資料(Authentication)帶進security中的Context。
            //Context是一種較抽象的概念,可以想像成該次請求的身分狀態
            Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            //為了將這個請求的使用者身分告訴伺服器,我們建立UsernamePasswordAuthenticationToken的物件,其中第三個參數放GrantedAuthority的List,              作為API的授權檢查
            //第一個參數(principal)傳入使用者詳請(UserDetails)。
            // 而第二個參數是credential,通常是密碼,但不傳入也無訪
            //經由傳入principal,我們得以在業務邏輯中從Context輕易獲取使用者身分的資料
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}
 | 
配置完後再將這個Filter加入Security的過濾鍊
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 | @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private   UserDetailsService userDetailsService;
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    /**
     * 協助帳號密碼認證的東西
     * @return
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
    }
    //加入Security的過濾鍊
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .antMatchers(HttpMethod.GET, "/users/**").hasAuthority(MemberAuthority.SYSTEM_ADMIN.name())
//                .antMatchers(HttpMethod.GET,"/h2/**").hasAuthority(MemberAuthority.SYSTEM_ADMIN.name())
                .antMatchers(HttpMethod.GET,"/login/**").permitAll()
//                .antMatchers(HttpMethod.POST,"login").permitAll()
//                .antMatchers(HttpMethod.POST, "/users").permitAll()
                .anyRequest().permitAll()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //於UsernamePasswordAuthenticationFilter進行認證
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable()
                .formLogin();
        http.headers().frameOptions().disable();
        //讓spring Security可以和h2建立連線
    }
    /**
     *
     * @param auth 配置全局驗證資訊,如Authentication Provider,UserDetailService等等資訊,
     *             authenticationManager會接收到UsernamePasswordAuthenticationToken傳入的資料後
     *             調用SecurityConfig中所配置的userDetailsService,passwordEncoder來協助驗證
     *
     * @throws Exception
     */
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
}
 | 
一些Code的示範
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
 | public class LogProcessTimeFilter extends OncePerRequestFilter {
    /**
     * @param request     請求
     * @param response    回應
     * @param filterChain 過濾鏈 會將現有的filter給串聯起來,當請求進入後端,需要依序經過它們才會達到Controller,相對的,當回應離開Controller,則是按照相反的方向經過那些Filter
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        filterChain.doFilter(request, response); //doFilter:相當於將請求送至Controller。
        long endTime = System.currentTimeMillis();
        long processTime = endTime - startTime;
        System.out.println("processTime = " + processTime);
    }
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
 | /**
 * Controller收到的請求主體(RequestBody)和回應主體(ResponseBody)
 * 分別由HttpServletRequest與HttpServletResponse的InputStream、OutputStream轉化而來,
 * 但資料流只能讀取一次,如果在Filter層就被讀掉,可能會導致後面都收不到資料
 * 為了保留主體中的資料,我們將請求主體與回應主體包裝成ContentCachingResponseWrapper ContentCachingRequestWrapper
 * 再如同往常傳入FilterChain
 *
 * 這兩個Wrapper的特色是會在內部備份一個ByteArrayOutputStream,我們只要呼叫這兩個Wrapper的
 * getContentAsByteArray就可以無限制地取得主體內容
 */
public class PrintResponseRequest extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        filterChain.doFilter(requestWrapper, responseWrapper);
//        logApi(request, response);
        logBody(requestWrapper,responseWrapper);
        responseWrapper.copyBodyToResponse();
    }
    private void logApi(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int httpStatus = response.getStatus(); //200,403,404之類的
        String httpMethod = request.getMethod();
        String uri = request.getRequestURI();
        String params = request.getQueryString();
        if (params != null) {
            uri += "?" + params;
        }
        System.out.println(String.join(" ", String.valueOf(httpStatus), httpMethod, uri));
    }
    private void logBody(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
        String requestBody = getContent(request.getContentAsByteArray());
        System.out.println("Request: " + requestBody);
        String responseBody = getContent(response.getContentAsByteArray());
        System.out.println("Response: " + responseBody);
    }
    /**
     * @param content
     * @return 返回JSON字串
     */
    private String getContent(byte [] content){
        String body = new String(content);
        return body.replaceAll("[\n\t]", ""); //去除換行\n與定位符號\t
    }
}
 | 
Interceptor
本身是AOP的一種應用,其實攔截器跟過濾器是可以互相替換的,功能其實差不多,只是攔截器可以在請求到達Controller或是回應回傳出Contrller時進行攔截,攔截成功時可以實做一些自定義的業務邏輯進行修改,且Interceptor是Springboot下的一個功能org.springframework.web.servlet.HandlerInterceptor
可以用來
- 性能監控:紀錄請求的處理時間,比如說請求處理太久(超過500毫秒)
- 登入檢測

|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 | graph LR;
request-->id1
id1-->id2-->id4-->id3-->id6-->id5
subgraph  攔截器1
direction TB
id1["preHandle()"] 
id3["postHandler()"]
id5["afterCompletion()"]
 end
subgraph  攔截器2
direction TB
id2["preHandle()"]
id4["postHandler()"]
id6["afterCompletion()"]
 end
 | 
要實現interceptor有兩種方式
- 實作HandlerInterceptor
- 繼承HandlerInterceptorAdapter
釋例
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 | public class LogInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        System.out.println("\n-------- LogInterception.preHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("Start Time: " + System.currentTimeMillis());
        request.setAttribute("startTime", startTime);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("\n-------- LogInterception.postHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("\n-------- LogInterception.afterCompletion --- ");
        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("End Time: " + endTime);
        System.out.println("Time Taken: " + (endTime - startTime));
    }
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 | @Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor());
        registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin");
        registry.addInterceptor(new AdminInterceptor()).addPathPatterns("/admin/*").excludePathPatterns("/admin/oldLogin");
    }
}
 | 
AspectJ
屬於一種AOP框架
AOP(JDK動態代理)
使用JDK的動態代理,要使用Proxy類裡面的方法來創建出代理對象 newProxyInstance(類加載器,增強方法所在的類,這個類實現的介面,實現這個接口(InvocationHandler)

編寫JDK動態代碼
| 1
2
3
4
5
6
 | public interface UserDao {
    public int add (int a,int b);
    public String update(String id);
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 | public class UserDaoImpl implements UserDao{
    @Override
    public int add(int a, int b) {
        System.out.println("add方法執行了");
        return a+b;
    }
    @Override
    public String update(String id) {
        return id;
    }
}
 | 
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
 | package com.example.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
 * @author Hoxton
 * @version 1.1.0
 */
public class JDKProxy {
    public static void main(String[] args) {
        Class[] interfaces = {UserDao.class};
        UserDaoImpl userDao = new UserDaoImpl();
        UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
        //此dao已經不是原本的dao,而是新的代理類dao了
        int result = dao.add(1, 2);
        System.out.println("result = " + result);
    }
}
//創建代理對象的代碼
class UserDaoProxy implements InvocationHandler {
    //1. 把創建的是誰的代理對象,把誰傳遞進來
    // 有參建構子
    private Object obj;
    public UserDaoProxy(Object obj) {
        this.obj = obj;
    }
    //增強的邏輯
    @Override
    public Object invoke(Object proxy, Method method, Object[] methodArgs) throws Throwable {
        //方法之前
        System.out.println("方法之前執行..." + method.getName() + "傳遞的參數..." + Arrays.toString(methodArgs));
        //被增強的方法執行
        Object res = method.invoke(obj, methodArgs);
        //方法之後
        System.out.println("方法之後執行..." + obj);
        return res;
    }
}
 | 
AOP專業術語
- 連接點 - 一個類裡面中,能被增強的方法就叫連接點,下面這個類就有四個連接點  
 | 1
2
3
4
5
6
 | class User{
    add();
    update();
    select();
    delete();
}
 |  
 
- 切入點 - 實際被增強的方法,就叫切入點 
- 通知(增強) - 實際增強的邏輯部分稱為通知(增強) 
- 通知有多種類型 - 前置通知 - 在切入點前執行 
- 後置通知 - 在切入點後執行 
- 環繞通知 - 在切入點前後執行 
- 異常通知 - 出現異常時執行 
- 最終通知 - 執行到try…catch的final時執行 
 
 
- 切面 - 是一個動作 - 把通知應用到切入點的過程,就叫切面
 
AOP(準備)
- Spring 框架一般都是基於AspectJ實現的AOP操作 - 什麼是AspectJ - AspectJ不是Spring的組成部分,是一個獨立的AOP框架, 一般把AspectJ和Spring框架一起使用,進行AOP操作
 
- 基於Aspect實現AOP操作 - xml配置文件實現
- 基於註解方法實現(主要使用)
 
- 再專案裡面引入AOP依賴 
- 切入點表達式 - 切入點表達式的作用: 知道對哪個類的哪個方法進行增強 
- 語法結構: - execution( [權限修飾符] [返回類型] [類全路徑] [方法名稱] ( [參數列表] ) ) - 權限修飾符: public, private, *(代表不論是public, private 都選) 
- 返回類型: String, int 
- 類全路徑: com.hoxton……. 
- 方法名稱: 就方法名稱 
- 參數列表: 有哪些參數 - 舉例 - 對com.hoxton.dao.BookDao類裡面的add方法進行增強 | 1
 | execution(* com.hoxton.dao.BookDao.add(..) )
 |  
 
 
- 對com.hoxton.dao.BookDao類的所有方法進行增強 | 1
 | execution(* com.hoxton.dao.BookDao.*(..))
 |  
 
 
- 對com.hoxton.dao包裡的所有類,類裡面的髓有方法進行增強 | 1
 | excution(* com.hoxton.dao.*.*(..))
 |  
 
 
 
 - within([package名].* )或( [package名]..*) - 舉例 
 
 
 
AOP操作(Aspect J 註解)
- 創建類,在類裡面定義方法
| 1
2
3
4
5
6
 | public class User {
    public void add(){
        System.out.println("add");
    }
}
 | 
- 創建增強類(編寫增強邏輯) - 在增強類的裡面,創建方法,讓不同方法代表不同通知類型 | 1
2
3
4
5
 | public class UserProxy {
    public void before(){
        System.out.println("before");
    }
}
 |  
 
 
- 進行通知的配置 - 在Spring
 
Log4j 2
| 1
2
3
4
 | flowchart TD;
8["ALL(全輸出不留情)"]--->7["Trace(更細的除錯資訊,通常用來追蹤城市流程的日誌)"]--->6["DEBUG(除錯資訊的日志)推薦★"]--->5["INFO(一般資訊的日志)推薦★"]--->4["WARN(可能導致錯誤的日志)"]--->3["ERROR(造成應用錯誤停止的日志)"]--->2["FETAL(造成應用程式停止的日志)"]--->1["OFF(不輸出任何日志)"]
 | 
參考
https://www.cnblogs.com/itlihao/p/14329905.html
https://blog.csdn.net/fly910905/article/details/86537648
SpringBoot攔截器(Interceptor)詳解
Spring Boot使用過濾器和攔截器分別實現REST介面簡易安全認證