Spring Aop自定义切面注解+包装Request+自定义过滤器+ThreadLocal

一、切面表达式

execution()是最常用的切点函数,其语法如下所示:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)

execution(<修饰符模式>? <返回类型模式><声明类型模式><方法名模式>(<参数模式>) <异常模式>?) 除了返回类型模式、方法名模式和参数模式外,其它项都是可选的

返回类型模式确定方法的返回类型必须是什么,以便匹配连接点。*最常用作返回类型模式。它匹配任何返回类型。只有当方法返回给定类型时,完全限定的类型名称才匹配。
参数模式稍微复杂一些:()匹配一个不带参数的方法,而(..)匹配任意数量(零个或更多)的参数。(*)模式匹配采用任意类型的一个参数的方法。(*String)匹配采用两个参数的方法。第一个可以是任何类型,而第二个必须是字符串。

例子可参考

二、实战代码

Controller

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.CustomizableThreadCreator;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/controller")
public class TestController implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @GetMapping("/test")
    public String test(@RequestParam String name){
        System.out.println("TestController请求进来了:"+Thread.currentThread().getName());
        TestEvent event = new TestEvent(this,name);
//        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
//        for (int i = 0; i <beanDefinitionNames.length ; i++) {
//            System.out.println(beanDefinitionNames[i]);
//        }
        applicationEventPublisher.publishEvent(event);
        System.out.println("TestController请求出去了:"+Thread.currentThread().getName());
        CustomizableThreadCreator bean = applicationContext.getBean(CustomizableThreadCreator.class);
        System.out.println("CustomizableThreadCreator:"+bean.getThreadNamePrefix());
        return "success";
    }

    @PostMapping("/test2")
    @LogAespect
    public String test2(@RequestBody TestRequest request){
        System.out.println("TestController请求进来了:"+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
            System.out.println("hobby:"+request.getHobby());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("TestController请求出去了:"+Thread.currentThread().getName());
        return "success";
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        TestController.applicationContext=applicationContext;
    }
}

定义切面
常用的两种:一种是基于表达式,另一种是基于注解的。注解十分灵活,所以编程中使用的较多。在你需要拦截的方法上面加上自定义注解@LogAespect即可。

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Aspect
@Component
public class TestAspect {

    @Pointcut("execution(public * com.test.realname.controller.TestController.test(..))")
    public void pointCut(){
        //这里一般无实际意义
    }

    @Before("pointCut()")
    public void before()
    {
        System.out.println("===before====");
    }

    @AfterReturning("pointCut()")
    public void afterReturning()
    {
        System.out.println("===afterReturning===");
    }

    @Around("@annotation(com.test.realname.controller.LogAespect)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long l = System.currentTimeMillis();
        HttpServletRequest request = MyFilter.CURRENT_REQUEST.get();
        String requestURI = request.getRequestURI();
        Object proceed = joinPoint.proceed();
        String s=null;
        if(proceed instanceof String){
            s = (String) proceed;
        }
        System.out.println("请求路径"+requestURI);
        System.out.println("响应"+s);
        System.out.println("cost time"+(System.currentTimeMillis()-l));
        return proceed;
    }
}

定义切面注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAespect {
}

request

import java.io.Serializable;

public class TestRequest implements Serializable {

    private String hobby;
    private String streamNo;

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }

    public String getStreamNo() {
        return streamNo;
    }

    public void setStreamNo(String streamNo) {
        this.streamNo = streamNo;
    }

    @Override
    public String toString() {
        return "TestRequest{" +
                "hobby='" + hobby + '\'' +
                ", streamNo='" + streamNo + '\'' +
                '}';
    }
}

request请求中的RequestBody只能读取一次,因为RequestBody是流ServletInputStream,只能读取一次,所以需要对request请求进行包装,使其能多次重复读取。

import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class HttpRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;
    public HttpRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body= StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException{
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
    }
}

自定义过滤器,将request缓存到ThreadLocal中,方便在切面中使用。

ThreadLocal记得在finally中清空,防止内存泄漏。

import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@Component
public class MyFilter extends OncePerRequestFilter {

    public static final ThreadLocal<HttpServletRequest> CURRENT_REQUEST = new ThreadLocal<>();

    public static boolean isJsonRequest(HttpServletRequest request){
        if(request==null){
            return false;
        }
        return StringUtils.contains(request.getContentType(),"json");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        boolean jsonRequest = isJsonRequest(httpServletRequest);
        try {
            if(jsonRequest){
                HttpRequestWrapper wrapper = new HttpRequestWrapper(httpServletRequest);
                CURRENT_REQUEST.set(wrapper);
                String s = StreamUtils.copyToString(wrapper.getInputStream(), StandardCharsets.UTF_8);
                Map map = JSON.parseObject(s, Map.class);
                System.out.println(map);
                filterChain.doFilter(wrapper,httpServletResponse);
            }else {
                CURRENT_REQUEST.set(httpServletRequest);
                filterChain.doFilter(httpServletRequest,httpServletResponse);
            }
        }finally {
            CURRENT_REQUEST.remove();
        }
    }
}

正常返回

{hobby=ball}
TestController请求进来了:http-nio-8080-exec-1
hobby:ball
TestController请求出去了:http-nio-8080-exec-1
请求路径/controller/test2
响应success
cost time1009

如果取消包装的话,就会直接报错

{hobby=ball}
2023-01-03 15:45:31.664 WARN 13596 — [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public java.lang.String com.test.realname.controller.TestController.test2(com.test.realname.controller.TestRequest)]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
动态数据源切换可以通过AOP(面向切面编程)和ThreadLocal来实现。其中,ThreadLocal是Java中的一个线程级别的变量,可以在同一个线程中共享数据。自定义注解可以用来标记需要切换数据源的方法。 实现步骤如下: 1. 定义数据源切换注解 ```java @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { String value() default "primary"; } ``` 2. 定义数据源切换切面 ```java @Aspect @Component public class DataSourceAspect { @Pointcut("@annotation(com.example.demo.annotation.DataSource)") public void dataSourcePointCut() { } @Before("dataSourcePointCut()") public void before(JoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); DataSource dataSource = signature.getMethod().getAnnotation(DataSource.class); if (dataSource != null) { DynamicDataSourceContextHolder.setDataSource(dataSource.value()); } else { DynamicDataSourceContextHolder.setDataSource("primary"); } } @After("dataSourcePointCut()") public void after(JoinPoint joinPoint) { DynamicDataSourceContextHolder.clearDataSource(); } } ``` 3. 定义ThreadLocal存储数据源信息 ```java public class DynamicDataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static void setDataSource(String dataSource) { contextHolder.set(dataSource); } public static String getDataSource() { return contextHolder.get(); } public static void clearDataSource() { contextHolder.remove(); } } ``` 4. 在数据源配置文件中定义多个数据源 ```java @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> dataSourceMap = new HashMap<>(2); dataSourceMap.put("primary", primaryDataSource()); dataSourceMap.put("secondary", secondaryDataSource()); dynamicDataSource.setTargetDataSources(dataSourceMap); dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); return dynamicDataSource; } } ``` 5. 在需要切换数据源的方法上添加@DataSource注解 ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @DataSource("primary") public List<User> listPrimaryUsers() { return userMapper.listUsers(); } @Override @DataSource("secondary") public List<User> listSecondaryUsers() { return userMapper.listUsers(); } } ``` 这样,在调用listPrimaryUsers和listSecondaryUsers方法时,就会自动切换到对应的数据源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值