java開源項目jeecgboot的超詳細解析
一.搭建
1.前端
npm install npm run serve
2.后端
老生常談的配置,修改mysql與redis即可。

二.業(yè)務功能介紹
功能上jeecgboot主要提供了系列的代碼生成器、模板頁面、報表頁面。
1.報表功能
主要提供報表的相關操作。提供了積木報表插件,可以自定義數據報表、圖形報表。并將報表掛載到菜單上。

2.在線開發(fā)
也就是代碼生成器,可以可視化的在頁面上新建數據庫表,并通過數據庫表生成前后臺代碼。減少業(yè)務代碼開發(fā)的時間。

3.系統(tǒng)管理
用戶管理、角色管理、機構管理、消息管理等基礎模塊。

4.系統(tǒng)監(jiān)控
主要負責各種日志、監(jiān)控的統(tǒng)一處理。

5.頁面組件樣式
常見案例、詳情頁、結果頁、異常頁、列表頁、表單頁主要提供了樣式頁面與控件頁面示例。在開發(fā)過程中如果需要模板直接復制代碼即可。詳情請

三.后臺架構介紹
1.概括

其中報表和代碼生成器沒有提供源碼,如果有興趣可以自行查看jar包源碼。

2.架構核心包jeecg-boot-base
jeecg-boot-base包括了下文的幾個部分。

1.接口包jeecg-boot-base-api

1.對外接口jeecg-system-cloud-api
使用feign+hystrix實現(xiàn)了服務間調用加熔斷,單機環(huán)境并沒有使用。

2.服務內接口jeecg-system-local-api
該包提供了下文使用的常用方法接口。僅提供了接口并無其他配置。

2.核心配置包jeecg-boot-base-core

1.通用類common 1.api
其中為通用接口與通用返回對象。
1.Result
其中Result為所有類的返回實體,這樣能夠通過code編碼和message獲取是否成功和成功/失敗的信息。此類是常用的架構設計

2.aspect
為項目的自定義注解,使用了AOP的切面方式實現(xiàn),這里就不詳細說了,比較簡單都可以看懂。

3.constant
存放著枚舉類與常量池,這里不多說了。


4.es
為操作es的通用類,主要是配置es連接和查詢時動態(tài)拼接and/or的方法。

5.exception
exception為自定義的異常類。
1.JeecgBootExceptionHandler
這里詳細說一下JeecgBootExceptionHandler,該類也是常見的架構設計之一,核心為@RestControllerAdvice、@ExceptionHandler。當業(yè)務代碼中沒有對異常攔截時,該類會自動攔截異常,并數據log日志。所以某些日志在該類配置后,就不需要在每個接口中都捕獲這個異常了。

6.handler
為下文規(guī)范提供了接口類。沒有其他特別說明。

7.system類
這里主要說controller、entity、service等業(yè)務代碼的父類

1.JeecgController<T, S extends IService>
所以controller的父類,提供了導入導出的功能。還可以在里面擴展分頁、排序、常用調用方法等,這樣就可以避免相同的代碼多次添加。這也是架構設計中常用的技巧。

2.JeecgEntity
將通用字段如id、創(chuàng)建人、修改人、創(chuàng)建時間、修改時間等字段統(tǒng)一封裝在一個實體中,使用其他實體繼承。這也是架構設計中常用的技巧。

3.service
主要提供Mybatis-plus提供的curd方法。

8.utli
提供了一大波的工具類,如果在工作中需要,直接復制使用。

2.通用配置類config

1.mybatis

1.MybatisInterceptor
MybatisInterceptor這里主要說MybatisInterceptor,該類負責在mybatis執(zhí)行語句前,攔截并獲取參數,將創(chuàng)建人、創(chuàng)建時間等字符動態(tài)插入。這里上部分核心代碼。
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
log.debug("------sqlId------" + sqlId);
//獲取sql類型是插入還是修改
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
//獲取插入參數
Object parameter = invocation.getArgs()[1];
if (parameter == null) {
return invocation.proceed();
}
if (SqlCommandType.INSERT == sqlCommandType) {
LoginUser sysUser = this.getLoginUser();
//通過反射獲取入參的類
Field[] fields = oConvertUtils.getAllFields(parameter);
for (Field field : fields) {
log.debug("------field.name------" + field.getName());
try {
//將創(chuàng)建人信息動態(tài)加入
if ("createBy".equals(field.getName())) {
field.setAccessible(true);
Object local_createBy = field.get(parameter);
field.setAccessible(false);
if (local_createBy == null || local_createBy.equals("")) {
if (sysUser != null) {
// 登錄人賬號
field.setAccessible(true);
field.set(parameter, sysUser.getUsername());
field.setAccessible(false);
}
}
}
}
2.MybatisPlusSaasConfig
該類主要負責多租戶,什么是多租戶呢?
多租戶:就是多個公司/客戶公用一套系統(tǒng)/數據庫,這就需要保證數據的權限。
該場景比較少不詳細說明。
2.oss
主要從application-dev.yml獲取到上傳的路徑與配置。

3.shiro
安全框架主要有兩個目標:認證與鑒權。
認證:判斷用戶名密碼是否正確。
鑒權:判斷用戶是否有權限訪問該接口。
這里本文著重講解,如果遇到shiro相關應用,可以項目直接移植使用。

1.CustomShiroFilterFactoryBean
該類主要負責解決資源中文路徑問題。這里有個通用的解決方式。
新建類集成ShiroFilterFactoryBean方法,并重寫核心方法createInstance(),并在注入時,注入新建的類CustomShiroFilterFactoryBean,這樣就達到的以往重新源碼的功能。因為spring提供的功能都是用該思想,所以修改源碼的地方就原來越少了,都可以使用該方式實現(xiàn)。
2.JwtFilter
同上文,復寫BasicHttpAuthenticationFilter的驗證登錄用戶的方法,在執(zhí)行登錄接口后判斷用戶是否正確。
3.ResourceCheckFilter
負責鑒權使用,判斷當前用戶是否有權限訪問。
//表示是否允許訪問 ,如果允許訪問返回true,否則false;
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
//獲取當前url
String url = getPathWithinApplication(servletRequest);
log.info("當前用戶正在訪問的 url => " + url);
return subject.isPermitted(url);
}
//onAccessDenied:表示當訪問拒絕時是否已經處理了; 如果返回 true 表示需要繼續(xù)處理; 如果返回 false
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.sendRedirect(request.getContextPath() + this.errorUrl);
// 返回 false 表示已經處理,例如頁面跳轉啥的,表示不在走以下的攔截器了(如果還有配置的話)
return false;
}
4.ShiroRealm
主要負責獲取用戶所有的菜單權限,并提供token的一系列方法。
//獲取所有菜單權限集合
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
}
//驗證用戶輸入的賬號和密碼是否正確
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
}
//校驗token的有效性
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
}
//刷新token有效時間
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
}
//清除當前用戶的權限認證緩存
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
5.ShiroConfig
此為shiro的核心配置類,大多數寫法都是固定寫法。
public class ShiroConfig {
@Value("${jeecg.shiro.excludeUrls}")
private String excludeUrls;
@Resource
LettuceConnectionFactory lettuceConnectionFactory;
@Autowired
private Environment env;
/**
* Filter Chain定義說明
*
* 1、一個URL可以配置多個Filter,使用逗號分隔
* 2、當設置多個過濾器時,全部驗證通過,才視為通過
* 3、部分過濾器可指定參數,如perms,roles
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 攔截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
if(oConvertUtils.isNotEmpty(excludeUrls)){
String[] permissionUrl = excludeUrls.split(",");
for(String url : permissionUrl){
filterChainDefinitionMap.put(url,"anon");
}
}
// 配置不會被攔截的鏈接 順序判斷 也就是不同通過token訪問的地址
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); /
// 添加自己的過濾器并且取名為jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
//如果cloudServer為空 則說明是單體 需要加載跨域配置【微服務跨域切換】
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
//前文定義的過濾器
filterMap.put("jwt", new JwtFilter(cloudServer==null));
shiroFilterFactoryBean.setFilters(filterMap);
// <!-- 過濾鏈定義,從上向下順序執(zhí)行,一般將/**放在最為下邊
filterChainDefinitionMap.put("/**", "jwt");
// 未授權界面返回JSON
shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
//加入security自定義配置類
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//加入上文提供的授權配置
securityManager.setRealm(myRealm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//使用redis緩存有關信息(實現(xiàn)在下文)
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
//下面的代碼是添加注解支持
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");
return defaultAdvisorAutoProxyCreator;
}
//管理shiro生命周期
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
//開啟shiro注解
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// 使用redis緩存用戶信息 ,并設置redis
public RedisCacheManager redisCacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
//配置redis實例
redisCacheManager.setRedisManager(redisManager());
//redis中針對不同用戶緩存(此處的id需要對應user實體中的id字段,用于唯一標識)
redisCacheManager.setPrincipalIdFieldName("id");
//用戶權限信息緩存時間
redisCacheManager.setExpire(200000);
return redisCacheManager;
}
//連接redsi,分為單機與集群
@Bean
public IRedisManager redisManager() {
log.info("===============(2)創(chuàng)建RedisManager,連接Redis..");
IRedisManager manager;
// redis 單機支持,在集群為空,或者集群無機器時候使用 add by jzyadmin@163.com
if (lettuceConnectionFactory.getClusterConfiguration() == null || lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().isEmpty()) {
RedisManager redisManager = new RedisManager();
redisManager.setHost(lettuceConnectionFactory.getHostName());
redisManager.setPort(lettuceConnectionFactory.getPort());
redisManager.setDatabase(lettuceConnectionFactory.getDatabase());
redisManager.setTimeout(0);
if (!StringUtils.isEmpty(lettuceConnectionFactory.getPassword())) {
redisManager.setPassword(lettuceConnectionFactory.getPassword());
}
manager = redisManager;
}else{
// redis集群支持,優(yōu)先使用集群配置
RedisClusterManager redisManager = new RedisClusterManager();
Set<HostAndPort> portSet = new HashSet<>();
lettuceConnectionFactory.getClusterConfiguration().getClusterNodes().forEach(node -> portSet.add(new HostAndPort(node.getHost() , node.getPort())));
//update-begin--Author:scott Date:20210531 for:修改集群模式下未設置redis密碼的bug issues/I3QNIC
if (oConvertUtils.isNotEmpty(lettuceConnectionFactory.getPassword())) {
JedisCluster jedisCluster = new JedisCluster(portSet, 2000, 2000, 5,
lettuceConnectionFactory.getPassword(), new GenericObjectPoolConfig());
redisManager.setPassword(lettuceConnectionFactory.getPassword());
redisManager.setJedisCluster(jedisCluster);
} else {
JedisCluster jedisCluster = new JedisCluster(portSet);
redisManager.setJedisCluster(jedisCluster);
}
manager = redisManager;
}
return manager;
}
}
4.sign
這里不詳細講解,主要描述前臺傳來的簽名是否合法。

5.thirdapp
根據application-dev.yml配置獲取是否開啟第三方接入驗證。@ConfigurationProperties(prefix = "third-app.type")獲取配置文件中的third-app.type的value值。

6.AutoPoiConfig、AutoPoiDictConfig
主要負責將excel中的數據轉換為數據字典。

7.CorsFilterCondition、JeecgCloudCondition
通過獲取application-dev.yml,主要判斷是否有CLOUD_SERVER_KEY = "spring.cloud.nacos.discovery.server-addr";的key值。
context.getEnvironment().getProperty(CommonConstant.CLOUD_SERVER_KEY);是從application-dev.yml生成的map獲取value值。

8.RestTemplateConfig
在服務間調用時設置連接時長,如果單體應用,改配置沒有使用。

9.StaticConfig
從application-dev.yml獲取配置,設置靜態(tài)參數初始化。

10.Swagger2Config
Swagger2文檔配置類,如果有需要請執(zhí)行復制使用。

11.WebMvcConfiguration
springboot的常用配置,如跨域配置,精度丟失配置,靜態(tài)資源配置。都是固定寫法,如果需要請自行參考。

12.WebSocketConfig
springboot提供的websocket的start配置方式,如果有疑問可以參考博主之前的博文-websocket的集成使用

3.業(yè)務接口modules.base
主要提供了日志相關的curd,不多做描述。

3.工具包jeecg-boot-base-tools
主要提供了一些功能的實現(xiàn)類與使用方法,不多說 ,比較簡單。

1.TransmitUserTokenFilter、UserTokenContext
主要負責將token放在上下文中。

2.JeecgRedisListerer、RedisReceiver
這里是發(fā)送消息模板的封裝。核心是從上下文中的getbean方法動態(tài)的指定想要調用的JeecgRedisListerer實現(xiàn)類。一種多態(tài)的實現(xiàn)方式。

3.JeecgRedisCacheWriter
可以看到思想還是上文所說,將RedisCacheWriter類中的方法全部復制出來,并生成新類JeecgRedisCacheWriter,在新類中修改,他的目的是信息模塊在存入緩存時,有統(tǒng)一的前綴。在使用時,注入使用JeecgRedisCacheWriter即可,跟修改源碼有這一樣效果,但是更加優(yōu)雅??梢钥闯鰏pring的設計思想是多牛批。

3.測試包jeecg-boot-base
主要負責調用其他功能,沒啥實質意義。
下圖類是xxljob執(zhí)行定時任務時的寫法,可以看一看。

4.業(yè)務包jeecg-boot-module-system
主要為業(yè)務代碼包,這里找?guī)讉€著重講解一下。

1.api.controller
為微服務為其他服務提供基礎應用接口的包,如果沒有微服務該包不生效。
2.message
該模塊為消息模塊業(yè)務,其中使用了quartz和spring提供的websocket start。如果有興趣可以參考博主給的連接。

3.monitor
提供了redis監(jiān)控的接口。如果需要可以自行查看,比較簡單。
4.quartz
定時任務start,如果想具體了解可以參考博主文章:quartz集成全解析
5.jeecg-boot-starter

springboot的核心就是提供各種各樣的start,在jeecg中也提供了很多的start,分別是上述的四個,其中job為xxl開源項目、cloud為在服務間調用時,將token放再頭中,這里不詳細講解。
下文示例均在jeecg-cloud-module/jeecg-cloud-system-start中。
1.jeecg-boot-starter-rabbitmq

1.MqListener、BaseRabbiMqHandler
在監(jiān)聽消息隊列時,使用以下方法即可。原因是必須加入ack與nack。防止隊列占用。
使用時,該類繼承了BaseRabbiMqHandler,并使用父類的方法,并使用new MqListener<BaseMap>()函數式方法獲取消息隊列中的信息。
@Slf4j
@RabbitListener(queues = CloudConstant.MQ_JEECG_PLACE_ORDER)
@RabbitComponent(value = "helloReceiver1")
public class HelloReceiver1 extends BaseRabbiMqHandler<BaseMap> {
@RabbitHandler
public void onMessage(BaseMap baseMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
//使用了父類的方法
super.onMessage(baseMap, deliveryTag, channel, new MqListener<BaseMap>() {
@Override
public void handler(BaseMap map, Channel channel) {
//業(yè)務處理
String orderId = map.get("orderId").toString();
log.info("MQ Receiver1,orderId : " + orderId);
}
});
}
}
BaseRabbiMqHandler主要的功能是提供了ack與nack,并將token放入頭中。
@Slf4j
public class BaseRabbiMqHandler<T> {
private String token= UserTokenContext.getToken();
public void onMessage(T t, Long deliveryTag, Channel channel, MqListener mqListener) {
try {
UserTokenContext.setToken(token);
mqListener.handler(t, channel);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.info("接收消息失敗,重新放回隊列");
try {
/**
* deliveryTag:該消息的index
* multiple:是否批量.true:將一次性拒絕所有小于deliveryTag的消息。
* requeue:被拒絕的是否重新入隊列
*/
channel.basicNack(deliveryTag, false, true);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}
public interface MqListener<T> {
default void handler(T map, Channel channel) {
}
}
2.RabbitMqClient
主要在隊列初始化時實現(xiàn)隊列的初始化,而是否初始化根據使用時的@RabbitListener、@RabbitComponent判斷。
public interface MqListener<T> {
default void handler(T map, Channel channel) {
}
}
@Bean
public void initQueue() {
//獲取帶RabbitComponent注解的類
Map<String, Object> beansWithRqbbitComponentMap = this.applicationContext.getBeansWithAnnotation(RabbitComponent.class);
Class<? extends Object> clazz = null;
//循環(huán)map
for (Map.Entry<String, Object> entry : beansWithRqbbitComponentMap.entrySet()) {
log.info("初始化隊列............");
//獲取到實例對象的class信息
clazz = entry.getValue().getClass();
Method[] methods = clazz.getMethods();
//判斷是否有RabbitListener注解
RabbitListener rabbitListener = clazz.getAnnotation(RabbitListener.class);
//類上有注解 就創(chuàng)建隊列
if (ObjectUtil.isNotEmpty(rabbitListener)) {
createQueue(rabbitListener);
}
//方法上有注解 就創(chuàng)建隊列
for (Method method : methods) {
RabbitListener methodRabbitListener = method.getAnnotation(RabbitListener.class);
if (ObjectUtil.isNotEmpty(methodRabbitListener)) {
createQueue(methodRabbitListener);
}
}
}
}
/**
* 初始化隊列
*
* @param rabbitListener
*/
private void createQueue(RabbitListener rabbitListener) {
String[] queues = rabbitListener.queues();
//創(chuàng)建交換機
DirectExchange directExchange = createExchange(DelayExchangeBuilder.DELAY_EXCHANGE);
rabbitAdmin.declareExchange(directExchange);
//創(chuàng)建隊列
if (ObjectUtil.isNotEmpty(queues)) {
for (String queueName : queues) {
Properties result = rabbitAdmin.getQueueProperties(queueName);
if (ObjectUtil.isEmpty(result)) {
Queue queue = new Queue(queueName);
addQueue(queue);
Binding binding = BindingBuilder.bind(queue).to(directExchange).with(queueName);
rabbitAdmin.declareBinding(binding);
log.info("創(chuàng)建隊列:" + queueName);
}else{
log.info("已有隊列:" + queueName);
}
}
}
}
3.RabbitMqConfig
為消息隊列的常用配置方式。這里不多描述。
4.event

這個包主要是為使用mq發(fā)送消息使用,多類別的消息會實現(xiàn)JeecgBusEventHandler類,而BaseApplicationEvent通過消息類型傳入的不同的參數選擇合適的業(yè)務類發(fā)送消息。
5.DelayExchangeBuilder
為延時隊列的交換機聲明與綁定。
2.jeecg-boot-starter-lock

1.如何使用分布式鎖
使用時有兩種方式,一種是使用注解方式,一種是使用redisson提供的API。
@Scheduled(cron = "0/5 * * * * ?")
@JLock(lockKey = CloudConstant.REDISSON_DEMO_LOCK_KEY1)
public void execute() throws InterruptedException {
log.info("執(zhí)行execute任務開始,休眠三秒");
Thread.sleep(3000);
System.out.println("=======================業(yè)務邏輯1=============================");
Map map = new BaseMap();
map.put("orderId", "BJ0001");
rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map);
//延遲10秒發(fā)送
map.put("orderId", "NJ0002");
rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map, 10000);
log.info("execute任務結束,休眠三秒");
}
public DemoLockTest() {
}
/**
* 測試分布式鎖【編碼方式】
*/
//@Scheduled(cron = "0/5 * * * * ?")
public void execute2() throws InterruptedException {
if (redissonLock.tryLock(CloudConstant.REDISSON_DEMO_LOCK_KEY2, -1, 6000)) {
log.info("執(zhí)行任務execute2開始,休眠十秒");
Thread.sleep(10000);
System.out.println("=======================業(yè)務邏輯2=============================");
log.info("定時execute2結束,休眠十秒");
redissonLock.unlock(CloudConstant.REDISSON_DEMO_LOCK_KEY2);
} else {
log.info("execute2獲取鎖失敗");
}
}
2.RepeatSubmitAspect
通過公平鎖判斷是否是多次點擊按鈕。
@Around("pointCut(jRepeat)")
public Object repeatSubmit(ProceedingJoinPoint joinPoint,JRepeat jRepeat) throws Throwable {
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
if (Objects.nonNull(jRepeat)) {
// 獲取參數
Object[] args = joinPoint.getArgs();
// 進行一些參數的處理,比如獲取訂單號,操作人id等
StringBuffer lockKeyBuffer = new StringBuffer();
String key =getValueBySpEL(jRepeat.lockKey(), parameterNames, args,"RepeatSubmit").get(0);
// 公平加鎖,lockTime后鎖自動釋放
boolean isLocked = false;
try {
isLocked = redissonLockClient.fairLock(key, TimeUnit.SECONDS, jRepeat.lockTime());
// 如果成功獲取到鎖就繼續(xù)執(zhí)行
if (isLocked) {
// 執(zhí)行進程
return joinPoint.proceed();
} else {
// 未獲取到鎖
throw new Exception("請勿重復提交");
}
} finally {
// 如果鎖還存在,在方法執(zhí)行完成后,釋放鎖
if (isLocked) {
redissonLockClient.unlock(key);
}
}
}
return joinPoint.proceed();
}
3.DistributedLockHandler
該類主要是jLock的切面類,通過jLock注解參數,判斷需要加鎖的類型,同時加鎖的方法也不相同。
//jLock切面,進行加鎖
@SneakyThrows
@Around("@annotation(jLock)")
public Object around(ProceedingJoinPoint joinPoint, JLock jLock) {
Object obj = null;
log.info("進入RedisLock環(huán)繞通知...");
RLock rLock = getLock(joinPoint, jLock);
boolean res = false;
//獲取超時時間
long expireSeconds = jLock.expireSeconds();
//等待多久,n秒內獲取不到鎖,則直接返回
long waitTime = jLock.waitTime();
//執(zhí)行aop
if (rLock != null) {
try {
if (waitTime == -1) {
res = true;
//一直等待加鎖
rLock.lock(expireSeconds, TimeUnit.MILLISECONDS);
} else {
res = rLock.tryLock(waitTime, expireSeconds, TimeUnit.MILLISECONDS);
}
if (res) {
obj = joinPoint.proceed();
} else {
log.error("獲取鎖異常");
}
} finally {
if (res) {
rLock.unlock();
}
}
}
log.info("結束RedisLock環(huán)繞通知...");
return obj;
}
//通過參數判斷加鎖類型
@SneakyThrows
private RLock getLock(ProceedingJoinPoint joinPoint, JLock jLock) {
//獲取key
String[] keys = jLock.lockKey();
if (keys.length == 0) {
throw new RuntimeException("keys不能為空");
}
//獲取參數
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
LockModel lockModel = jLock.lockModel();
if (!lockModel.equals(LockModel.MULTIPLE) && !lockModel.equals(LockModel.REDLOCK) && keys.length > 1) {
throw new RuntimeException("參數有多個,鎖模式為->" + lockModel.name() + ".無法鎖定");
}
RLock rLock = null;
String keyConstant = jLock.keyConstant();
//判斷鎖類型
if (lockModel.equals(LockModel.AUTO)) {
if (keys.length > 1) {
lockModel = LockModel.REDLOCK;
} else {
lockModel = LockModel.REENTRANT;
}
}
//根據不同的鎖類型執(zhí)行不同的加鎖方式
switch (lockModel) {
case FAIR:
rLock = redissonClient.getFairLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0));
break;
case REDLOCK:
List<RLock> rLocks = new ArrayList<>();
for (String key : keys) {
List<String> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant);
for (String s : valueBySpEL) {
rLocks.add(redissonClient.getLock(s));
}
}
RLock[] locks = new RLock[rLocks.size()];
int index = 0;
for (RLock r : rLocks) {
locks[index++] = r;
}
rLock = new RedissonRedLock(locks);
break;
case MULTIPLE:
rLocks = new ArrayList<>();
for (String key : keys) {
List<String> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant);
for (String s : valueBySpEL) {
rLocks.add(redissonClient.getLock(s));
}
}
locks = new RLock[rLocks.size()];
index = 0;
for (RLock r : rLocks) {
locks[index++] = r;
}
rLock = new RedissonMultiLock(locks);
break;
case REENTRANT:
List<String> valueBySpEL = getValueBySpEL(keys[0], parameterNames, args, keyConstant);
//如果spel表達式是數組或者LIST 則使用紅鎖
if (valueBySpEL.size() == 1) {
rLock = redissonClient.getLock(valueBySpEL.get(0));
} else {
locks = new RLock[valueBySpEL.size()];
index = 0;
for (String s : valueBySpEL) {
locks[index++] = redissonClient.getLock(s);
}
rLock = new RedissonRedLock(locks);
}
break;
case READ:
rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).readLock();
break;
case WRITE:
rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).writeLock();
break;
}
return rLock;
}
4.RedissonLockClient
redisson客戶端,提供了一大波方法,請自行查看。
public class RedissonLockClient {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 獲取鎖
*/
public RLock getLock(String lockKey) {
return redissonClient.getLock(lockKey);
}
/**
* 加鎖操作
*
* @return boolean
*/
public boolean tryLock(String lockName, long expireSeconds) {
return tryLock(lockName, 0, expireSeconds);
}
.
.
.
5.core包
主要通過application.yml配置文件獲取redis連接類型,通過根據該參數動態(tài)的選擇策略類,連接redis。
public class RedissonManager {
public Redisson getRedisson() {
return redisson;
}
//Redisson連接方式配置工廠
static class RedissonConfigFactory {
private RedissonConfigFactory() {
}
private static volatile RedissonConfigFactory factory = null;
public static RedissonConfigFactory getInstance() {
if (factory == null) {
synchronized (Object.class) {
if (factory == null) {
factory = new RedissonConfigFactory();
}
}
}
return factory;
}
//根據連接類型創(chuàng)建連接方式的配置
Config createConfig(RedissonProperties redissonProperties) {
Preconditions.checkNotNull(redissonProperties);
Preconditions.checkNotNull(redissonProperties.getAddress(), "redis地址未配置");
RedisConnectionType connectionType = redissonProperties.getType();
// 聲明連接方式
RedissonConfigStrategy redissonConfigStrategy;
if (connectionType.equals(RedisConnectionType.SENTINEL)) {
redissonConfigStrategy = new SentinelRedissonConfigStrategyImpl();
} else if (connectionType.equals(RedisConnectionType.CLUSTER)) {
redissonConfigStrategy = new ClusterRedissonConfigStrategyImpl();
} else if (connectionType.equals(RedisConnectionType.MASTERSLAVE)) {
redissonConfigStrategy = new MasterslaveRedissonConfigStrategyImpl();
} else {
redissonConfigStrategy = new StandaloneRedissonConfigStrategyImpl();
}
Preconditions.checkNotNull(redissonConfigStrategy, "連接方式創(chuàng)建異常");
return redissonConfigStrategy.createRedissonConfig(redissonProperties);
}
}
}
//策略實現(xiàn),此類是指定redis的連接方式是哨兵。
public class SentinelRedissonConfigStrategyImpl implements RedissonConfigStrategy {
@Override
public Config createRedissonConfig(RedissonProperties redissonProperties) {
Config config = new Config();
try {
String address = redissonProperties.getAddress();
String password = redissonProperties.getPassword();
int database = redissonProperties.getDatabase();
String[] addrTokens = address.split(",");
String sentinelAliasName = addrTokens[0];
// 設置redis配置文件sentinel.conf配置的sentinel別名
config.useSentinelServers().setMasterName(sentinelAliasName);
config.useSentinelServers().setDatabase(database);
if (StringUtils.isNotBlank(password)) {
config.useSentinelServers().setPassword(password);
}
// 設置哨兵節(jié)點的服務IP和端口
for (int i = 1; i < addrTokens.length; i++) {
config.useSentinelServers().addSentinelAddress(GlobalConstant.REDIS_CONNECTION_PREFIX+ addrTokens[i]);
}
log.info("初始化哨兵方式Config,redisAddress:" + address);
} catch (Exception e) {
log.error("哨兵Redisson初始化錯誤", e);
e.printStackTrace();
}
return config;
}
}
6.jeecg-cloud-module

這里詳細的說一下jeecg-cloud-gateway,因為其他的都是開源項目沒下載即用。
jeecg-cloud-system-start為封裝start的使用方法,上文已經介紹了。
1.jeecg-cloud-gateway

1.GatewayRoutersConfiguration
當固定的幾個路由,有特殊化的執(zhí)行方法。

2.RateLimiterConfiguration
主要配置限流,與application.yml一起使用,下文配置含義是,發(fā)送過來的請求只能容納redis-rate-limiter.burstCapacity的配置(3次)多余的會全部丟棄(限流),每秒消費redis-rate-limiter.replenishRate(1次)。


3.FallbackController
熔斷的執(zhí)行方法。

4.GlobalAccessTokenFilter
全局攔截器,在調用其他服務時,將用戶信息放在請求頭中。

5.SentinelFilterContextConfig
使Sentinel鏈路流控模式生效,固定寫法。
6.HystrixFallbackHandler、SentinelBlockRequestHandler
在降級/限流時,將異常信息轉換成json返回給前臺。
7.LoderRouderHandler
動態(tài)刷新路由。

8.MySwaggerResourceProvider、SwaggerResourceController
將swagger地址統(tǒng)一管理起來
9.DynamicRouteLoader、DynamicRouteService
DynamicRouteLoader:通過application.yml判斷從nacos/redis中獲取路由信息,并實現(xiàn)動態(tài)的加載。
DynamicRouteService:為底層處理路由的API。
四.總體感想
文章到這里差不多就接近尾聲了,大多數功能附帶著代碼都講述了一遍。在功能上來說,jeecg提供了很多常用功能,如rabbitMq封裝、積木報表、代碼生成器等。這些在日常工作中有很大的概率碰上,如果有以上需求,可以來框架中直接復制粘貼即可。
但是在格式規(guī)范上,如出入參的規(guī)范,代碼的寫法,代碼的格式化等方面,并不是特別統(tǒng)一,且沒有嚴格規(guī)范??傮w來說非常適合做私活與畢業(yè)設計,同時也是最早一批開源的前后端項目腳手架,爆贊。
到此這篇關于java開源項目jeecgboot的文章就介紹到這了,更多相關java開源項目jeecgboot內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Springboot ApplicationRunner的使用解讀
這篇文章主要介紹了Springboot ApplicationRunner的使用解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05
微信小程序調用微信登陸獲取openid及java做為服務端示例
這篇文章主要介紹了微信小程序調用微信登陸獲取openid及java做為服務端示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-01-01
Java并發(fā)系列之CyclicBarrier源碼分析
這篇文章主要為大家詳細分析了Java并發(fā)系列之CyclicBarrier源碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-03-03
AsyncHttpClient KeepAliveStrategy源碼流程解讀
這篇文章主要為大家介紹了AsyncHttpClient KeepAliveStrategy源碼流程解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-12-12
關于eclipse中運行tomcat提示端口被占用的4種解決
這篇文章主要介紹了關于eclipse中運行tomcat提示端口被占用的4種解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01

