1. 程式人生 > >Spring通過AOP在Service層注入使用者資訊

Spring通過AOP在Service層注入使用者資訊

使用基於註解形式的AOP配置,在Service層或其他層,注入使用者登入資訊,這樣就不需要在用到使用者登入資訊時總是注入Http相關物件,不用手動setter使用者資訊,且使用者登入資訊會隨著session失效而自動登出。
.
相關類如下:
相關類截圖

相關類說明:

  1. AppUserContextConfig:模組的配置類,負責掃描模組的Bean,當需要使用此模組時,用Import(AppUserContextConfig.class)的形式引入。
  2. UserEnv :自定義註解,使用時標識在屬性或者getter方法上,value指明要注入的使用者資訊的名稱,未指定則使用屬性名稱首字母小寫作為使用者資訊屬性的名稱。
  3. UserEnvAnnotationProcessor:@UserEnv註解的解析類,負責解析@UserEnv上的資訊並從使用者登入資訊中獲取相應的屬性值。
  4. AopArchitecture:AOP結構類,定義了Spring容器內所有的切入點
  5. AspectAdviceConfig:切面通知定義類,引用了AopArchitecture的切入點,定義了Advice,即通知方法
  6. LoginUser:儲存使用者登入資訊的Model,使用者登入資訊類是其屬性的一個子集,使用者登入後將登入資訊拷貝到此Model中,@UserEnv的屬性值就是從此Model中獲取
  7. AppUser:工具類,負責提供靜態儲存使用者登入資訊及清空登入資訊的方法,和靜態的獲取登入資訊的方法

下面先看使用方法:

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public boolean login(@RequestBody User user, HttpServletRequest request) {
        user = userService.login(user);
        if
(user == null) { return false; } //拷貝使用者登入資訊 AppUser.resideUserEnv(user); //設定session有效期,當失效時,LoginUser會自動銷燬 request.getSession().setMaxInactiveInterval(30); return true; } @RequestMapping(method = RequestMethod.PUT) //測試注入的方法 public boolean testUserEnvInjection(@RequestBody User user) { return userService.modifyUser(user); } @RequestMapping(value = "/logout", method = RequestMethod.DELETE) public void logout() { System.out.println(AppUser.isLogin()); //清空使用者登入資訊 AppUser.clearUserEnv(); System.out.println(AppUser.isLogin()); } }

Service層很簡單,僅僅是做一個登入驗證:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User login(User user) {
        String userName = user.getUserName();
        String password = user.getPassword();
        User result = null;
        if (Character.isDigit(userName.charAt(0))) {
            result = userMapper.loginByTele(userName, password);
        } else {
            result = userMapper.loginByName(userName, password);
        }
        return result;
    }

    @Override
    public boolean modifyUser(@UserEnv User user) {
        return false;
    }
}

以上就是使用此模組的方法,很簡單。
下面就開始看模組內各類的程式碼及註釋:
先看模組引入類: AppUserContextConfig

@Configuration
@ComponentScan() // 當此註解內不指定掃描包路徑時,會使用此類的包名作為掃描的路徑,即"com.bob.config.mvc.userenv"
public class AppUserContextConfig {

}

當需要使用此模組時,Import(AppUserContextConfig .class)就能使用,切記不適用時不要掃描到這些類.
接下來看AOP結構定義類,此類定義了框架內所有的切入點,這麼做僅僅是為了讓所有的切入點配置在一起,不是強制的。

@Order(1)
@Aspect
@Component
public class AopArchitecture {

    /**
     * 面向Service層的切入點
     */
    @Pointcut("execution(public * com.bob.config.mvc.service.*.*(..))")
    public void serviceMethod() {
    }

    /**
     * 面向{@code UserEnv }註解的切入點
     */
    @Pointcut("@args(com.bob.config.mvc.userenv.ann.UserEnv)")
    public void injectMethod() {
    }

}

接下來看Advice定義類,此類定義了在AOP切入點執行的方法,我們在此使用註解的形式注入使用者登入資訊

/**
 * 面向切面的功能實現,對@UserEnv註解的屬性使用使用者資訊填充
 * 
 * @since 2016年12月6日 下午4:52:22
 *
 */
@Order(2)
@Aspect
@Component
public class AspectAdviceConfig {

    final static Logger LOGGER = LoggerFactory.getLogger(AspectAdviceConfig.class);

    @Autowired
    private UserEnvAnnotationProcessor userEnvAnnotationProcessor;

    /**
     * Service層注入使用者登入資訊
     * 
     * {@link com.bob.config.mvc.userenv.aspect.AopArchitecture#serviceMethod}
     * 
     * @param joinpoint
     * @throws Exception
     */
    @Before("com.bob.config.mvc.userenv.aspect.AopArchitecture.serviceMethod()")
    public void beforeServiceCall(JoinPoint joinpoint) throws Exception {
        Object[] args = joinpoint.getArgs();
        Signature sig = joinpoint.getSignature();
        MethodSignature msig = (MethodSignature) sig;
        Method method = msig.getMethod();
        boolean debug = LOGGER.isDebugEnabled();
        if (args != null && args.length != 0) {
            long t1 = System.nanoTime(), t2 = 0;
            boolean injected = false;
            StringBuilder sb = null;
            if (debug) {
                sb = new StringBuilder(1024);
                sb.append("觸發Service方法執行前注入, 方法名稱: [").append(sig.toLongString()).append("], ");
                sb.append("注入引數列表: ");
            }
            Annotation[][] annotations = method.getParameterAnnotations();
            int i, j;
            for (i = 0; i < annotations.length; i++) {
                Object bean = args[i];
                if (bean == null) {
                    continue;
                }
                Annotation[] pa = annotations[i];
                if (pa.length == 0) {
                    continue;
                }
                if (UserEnv.class.isInstance(pa[0])) {
                    injected = populateEnv(bean, debug, sb);
                    continue;
                }
                for (j = 1; j < pa.length; j++) {
                    if (UserEnv.class.isInstance(pa[j])) {
                        injected = populateEnv(bean, debug, sb);
                        break;
                    }
                }
            }
            if (debug && injected) {
                t2 = System.nanoTime();
                sb.append("\n完成注入共計耗時: ").append((t2 - t1) / 1000000.0).append("ms");
                LOGGER.debug(sb.toString());
            }
        }

    }

    /**
     * Service層方法執行過完之後
     * 
     * {@link com.bob.config.mvc.userenv.aspect.AopArchitecture#serviceMethod}
     * 
     * @param joinpoint
     * @param retVal
     */
    @AfterReturning(pointcut = "com.bob.config.mvc.userenv.aspect.AopArchitecture.serviceMethod()", returning = "retVal")
    public void afterServiceCall(JoinPoint joinpoint, Object retVal) {

    }

    /**
     * 填充@UserEnv標識的變數
     * 
     * @param bean
     * @param debug
     * @param sb
     * @throws Exception
     * @return
     */
    private boolean populateEnv(Object bean, boolean debug, StringBuilder sb) throws Exception {
        boolean result = false;
        try {
            result = userEnvAnnotationProcessor.process(bean, sb);
        } catch (IllegalArgumentException | IllegalAccessException e) {
            throw e;
        }
        return result;
    }

}

接下來看自定義註解,通過註解標識在屬性上或者getter方法上,來指定待注入的屬性。通過註解的value值,或者是待注入屬性的名稱,優先value,來從使用者登入資訊擇取想要的資訊。

/**
 * @since 2016年12月7日 下午2:01:13
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER })
public @interface UserEnv {

    /**
     * 環境變數的名稱
     * 
     * @return
     */
    String value() default "";

}

接下來看註解的解析類,解析結果將被快取,設定快取的大小,使用LinkedHashMap進行快取淘汰,注入前驗證型別符合,

/**
 * {@link @UserEnv}註解的解析器
 * 
 * @since 2016年12月7日 下午5:03:15
 * @version $Id$
 * @author JiangJibo
 *
 */
@Component
public class UserEnvAnnotationProcessor {

    final static Logger LOGGER = LoggerFactory.getLogger(UserEnvAnnotationProcessor.class);

    private static final int CACHE_LIMIT = 256;

    private static final Object INJECTION_LOCK = new Object();

    private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>();

    private final Set<Class<?>> nonAnnotatedClass = new HashSet<Class<?>>();

    /**
     * 解析Class後生成的InjectionElement快取
     */
    @SuppressWarnings("serial")
    private volatile Map<Class<?>, Set<InjectionElement>> injectionMetadataCache =

            new LinkedHashMap<Class<?>, Set<InjectionElement>>(64, 0.75f, true) {

                /* (non-Javadoc)
                 * @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
                 */
                @Override
                protected boolean removeEldestEntry(Entry<Class<?>, Set<InjectionElement>> eldest) {
                    return size() > getCacheLimit();
                }

            };

    /**
     * 預設注入的註解為{@link @UserEnv}
     */
    public UserEnvAnnotationProcessor() {
        autowiredAnnotationTypes.add(UserEnv.class);
    }

    /**
     * 處理Bean,注入{@link @UserEnv}註解
     * 
     * @param bean
     * @param sb
     * @return
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    public boolean process(Object bean, StringBuilder sb) throws IllegalArgumentException, IllegalAccessException {
        Assert.notNull(bean, "被注入物件不能為空");
        Class<?> beanClass = bean.getClass();
        if (!beanClass.isAnnotationPresent(UserEnv.class)) {
            LOGGER.warn("[{}]未標識有[{}]註解", beanClass.getSimpleName(), UserEnv.class.getSimpleName());
            return false;
        }
        if (nonAnnotatedClass.contains(beanClass)) {
            LOGGER.warn("[{}]內部不含指定的[{}]註解屬性或方法", beanClass.getSimpleName(), UserEnv.class.getSimpleName());
            return false;
        }
        Set<InjectionElement> injectionElements = injectionMetadataCache.get(beanClass);
        if (injectionElements == null) {
            synchronized (INJECTION_LOCK) {
                injectionElements = injectionMetadataCache.get(beanClass);
                if (injectionElements == null) {
                    injectionElements = buildAutowiredMetadata(beanClass);
                    if (injectionElements.isEmpty()) {
                        LOGGER.warn("未找到標註[{}]註解的屬性或方法", autowiredAnnotationTypes.toString());
                        nonAnnotatedClass.add(beanClass);
                        return false;
                    }
                    injectionMetadataCache.put(beanClass, injectionElements);
                }
            }
        }
        return injectByField(bean, injectionElements, sb) | injectByMethod(bean, injectionElements, sb);
    }

    /**
     * 根據屬性注入
     * 
     * @param bean
     * @param sb
     * @return
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    private boolean injectByField(Object bean, Set<InjectionElement> injectionElements, StringBuilder sb)
            throws IllegalArgumentException, IllegalAccessException {
        boolean success = false;
        for (InjectionElement injectionElement : injectionElements) {
            if (!injectionElement.isField) {
                continue;
            }
            Field field = (Field) injectionElement.member;
            ReflectionUtils.makeAccessible(field);
            String fieldName = field.getName();
            String key = injectionElement.key;
            key = StringUtils.hasText(key) ? key : fieldName;
            if (!isTypeMatch(field.getType(), key, fieldName)) {
                continue;
            }
            Object value = AppUser.getUserEnv(key);
            field.set(bean, value);
            sb.append("[" + fieldName + "]  =  [" + value + "],");
            success = true;
        }
        return success;
    }

    /**
     * 根據getter()方法注入
     * 
     * @param bean
     * @param sb
     * @return
     * @throws SecurityException
     * @throws NoSuchMethodException
     */
    private boolean injectByMethod(Object bean, Set<InjectionElement> injectionElements, StringBuilder sb) {
        boolean success = false;
        for (InjectionElement injectionElement : injectionElements) {
            if (injectionElement.isField) {
                continue;
            }
            Method method = (Method) injectionElement.member;
            PropertyDescriptor proDesc = BeanUtils.findPropertyForMethod(method);
            String fieldName = proDesc.getName();
            String key = injectionElement.key;
            key = StringUtils.hasText(key) ? key : fieldName;
            if (!isTypeMatch(method.getReturnType(), key, fieldName)) {
                continue;
            }
            Object value = AppUser.getUserEnv(key);
            ReflectionUtils.invokeMethod(proDesc.getWriteMethod(), bean, value);
            sb.append("[" + fieldName + "]  =  [" + value + "],");
            success = true;
        }
        return success;
    }

    /**
     * 校驗待注入的成員的型別
     * 
     * @param fieldType
     * @param key
     * @param fieldName
     * @return
     */
    private boolean isTypeMatch(Class<?> fieldType, String key, String fieldName) {
        if (!fieldType.isAssignableFrom(String.class) && !fieldType.isAssignableFrom(Integer.class)) {
            LOGGER.warn("環境變數[{}]注入失敗,暫時只支援String和int型別的注入", fieldName);
            return false;
        }
        Object value = AppUser.getUserEnv(key);
        if (null == value) {
            LOGGER.warn("環境變數[{}]注入失敗,未查詢到指定名稱的環境變數", fieldName);
            return false;
        }
        if (!value.getClass().isAssignableFrom(fieldType)) {
            LOGGER.warn("環境變數:[{}]的型別不匹配待注入的屬性:[{}]", key.getClass().getName(), fieldName);
            return false;
        }
        return true;
    }

    /**
     * 獲取類上標註了{@link @UserEnv}註解的成員資訊
     * 
     * @param clazz
     * @return
     */
    private Set<InjectionElement> buildAutowiredMetadata(final Class<?> clazz) {

        final Set<InjectionElement> injectionElements = new HashSet<InjectionElement>();

        ReflectionUtils.doWithLocalFields(clazz, new ReflectionUtils.FieldCallback() {

            @Override
            public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                AnnotationAttributes ann = findAutowiredAnnotation(field);
                if (ann != null) {
                    if (Modifier.isStatic(field.getModifiers())) {
                        LOGGER.warn("[{}]註解不適用於靜態方法:[{}]", autowiredAnnotationTypes.toString(), field);
                        return;
                    }
                    injectionElements.add(new InjectionElement(field, ann.getString("value")));
                }
            }
        });

        ReflectionUtils.doWithLocalMethods(clazz, new ReflectionUtils.MethodCallback() {

            @Override
            public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {

                AnnotationAttributes ann = findAutowiredAnnotation(method);
                if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                    if (Modifier.isStatic(method.getModifiers())) {
                        LOGGER.warn("註解不適用於靜態方法: [{}]", autowiredAnnotationTypes.toString(), method);
                        return;
                    }
                    if (!method.getName().startsWith("get") || method.getParameterTypes().length > 0) {
                        LOGGER.warn("[{}]註解只適用於getter方法,而非: [{}]方法", autowiredAnnotationTypes.toString(), method);
                        return;
                    }
                    injectionElements.add(new InjectionElement(method, ann.getString("value")));
                }
            }
        });

        return injectionElements;
    }

    /**
     * 獲取一個成員成標註了{@link @UserEnv}註解(或元註解)的註解資料
     * 
     * @param ao
     * @return
     */
    private AnnotationAttributes findAutowiredAnnotation(AnnotatedElement ao) {
        if (ao.getAnnotations().length > 0) {
            for (Class<? extends Annotation> type : this.autowiredAnnotationTypes) {
                AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ao, type);
                if (attributes != null) {
                    return attributes;
                }
            }
        }
        return null;
    }

    /**
     * 獲取快取容量
     * 
     * @return
     */
    public int getCacheLimit() {
        return CACHE_LIMIT;
    }

    /**
     * 清空快取
     */
    public void clearCache() {
        this.injectionMetadataCache.clear();
    }

    /**
     * 待注入的成員資訊
     * 
     * @since 2017年1月22日 上午9:12:19
     * @version $Id$
     * @author JiangJibo
     *
     */
    private static class InjectionElement {

        private Member member;
        private boolean isField;
        private String key;

        /**
         * @param member
         * @param key
         */
        public InjectionElement(Member member, String key) {
            this.member = member;
            this.isField = (member instanceof Field);
            this.key = key;
        }

    }

}

接下來看儲存使用者登入資訊的LoginUser類,我們將此類定義成一個Bean,作用域是session,因此此Bean到時候會儲存在HttpSession中,會隨著session的失效而銷燬。且對生成這個Bean的建構函式做了限制,例項化此Bean是一定要將使用者的原始資訊物件作為引數,LoginUser將會拷貝使用者的原始登入資訊。

@Component()
@Scope("session")
public class LoginUser implements Serializable {

    private static final long serialVersionUID = 8555472524102629901L;

    private volatile Integer id;
    private volatile String userName;
    private volatile String password;
    private volatile Integer age;
    private volatile String telephone;
    private volatile String adress;

    @Autowired()
    public LoginUser(Object userInfo) {
        BeanUtils.copyProperties(userInfo, this);
    }

    setter,getter ....

}

最後看工具類AppUser,此類負責將登入資訊存入LoginUser,及從LoginUser從獲取指定的資訊,從Spring容器中獲取LoginUser Bean。

@Component
public class AppUser implements BeanFactoryAware {

    final static Logger LOGGER = LoggerFactory.getLogger(AppUser.class);

    private static BeanFactory beanFactory;

    public static final String USER_ENV_BEAN_NAME = ClassUtils.getShortNameAsProperty(LoginUser.class);

    /**
     * 獲取當前Bean例項
     * 
     * @return
     */
    public static AppUser getAppUser() {
        return beanFactory.getBean("appUser", AppUser.class);
    }

    /**
     * 根據使用者屬性名稱獲取登入使用者資訊
     * 
     * @param name
     * @return
     */
    public static Object getUserEnv(String name) {
        LoginUser loginUser = beanFactory.getBean(USER_ENV_BEAN_NAME, LoginUser.class);
        try {
            Field field = ReflectionUtils.findField(LoginUser.class, name);
            field.setAccessible(true);
            return field.get(loginUser);
        } catch (Exception e) {
            LOGGER.error("嘗試注入屬性:[{}]時出現異常", name, e);
            throw new CustomizedException(e);
        }
    }

    /**
     * 將使用者的登入資訊寄存在應用(session)中
     *
     * @param user
     */
    public static void resideUserEnv(Object user) {
        beanFactory.getBean(LoginUser.class, user);
    }

    /**
     * 清除使用者登入資訊
     */
    public static void clearUserEnv() {
        ((DefaultListableBeanFactory) beanFactory).destroyScopedBean(USER_ENV_BEAN_NAME);
    }

    /**
     * 驗證是否有使用者登入
     * 
     * @return
     */
    public static boolean isLogin() {
        try {
            beanFactory.getBean(USER_ENV_BEAN_NAME);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        AppUser.beanFactory = beanFactory;
    }

}